Files

707 lines
19 KiB
Bash
Executable File

#!/bin/bash
# TUIkit CLI
# Modern command-line tool for creating TUIkit projects
set -e
VERSION="1.0.0"
TUIKIT_INSTALL_PATH="/usr/local/bin/tuikit"
# Colors
BLUE='\033[0;34m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Uninstall tuikit
uninstall_tuikit() {
echo -e "${YELLOW}Removing tuikit...${NC}"
if [ -f "$TUIKIT_INSTALL_PATH" ]; then
sudo rm -f "$TUIKIT_INSTALL_PATH" 2>/dev/null || rm -f "$TUIKIT_INSTALL_PATH"
fi
echo -e "${GREEN}tuikit has been removed.${NC}"
}
# Check if running on Linux and Swift is available
check_linux_swift() {
# Only check on Linux
if [ "$(uname)" != "Linux" ]; then
return 0
fi
# Check if Swift is installed
if command -v swift &> /dev/null; then
return 0
fi
echo -e "${YELLOW}"
echo "╔════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ Swift is not installed on this system ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo ""
echo -e "TUIkit requires Swift 6.0 or later to create projects."
echo ""
echo -e "${YELLOW}Would you like to install Swift and its dependencies?${NC}"
echo -e "This will install: Swift, clang, libcurl, and other required packages."
echo ""
read -p "Install Swift? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo ""
echo -e "${RED}Swift installation declined.${NC}"
uninstall_tuikit
echo -e "${YELLOW}Please install Swift manually from https://swift.org/download/${NC}"
exit 1
fi
install_swift_linux
}
# Install Swift on Linux
install_swift_linux() {
echo ""
echo -e "${GREEN}Installing Swift and dependencies...${NC}"
echo ""
# Detect package manager and distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
DISTRO=$ID
else
DISTRO="unknown"
fi
case "$DISTRO" in
ubuntu|debian|linuxmint|pop)
install_swift_debian
;;
fedora|rhel|centos|rocky|almalinux)
install_swift_fedora
;;
arch|manjaro)
install_swift_arch
;;
*)
install_swift_generic
;;
esac
# Verify installation
if command -v swift &> /dev/null; then
SWIFT_VERSION=$(swift --version 2>&1 | head -1)
echo ""
echo -e "${GREEN}Swift installed successfully!${NC}"
echo -e "${BLUE}Version: ${NC}$SWIFT_VERSION"
echo ""
else
echo ""
echo -e "${RED}Swift installation failed.${NC}"
echo -e "${YELLOW}Please install Swift manually from https://swift.org/download/${NC}"
uninstall_tuikit
exit 1
fi
}
# Install Swift on Debian/Ubuntu
install_swift_debian() {
echo -e "${BLUE}Detected Debian/Ubuntu-based system${NC}"
echo ""
# Install dependencies
sudo apt-get update
sudo apt-get install -y \
binutils \
git \
gnupg2 \
libc6-dev \
libcurl4-openssl-dev \
libedit2 \
libgcc-11-dev \
libpython3-dev \
libsqlite3-0 \
libstdc++-11-dev \
libxml2-dev \
libz3-dev \
pkg-config \
tzdata \
unzip \
zlib1g-dev \
curl
# Try to install Swift via swiftly (recommended method)
if install_swift_via_swiftly; then
return 0
fi
# Fallback: Try apt repository
echo -e "${YELLOW}Trying Swift apt repository...${NC}"
if curl -s https://archive.swiftlang.xyz/install.sh | sudo bash; then
sudo apt-get update
sudo apt-get install -y swiftlang
else
echo -e "${YELLOW}apt repository not available, downloading from swift.org...${NC}"
install_swift_tarball
fi
}
# Install Swift on Fedora/RHEL
install_swift_fedora() {
echo -e "${BLUE}Detected Fedora/RHEL-based system${NC}"
echo ""
# Install dependencies
sudo dnf install -y \
binutils \
gcc \
git \
glibc-static \
gzip \
libbsd \
libcurl-devel \
libedit \
libicu \
libstdc++-static \
libuuid \
libxml2-devel \
sqlite \
tar \
tzdata \
curl
# Try swiftly first
if install_swift_via_swiftly; then
return 0
fi
# Fallback to tarball
install_swift_tarball
}
# Install Swift on Arch Linux
install_swift_arch() {
echo -e "${BLUE}Detected Arch-based system${NC}"
echo ""
# Swift is available in AUR
if command -v yay &> /dev/null; then
yay -S --noconfirm swift-bin
elif command -v paru &> /dev/null; then
paru -S --noconfirm swift-bin
else
echo -e "${YELLOW}No AUR helper found. Installing yay...${NC}"
sudo pacman -S --needed --noconfirm git base-devel
git clone https://aur.archlinux.org/yay.git /tmp/yay
cd /tmp/yay && makepkg -si --noconfirm
cd - > /dev/null
yay -S --noconfirm swift-bin
fi
}
# Install Swift via swiftly (cross-platform installer)
install_swift_via_swiftly() {
echo -e "${BLUE}Installing Swift via swiftly...${NC}"
if curl -L https://swiftlang.github.io/swiftly/swiftly-install.sh | bash -s -- -y; then
# Source the environment
if [ -f "$HOME/.local/share/swiftly/env.sh" ]; then
source "$HOME/.local/share/swiftly/env.sh"
fi
# Install latest Swift
"$HOME/.local/bin/swiftly" install latest
return 0
fi
return 1
}
# Install Swift from official tarball (generic fallback)
install_swift_tarball() {
echo -e "${BLUE}Downloading Swift from swift.org...${NC}"
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
SWIFT_ARCH="x86_64"
;;
aarch64|arm64)
SWIFT_ARCH="aarch64"
;;
*)
echo -e "${RED}Unsupported architecture: $ARCH${NC}"
return 1
;;
esac
# Get Ubuntu version for tarball selection
if [ -f /etc/os-release ]; then
. /etc/os-release
UBUNTU_VERSION="${VERSION_ID}"
else
UBUNTU_VERSION="22.04"
fi
# Download and install (using Swift 6.0.2 as stable version)
SWIFT_VERSION="6.0.2"
SWIFT_RELEASE="swift-${SWIFT_VERSION}-RELEASE"
SWIFT_PLATFORM="ubuntu${UBUNTU_VERSION}"
DOWNLOAD_URL="https://download.swift.org/swift-${SWIFT_VERSION}-release/${SWIFT_PLATFORM//.}/${SWIFT_RELEASE}/${SWIFT_RELEASE}-${SWIFT_PLATFORM}-${SWIFT_ARCH}.tar.gz"
echo -e "${BLUE}Downloading: ${NC}$DOWNLOAD_URL"
TEMP_DIR=$(mktemp -d)
curl -L "$DOWNLOAD_URL" -o "$TEMP_DIR/swift.tar.gz"
echo -e "${BLUE}Extracting...${NC}"
sudo mkdir -p /opt/swift
sudo tar -xzf "$TEMP_DIR/swift.tar.gz" -C /opt/swift --strip-components=1
# Add to PATH
echo 'export PATH="/opt/swift/usr/bin:$PATH"' | sudo tee /etc/profile.d/swift.sh
export PATH="/opt/swift/usr/bin:$PATH"
rm -rf "$TEMP_DIR"
}
# Generic installation (fallback)
install_swift_generic() {
echo -e "${YELLOW}Unknown Linux distribution.${NC}"
echo -e "${BLUE}Attempting generic installation...${NC}"
# Try swiftly first
if install_swift_via_swiftly; then
return 0
fi
# Fallback to tarball
install_swift_tarball
}
# Help text
show_help() {
echo -e "${GREEN}TUIkit CLI v${VERSION}${NC}"
echo ""
echo "Usage:"
echo " tuikit init [options] [project-name]"
echo ""
echo "Options:"
echo " git Initialize Git repository with initial commit"
echo " sqlite Include GRDB for SQLite database storage"
echo " testing Include Swift Testing framework"
echo " xctest Include XCTest framework"
echo ""
echo "Examples:"
echo " tuikit init MyApp # Basic TUIkit app"
echo " tuikit init git MyApp # With Git repository"
echo " tuikit init sqlite MyApp # With SQLite"
echo " tuikit init testing MyApp # With Swift Testing"
echo " tuikit init git sqlite testing MyApp # With Git, SQLite and Testing"
echo ""
echo "If no project name is provided, you'll be prompted to enter one."
}
# Check Linux Swift installation first
check_linux_swift
# Parse arguments
INIT_GIT=false
INCLUDE_SQLITE=false
TEST_FRAMEWORK="None"
PROJECT_NAME=""
if [ "$1" == "init" ]; then
shift
# Parse options and project name
while [ $# -gt 0 ]; do
case "$1" in
git)
INIT_GIT=true
shift
;;
sqlite)
INCLUDE_SQLITE=true
shift
;;
testing)
TEST_FRAMEWORK="Swift Testing"
shift
;;
xctest)
TEST_FRAMEWORK="XCTest"
shift
;;
-h|--help)
show_help
exit 0
;;
*)
# Assume it's the project name
PROJECT_NAME="$1"
shift
;;
esac
done
else
show_help
exit 1
fi
# Prompt for project name if not provided
if [ -z "$PROJECT_NAME" ]; then
echo -e "${YELLOW}Enter project name:${NC}"
read -r PROJECT_NAME
fi
if [ -z "$PROJECT_NAME" ]; then
echo -e "${RED}Error: Project name cannot be empty${NC}"
exit 1
fi
# Banner with grayscale block style (like opencode)
show_banner() {
# Grayscale ANSI colors (gradient from dark to light)
local C1='\033[38;5;240m' # Darkest (T)
local C2='\033[38;5;244m' # (U)
local C3='\033[38;5;248m' # (I)
local C4='\033[38;5;252m' # (k)
local C5='\033[38;5;254m' # (i)
local C6='\033[38;5;255m' # Brightest (t)
local D='\033[38;5;236m' # Dark inner
local R='\033[0m' # Reset
echo ""
# TUIkit in block letters with inner shadow effect
echo -e "${C1}${D}${C1}█████${D}${R} ${C2}${D}${R} ${C2}${D}${R} ${C3}${D}${C3}${R} ${C4}${D}${R} ${C4}${D}${R} ${C5}${D}${R} ${C6}${D}${C6}███${R}"
echo -e " ${C1}${D}${R} ${C2}${D}${R} ${C2}${D}${R} ${C3}${D}${R} ${C4}${D}${R} ${C4}${D}${R} ${C5}${D}${R} ${C6}${D}${R}"
echo -e " ${C1}${D}${R} ${C2}${D}${R} ${C2}${D}${R} ${C3}${D}${R} ${C4}${D}${C4}${D}${R} ${C5}${D}${R} ${C6}${D}${R}"
echo -e " ${C1}${D}${R} ${C2}${D}${R} ${C2}${D}${R} ${C3}${D}${R} ${C4}${D}${R} ${C4}${D}${R} ${C5}${D}${R} ${C6}${D}${R}"
echo -e " ${C1}${D}${R} ${C2}${D}${C2}██${D}${C2}${R} ${C3}${D}${C3}${R} ${C4}${D}${R} ${C4}${D}${R} ${C5}${D}${R} ${C6}${D}${C6}${R}"
echo ""
}
show_banner
# Separate path and project name
# If PROJECT_NAME contains a path, extract the directory and basename
if [[ "$PROJECT_NAME" == *"/"* ]]; then
# Expand ~ to home directory
PROJECT_PATH="${PROJECT_NAME/#\~/$HOME}"
PROJECT_BASE=$(basename "$PROJECT_PATH")
else
PROJECT_PATH="$(pwd)/$PROJECT_NAME"
PROJECT_BASE="$PROJECT_NAME"
fi
# Create project directory
echo -e "${GREEN}Creating project directory...${NC}"
mkdir -p "$PROJECT_PATH"
cd "$PROJECT_PATH"
# Use PROJECT_BASE for all file content (package name, etc.)
PROJECT_NAME="$PROJECT_BASE"
# Create Package.swift
echo -e "${GREEN}Creating Package.swift...${NC}"
# Build dependencies array
DEPENDENCIES=" .package(url: \"https://github.com/phranck/TUIkit.git\", branch: \"main\")"
if [ "$INCLUDE_SQLITE" = true ]; then
DEPENDENCIES="${DEPENDENCIES},
.package(url: \"https://github.com/groue/GRDB.swift.git\", from: \"7.0.0\")"
fi
# Build target dependencies
TARGET_DEPS=" .product(name: \"TUIkit\", package: \"TUIkit\")"
if [ "$INCLUDE_SQLITE" = true ]; then
TARGET_DEPS="${TARGET_DEPS},
.product(name: \"GRDB\", package: \"GRDB.swift\")"
fi
# Build targets array
TARGETS=" .executableTarget(
name: \"${PROJECT_NAME}\",
dependencies: [
${TARGET_DEPS}
],
path: \"Sources\"
)"
if [ "$TEST_FRAMEWORK" != "None" ]; then
TARGETS="${TARGETS},
.testTarget(
name: \"${PROJECT_NAME}Tests\",
dependencies: [\"${PROJECT_NAME}\"],
path: \"Tests\"
)"
fi
cat > Package.swift << EOF
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "${PROJECT_NAME}",
platforms: [.macOS(.v15)],
dependencies: [
${DEPENDENCIES}
],
targets: [
${TARGETS}
]
)
EOF
# Create Sources directory and files
echo -e "${GREEN}Creating source files...${NC}"
mkdir -p Sources
cat > Sources/App.swift << EOF
import TUIkit
@main
struct ${PROJECT_NAME}App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
EOF
cat > Sources/ContentView.swift << 'EOF'
import TUIkit
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, TUIkit!")
.foregroundStyle(.palette.accent)
.bold()
Text("Welcome to your new terminal app")
.foregroundStyle(.palette.foregroundSecondary)
}
.padding()
}
}
EOF
# Create Database.swift if SQLite is enabled
if [ "$INCLUDE_SQLITE" = true ]; then
cat > Sources/Database.swift << 'EOF'
import Foundation
import GRDB
/// Example model using GRDB
struct Item: Codable, FetchableRecord, PersistableRecord, Sendable {
var id: Int64?
var title: String
var isCompleted: Bool
// Define the database table
static let databaseTableName = "items"
}
// MARK: - Database Setup
extension Item {
/// Creates the items table
static func createTable(in db: Database) throws {
try db.create(table: databaseTableName, ifNotExists: true) { table in
table.autoIncrementedPrimaryKey("id")
table.column("title", .text).notNull()
table.column("isCompleted", .boolean).notNull().defaults(to: false)
}
}
}
EOF
fi
# Create Tests if needed
if [ "$TEST_FRAMEWORK" != "None" ]; then
echo -e "${BLUE}Creating test files...${NC}"
mkdir -p Tests
if [ "$TEST_FRAMEWORK" = "Swift Testing" ]; then
cat > Tests/${PROJECT_NAME}Tests.swift << EOF
import Testing
@testable import $PROJECT_NAME
@Test func example() async throws {
#expect(true)
}
EOF
else
cat > Tests/${PROJECT_NAME}Tests.swift << EOF
import XCTest
@testable import $PROJECT_NAME
final class ${PROJECT_NAME}Tests: XCTestCase {
func testExample() throws {
XCTAssertTrue(true)
}
}
EOF
fi
fi
# Create README.md
echo -e "${GREEN}Creating README.md...${NC}"
cat > README.md << EOF
# $PROJECT_NAME
A TUIkit terminal application.
## Quick Start
\`\`\`bash
swift run
\`\`\`
## Project Structure
\`\`\`
$PROJECT_NAME/
├── Sources/
│ ├── App.swift # Main application entry point
│ └── ContentView.swift # Root view
EOF
if [ "$INCLUDE_SQLITE" = true ]; then
cat >> README.md << 'EOF'
│ └── Database.swift # SQLite database helper
EOF
fi
if [ "$TEST_FRAMEWORK" != "None" ]; then
cat >> README.md << EOF
└── Tests/ # $TEST_FRAMEWORK suite
EOF
fi
cat >> README.md << 'EOF'
```
EOF
if [ "$INCLUDE_SQLITE" = true ]; then
cat >> README.md << 'EOF'
## Database
This project includes [GRDB](https://github.com/groue/GRDB.swift) for local SQLite persistence.
**Quick Start:**
```swift
import GRDB
// Open database
let dbQueue = try DatabaseQueue(path: "db.sqlite")
// Create table
try dbQueue.write { db in
try Item.createTable(in: db)
}
// Insert
try dbQueue.write { db in
var item = Item(id: nil, title: "Task", isCompleted: false)
try item.insert(db)
}
// Fetch all
let items = try dbQueue.read { db in
try Item.fetchAll(db)
}
```
See [GRDB documentation](https://github.com/groue/GRDB.swift) for more details.
EOF
fi
cat >> README.md << 'EOF'
## Learn More
- [TUIkit Documentation](https://tuikit.layered.work/documentation/tuikit/)
- [TUIkit GitHub](https://github.com/phranck/TUIkit)
EOF
# Create .gitignore
cat > .gitignore << 'EOF'
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
EOF
# Note: We don't create .swiftpm directory - Xcode generates it automatically
# and handles scheme/workspace settings better on its own
# Set Xcode preference for Swift packages to prefer macOS
defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM -bool YES 2>/dev/null || true
defaults write com.apple.dt.Xcode IDEPreferredPlatformForSwiftPackages -string "macosx" 2>/dev/null || true
# Initialize Git repository if requested
if [ "$INIT_GIT" = true ]; then
echo -e "${GREEN}Initializing Git repository...${NC}"
git init -q
git add .
git commit -q -m "Initial commit"
fi
# Success message
echo -e "${GREEN}"
echo "╔════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ Project created successfully! ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo -e "${BLUE}Project: ${NC}${GREEN}$PROJECT_NAME${NC}"
echo -e "${BLUE}Location: ${NC}$(pwd)"
echo -e "${BLUE}Git Repository: ${NC}$([ "$INIT_GIT" = true ] && echo "Yes" || echo "No")"
echo -e "${BLUE}Test Framework: ${NC}$TEST_FRAMEWORK"
echo -e "${BLUE}GRDB: ${NC}$([ "$INCLUDE_SQLITE" = true ] && echo "Yes" || echo "No")"
echo ""
read -p "Open project in Xcode? [Y/n] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
open "$PROJECT_PATH/Package.swift"
echo -e "${GREEN}Opening in Xcode...${NC}"
# Show macOS alert about deployment target (only on macOS)
if [ "$(uname)" = "Darwin" ]; then
echo ""
echo -e "${YELLOW}╔════════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ IMPORTANT: Set Run Destination to \"My Mac\" ║${NC}"
echo -e "${YELLOW}║ in Xcode's toolbar before building! ║${NC}"
echo -e "${YELLOW}╚════════════════════════════════════════════════════╝${NC}"
fi
else
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo " 1. cd $PROJECT_PATH"
echo " 2. open Package.swift"
echo " 3. Set Run Destination to \"My Mac\" in Xcode"
echo " 4. Wait for dependencies to resolve"
echo " 5. Press Cmd+B to build"
fi