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
43 changes: 39 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
name: Release

# Her PR'da da calisir (kuru kosu): yayinlanacak artefakti ayni yoldan
# derleyip paketler ve gercekten calistirir. Yukleme adimi yalnizca v*
# etiketinde isler. Boylece bozuk bir paket etiketlenmeden yakalanir.
on:
push:
tags: ["v*"]
pull_request:
branches: ["main"]

permissions:
contents: write
Expand All @@ -24,9 +29,12 @@ jobs:
target: x86_64-pc-windows-msvc
asset: claude-code-setup-windows-x86_64.exe
mcpb: claude-code-setup-windows.mcpb
# Universal ikili: x86_64 + arm64 tek dosyada (lipo). Yalnizca x86_64
# yayinlamak Apple Silicon'da Rosetta 2'yi sart kosuyordu; macOS
# Rosetta'yi varsayilan olarak kurmuyor.
- os: macos-latest
target: x86_64-apple-darwin
asset: claude-code-setup-macos-x86_64
target: x86_64-apple-darwin,aarch64-apple-darwin
asset: claude-code-setup-macos-universal
mcpb: claude-code-setup-macos.mcpb
runs-on: ${{ matrix.os }}

Expand All @@ -48,10 +56,21 @@ jobs:
python-version: "3.x"

- name: Build release binary
if: runner.os != 'macOS'
run: cargo build --release --target ${{ matrix.target }}

- name: Stage asset (Unix)
if: runner.os != 'Windows'
- name: Build universal macOS binary
if: runner.os == 'macOS'
run: |
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output ${{ matrix.asset }} target/x86_64-apple-darwin/release/claude-code-setup target/aarch64-apple-darwin/release/claude-code-setup
archs=$(lipo -archs ${{ matrix.asset }})
echo "dilimler: $archs"
[[ "$archs" == *x86_64* && "$archs" == *arm64* ]] || { echo "universal degil: $archs"; exit 1; }

- name: Stage asset (Linux)
if: runner.os == 'Linux'
run: cp target/${{ matrix.target }}/release/claude-code-setup ${{ matrix.asset }}

- name: Stage asset (Windows)
Expand All @@ -64,7 +83,23 @@ jobs:
- name: Package Claude Desktop extension
run: python package-extension.py ${{ matrix.asset }} ${{ matrix.mcpb }}

# Yayinlanacak paketi Claude Desktop'in yapacagi gibi ac ve calistir:
# isletim sisteminin kendi unzip'i (calistirma izni ancak boyle sinanir),
# MCP el sikismasi, arac listesi, salt-okunur bir aracin gercek cagrisi.
# macOS'ta bu varsayilan kosu universal ikilinin arm64 dilimini calistirir.
- name: Smoke test packaged extension
run: python smoke-test-extension.py ${{ matrix.mcpb }}

# Varsayilan kosu ARM koşucuda arm64 dilimini calistirir. Intel Mac'lerin
# alacagi x86_64 dilimini de ayrica calistir (koşucuda Rosetta ile).
- name: Smoke test x86_64 slice
if: runner.os == 'macOS'
env:
SMOKE_ARCH: x86_64
run: python smoke-test-extension.py ${{ matrix.mcpb }}

- name: Upload to GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,3 @@ jobs:
- name: Run tests
run: cargo test --verbose

- name: Build release
run: cargo build --release --verbose
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "claude-code-setup"
version = "0.1.6"
version = "0.1.7"
edition = "2021"
authors = ["Ercan ER <ercaner1988@gmail.com>", "Kassam <agent@hermes.local>"]
description = "Bagimsiz Rust CLI: Claude Code ortam denetimi, dinamik MCP yonetimi, semantik + graf hafiza motoru"
Expand Down
12 changes: 11 additions & 1 deletion INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,17 @@ Invoke-WebRequest -Uri "https://github.com/Ercaner1988/claude-code-setup-rustifi
2. **Kurulum Komutunu Yapıştırın ve Çalıştırın:**

```bash
curl -LO https://github.com/Ercaner1988/claude-code-setup-rustified/releases/latest/download/claude-code-setup-macos-x86_64 && chmod +x claude-code-setup-macos-x86_64 && ./claude-code-setup-macos-x86_64 install --hooks
curl -fsSL https://raw.githubusercontent.com/Ercaner1988/claude-code-setup-rustified/main/install-macos.sh | bash
```

