phase1: SPM project, Makefile bundle pipeline, app entry with Silicon gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 11 changed files with +367 and −1
added
Makefile
+85 −0
@@ -0,0 +1,85 @@ | ||
| 1 | +# | |
| 2 | +# Makefile | |
| 3 | +# Zyquo Local | |
| 4 | +# | |
| 5 | +# Author: Simon-Pierre Boucher | |
| 6 | +# Mail: contact@spboucher.ai | |
| 7 | +# | |
| 8 | + | |
| 9 | +# ── Build environment (see docs/BUILD.md) ──────────────────────────────────── | |
| 10 | +# 1. SDK pin: macOS 27 CLT SDKs ship SwiftUI @State/@Bindable as Xcode-only | |
| 11 | +# macro plugins; 26.5 is the last SDK where plain `swift build` works. | |
| 12 | +# 2. Metal: mlx-swift compiles .metal kernels at build time; the compiler is | |
| 13 | +# resolved via PATH from the standalone Metal toolchain. | |
| 14 | +export SDKROOT := /Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk | |
| 15 | +export PATH := $(HOME)/Developer/Metal.xctoolchain/usr/bin:$(PATH) | |
| 16 | + | |
| 17 | +APP_NAME := Zyquo Local | |
| 18 | +EXEC_NAME := ZyquoLocal | |
| 19 | +BUNDLE_ID := com.zyquo.local | |
| 20 | +VERSION := 0.1.0 | |
| 21 | +BUILD_DIR := .build | |
| 22 | +DIST_DIR := dist | |
| 23 | +APP_BUNDLE := $(DIST_DIR)/$(APP_NAME).app | |
| 24 | +RELEASE_BIN := $(BUILD_DIR)/release/$(EXEC_NAME) | |
| 25 | +DEBUG_BIN := $(BUILD_DIR)/debug/$(EXEC_NAME) | |
| 26 | +SIGN_IDENTITY := Developer ID Application: Simon-Pierre Boucher (3YM54G49SN) | |
| 27 | +NOTARY_PROFILE := MacLustr-Notarize | |
| 28 | + | |
| 29 | +.PHONY: build release-build test clean dev bundle-debug bundle-release icon run poc release headers-check | |
| 30 | + | |
| 31 | +build: | |
| 32 | + swift build | |
| 33 | + | |
| 34 | +release-build: | |
| 35 | + swift build -c release | |
| 36 | + | |
| 37 | +test: | |
| 38 | + swift test | |
| 39 | + | |
| 40 | +clean: | |
| 41 | + swift package clean | |
| 42 | + rm -rf $(DIST_DIR) | |
| 43 | + | |
| 44 | +# ── App bundle assembly ────────────────────────────────────────────────────── | |
| 45 | +# $(1) = built products dir (.build/debug or .build/release) | |
| 46 | +define assemble_bundle | |
| 47 | + rm -rf "$(APP_BUNDLE)" | |
| 48 | + mkdir -p "$(APP_BUNDLE)/Contents/MacOS" "$(APP_BUNDLE)/Contents/Resources" | |
| 49 | + cp "$(1)/$(EXEC_NAME)" "$(APP_BUNDLE)/Contents/MacOS/$(EXEC_NAME)" | |
| 50 | + cp Support/Info.plist "$(APP_BUNDLE)/Contents/Info.plist" | |
| 51 | + printf 'APPL????' > "$(APP_BUNDLE)/Contents/PkgInfo" | |
| 52 | + @# MLX metallib + any other SPM resource bundles must ship in Resources | |
| 53 | + for b in "$(1)"/*.bundle; do \ | |
| 54 | + [ -e "$$b" ] && cp -R "$$b" "$(APP_BUNDLE)/Contents/Resources/" || true; \ | |
| 55 | + done | |
| 56 | + [ -f assets/icon/AppIcon.icns ] && cp assets/icon/AppIcon.icns "$(APP_BUNDLE)/Contents/Resources/AppIcon.icns" || true | |
| 57 | +endef | |
| 58 | + | |
| 59 | +bundle-debug: build | |
| 60 | + $(call assemble_bundle,$(BUILD_DIR)/debug) | |
| 61 | + codesign --force --deep --sign - "$(APP_BUNDLE)" | |
| 62 | + | |
| 63 | +bundle-release: release-build | |
| 64 | + $(call assemble_bundle,$(BUILD_DIR)/release) | |
| 65 | + | |
| 66 | +# Debug bundle, ad-hoc signed, launched. | |
| 67 | +dev: bundle-debug | |
| 68 | + open "$(APP_BUNDLE)" | |
| 69 | + | |
| 70 | +run: dev | |
| 71 | + | |
| 72 | +# Inference proof-of-concept: make poc MODEL=<dir> PROMPT="..." | |
| 73 | +poc: build | |
| 74 | + "$(DEBUG_BIN)" --poc "$(MODEL)" "$(PROMPT)" | |
| 75 | + | |
| 76 | +# Icon pipeline (Phase 5): SVG → PNGs → icns via scripts/make-icon.sh | |
| 77 | +icon: | |
| 78 | + scripts/make-icon.sh | |
| 79 | + | |
| 80 | +# Signed, notarized, stapled release (Phase 8): scripts/release.sh | |
| 81 | +release: | |
| 82 | + scripts/release.sh | |
| 83 | + | |
| 84 | +headers-check: | |
| 85 | + scripts/check-headers.sh | |
added
Package.swift
+35 −0
@@ -0,0 +1,35 @@ | ||
| 1 | +// swift-tools-version: 6.1 | |
| 2 | +// | |
| 3 | +// Package.swift | |
| 4 | +// Zyquo Local | |
| 5 | +// | |
| 6 | +// Author: Simon-Pierre Boucher | |
| 7 | +// Mail: contact@spboucher.ai | |
| 8 | +// | |
| 9 | + | |
| 10 | +import PackageDescription | |
| 11 | + | |
| 12 | +let package = Package( | |
| 13 | + name: "ZyquoLocal", | |
| 14 | + platforms: [.macOS(.v14)], | |
| 15 | + dependencies: [ | |
| 16 | + .package(url: "https://github.com/ml-explore/mlx-swift-lm", .upToNextMajor(from: "3.31.4")), | |
| 17 | + .package(url: "https://github.com/huggingface/swift-huggingface", from: "0.9.0"), | |
| 18 | + .package(url: "https://github.com/huggingface/swift-transformers", from: "1.3.0"), | |
| 19 | + .package(url: "https://github.com/swiftlang/swift-markdown", from: "0.5.0"), | |
| 20 | + ], | |
| 21 | + targets: [ | |
| 22 | + .executableTarget( | |
| 23 | + name: "ZyquoLocal", | |
| 24 | + dependencies: [ | |
| 25 | + .product(name: "MLXLLM", package: "mlx-swift-lm"), | |
| 26 | + .product(name: "MLXLMCommon", package: "mlx-swift-lm"), | |
| 27 | + .product(name: "MLXHuggingFace", package: "mlx-swift-lm"), | |
| 28 | + .product(name: "HuggingFace", package: "swift-huggingface"), | |
| 29 | + .product(name: "Tokenizers", package: "swift-transformers"), | |
| 30 | + .product(name: "Markdown", package: "swift-markdown"), | |
| 31 | + ], | |
| 32 | + path: "Sources/ZyquoLocal" | |
| 33 | + ) | |
| 34 | + ] | |
| 35 | +) | |
added
Sources/ZyquoLocal/App/ContentView.swift
+22 −0
@@ -0,0 +1,22 @@ | ||
| 1 | +// | |
| 2 | +// ContentView.swift | |
| 3 | +// Zyquo Local | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Mail: contact@spboucher.ai | |
| 7 | +// | |
| 8 | + | |
| 9 | +import SwiftUI | |
| 10 | + | |
| 11 | +/// Placeholder root view — replaced by the full NavigationSplitView in Phase 6. | |
| 12 | +struct ContentView: View { | |
| 13 | + var body: some View { | |
| 14 | + VStack(spacing: 12) { | |
| 15 | + Text("Zyquo Local") | |
| 16 | + .font(.system(size: 20, weight: .semibold)) | |
| 17 | + Text("Phase 1 skeleton — inference engine and UI arrive in later phases.") | |
| 18 | + .foregroundStyle(.secondary) | |
| 19 | + } | |
| 20 | + .frame(maxWidth: .infinity, maxHeight: .infinity) | |
| 21 | + } | |
| 22 | +} | |
added
Sources/ZyquoLocal/App/HardwareGate.swift
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +// | |
| 2 | +// HardwareGate.swift | |
| 3 | +// Zyquo Local | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Mail: contact@spboucher.ai | |
| 7 | +// | |
| 8 | + | |
| 9 | +import Foundation | |
| 10 | + | |
| 11 | +/// MLX requires Apple Silicon. The binary is arm64-only, so this gate is a | |
| 12 | +/// defensive runtime check (e.g. against a future universal build). | |
| 13 | +enum HardwareGate { | |
| 14 | + static var isAppleSilicon: Bool { | |
| 15 | + #if arch(arm64) | |
| 16 | + return true | |
| 17 | + #else | |
| 18 | + return false | |
| 19 | + #endif | |
| 20 | + } | |
| 21 | + | |
| 22 | + /// Physical unified memory in bytes (`sysctl hw.memsize`). | |
| 23 | + static var physicalMemory: UInt64 { | |
| 24 | + var size: UInt64 = 0 | |
| 25 | + var len = MemoryLayout<UInt64>.size | |
| 26 | + sysctlbyname("hw.memsize", &size, &len, nil, 0) | |
| 27 | + return size | |
| 28 | + } | |
| 29 | +} | |
added
Sources/ZyquoLocal/App/Main.swift
+24 −0
@@ -0,0 +1,24 @@ | ||
| 1 | +// | |
| 2 | +// Main.swift | |
| 3 | +// Zyquo Local | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Mail: contact@spboucher.ai | |
| 7 | +// | |
| 8 | + | |
| 9 | +import AppKit | |
| 10 | +import SwiftUI | |
| 11 | + | |
| 12 | +/// Entry point. Dispatches to the CLI proof-of-concept / verification harness | |
| 13 | +/// when invoked with flags, otherwise boots the SwiftUI application. | |
| 14 | +@main | |
| 15 | +enum Main { | |
| 16 | + static func main() async { | |
| 17 | + let args = CommandLine.arguments | |
| 18 | + if args.contains("--poc") { | |
| 19 | + await PoCRunner.run(arguments: args) | |
| 20 | + return | |
| 21 | + } | |
| 22 | + ZyquoLocalApp.main() | |
| 23 | + } | |
| 24 | +} | |
added
Sources/ZyquoLocal/App/PoCRunner.swift
+18 −0
@@ -0,0 +1,18 @@ | ||
| 1 | +// | |
| 2 | +// PoCRunner.swift | |
| 3 | +// Zyquo Local | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Mail: contact@spboucher.ai | |
| 7 | +// | |
| 8 | + | |
| 9 | +import Foundation | |
| 10 | + | |
| 11 | +/// CLI proof-of-concept mode: `ZyquoLocal --poc <model-dir> "<prompt>"`. | |
| 12 | +/// Fully implemented in Phase 2 alongside the inference engine. | |
| 13 | +enum PoCRunner { | |
| 14 | + static func run(arguments: [String]) async { | |
| 15 | + FileHandle.standardError.write(Data("--poc arrives with the Phase 2 inference engine.\n".utf8)) | |
| 16 | + exit(64) | |
| 17 | + } | |
| 18 | +} | |
added
Sources/ZyquoLocal/App/UnsupportedHardwareView.swift
+28 −0
@@ -0,0 +1,28 @@ | ||
| 1 | +// | |
| 2 | +// UnsupportedHardwareView.swift | |
| 3 | +// Zyquo Local | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Mail: contact@spboucher.ai | |
| 7 | +// | |
| 8 | + | |
| 9 | +import SwiftUI | |
| 10 | + | |
| 11 | +/// Polite full-window notice shown on non-Apple-Silicon hardware. | |
| 12 | +struct UnsupportedHardwareView: View { | |
| 13 | + var body: some View { | |
| 14 | + VStack(spacing: 16) { | |
| 15 | + Image(systemName: "cpu") | |
| 16 | + .font(.system(size: 56, weight: .light)) | |
| 17 | + .foregroundStyle(.secondary) | |
| 18 | + Text("Apple Silicon Required") | |
| 19 | + .font(.title2.weight(.semibold)) | |
| 20 | + Text("Zyquo Local runs language models on-device with MLX, which requires an Apple Silicon Mac (M1 or later). This Mac's processor is not supported.") | |
| 21 | + .multilineTextAlignment(.center) | |
| 22 | + .foregroundStyle(.secondary) | |
| 23 | + .frame(maxWidth: 420) | |
| 24 | + } | |
| 25 | + .frame(minWidth: 640, minHeight: 420) | |
| 26 | + .padding(40) | |
| 27 | + } | |
| 28 | +} | |
added
Sources/ZyquoLocal/App/ZyquoLocalApp.swift
+33 −0
@@ -0,0 +1,33 @@ | ||
| 1 | +// | |
| 2 | +// ZyquoLocalApp.swift | |
| 3 | +// Zyquo Local | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Mail: contact@spboucher.ai | |
| 7 | +// | |
| 8 | + | |
| 9 | +import SwiftUI | |
| 10 | + | |
| 11 | +struct ZyquoLocalApp: App { | |
| 12 | + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate | |
| 13 | + | |
| 14 | + var body: some Scene { | |
| 15 | + WindowGroup { | |
| 16 | + if HardwareGate.isAppleSilicon { | |
| 17 | + ContentView() | |
| 18 | + .frame(minWidth: 980, minHeight: 640) | |
| 19 | + } else { | |
| 20 | + UnsupportedHardwareView() | |
| 21 | + } | |
| 22 | + } | |
| 23 | + .defaultSize(width: 1240, height: 800) | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | |
| 27 | +final class AppDelegate: NSObject, NSApplicationDelegate { | |
| 28 | + func applicationDidFinishLaunching(_ notification: Notification) { | |
| 29 | + // Proper foreground activation when launched from a terminal. | |
| 30 | + NSApp.setActivationPolicy(.regular) | |
| 31 | + NSApp.activate(ignoringOtherApps: true) | |
| 32 | + } | |
| 33 | +} | |
added
Support/Info.plist
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!-- | |
| 3 | + Info.plist | |
| 4 | + Zyquo Local | |
| 5 | + | |
| 6 | + Author: Simon-Pierre Boucher | |
| 7 | + Mail: contact@spboucher.ai | |
| 8 | +--> | |
| 9 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 10 | +<plist version="1.0"> | |
| 11 | +<dict> | |
| 12 | + <key>CFBundleDevelopmentRegion</key> | |
| 13 | + <string>en</string> | |
| 14 | + <key>CFBundleDisplayName</key> | |
| 15 | + <string>Zyquo Local</string> | |
| 16 | + <key>CFBundleName</key> | |
| 17 | + <string>Zyquo Local</string> | |
| 18 | + <key>CFBundleExecutable</key> | |
| 19 | + <string>ZyquoLocal</string> | |
| 20 | + <key>CFBundleIdentifier</key> | |
| 21 | + <string>com.zyquo.local</string> | |
| 22 | + <key>CFBundleInfoDictionaryVersion</key> | |
| 23 | + <string>6.0</string> | |
| 24 | + <key>CFBundlePackageType</key> | |
| 25 | + <string>APPL</string> | |
| 26 | + <key>CFBundleShortVersionString</key> | |
| 27 | + <string>0.1.0</string> | |
| 28 | + <key>CFBundleVersion</key> | |
| 29 | + <string>1</string> | |
| 30 | + <key>CFBundleIconFile</key> | |
| 31 | + <string>AppIcon</string> | |
| 32 | + <key>LSMinimumSystemVersion</key> | |
| 33 | + <string>14.0</string> | |
| 34 | + <key>LSApplicationCategoryType</key> | |
| 35 | + <string>public.app-category.productivity</string> | |
| 36 | + <key>LSArchitecturePriority</key> | |
| 37 | + <array> | |
| 38 | + <string>arm64</string> | |
| 39 | + </array> | |
| 40 | + <key>NSHighResolutionCapable</key> | |
| 41 | + <true/> | |
| 42 | + <key>NSHumanReadableCopyright</key> | |
| 43 | + <string>© 2026 Simon-Pierre Boucher</string> | |
| 44 | + <key>NSMainNibFile</key> | |
| 45 | + <string></string> | |
| 46 | + <key>NSPrincipalClass</key> | |
| 47 | + <string>NSApplication</string> | |
| 48 | +</dict> | |
| 49 | +</plist> | |
modified
docs/PLAN.md
+14 −1
@@ -34,7 +34,20 @@ needs `SDKROOT` pinned to MacOSX26.5.sdk (macro-plugin issue on 27.x SDKs). | ||
| 34 | 34 | Downloads: custom URLSession manager (Range-resume verified 206); tokenization |
| 35 | 35 | via swift-transformers. 30-model catalog live-verified with real sizes. |
| 36 | 36 | |
| 37 | −## Phase 1 — Project setup — pending | |
| 37 | +## Phase 1 — Project setup — ✅ DONE (2026-07-30) | |
| 38 | + | |
| 39 | +- [x] Package.swift (tools 6.1, mlx-swift-lm 3.31.4 + swift-huggingface + swift-transformers + swift-markdown) | |
| 40 | +- [x] Makefile (SDK pin + Metal PATH, bundle assembly incl. *.bundle resources, dev/release targets) | |
| 41 | +- [x] Support/Info.plist per spec (com.zyquo.local, LSMinimumSystemVersion 14.0, arm64) | |
| 42 | +- [x] @main entry with CLI dispatch (--poc) + SwiftUI app + Apple Silicon gate | |
| 43 | +- [x] scripts/check-headers.sh sweep (green) | |
| 44 | +- [x] `make build` green, zero warnings from our sources (68 s full graph) | |
| 45 | +- [x] `make bundle-debug` produces an ad-hoc-signed arm64 bundle; CLI dispatch smoke-tested | |
| 46 | + | |
| 47 | +**Checkpoint:** Full dependency graph (2 040 tasks incl. Cmlx Metal kernels) | |
| 48 | +compiles under the pinned recipe. Bundle carries mlx-swift_Cmlx.bundle (metallib), | |
| 49 | +Hub + Crypto resource bundles. `codesign -dv`: com.zyquo.local, arm64, adhoc. | |
| 50 | +Executable dispatches --poc and exits 64 as designed. | |
| 38 | 51 | ## Phase 2 — Architecture + inference PoC — pending |
| 39 | 52 | ## Phase 3 — Hub browse & download — pending |
| 40 | 53 | ## Phase 4 — Design system & UI spec — pending |
added
scripts/check-headers.sh
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# | |
| 3 | +# check-headers.sh | |
| 4 | +# Zyquo Local | |
| 5 | +# | |
| 6 | +# Author: Simon-Pierre Boucher | |
| 7 | +# Mail: contact@spboucher.ai | |
| 8 | +# | |
| 9 | +# Verifies that every code file carries the mandatory Author/Mail header. | |
| 10 | + | |
| 11 | +set -euo pipefail | |
| 12 | +cd "$(dirname "$0")/.." | |
| 13 | + | |
| 14 | +fail=0 | |
| 15 | +while IFS= read -r f; do | |
| 16 | + if ! head -8 "$f" | grep -q "Author: Simon-Pierre Boucher"; then | |
| 17 | + echo "MISSING HEADER: $f" | |
| 18 | + fail=1 | |
| 19 | + fi | |
| 20 | + if ! head -8 "$f" | grep -q "Mail: contact@spboucher.ai"; then | |
| 21 | + echo "MISSING MAIL: $f" | |
| 22 | + fail=1 | |
| 23 | + fi | |
| 24 | +done < <(find Sources Support scripts assets -type f \( -name '*.swift' -o -name '*.sh' -o -name '*.plist' \) 2>/dev/null; ls Package.swift Makefile) | |
| 25 | + | |
| 26 | +if [ "$fail" -eq 0 ]; then | |
| 27 | + echo "All code files carry the mandatory header." | |
| 28 | +else | |
| 29 | + exit 1 | |
| 30 | +fi | |
| 31 | ||