diff --git a/.github/workflows/docc.yml b/.github/workflows/docc.yml
new file mode 100644
index 0000000..a7946fb
--- /dev/null
+++ b/.github/workflows/docc.yml
@@ -0,0 +1,56 @@
+name: DocC
+
+on:
+ push:
+ branches: ["main"]
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: macos-14
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Select Xcode 15.2
+ uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: '15.2'
+
+ - name: Build DocC
+ run: |
+ swift package --allow-writing-to-directory ./docs \
+ generate-documentation --target LucaCore \
+ --disable-indexing \
+ --output-path ./docs \
+ --transform-for-static-hosting \
+ --hosting-base-path Luca
+
+ # Add redirect from root to documentation
+ echo '' > docs/index.html
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: 'docs'
+
+ - id: deployment
+ name: Deploy to GitHub Pages
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 69e10fa..452e7e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ xcuserdata/
# Artifacts
.build/
.luca
+docs/
diff --git a/Package.resolved b/Package.resolved
index 3f8b191..3aba835 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -54,6 +54,24 @@
"version" : "4.0.0"
}
},
+ {
+ "identity" : "swift-docc-plugin",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-docc-plugin",
+ "state" : {
+ "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06",
+ "version" : "1.4.5"
+ }
+ },
+ {
+ "identity" : "swift-docc-symbolkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-docc-symbolkit",
+ "state" : {
+ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
+ "version" : "1.0.0"
+ }
+ },
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
diff --git a/Package.swift b/Package.swift
index 0b4c714..db38b79 100644
--- a/Package.swift
+++ b/Package.swift
@@ -14,7 +14,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-argument-parser", exact: "1.6.1"),
.package(url: "https://github.com/apple/swift-crypto.git", exact: "4.0.0"),
.package(url: "https://github.com/tuist/Noora", exact: "0.49.1"),
- .package(url: "https://github.com/jpsim/Yams.git", exact: "6.1.0")
+ .package(url: "https://github.com/jpsim/Yams.git", exact: "6.1.0"),
+ .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0")
],
targets: [
.executableTarget(
diff --git a/Sources/LucaCore/Core/Downloader/Downloader.swift b/Sources/LucaCore/Core/Downloader/Downloader.swift
index b178649..9090238 100644
--- a/Sources/LucaCore/Core/Downloader/Downloader.swift
+++ b/Sources/LucaCore/Core/Downloader/Downloader.swift
@@ -2,6 +2,21 @@
import Foundation
+/// Downloads release archives and executables from remote URLs.
+///
+/// The `Downloader` handles fetching tool releases from remote servers,
+/// supporting both archive formats (ZIP, tar.gz) and standalone executables.
+///
+/// ## Supported File Types
+///
+/// - `.zip` - ZIP archives
+/// - `.tar.gz` - Gzipped tar archives
+/// - No extension - Treated as executable files
+///
+/// ## Topics
+///
+/// ### Downloading Files
+/// - ``downloadRelease(at:)``
struct Downloader: Downloading {
enum DownloaderError: Error, LocalizedError, Equatable {
@@ -26,6 +41,12 @@ struct Downloader: Downloading {
self.fileDownloader = fileDownloader
}
+ /// Downloads a release from the specified URL.
+ ///
+ /// - Parameter url: The URL to download from.
+ /// - Returns: A URL to the downloaded file in a temporary location.
+ /// - Throws: ``DownloaderError/unsupportedFileType(_:)`` if the URL has
+ /// an unsupported file extension.
func downloadRelease(at url: URL) async throws -> URL {
if SupportedFileTypes.allCases.contains(where: {
url.lastPathComponent.hasSuffix($0.rawValue)
diff --git a/Sources/LucaCore/Core/Installer/Installer.swift b/Sources/LucaCore/Core/Installer/Installer.swift
index 8046001..b1759cf 100644
--- a/Sources/LucaCore/Core/Installer/Installer.swift
+++ b/Sources/LucaCore/Core/Installer/Installer.swift
@@ -3,6 +3,34 @@
import Foundation
import Noora
+/// Downloads, verifies, and installs tools from remote URLs.
+///
+/// The `Installer` orchestrates the complete installation process for development tools:
+/// 1. Downloads the release archive or executable from the specified URL
+/// 2. Validates the checksum (if provided)
+/// 3. Extracts archives or handles executables
+/// 4. Validates architecture compatibility
+/// 5. Sets executable permissions
+/// 6. Creates symlinks in the project's `.luca/active/` directory
+///
+/// ## Usage
+///
+/// ```swift
+/// let installer = Installer(
+/// fileManager: FileManager.default,
+/// ignoreArchitectureCheck: false,
+/// printer: Printer()
+/// )
+/// try await installer.install(installationType: .spec(path: lucafilePath))
+/// ```
+///
+/// ## Topics
+///
+/// ### Installing Tools
+/// - ``install(installationType:)``
+///
+/// ### Installation Types
+/// - ``InstallationType``
public struct Installer {
enum InstallerError: Error, LocalizedError {
@@ -53,6 +81,12 @@ public struct Installer {
self.noora = noora
}
+ /// Installs tools based on the specified installation type.
+ ///
+ /// - Parameter installationType: Specifies how to determine which tools to install.
+ /// Can be either `.spec` to read from a Lucafile, or `.github` to install directly
+ /// from a GitHub release.
+ /// - Throws: An error if downloading, extracting, or linking fails.
public func install(installationType: InstallationType) async throws {
if quiet {
try await installQuietly(installationType: installationType)
diff --git a/Sources/LucaCore/Core/SpecLoader/SpecLoader.swift b/Sources/LucaCore/Core/SpecLoader/SpecLoader.swift
index 5fa043b..dc665e4 100644
--- a/Sources/LucaCore/Core/SpecLoader/SpecLoader.swift
+++ b/Sources/LucaCore/Core/SpecLoader/SpecLoader.swift
@@ -3,6 +3,32 @@
import Foundation
import Yams
+/// Loads and parses Lucafile specifications.
+///
+/// The `SpecLoader` reads YAML-formatted Lucafiles and converts them into
+/// ``Spec`` objects that define the tools required for a project.
+///
+/// ## Lucafile Format
+///
+/// A Lucafile is a YAML file with the following structure:
+///
+/// ```yaml
+/// ---
+/// tools:
+/// - name: SwiftLint
+/// version: 0.61.0
+/// url: https://github.com/realm/SwiftLint/releases/...
+/// binaryPath: bin/swiftlint
+/// ```
+///
+/// ## Topics
+///
+/// ### Loading Specs
+/// - ``loadSpec(at:)``
+///
+/// ### Related Types
+/// - ``Spec``
+/// - ``Tool``
struct SpecLoader: SpecLoading {
enum SpecLoaderError: Error, LocalizedError {
@@ -25,6 +51,12 @@ struct SpecLoader: SpecLoading {
self.fileManager = fileManager
}
+ /// Loads a spec from the specified file path.
+ ///
+ /// - Parameter path: The URL to the Lucafile.
+ /// - Returns: A ``Spec`` containing the parsed tool definitions.
+ /// - Throws: ``SpecLoaderError/missingSpec(_:)`` if the file doesn't exist,
+ /// or ``SpecLoaderError/invalidSpec(_:_:)`` if the YAML is malformed.
func loadSpec(at path: URL) throws -> Spec {
guard let data = fileManager.contents(atPath: path.path) else {
throw SpecLoaderError.missingSpec(path.path)
diff --git a/Sources/LucaCore/Core/SymLinker/SymLinker.swift b/Sources/LucaCore/Core/SymLinker/SymLinker.swift
index 90c0487..0252b8c 100644
--- a/Sources/LucaCore/Core/SymLinker/SymLinker.swift
+++ b/Sources/LucaCore/Core/SymLinker/SymLinker.swift
@@ -2,6 +2,19 @@
import Foundation
+/// Creates and manages symbolic links for installed tools.
+///
+/// The `SymLinker` is responsible for creating symbolic links in the project's
+/// `.luca/active/` directory that point to the actual tool binaries stored
+/// in `~/.luca/tools/`.
+///
+/// This allows projects to reference tools via a consistent path while
+/// supporting multiple versions installed globally.
+///
+/// ## Topics
+///
+/// ### Creating Symlinks
+/// - ``setSymLink(for:)``
struct SymLinker: SymLinking {
enum SymLinkerError: Error, LocalizedError, Equatable {
@@ -23,6 +36,12 @@ struct SymLinker: SymLinking {
// MARK: - Internal
+ /// Creates a symbolic link for the specified tool.
+ ///
+ /// - Parameter tool: The tool to create a symlink for.
+ /// - Returns: The URL of the created symlink.
+ /// - Throws: ``SymLinkerError/missingBinaryFile(binaryName:expectedLocation:)``
+ /// if the binary file doesn't exist at the expected location.
@discardableResult
func setSymLink(for tool: Tool) throws -> URL {
let symLinkFile = fileManager.activeFolder
diff --git a/Sources/LucaCore/Core/Unarchiver/Unarchiver.swift b/Sources/LucaCore/Core/Unarchiver/Unarchiver.swift
index 3ca4d09..72fe6be 100644
--- a/Sources/LucaCore/Core/Unarchiver/Unarchiver.swift
+++ b/Sources/LucaCore/Core/Unarchiver/Unarchiver.swift
@@ -2,6 +2,20 @@
import Foundation
+/// Extracts tool archives to their installation destination.
+///
+/// The `Unarchiver` handles extracting ZIP and tar.gz archives using
+/// system utilities (`unzip` and `tar`).
+///
+/// ## Supported Formats
+///
+/// - **ZIP** - Extracted using `/usr/bin/unzip`
+/// - **tar.gz** - Extracted using `/usr/bin/tar`
+///
+/// ## Topics
+///
+/// ### Extracting Archives
+/// - ``unarchive(filePath:installationDestination:)``
struct Unarchiver: Unarchiving {
enum UnarchiverError: Error, LocalizedError {
@@ -29,6 +43,15 @@ struct Unarchiver: Unarchiving {
self.fileTypeDetector = fileTypeDetector
}
+ /// Extracts an archive to the specified destination.
+ ///
+ /// - Parameters:
+ /// - filePath: The path to the archive file.
+ /// - installationDestination: The directory to extract files into.
+ /// - Throws: ``UnarchiverError/unrecognisedFileType(_:)`` if the file type
+ /// cannot be determined, ``UnarchiverError/notAnArchive(_:)`` if the file
+ /// is an executable, or ``UnarchiverError/failedToUnarchive(_:)`` if
+ /// extraction fails.
func unarchive(filePath: URL, installationDestination: URL) throws {
try fileManager.createDirectory(at: installationDestination, withIntermediateDirectories: true)
diff --git a/Sources/LucaCore/LucaCore.docc/Contributing.md b/Sources/LucaCore/LucaCore.docc/Contributing.md
new file mode 100644
index 0000000..2d6e1f1
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Contributing.md
@@ -0,0 +1,149 @@
+# Contributing to Luca
+
+Learn how to contribute to the Luca project.
+
+## Overview
+
+Thank you for considering contributing to Luca! This guide will help you get started with the development process.
+
+## Ways to Contribute
+
+- Reporting bugs
+- Suggesting features
+- Writing or improving documentation
+- Fixing bugs
+- Implementing features
+- Reviewing code
+- Answering questions
+
+## Prerequisites
+
+Before you begin contributing, ensure you have:
+
+- Git installed
+- Swift 5.7 or later
+- Xcode (for developing on macOS)
+- A GitHub account
+
+## Development Environment Setup
+
+### 1. Fork and Clone
+
+```bash
+# Fork the repository on GitHub, then clone your fork
+git clone https://github.com/YOUR-USERNAME/Luca.git
+cd Luca
+```
+
+### 2. Add Upstream Remote
+
+```bash
+git remote add upstream https://github.com/LucaTools/Luca.git
+```
+
+### 3. Create a Feature Branch
+
+```bash
+git checkout -b feature/your-feature-name
+```
+
+## Building and Testing
+
+### Build the Project
+
+```bash
+swift build
+```
+
+### Run Tests
+
+```bash
+swift test
+```
+
+### Build Documentation Locally
+
+```bash
+swift package --disable-sandbox preview-documentation --target LucaCore
+```
+
+This starts a local server at `http://localhost:8000` where you can preview the documentation.
+
+## Code Style Guidelines
+
+- Follow Swift's official style guide
+- Write clear, readable code with descriptive names
+- Add comments where necessary
+- Write unit tests for new features
+
+## Commit Message Guidelines
+
+- Use the present tense ("Add feature" not "Added feature")
+- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
+- Limit the first line to 72 characters or less
+- Reference issues and pull requests after the first line
+
+### Example Commit Message
+
+```
+Add support for downloading from private repositories
+
+This adds authentication options when specifying ZIP URLs.
+Fixes #123
+```
+
+## Submitting Changes
+
+### 1. Push Your Changes
+
+```bash
+git push origin feature/your-feature-name
+```
+
+### 2. Create a Pull Request
+
+1. Go to your fork on GitHub
+2. Select your branch
+3. Click 'Pull Request'
+4. Fill out the pull request template
+
+### 3. Code Review
+
+Request a code review from one of the maintainers.
+
+## Pull Request Checklist
+
+- [ ] Update documentation if interface changes
+- [ ] Add tests for new functionality
+- [ ] Ensure all tests pass
+- [ ] Update CHANGELOG.md with your changes
+
+## Architecture Overview
+
+### Project Structure
+
+```
+Sources/
+├── LucaCLI/ # Command-line interface
+│ └── Commands/ # CLI commands (install, uninstall, etc.)
+└── LucaCore/ # Core library
+ ├── Core/ # Core functionality
+ └── Models/ # Data models
+```
+
+### Key Components
+
+- **Installer**: Orchestrates the tool installation process
+- **SpecLoader**: Parses Lucafile configurations
+- **Downloader**: Handles file downloads from URLs
+- **Unarchiver**: Extracts archives (ZIP, tar.gz)
+- **SymLinker**: Creates symlinks in project directories
+
+## Resources
+
+- [Issue Tracker](https://github.com/LucaTools/Luca/issues)
+- [Pull Request Template](https://github.com/LucaTools/Luca/blob/main/.github/PULL_REQUEST_TEMPLATE.md)
+
+## Recognition
+
+Contributors are recognized in the project's README and CHANGELOG.
diff --git a/Sources/LucaCore/LucaCore.docc/GettingStarted.md b/Sources/LucaCore/LucaCore.docc/GettingStarted.md
new file mode 100644
index 0000000..08e9957
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/GettingStarted.md
@@ -0,0 +1,91 @@
+# Getting Started with Luca
+
+Learn how to install Luca and set up your first project with managed tools.
+
+## Overview
+
+Luca is a lightweight tool manager for macOS that helps developers install, manage, and activate specific versions of development tools in their projects. It creates project-specific tool environments without polluting your global PATH.
+
+## Installation
+
+### Using the Install Script (Recommended)
+
+Install the latest stable version with a single command:
+
+```bash
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/LucaTools/LucaScripts/HEAD/install.sh)"
+```
+
+#### Pinning a Specific Version
+
+To pin a specific version of Luca for your project, create a `.luca-version` file before running the install script:
+
+```bash
+echo "1.0.0" > .luca-version
+```
+
+### Building from Source
+
+If you prefer to build from source:
+
+```bash
+git clone https://github.com/LucaTools/Luca.git
+cd Luca
+swift build -c release
+cp -f .build/release/luca /usr/local/bin/luca
+```
+
+## Requirements
+
+- macOS 13.0 or later
+- Swift 5.7 or later (for building from source)
+
+## Quick Start
+
+### 1. Create a Lucafile
+
+Create a `Lucafile` in your project's root directory:
+
+```yaml
+---
+tools:
+ - name: SwiftLint
+ binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint
+ version: 0.61.0
+ url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip
+```
+
+### 2. Install Tools
+
+Run Luca to download and install the tools:
+
+```bash
+luca install
+```
+
+### 3. Use Your Tools
+
+Tools are symlinked to `.luca/active/` in your project directory:
+
+```bash
+.luca/active/swiftlint --version
+```
+
+## Installing from GitHub Directly
+
+You can also install tools directly from GitHub releases without a Lucafile:
+
+```bash
+luca install TogglesPlatform/ToggleGen@1.0.0
+```
+
+If the release asset naming is ambiguous, specify the asset name:
+
+```bash
+luca install krzysztofzablocki/sourcery@2.2.7 --asset sourcery-2.2.7.zip
+```
+
+## Next Steps
+
+- Learn about the configuration format
+- Follow the tutorial for a hands-on introduction
diff --git a/Sources/LucaCore/LucaCore.docc/LucaCore.md b/Sources/LucaCore/LucaCore.docc/LucaCore.md
new file mode 100644
index 0000000..67b068e
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/LucaCore.md
@@ -0,0 +1,76 @@
+# ``LucaCore``
+
+A lightweight tool manager for macOS that helps developers install, manage, and activate specific versions of development tools.
+
+## Overview
+
+Luca helps you manage command-line tools by downloading, verifying, and installing binaries from GitHub releases. Define your tools in a `Lucafile` and let Luca handle the rest.
+
+### Key Features
+
+- **Version-specific installations**: Install specific versions of tools needed for your project
+- **Project isolation**: Each project can have its own set of active tools
+- **Simple specification**: Define required tools in a simple YAML file (Lucafile)
+- **Zero configuration**: Just create a Lucafile and run `luca install`
+- **No PATH pollution**: Tools are symlinked locally in your project directory
+- **Idempotent operations**: Safe to run multiple times
+
+## Topics
+
+### Essentials
+
+-
+-
+
+### Tutorials
+
+-
+
+### Contributing
+
+-
+
+### Core Components
+
+- ``Installer``
+- ``SymLinker``
+- ``SpecLoader``
+- ``Tool``
+- ``Spec``
+
+### Downloading and Extraction
+
+- ``Downloader``
+- ``Unarchiver``
+- ``FileTypeDetector``
+
+### Validation
+
+- ``ChecksumValidator``
+- ``ChecksumCalculator``
+- ``ArchitectureValidator``
+
+### Tool Management
+
+- ``InstalledToolsLister``
+- ``LinkedToolsLister``
+- ``Uninstaller``
+- ``Unlinker``
+
+### Git Integration
+
+- ``GitHookInstaller``
+- ``GitIgnoreManager``
+
+### GitHub Integration
+
+- ``GitHubReleaseURLFactory``
+- ``ReleaseInfoProvider``
+
+### Utilities
+
+- ``PermissionManager``
+- ``Printer``
+- ``BinaryFinder``
+- ``ToolFactory``
+- ``VersionLister``
diff --git a/Sources/LucaCore/LucaCore.docc/Lucafile.md b/Sources/LucaCore/LucaCore.docc/Lucafile.md
new file mode 100644
index 0000000..0aa6cbe
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Lucafile.md
@@ -0,0 +1,150 @@
+# Lucafile Configuration
+
+Learn how to configure your project's tool dependencies using a Lucafile.
+
+## Overview
+
+A Lucafile is a YAML configuration file that defines the tools your project needs. Luca reads this file to download, install, and manage tool versions specific to your project.
+
+## File Location
+
+Place your `Lucafile` (or `Lucafile.yml`) in your project's root directory.
+
+## Basic Structure
+
+```yaml
+---
+tools:
+ - name: ToolName
+ version: 1.2.3
+ url: https://example.com/tool-1.2.3.zip
+```
+
+## Configuration Fields
+
+### Required Fields
+
+| Field | Description |
+|-------|-------------|
+| `name` | Logical name for the tool (used for organizing and referencing) |
+| `version` | The version to install |
+| `url` | Remote URL to the archive or executable file |
+
+### Optional Fields
+
+| Field | Description |
+|-------|-------------|
+| `binaryPath` | Path to the binary within an archive (required for archives with nested structure) |
+| `desiredBinaryName` | Custom name for the binary when stored locally |
+| `checksum` | Hash value for verifying the downloaded file |
+| `algorithm` | Hash algorithm used: `md5`, `sha1`, `sha256`, or `sha512` |
+
+## Examples
+
+### Simple Executable
+
+For tools distributed as standalone executables:
+
+```yaml
+tools:
+ - name: FirebaseCLI
+ version: 14.12.1
+ url: https://github.com/firebase/firebase-tools/releases/download/v14.12.1/firebase-tools-macos
+```
+
+### Archive with Binary Path
+
+For tools distributed in archives with nested directory structures:
+
+```yaml
+tools:
+ - name: SwiftLint
+ binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint
+ version: 0.61.0
+ url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip
+```
+
+### Simple Archive
+
+For tools where the binary is at the root of the archive:
+
+```yaml
+tools:
+ - name: Tuist
+ binaryPath: tuist
+ version: 4.80.0
+ url: https://github.com/tuist/tuist/releases/download/4.80.0/tuist.zip
+```
+
+### With Checksum Verification
+
+For enhanced security, verify downloads with checksums:
+
+```yaml
+tools:
+ - name: SwiftLint
+ binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint
+ version: 0.61.0
+ url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip
+ checksum: e0a6540d01434f436335a9b12c7fabc3a6f5c3e8...
+ algorithm: sha256
+```
+
+### Complete Example
+
+A typical Lucafile with multiple tools:
+
+```yaml
+---
+tools:
+ - name: FirebaseCLI
+ version: 14.12.1
+ url: https://github.com/firebase/firebase-tools/releases/download/v14.12.1/firebase-tools-macos
+
+ - name: SwiftLint
+ binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint
+ version: 0.61.0
+ url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip
+
+ - name: Tuist
+ binaryPath: tuist
+ version: 4.80.0
+ url: https://github.com/tuist/tuist/releases/download/4.80.0/tuist.zip
+
+ - name: Sourcery
+ binaryPath: bin/sourcery
+ version: 2.2.7
+ url: https://github.com/krzysztofzablocki/Sourcery/releases/download/2.2.7/sourcery-2.2.7.zip
+ checksum: abc123...
+ algorithm: sha256
+```
+
+## How Tools Are Stored
+
+When you run `luca install`, Luca:
+
+1. Downloads the specified files
+2. Verifies checksums (if provided)
+3. Extracts archives (if applicable)
+4. Stores binaries in `~/.luca/tools/{name}/{version}/`
+5. Creates symlinks in `.luca/active/` in your project
+
+## Best Practices
+
+### Version Pinning
+
+Always specify exact versions to ensure reproducible builds across your team.
+
+### Checksum Verification
+
+Use checksums for critical tools to verify integrity and protect against tampering.
+
+### Git Integration
+
+Add `.luca/active/` to your `.gitignore` file. Luca can manage this automatically with the `--install-git-hook` flag.
+
+## Related
+
+-
+- ``Spec``
+- ``SpecLoader``
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/InstallingTools.tutorial b/Sources/LucaCore/LucaCore.docc/Tutorials/InstallingTools.tutorial
new file mode 100644
index 0000000..521ccf1
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/InstallingTools.tutorial
@@ -0,0 +1,106 @@
+@Tutorial(time: 10) {
+ @Intro(title: "Installing Your First Tool") {
+ Learn how to install Luca and configure your first project with managed tools.
+
+ In this tutorial, you'll install Luca, create a Lucafile, and set up
+ SwiftLint for your project.
+ }
+
+ @Section(title: "Install Luca") {
+ @ContentAndMedia {
+ First, you need to install Luca on your machine. You can either use
+ the install script or build from source.
+ }
+
+ @Steps {
+ @Step {
+ Open Terminal and run the install script to download and install
+ the latest version of Luca.
+
+ @Code(name: "Terminal", file: "install-luca.sh")
+ }
+
+ @Step {
+ Verify the installation by checking the version.
+
+ @Code(name: "Terminal", file: "verify-install.sh")
+ }
+ }
+ }
+
+ @Section(title: "Create a Lucafile") {
+ @ContentAndMedia {
+ A Lucafile defines the tools your project needs. Create one in your
+ project's root directory to specify which tools and versions to install.
+ }
+
+ @Steps {
+ @Step {
+ Navigate to your project directory.
+
+ @Code(name: "Terminal", file: "cd-project.sh")
+ }
+
+ @Step {
+ Create a new file named `Lucafile` with your preferred text editor.
+ Start with the YAML header.
+
+ @Code(name: "Lucafile", file: "Lucafile-01.yml")
+ }
+
+ @Step {
+ Add a tools section with your first tool definition. Here we'll
+ add SwiftLint as an example.
+
+ @Code(name: "Lucafile", file: "Lucafile-02.yml")
+ }
+ }
+ }
+
+ @Section(title: "Install Tools") {
+ @ContentAndMedia {
+ With your Lucafile ready, use Luca to download and install the
+ specified tools.
+ }
+
+ @Steps {
+ @Step {
+ Run `luca install` to download and set up all tools defined in
+ your Lucafile.
+
+ @Code(name: "Terminal", file: "luca-install.sh")
+ }
+
+ @Step {
+ Luca downloads the tool, extracts it, and creates a symlink in
+ `.luca/active/`. You can now use the tool directly.
+
+ @Code(name: "Terminal", file: "use-swiftlint.sh")
+ }
+ }
+ }
+
+ @Section(title: "Install from GitHub Directly") {
+ @ContentAndMedia {
+ You can also install tools directly from GitHub without modifying
+ your Lucafile. This is useful for quick installations or testing
+ different versions.
+ }
+
+ @Steps {
+ @Step {
+ Install a tool by specifying the GitHub organization, repository,
+ and version.
+
+ @Code(name: "Terminal", file: "install-github.sh")
+ }
+
+ @Step {
+ If the release has multiple assets, specify which one to download
+ using the `--asset` flag.
+
+ @Code(name: "Terminal", file: "install-asset.sh")
+ }
+ }
+ }
+}
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Luca.tutorial b/Sources/LucaCore/LucaCore.docc/Tutorials/Luca.tutorial
new file mode 100644
index 0000000..18f4f0e
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Luca.tutorial
@@ -0,0 +1,19 @@
+@Tutorials(name: "Luca") {
+ @Intro(title: "Meet Luca") {
+ Learn how to use Luca to manage your development tools efficiently.
+ Luca helps you install, version, and organize command-line tools
+ specific to each of your projects.
+ }
+
+ @Chapter(name: "Getting Started") {
+ Install Luca and set up your first Lucafile to manage project tools.
+
+ @TutorialReference(tutorial: "doc:InstallingTools")
+ }
+
+ @Chapter(name: "Managing Tools") {
+ Learn how to list, update, and remove tools from your projects.
+
+ @TutorialReference(tutorial: "doc:ManagingTools")
+ }
+}
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/ManagingTools.tutorial b/Sources/LucaCore/LucaCore.docc/Tutorials/ManagingTools.tutorial
new file mode 100644
index 0000000..6a2bbfb
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/ManagingTools.tutorial
@@ -0,0 +1,98 @@
+@Tutorial(time: 8) {
+ @Intro(title: "Managing Your Tools") {
+ Learn how to list, inspect, and remove tools managed by Luca.
+
+ In this tutorial, you'll learn the commands for viewing installed tools,
+ checking which tools are linked in your project, and removing tools you
+ no longer need.
+ }
+
+ @Section(title: "List Installed Tools") {
+ @ContentAndMedia {
+ Luca keeps track of all tools installed on your system. You can view
+ them along with their installed versions.
+ }
+
+ @Steps {
+ @Step {
+ Run `luca installed` to see all tools and versions that have been
+ installed globally.
+
+ @Code(name: "Terminal", file: "luca-installed.sh")
+ }
+
+ @Step {
+ The output shows each tool name with its installed versions listed
+ below it.
+
+ @Code(name: "Output", file: "installed-output.txt")
+ }
+ }
+ }
+
+ @Section(title: "List Linked Tools") {
+ @ContentAndMedia {
+ Each project can have different tools linked. Use `luca linked` to
+ see which tools are active in your current project.
+ }
+
+ @Steps {
+ @Step {
+ Navigate to your project directory and run `luca linked`.
+
+ @Code(name: "Terminal", file: "luca-linked.sh")
+ }
+
+ @Step {
+ The output shows detailed information about each linked tool,
+ including its version, binary name, and storage location.
+
+ @Code(name: "Output", file: "linked-output.txt")
+ }
+ }
+ }
+
+ @Section(title: "Unlink Tools") {
+ @ContentAndMedia {
+ Remove a tool's symlink from your project without uninstalling it
+ globally. This is useful when you no longer need a tool in a
+ specific project.
+ }
+
+ @Steps {
+ @Step {
+ Use `luca unlink` with the binary name to remove the symlink.
+
+ @Code(name: "Terminal", file: "luca-unlink.sh")
+ }
+
+ @Step {
+ The tool remains installed globally and can be re-linked later
+ by running `luca install` again.
+ }
+ }
+ }
+
+ @Section(title: "Uninstall Tools") {
+ @ContentAndMedia {
+ Completely remove a tool version from your system. This removes
+ the tool from the global installation directory.
+ }
+
+ @Steps {
+ @Step {
+ To uninstall a specific version, use `luca uninstall` with the
+ tool name and version.
+
+ @Code(name: "Terminal", file: "luca-uninstall.sh")
+ }
+
+ @Step {
+ If you don't specify a version, Luca will prompt you to select
+ which version to uninstall.
+
+ @Code(name: "Terminal", file: "luca-uninstall-prompt.sh")
+ }
+ }
+ }
+}
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/Lucafile-01.yml b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/Lucafile-01.yml
new file mode 100644
index 0000000..5b3da25
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/Lucafile-01.yml
@@ -0,0 +1,2 @@
+---
+tools:
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/Lucafile-02.yml b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/Lucafile-02.yml
new file mode 100644
index 0000000..ec3f104
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/Lucafile-02.yml
@@ -0,0 +1,6 @@
+---
+tools:
+ - name: SwiftLint
+ binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint
+ version: 0.61.0
+ url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/cd-project.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/cd-project.sh
new file mode 100644
index 0000000..e998814
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/cd-project.sh
@@ -0,0 +1 @@
+cd ~/Projects/MyApp
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-asset.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-asset.sh
new file mode 100644
index 0000000..0dc7965
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-asset.sh
@@ -0,0 +1 @@
+luca install krzysztofzablocki/sourcery@2.2.7 --asset sourcery-2.2.7.zip
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-github.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-github.sh
new file mode 100644
index 0000000..524d195
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-github.sh
@@ -0,0 +1 @@
+luca install TogglesPlatform/ToggleGen@1.0.0
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-luca.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-luca.sh
new file mode 100644
index 0000000..82055a4
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/install-luca.sh
@@ -0,0 +1 @@
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/LucaTools/LucaScripts/HEAD/install.sh)"
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/installed-output.txt b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/installed-output.txt
new file mode 100644
index 0000000..f5a50e0
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/installed-output.txt
@@ -0,0 +1,11 @@
+FirebaseCLI:
+ - 14.12.1
+Sourcery:
+ - 2.2.5
+ - 2.2.7
+SwiftLint:
+ - 0.53.0
+ - 0.61.0
+tuist:
+ - 4.78.0
+ - 4.80.0
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/linked-output.txt b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/linked-output.txt
new file mode 100644
index 0000000..c0167b7
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/linked-output.txt
@@ -0,0 +1,8 @@
+SwiftLint:
+ version: 0.61.0
+ binary: swiftlint
+ location: /Users/you/.luca/tools/SwiftLint/0.61.0/swiftlint
+tuist:
+ version: 4.80.0
+ binary: tuist
+ location: /Users/you/.luca/tools/tuist/4.80.0/tuist
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-install.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-install.sh
new file mode 100644
index 0000000..14ac100
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-install.sh
@@ -0,0 +1 @@
+luca install
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-installed.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-installed.sh
new file mode 100644
index 0000000..97609c3
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-installed.sh
@@ -0,0 +1 @@
+luca installed
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-linked.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-linked.sh
new file mode 100644
index 0000000..71e855a
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-linked.sh
@@ -0,0 +1 @@
+luca linked
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-uninstall-prompt.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-uninstall-prompt.sh
new file mode 100644
index 0000000..6f86ab0
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-uninstall-prompt.sh
@@ -0,0 +1,2 @@
+luca uninstall SwiftLint
+# Luca will prompt you to select a version to uninstall
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-uninstall.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-uninstall.sh
new file mode 100644
index 0000000..a99a253
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-uninstall.sh
@@ -0,0 +1 @@
+luca uninstall SwiftLint@0.53.0
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-unlink.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-unlink.sh
new file mode 100644
index 0000000..45f6b69
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/luca-unlink.sh
@@ -0,0 +1 @@
+luca unlink swiftlint
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/use-swiftlint.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/use-swiftlint.sh
new file mode 100644
index 0000000..45e6cc6
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/use-swiftlint.sh
@@ -0,0 +1,2 @@
+.luca/active/swiftlint --version
+# Output: 0.61.0
diff --git a/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/verify-install.sh b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/verify-install.sh
new file mode 100644
index 0000000..a16a4f6
--- /dev/null
+++ b/Sources/LucaCore/LucaCore.docc/Tutorials/Resources/verify-install.sh
@@ -0,0 +1 @@
+luca --version
diff --git a/Sources/LucaCore/Models/Spec.swift b/Sources/LucaCore/Models/Spec.swift
index 621f95c..ee5dc04 100644
--- a/Sources/LucaCore/Models/Spec.swift
+++ b/Sources/LucaCore/Models/Spec.swift
@@ -2,6 +2,33 @@
import Foundation
+/// A specification defining the tools required for a project.
+///
+/// A `Spec` represents the parsed contents of a Lucafile. It contains
+/// an array of ``Tool`` definitions that Luca will install and manage.
+///
+/// ## Lucafile Example
+///
+/// ```yaml
+/// ---
+/// tools:
+/// - name: SwiftLint
+/// version: 0.61.0
+/// url: https://github.com/realm/SwiftLint/releases/...
+/// - name: Tuist
+/// version: 4.80.0
+/// url: https://github.com/tuist/tuist/releases/...
+/// ```
+///
+/// ## Topics
+///
+/// ### Properties
+/// - ``tools``
+///
+/// ### Related Types
+/// - ``Tool``
+/// - ``SpecLoader``
struct Spec: Codable {
+ /// The list of tools defined in the specification.
let tools: [Tool]
}
diff --git a/Sources/LucaCore/Models/Tool.swift b/Sources/LucaCore/Models/Tool.swift
index ca2ef0c..6f6b470 100644
--- a/Sources/LucaCore/Models/Tool.swift
+++ b/Sources/LucaCore/Models/Tool.swift
@@ -3,6 +3,37 @@
import Foundation
/// A single installable tool (binary) described in the spec file.
+///
+/// A `Tool` represents a command-line tool that can be downloaded, installed,
+/// and linked by Luca. Each tool is identified by a name and version, and
+/// includes information about where to download it and how to locate the
+/// binary within the downloaded archive.
+///
+/// ## Example
+///
+/// ```yaml
+/// - name: SwiftLint
+/// version: 0.61.0
+/// url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip
+/// binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint
+/// ```
+///
+/// ## Topics
+///
+/// ### Required Properties
+/// - ``name``
+/// - ``version``
+/// - ``url``
+///
+/// ### Optional Properties
+/// - ``binaryPath``
+/// - ``desiredBinaryName``
+/// - ``checksum``
+/// - ``algorithm``
+///
+/// ### Computed Properties
+/// - ``expectedBinaryName``
+/// - ``effectiveBinaryPath``
struct Tool: Codable {
/// Logical name of the tool (used for directory hierarchy).
let name: String