Kurucu Mac'inin Intel mi Apple Silicon mı olduğuna bakmaz, gerek de yok:
indirdiği ikili ikisinde de yerel olarak çalışır (universal), Rosetta gerekmez.

Güvenlik kancasını bir **git deposunun kök dizininde** kur — kanca depoya
özeldir, ev dizininde çalıştırınca atlanır:

```bash
cd ~/depom && claude-code-setup install-hooks
```

---
Expand Down
15 changes: 11 additions & 4 deletions install-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,27 @@ echo -e "${GREEN} Found: $LATEST_TAG${NC}"
# Pick the asset for THIS operating system. Bu betik hem macOS hem Linux icin
# kullaniliyor; macOS ikilisini sabitlemek Linux'ta "cannot execute binary
# file" hatasi veriyordu.
# macOS: once universal ikili (Intel + Apple Silicon, Rosetta gerektirmez),
# yoksa eski surumlerdeki x86_64 ikili. Geri dusus, universal'in henuz
# yayinlanmadigi surumlerde kurucunun kirilmamasi icin.
case "$(uname -s)" in
Darwin) ASSET_MARKER="macos-x86_64"; OS_LABEL="macOS" ;;
Linux) ASSET_MARKER="linux-x86_64"; OS_LABEL="Linux" ;;
Darwin) ASSET_MARKERS="macos-universal macos-x86_64"; OS_LABEL="macOS" ;;
Linux) ASSET_MARKERS="linux-x86_64"; OS_LABEL="Linux" ;;
*)
echo "Unsupported operating system: $(uname -s)"
echo "Supported: macOS, Linux. On Windows use install-windows.ps1."
exit 1
;;
esac

DOWNLOAD_URL=$(echo "$API_RESPONSE" | grep -o "\"browser_download_url\": *\"[^\"]*${ASSET_MARKER}[^\"]*" | head -1 | cut -d'"' -f4)
DOWNLOAD_URL=""
for ASSET_MARKER in $ASSET_MARKERS; do
DOWNLOAD_URL=$(echo "$API_RESPONSE" | grep -o "\"browser_download_url\": *\"[^\"]*${ASSET_MARKER}[^\"]*" | head -1 | cut -d'"' -f4)
[[ -n "$DOWNLOAD_URL" ]] && break
done

