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
36 changes: 34 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ let package = Package(
platforms: [.macOS(.v13)],
products: [
.executable(name: "luca", targets: ["LucaCLI"]),
.library(name: "LucaCore", targets: ["LucaCore"])
.library(name: "LucaCore", targets: ["LucaCore"]),
.library(name: "LucaFoundation", targets: ["LucaFoundation"]),
.library(name: "PipelineCore", targets: ["PipelineCore"]),
.library(name: "ManagerCore", targets: ["ManagerCore"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", exact: "1.7.1"),
Expand All @@ -31,16 +34,45 @@ let package = Package(
.target(
name: "LucaCore",
dependencies: [
.target(name: "LucaFoundation"),
.target(name: "PipelineCore"),
.target(name: "ManagerCore")
],
path: "Sources/LucaCore"
),
.target(
name: "LucaFoundation",
dependencies: [
.product(name: "Noora", package: "Noora"),
.product(name: "Yams", package: "Yams")
],
path: "Sources/LucaFoundation"
),
.target(
name: "PipelineCore",
dependencies: [
.target(name: "LucaFoundation"),
.product(name: "Noora", package: "Noora"),
.product(name: "Yams", package: "Yams")
],
path: "Sources/PipelineCore"
),
.target(
name: "ManagerCore",
dependencies: [
.target(name: "LucaFoundation"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "Noora", package: "Noora"),
.product(name: "Yams", package: "Yams")
],
path: "Sources/LucaCore"
path: "Sources/ManagerCore"
),
.testTarget(
name: "LucaTests",
dependencies: [
.target(name: "LucaCore"),
.target(name: "PipelineCore"),
.target(name: "ManagerCore")
],
path: "Tests",
resources: [
Expand Down
2 changes: 1 addition & 1 deletion Sources/LucaCLI/Utils/FileManagerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Foundation
import LucaCore

public struct FileManagerWrapper: FileManaging {
public struct FileManagerWrapper: FileManaging, PipelineValidatorFileManaging {

private(set) var fileManager: FileManager

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// GlobalSpecFinder.swift

import Foundation
import LucaFoundation

/// Searches `~/.config/luca/` for the first recognised spec file.
///
Expand Down
5 changes: 5 additions & 0 deletions Sources/LucaCore/LucaCore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// LucaCore.swift

@_exported import LucaFoundation
@_exported import PipelineCore
@_exported import ManagerCore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Yams
/// ### Related Types
/// - ``Spec``
/// - ``Tool``

// MARK: - Helpers

private func formattedParseError(_ error: Error) -> String {
Expand Down Expand Up @@ -67,13 +68,13 @@ private func codingPath(from keys: [CodingKey]) -> String {
return result
}

struct SpecLoader: SpecLoading {
public struct SpecLoader: SpecLoading {

enum SpecLoaderError: Error, LocalizedError {
public enum SpecLoaderError: Error, LocalizedError {
case missingSpec(String)
case invalidSpec(String, Error)

var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .missingSpec(let path):
return "Missing spec at path: \(path)"
Expand All @@ -82,20 +83,20 @@ struct SpecLoader: SpecLoading {
}
}
}

private let fileManager: FileManager
init(fileManager: FileManager) {

public init(fileManager: FileManager) {
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 {
public func loadSpec(at path: URL) throws -> Spec {
guard let data = fileManager.contents(atPath: path.path) else {
throw SpecLoaderError.missingSpec(path.path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Foundation

/// Reads and decodes a Lucafile from disk.
protocol SpecLoading {
public protocol SpecLoading {
/// Loads and decodes the spec file at the given path.
/// - Parameter path: The URL of the Lucafile (YAML).
/// - Returns: A decoded ``Spec`` containing the tool definitions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import Foundation
/// `SubprocessRunner` wraps `Foundation.Process`, inheriting the parent's
/// stdout and stderr so output flows through to the terminal. The exit code
/// is returned directly to the caller.
struct SubprocessRunner: SubprocessRunning {
public struct SubprocessRunner: SubprocessRunning {

public init() {}

/// Runs the executable at the given URL with the provided arguments and extra environment variables.
///
Expand All @@ -19,7 +21,7 @@ struct SubprocessRunner: SubprocessRunning {
/// - workingDirectory: The working directory for the process. When `nil`, the process inherits the current directory.
/// - inheritStdin: When `true`, stdin is inherited from the parent process. When `false`, stdin is set to `/dev/null`.
/// - Returns: The process termination status.
func run(executableURL: URL, arguments: [String], environment: [String: String], workingDirectory: URL?, inheritStdin: Bool) async throws -> Int32 {
public func run(executableURL: URL, arguments: [String], environment: [String: String], workingDirectory: URL?, inheritStdin: Bool) async throws -> Int32 {
try await withCheckedThrowingContinuation { continuation in
let process = Process()
process.executableURL = executableURL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Foundation

/// Runs an external process and returns its exit code.
protocol SubprocessRunning: Sendable {
public protocol SubprocessRunning: Sendable {
/// Runs the executable at the given URL with the provided arguments and extra environment variables.
///
/// - Parameters:
Expand All @@ -16,7 +16,7 @@ protocol SubprocessRunning: Sendable {
func run(executableURL: URL, arguments: [String], environment: [String: String], workingDirectory: URL?, inheritStdin: Bool) async throws -> Int32
}

extension SubprocessRunning {
public extension SubprocessRunning {
/// Convenience overload using no extra environment, current working directory, and closed stdin.
func run(executableURL: URL, arguments: [String]) async throws -> Int32 {
try await run(executableURL: executableURL, arguments: arguments, environment: [:], workingDirectory: nil, inheritStdin: false)
Expand Down
15 changes: 15 additions & 0 deletions Sources/LucaFoundation/Models/ChecksumAlgorithm.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// ChecksumAlgorithm.swift

import Foundation

/// The hashing algorithm to use when computing or verifying a checksum.
public enum ChecksumAlgorithm: String, Codable, CaseIterable, Sendable {
/// MD5 (128-bit hash; insecure for cryptographic use, provided for compatibility).
case md5
/// SHA-1 (160-bit hash; insecure for cryptographic use, provided for compatibility).
case sha1
/// SHA-256 (256-bit hash; default algorithm).
case sha256
/// SHA-512 (512-bit hash).
case sha512
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import Foundation
/// - ``name``
/// - ``repository``
/// - ``version``
struct Skill: Codable {
public struct Skill: Codable {
/// The name of the specific skill to install. Leaving this `nil` indicates that all available skills should be installed.
let name: String?
public let name: String?
/// The repository reference — either `owner/repo` (GitHub shorthand) or a full HTTPS/GIT URL
let repository: String
public let repository: String
/// An optional git ref to pin the skill to. Accepts a tag (e.g. `v1.2.0`) or a commit SHA1 (e.g. `abc1234`).
/// When `nil`, the default branch HEAD is used.
let version: String?
public let version: String?

init(name: String?, repository: String, version: String? = nil) {
public init(name: String?, repository: String, version: String? = nil) {
self.name = name
self.repository = repository
self.version = version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Foundation

typealias Agent = String
public typealias Agent = String

/// A specification defining the tools required for a project.
///
Expand Down Expand Up @@ -49,22 +49,22 @@ typealias Agent = String
/// - ``Tool``
/// - ``Skill``
/// - ``SpecLoader``
struct Spec: Codable {
public struct Spec: Codable {
/// The list of tools defined in the specification.
let tools: [Tool]?
public let tools: [Tool]?
/// The list of agentic skills defined in the specification.
let skills: [Skill]?
public let skills: [Skill]?
/// The list of agent identifiers to target when installing skills (e.g. `claude-code`, `github-copilot`).
/// When `nil`, skills are installed for all supported agents.
let agents: [Agent]?
public let agents: [Agent]?

init(tools: [Tool]?, skills: [Skill]?, agents: [Agent]?) {
public init(tools: [Tool]?, skills: [Skill]?, agents: [Agent]?) {
self.tools = tools
self.skills = skills
self.agents = agents
}

init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let repos = try container.decodeIfPresent([String: String].self, forKey: .repos)
tools = try container.decodeIfPresent([Tool].self, forKey: .tools)
Expand All @@ -79,7 +79,7 @@ struct Spec: Codable {
agents = try container.decodeIfPresent([Agent].self, forKey: .agents)
}

func encode(to encoder: Encoder) throws {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(tools, forKey: .tools)
try container.encodeIfPresent(skills, forKey: .skills)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,38 +35,58 @@ import Foundation
/// ### Computed Properties
/// - ``expectedBinaryName``
/// - ``effectiveBinaryPath``
struct Tool: Codable {
public struct Tool: Codable {
/// Logical name of the tool (used for directory hierarchy).
let name: String
public let name: String
/// Version string (used to build folder names and allow side‑by‑side installs).
let version: String
public let version: String
/// Remote URL to an archive containing the tool or an executable file.
let url: URL
public let url: URL
/// Path (possibly nested) to the binary inside the unzipped archive.
let binaryPath: String?
public let binaryPath: String?
/// Name of the binary stored locally. Requires `url` to point to an executable file, ignored otherwise.
let desiredBinaryName: String?
public let desiredBinaryName: String?
/// The checksum hash of asset associated with the tool.
let checksum: String?
public let checksum: String?
/// The algorithm used to generate the checksum.
let algorithm: ChecksumAlgorithm?
public let algorithm: ChecksumAlgorithm?
/// Per-tool override for architecture validation.
/// `true` always skips; `false` always validates; `nil` falls back to the CLI `--ignore-arch-check` flag.
let ignoreArchCheck: Bool?
public let ignoreArchCheck: Bool?

public init(
name: String,
version: String,
url: URL,
binaryPath: String? = nil,
desiredBinaryName: String? = nil,
checksum: String? = nil,
algorithm: ChecksumAlgorithm? = nil,
ignoreArchCheck: Bool? = nil
) {
self.name = name
self.version = version
self.url = url
self.binaryPath = binaryPath
self.desiredBinaryName = desiredBinaryName
self.checksum = checksum
self.algorithm = algorithm
self.ignoreArchCheck = ignoreArchCheck
}
}

extension Tool {
/// Resolves the expected binary name for comparison with linked tools.
/// Priority: desiredBinaryName > binaryPath basename > tool name.
var expectedBinaryName: String {
public var expectedBinaryName: String {
if let desiredBinaryName { return desiredBinaryName }
if let binaryPath { return URL(fileURLWithPath: binaryPath).lastPathComponent }
return name
}

/// Resolves the path to the binary file within the tool's installation directory.
/// Priority: desiredBinaryName > binaryPath > name.
var effectiveBinaryPath: String {
public var effectiveBinaryPath: String {
desiredBinaryName ?? binaryPath ?? name
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// ChecksumCalculator.swift

import Foundation
import LucaFoundation
import Crypto

/// Computes a hex-encoded cryptographic hash for a file on disk.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
// ChecksumValidating.swift

import Foundation

/// The hashing algorithm to use when computing or verifying a checksum.
public enum ChecksumAlgorithm: String, Codable, CaseIterable, Sendable {
/// MD5 (128-bit hash; insecure for cryptographic use, provided for compatibility).
case md5
/// SHA-1 (160-bit hash; insecure for cryptographic use, provided for compatibility).
case sha1
/// SHA-256 (256-bit hash; default algorithm).
case sha256
/// SHA-512 (512-bit hash).
case sha512
}
import LucaFoundation

/// Verifies a file's integrity by comparing its computed checksum against an expected value.
protocol ChecksumValidating {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// ChecksumValidator.swift

import Foundation
import LucaFoundation
import Crypto

/// Validates a file's integrity by comparing its computed hash against an expected checksum string.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// FileManaging.swift

import Foundation
import LucaFoundation

/// The full file-system interface used by components that require access to multiple file manager capabilities.
public protocol FileManaging:
Expand All @@ -14,7 +15,6 @@ public protocol FileManaging:
InstalledSkillsListerFileManaging,
InstalledToolsFileManaging,
PermissionManagerFileManaging,
PipelineValidatorFileManaging,
SelfUpdaterFileManaging,
SkillSymLinkerFileManaging,
SkillUninstallerFileManaging,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// GitHookInstaller.swift

import Foundation
import LucaFoundation
import Noora

/// Installs a post-checkout Git hook into the current repository.
Expand Down
Loading