Add cross-platform packaging, icons, and Claude Code config discovery
- Auto-discovers Claude Code (~/.claude/settings.json) alongside Claude Desktop - Generated icons/app.icns (macOS) and icons/app.ico (Windows) from rounded PNGs - bcc.spec: PyInstaller spec for all platforms (.app on macOS, onefile on Windows/Linux) - .github/workflows/release.yml: builds and publishes GitHub Release on version tags - scripts/build_icons.py: regenerates icon files from source PNGs - requirements-dev.txt: adds pyinstaller and pillow for building - CLAUDE.md: initial repo guidance for Claude Code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -0,0 +1,129 @@
|
|||||||
|
name: Build & Release
|
||||||
|
|
||||||
|
# Trigger on version tags (e.g. git tag v1.0.0 && git push --tags)
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
# Manual run from Actions tab (useful for testing the workflow itself)
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macOS
|
||||||
|
artifact: BetterClaudeConfig-macOS.zip
|
||||||
|
- os: windows-latest
|
||||||
|
platform: Windows
|
||||||
|
artifact: BetterClaudeConfig-Windows.zip
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: Linux
|
||||||
|
artifact: BetterClaudeConfig-Linux.tar.gz
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
name: Build (${{ matrix.platform }})
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install -r requirements-dev.txt
|
||||||
|
|
||||||
|
# macOS: build app.icns from source PNGs (iconutil is built into macOS)
|
||||||
|
- name: Generate app.icns (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: python scripts/build_icons.py
|
||||||
|
|
||||||
|
# Windows: generate app.ico using Pillow (already installed via requirements-dev.txt)
|
||||||
|
- name: Generate app.ico (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: python scripts/build_icons.py
|
||||||
|
|
||||||
|
- name: Build with PyInstaller
|
||||||
|
run: pyinstaller bcc.spec
|
||||||
|
|
||||||
|
# ── Package ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Package (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
cd dist
|
||||||
|
zip -r --symlinks "../${{ matrix.artifact }}" BetterClaudeConfig.app
|
||||||
|
|
||||||
|
- name: Package (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Compress-Archive -Path dist\BetterClaudeConfig.exe `
|
||||||
|
-DestinationPath "${{ matrix.artifact }}"
|
||||||
|
|
||||||
|
- name: Package (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
tar -czf "${{ matrix.artifact }}" -C dist BetterClaudeConfig
|
||||||
|
|
||||||
|
# ── Upload artifact for the release job ──────────────────────────────
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: ${{ matrix.artifact }}
|
||||||
|
|
||||||
|
# ── Create GitHub Release with all three artifacts ──────────────────────
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Publish Release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only publish when a tag was pushed (not on workflow_dispatch without a tag)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: Better Claude Config ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
generate_release_notes: true
|
||||||
|
files: artifacts/*
|
||||||
|
body: |
|
||||||
|
## Better Claude Config ${{ github.ref_name }}
|
||||||
|
|
||||||
|
A GUI for managing MCP server configurations for Claude Desktop and Claude Code — no hand-editing JSON.
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
| Platform | File |
|
||||||
|
|----------|------|
|
||||||
|
| macOS | `BetterClaudeConfig-macOS.zip` — unzip and drag **BetterClaudeConfig.app** to Applications |
|
||||||
|
| Windows | `BetterClaudeConfig-Windows.zip` — unzip and run **BetterClaudeConfig.exe** |
|
||||||
|
| Linux | `BetterClaudeConfig-Linux.tar.gz` — extract and run **BetterClaudeConfig** |
|
||||||
|
|
||||||
|
### macOS note
|
||||||
|
The app is not code-signed. On first launch, right-click → **Open** to bypass Gatekeeper, or run:
|
||||||
|
```
|
||||||
|
xattr -cr /Applications/BetterClaudeConfig.app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
No Python installation needed — the app is self-contained.
|
||||||
@@ -8,7 +8,8 @@ venv/
|
|||||||
# PyInstaller / build
|
# PyInstaller / build
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.spec
|
# Note: bcc.spec is tracked; *.spec here would exclude it — keep this explicit:
|
||||||
|
*.spec.bak
|
||||||
|
|
||||||
# App runtime
|
# App runtime
|
||||||
.bcc_backups/
|
.bcc_backups/
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install runtime dependency
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python bcc.py # run the GUI
|
||||||
|
python test_core.py # run unit tests (23 tests, no GUI needed)
|
||||||
|
|
||||||
|
# Build a self-contained binary
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
python scripts/build_icons.py # regenerate icons/app.icns + icons/app.ico if needed
|
||||||
|
pyinstaller bcc.spec
|
||||||
|
# macOS → dist/BetterClaudeConfig.app
|
||||||
|
# Windows → dist/BetterClaudeConfig.exe
|
||||||
|
# Linux → dist/BetterClaudeConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+. Runtime dependency: `PySide6>=6.6`. Build-time: `pyinstaller>=6.0`, `pillow>=10.0`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The codebase is split into two layers:
|
||||||
|
|
||||||
|
**`bcc_core.py`** — All logic with no GUI imports. Contains:
|
||||||
|
- `Profile` / `ServerEntry` dataclasses (the data model)
|
||||||
|
- `discover_profiles()` — scans the platform's app-support directory for `Claude*` folders (Claude Desktop) **and** always adds `~/.claude/settings.json` (Claude Code, cross-platform)
|
||||||
|
- `load_config` / `extract_servers` / `apply_servers` / `write_config` — the read/write pipeline; writes are atomic with rotating timestamped backups in `.bcc_backups/`
|
||||||
|
- `parse_pasted_json()` — accepts three JSON shapes (full config, inner map, or bare server object)
|
||||||
|
- `check_dependency()` / `diagnostics_text()` / `pin_command_path()` — PATH resolution logic; distinguishes "found on normal PATH" (ok) vs "found only on augmented PATH" (warn) vs "not found" (missing). Uses an `lru_cache`-memoized `augmented_path()` that extends the inherited PATH with common runtime locations (nvm, homebrew, cargo, volta, etc.)
|
||||||
|
- `test_remote()` — synchronous HTTP reachability check, intended to run off the UI thread
|
||||||
|
|
||||||
|
**`bcc.py`** — PySide6 GUI that is a thin shell over `bcc_core`. Key classes:
|
||||||
|
- `MainWindow` — manages the profile combo, two server tables (active/disabled), action bar, and dirty state
|
||||||
|
- `ServerEditor` — right-panel form with a `QStackedWidget` for stdio vs remote pages; calls `core.check_dependency()` on every field change
|
||||||
|
- `KeyValueTable` — reusable widget for env vars and headers
|
||||||
|
- `ConnTester(QThread)` — background thread for remote reachability tests
|
||||||
|
|
||||||
|
**The cardinal rule**: `apply_servers()` only ever writes to `mcpServers` and `_disabledMcpServers`. All other keys in the user's config are preserved verbatim and in their original order.
|
||||||
|
|
||||||
|
Disabled servers are parked under `_disabledMcpServers` (which Claude Desktop ignores) so they can be re-enabled without losing their definition.
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
- **`bcc.spec`** — PyInstaller spec; handles macOS (onedir → `.app` bundle) and Windows/Linux (onefile). Platform-aware icon selection.
|
||||||
|
- **`icons/app.icns`** (macOS) and **`icons/app.ico`** (Windows) are pre-generated and committed; source PNGs are in `icons/twin-gears/rounded/`.
|
||||||
|
- **`scripts/build_icons.py`** — regenerates both icon files from source PNGs. Uses `iconutil` (macOS-only) for `.icns`, Pillow for `.ico`.
|
||||||
|
- **`.github/workflows/release.yml`** — builds on all three platforms on tag push, publishes as GitHub Release assets.
|
||||||
|
|
||||||
|
To release: `git tag vX.Y.Z && git push --tags`.
|
||||||
|
|
||||||
|
## Key constants
|
||||||
|
|
||||||
|
- `ACCENT` at the top of `bcc.py` — the one constant to change for a rebrand
|
||||||
|
- `KNOWN_FIELDS` in `bcc_core.py` — fields the editor renders; anything else on a server object is preserved as `_extra` in `ServerEditor`
|
||||||
|
- `DISABLED_KEY = "_disabledMcpServers"` — the parking key for disabled servers
|
||||||
|
- `MAX_BACKUPS = 15` — rolling backup limit per config file
|
||||||
@@ -1,26 +1,39 @@
|
|||||||
# Better Claude Config (BCC)
|
# Better Claude Config (BCC)
|
||||||
|
|
||||||
A small, cross-platform GUI for editing the `mcpServers` block of one or more
|
A small, cross-platform GUI for editing the `mcpServers` block of Claude Desktop
|
||||||
Claude Desktop installs — without ever hand-writing JSON.
|
and Claude Code installs — without ever hand-writing JSON.
|
||||||
|
|
||||||
BCC only ever touches `mcpServers` (and its own `_disabledMcpServers` parking
|
BCC only ever touches `mcpServers` (and its own `_disabledMcpServers` parking
|
||||||
key). Every other key in your config is preserved verbatim, in its original
|
key). Every other key in your config is preserved verbatim, in its original
|
||||||
order. Each write is atomic and makes a timestamped backup first.
|
order. Each write is atomic and makes a timestamped backup first.
|
||||||
|
|
||||||
## Install & run
|
## Download (no Python required)
|
||||||
|
|
||||||
|
Pre-built self-contained binaries are attached to every [GitHub Release](../../releases):
|
||||||
|
|
||||||
|
| Platform | Download | Run |
|
||||||
|
|----------|----------|-----|
|
||||||
|
| macOS | `BetterClaudeConfig-macOS.zip` | Unzip → drag **BetterClaudeConfig.app** to Applications |
|
||||||
|
| Windows | `BetterClaudeConfig-Windows.zip` | Unzip → double-click **BetterClaudeConfig.exe** |
|
||||||
|
| Linux | `BetterClaudeConfig-Linux.tar.gz` | Extract → run **BetterClaudeConfig** |
|
||||||
|
|
||||||
|
> **macOS Gatekeeper note:** the app is not notarized. On first launch, right-click → **Open**, or run `xattr -cr /Applications/BetterClaudeConfig.app` in a terminal.
|
||||||
|
|
||||||
|
## Run from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python bcc.py
|
python bcc.py
|
||||||
```
|
```
|
||||||
|
|
||||||
(Python 3.10+.)
|
(Python 3.10+, PySide6 6.6+.)
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
- **Auto-discovers installs** — scans the platform's app-support folder for any
|
- **Auto-discovers installs** — scans the platform's app-support folder for any
|
||||||
`Claude*` directory (so `Claude` and `Claude-Work` both show up), and lets you
|
`Claude*` directory (so `Claude` and `Claude-Work` both show up) **and** finds
|
||||||
**Add config…** to point at any file manually.
|
Claude Code at `~/.claude/settings.json`. Use **Add config…** to point at any
|
||||||
|
other file manually.
|
||||||
- **Form-based editing** — name, command, args (one per line), env vars, or for
|
- **Form-based editing** — name, command, args (one per line), env vars, or for
|
||||||
remote servers: URL, transport, and headers. No raw JSON.
|
remote servers: URL, transport, and headers. No raw JSON.
|
||||||
- **Paste JSON** — drop in any snippet from an MCP doc (full `mcpServers` block,
|
- **Paste JSON** — drop in any snippet from an MCP doc (full `mcpServers` block,
|
||||||
@@ -55,17 +68,40 @@ After saving, **restart that Claude install** for changes to take effect.
|
|||||||
|
|
||||||
## Config locations it scans
|
## Config locations it scans
|
||||||
|
|
||||||
| OS | Base it scans for `Claude*` |
|
**Claude Desktop**
|
||||||
|---------|---------------------------------|
|
|
||||||
|
| OS | Base it scans for `Claude*` |
|
||||||
|
|---------|----------------------------------|
|
||||||
| macOS | `~/Library/Application Support` |
|
| macOS | `~/Library/Application Support` |
|
||||||
| Windows | `%APPDATA%` |
|
| Windows | `%APPDATA%` |
|
||||||
| Linux | `~/.config` |
|
| Linux | `~/.config` |
|
||||||
|
|
||||||
|
**Claude Code** (all platforms): `~/.claude/settings.json`
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `bcc.py` — the GUI.
|
- `bcc.py` — the GUI.
|
||||||
- `bcc_core.py` — all the file/JSON/validation/dependency logic (no GUI deps).
|
- `bcc_core.py` — all the file/JSON/validation/dependency logic (no GUI deps).
|
||||||
- `test_core.py` — unit suite for the core (`python test_core.py`).
|
- `test_core.py` — unit suite for the core (`python test_core.py`).
|
||||||
|
- `bcc.spec` — PyInstaller build spec (cross-platform).
|
||||||
|
- `scripts/build_icons.py` — regenerates `icons/app.icns` and `icons/app.ico` from source PNGs.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
python scripts/build_icons.py # regenerate icons if needed
|
||||||
|
pyinstaller bcc.spec
|
||||||
|
# macOS → dist/BetterClaudeConfig.app
|
||||||
|
# Windows → dist/BetterClaudeConfig.exe
|
||||||
|
# Linux → dist/BetterClaudeConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Releases are built automatically by GitHub Actions (`.github/workflows/release.yml`) when a version tag is pushed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.0.0 && git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
## Rebrand
|
## Rebrand
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# PyInstaller spec for Better Claude Config.
|
||||||
|
#
|
||||||
|
# macOS → onedir build wrapped in a .app bundle (double-click native)
|
||||||
|
# Windows → onefile .exe (no console window)
|
||||||
|
# Linux → onefile binary (no console window)
|
||||||
|
#
|
||||||
|
# Build:
|
||||||
|
# pip install pyinstaller
|
||||||
|
# pyinstaller bcc.spec
|
||||||
|
#
|
||||||
|
# Output:
|
||||||
|
# macOS → dist/BetterClaudeConfig.app
|
||||||
|
# Windows → dist/BetterClaudeConfig.exe
|
||||||
|
# Linux → dist/BetterClaudeConfig
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
APP_NAME = "BetterClaudeConfig"
|
||||||
|
BUNDLE_ID = "com.betterclaude.config"
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
icon = "icons/app.icns"
|
||||||
|
elif sys.platform == "win32":
|
||||||
|
icon = "icons/app.ico"
|
||||||
|
else:
|
||||||
|
icon = None # Linux: icon embedded via .desktop file, not the binary
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
["bcc.py"],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=["_tkinter", "tkinter"],
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
# ------------------------------------------------------------------ macOS
|
||||||
|
# onedir so PyInstaller produces a proper .app bundle structure.
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name=APP_NAME,
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
console=False,
|
||||||
|
argv_emulation=True, # lets macOS open files by passing argv
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=icon,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
upx_exclude=[],
|
||||||
|
name=APP_NAME,
|
||||||
|
)
|
||||||
|
app = BUNDLE(
|
||||||
|
coll,
|
||||||
|
name=f"{APP_NAME}.app",
|
||||||
|
icon=icon,
|
||||||
|
bundle_identifier=BUNDLE_ID,
|
||||||
|
info_plist={
|
||||||
|
"CFBundleName": "Better Claude Config",
|
||||||
|
"CFBundleDisplayName": "Better Claude Config",
|
||||||
|
"CFBundleShortVersionString": "1.0.0",
|
||||||
|
"CFBundleVersion": "1.0.0",
|
||||||
|
"NSHighResolutionCapable": True,
|
||||||
|
"NSRequiresAquaSystemAppearance": False, # supports dark mode
|
||||||
|
"LSMinimumSystemVersion": "11.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# ----------------------------------------------- Windows / Linux (onefile)
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name=APP_NAME,
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=icon,
|
||||||
|
)
|
||||||
@@ -78,11 +78,12 @@ def app_support_base() -> Path:
|
|||||||
|
|
||||||
def discover_profiles() -> list[Profile]:
|
def discover_profiles() -> list[Profile]:
|
||||||
"""
|
"""
|
||||||
Find every `Claude*` data directory in the platform's app-support base.
|
Find every `Claude*` data directory in the platform's app-support base
|
||||||
|
(Claude Desktop installs), then also check for a Claude Code global config.
|
||||||
|
|
||||||
This catches the standard `Claude` folder plus any sibling like
|
Claude Desktop: scans the platform app-support folder for any `Claude*`
|
||||||
`Claude-Work`, `Claude-Personal`, etc. A directory is listed even if the
|
directory (catches `Claude`, `Claude-Work`, etc.).
|
||||||
config file doesn't exist yet (it will be created on first save).
|
Claude Code: always at ~/.claude/settings.json on every platform.
|
||||||
"""
|
"""
|
||||||
base = app_support_base()
|
base = app_support_base()
|
||||||
out: list[Profile] = []
|
out: list[Profile] = []
|
||||||
@@ -93,6 +94,12 @@ def discover_profiles() -> list[Profile]:
|
|||||||
seen.add(d.name)
|
seen.add(d.name)
|
||||||
cfg = d / CONFIG_FILENAME
|
cfg = d / CONFIG_FILENAME
|
||||||
out.append(Profile(label=d.name, path=cfg, config_exists=cfg.is_file()))
|
out.append(Profile(label=d.name, path=cfg, config_exists=cfg.is_file()))
|
||||||
|
|
||||||
|
# Claude Code global settings (~/.claude/settings.json) — same mcpServers format,
|
||||||
|
# cross-platform (the ~/.claude/ directory is always in the same place).
|
||||||
|
cc_cfg = Path.home() / ".claude" / "settings.json"
|
||||||
|
out.append(Profile(label="Claude Code", path=cc_cfg, config_exists=cc_cfg.is_file()))
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 750 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
# Runtime (also in requirements.txt)
|
||||||
|
PySide6>=6.6
|
||||||
|
|
||||||
|
# Build / packaging
|
||||||
|
pyinstaller>=6.0
|
||||||
|
pillow>=10.0 # generates icons/app.ico during CI (Windows build)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate icons/app.ico (Windows) and icons/app.icns (macOS) from the
|
||||||
|
rounded PNG source files in icons/twin-gears/rounded/.
|
||||||
|
|
||||||
|
Run from the repo root:
|
||||||
|
python scripts/build_icons.py
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
pip install pillow # for .ico
|
||||||
|
iconutil # built into macOS; for .icns
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parent.parent
|
||||||
|
SRC = ROOT / "icons" / "twin-gears" / "rounded"
|
||||||
|
OUT = ROOT / "icons"
|
||||||
|
|
||||||
|
|
||||||
|
def build_ico():
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
print("Pillow not installed — skipping .ico generation (pip install pillow)")
|
||||||
|
return
|
||||||
|
|
||||||
|
sizes = [16, 32, 48, 64, 128, 256]
|
||||||
|
imgs = []
|
||||||
|
for s in sizes:
|
||||||
|
p = SRC / f"icon-{s}.png"
|
||||||
|
if not p.exists():
|
||||||
|
print(f" Missing {p.name}, skipping")
|
||||||
|
continue
|
||||||
|
imgs.append(Image.open(p).convert("RGBA"))
|
||||||
|
|
||||||
|
if not imgs:
|
||||||
|
print(" No source PNGs found — cannot build .ico")
|
||||||
|
return
|
||||||
|
|
||||||
|
dest = OUT / "app.ico"
|
||||||
|
imgs[-1].save(dest, format="ICO", append_images=imgs[:-1])
|
||||||
|
print(f" Generated {dest.relative_to(ROOT)} ({dest.stat().st_size // 1024 + 1} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
def build_icns():
|
||||||
|
if shutil.which("iconutil") is None:
|
||||||
|
print(" iconutil not found (macOS-only) — skipping .icns generation")
|
||||||
|
return
|
||||||
|
|
||||||
|
iconset = Path(tempfile.mkdtemp()) / "app.iconset"
|
||||||
|
iconset.mkdir()
|
||||||
|
mapping = {
|
||||||
|
"icon_16x16.png": "icon-16.png",
|
||||||
|
"icon_16x16@2x.png": "icon-32.png",
|
||||||
|
"icon_32x32.png": "icon-32.png",
|
||||||
|
"icon_32x32@2x.png": "icon-64.png",
|
||||||
|
"icon_128x128.png": "icon-128.png",
|
||||||
|
"icon_128x128@2x.png": "icon-256.png",
|
||||||
|
"icon_256x256.png": "icon-256.png",
|
||||||
|
"icon_256x256@2x.png": "icon-512.png",
|
||||||
|
"icon_512x512.png": "icon-512.png",
|
||||||
|
"icon_512x512@2x.png": "icon-1024.png",
|
||||||
|
}
|
||||||
|
for dst_name, src_name in mapping.items():
|
||||||
|
src = SRC / src_name
|
||||||
|
if src.exists():
|
||||||
|
shutil.copy2(src, iconset / dst_name)
|
||||||
|
else:
|
||||||
|
print(f" Missing {src_name}, skipping {dst_name}")
|
||||||
|
|
||||||
|
dest = OUT / "app.icns"
|
||||||
|
subprocess.run(
|
||||||
|
["iconutil", "-c", "icns", str(iconset), "-o", str(dest)],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
shutil.rmtree(iconset.parent)
|
||||||
|
print(f" Generated {dest.relative_to(ROOT)} ({dest.stat().st_size // 1024} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Building icons/app.ico ...")
|
||||||
|
build_ico()
|
||||||
|
print("Building icons/app.icns ...")
|
||||||
|
build_icns()
|
||||||
|
print("Done.")
|
||||||
@@ -18,7 +18,8 @@ for d in ("Claude", "Claude-Work", "NotClaude"):
|
|||||||
c.app_support_base = lambda: base
|
c.app_support_base = lambda: base
|
||||||
profs = c.discover_profiles()
|
profs = c.discover_profiles()
|
||||||
labels = sorted(p.label for p in profs)
|
labels = sorted(p.label for p in profs)
|
||||||
check("discovers Claude + Claude-Work, not NotClaude", labels == ["Claude", "Claude-Work"])
|
check("discovers Claude + Claude-Work, not NotClaude",
|
||||||
|
"Claude" in labels and "Claude-Work" in labels and "NotClaude" not in labels)
|
||||||
check("flags missing config file", any(p.label=="Claude-Work" and not p.config_exists for p in profs))
|
check("flags missing config file", any(p.label=="Claude-Work" and not p.config_exists for p in profs))
|
||||||
|
|
||||||
# ---- 2. write preserves OTHER keys + order, only touches mcpServers ----
|
# ---- 2. write preserves OTHER keys + order, only touches mcpServers ----
|
||||||
|
|||||||