if [[ -z "$DOWNLOAD_URL" ]]; then
echo "${OS_LABEL} x64 binary not found in release"
echo "${OS_LABEL} binary not found in release (looked for: $ASSET_MARKERS)"
exit 1
fi

Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": "0.3",
"name": "claude-code-setup",
"display_name": "Claude Code Setup",
"version": "0.1.6",
"version": "0.1.7",
"description": "MCP management, semantic memory engine, and security auditing for Claude Code environment setup.",
"author": {
"name": "Ercan ER",
Expand Down
147 changes: 147 additions & 0 deletions smoke-test-extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Paketlenmis .mcpb eklentisini gercekten calistirip dogrular.

Kullanim: python smoke-test-extension.py <paket.mcpb>

Claude Desktop'in bir eklentiyi kurarken yaptigini taklit eder:

1. Paketi isletim sisteminin KENDI acicisiyla acar. macOS/Linux'ta bu
`unzip`tir; ikilinin calistirma izni ancak boyle sinanir (Python'un
zipfile modulu Unix izinlerini uygulamaz, sinanmasi gereken seyi atlar).
2. manifest.json'daki komutu ${__dirname} cozerek calistirir.
3. MCP el sikismasini yapar, araclari listeler, salt-okunur bir araci
gercekten cagirir.

macOS'ta SMOKE_ARCH=x86_64 (veya arm64) verilirse ikili `arch -<mimari>`
ile o dilimden calistirilir. Universal ikilinin iki diliminin de gercekten
calistigi ancak boyle sinanir; varsayilan kosu yalnizca yerel dilimi calistirir.

Sinirlari: Claude Desktop'in kendi aciciyi degil isletim sisteminin
`unzip`ini kullanir; macOS Gatekeeper davranisini sinamaz (CI koşucusunda
indirilen dosyaya karantina isareti konmaz).
"""
import json
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
import zipfile

READ_ONLY_TOOL = "status"


def fail(message: str) -> None:
print(f"BASARISIZ: {message}")
raise SystemExit(1)


def extract(package: pathlib.Path, dest: pathlib.Path) -> None:
if os.name == "nt":
zipfile.ZipFile(package).extractall(dest)
return
if shutil.which("unzip") is None:
fail("`unzip` bulunamadi; calistirma izni sinanamaz, zipfile'a dusmuyoruz")
subprocess.run(["unzip", "-q", str(package), "-d", str(dest)], check=True)


def rpc(request_id: int, method: str, params: dict | None = None) -> str:
message = {"jsonrpc": "2.0", "id": request_id, "method": method}
if params is not None:
message["params"] = params
return json.dumps(message)


def main() -> int:
if len(sys.argv) != 2:
print(__doc__)
return 2

package = pathlib.Path(sys.argv[1]).resolve()
if not package.is_file():
fail(f"paket bulunamadi: {package}")

with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
extract(package, root)

manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
config = manifest["server"]["mcp_config"]
command = pathlib.Path(config["command"].replace("${__dirname}", str(root)))
args = config.get("args", [])

if not command.is_file():
fail(f"manifest'in gosterdigi ikili pakette yok: {command.name}")
if os.name != "nt" and not os.access(command, os.X_OK):
fail(f"{command.name} calistirilabilir degil (mod {oct(command.stat().st_mode & 0o777)})")

requests = "\n".join([
rpc(1, "initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "smoke-test", "version": "1"},
}),
rpc(2, "tools/list"),
rpc(3, "tools/call", {"name": READ_ONLY_TOOL, "arguments": {}}),
]) + "\n"

launch = [str(command), *args]
arch = os.environ.get("SMOKE_ARCH", "").strip()
if arch:
if sys.platform != "darwin":
fail("SMOKE_ARCH yalnizca macOS'ta anlamli")
launch = ["arch", f"-{arch}", *launch]

try:
proc = subprocess.run(
launch, input=requests, capture_output=True,
text=True, encoding="utf-8", errors="replace", timeout=120,
)
except OSError as err:
# Burasi, ikili bu mimaride hic calistirilamiyorsa dusulen yer
# (ornegin Rosetta'siz bir ARM Mac'te x86_64 ikili).
fail(f"ikili baslatilamadi: {err}")

responses = {}
for line in proc.stdout.splitlines():
line = line.strip()
if line.startswith("{"):
message = json.loads(line)
responses[message.get("id")] = message

for request_id in (1, 2, 3):
if request_id not in responses:
fail(f"istek {request_id} yanitsiz kaldi. stderr:\n{proc.stderr[-2000:]}")
if "error" in responses[request_id]:
fail(f"istek {request_id} hata dondu: {responses[request_id]['error']}")

server = responses[1]["result"]["serverInfo"]
if server["name"] != manifest["name"]:
fail(f"sunucu adi {server['name']!r}, manifest {manifest['name']!r}")
if server["version"] != manifest["version"]:
fail(f"sunucu surumu {server['version']!r}, manifest {manifest['version']!r} "
"(Cargo.toml ile manifest.json kaymis)")

served = sorted(tool["name"] for tool in responses[2]["result"]["tools"])
declared = sorted(tool["name"] for tool in manifest.get("tools", []))
if served != declared:
fail(f"manifest'in ilan ettigi araclar sunulanlarla ayni degil\n"
f" ilan edilen: {declared}\n sunulan: {served}")

call = responses[3]["result"]
text = " ".join(part.get("text", "") for part in call.get("content", []))
if call.get("isError") or not text.strip():
fail(f"{READ_ONLY_TOOL} araci basarisiz: {text[:500]}")

print(f"GECTI: {package.name}")
print(f" platform : {manifest['compatibility']['platforms']}")
print(f" dilim : {arch or 'yerel'}")
print(f" sunucu : {server['name']} {server['version']}")
print(f" araclar : {len(served)} -> {', '.join(served)}")
print(f" {READ_ONLY_TOOL:9}: {text.strip().splitlines()[0]}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading