diff --git a/Sources/LucaCLI/Commands/InstallCommand.swift b/Sources/LucaCLI/Commands/InstallCommand.swift index 2c136f4..08f3939 100644 --- a/Sources/LucaCLI/Commands/InstallCommand.swift +++ b/Sources/LucaCLI/Commands/InstallCommand.swift @@ -106,13 +106,21 @@ luca install @Flag(help: "Skip architecture compatibility validation during installation.") var ignoreArchCheck: Bool = false + + @Flag(help: "Suppress all output except final success message.") + var quiet: Bool = false func run() async throws { - let printer = Printer() + let printer: Printing = quiet ? QuietPrinter() : Printer() Header(printer: printer).printHeader() let fileManager = FileManagerWrapper(fileManager: .default) - let installer = Installer(fileManager: fileManager, ignoreArchitectureCheck: ignoreArchCheck, printer: printer) + let installer = Installer( + fileManager: fileManager, + ignoreArchitectureCheck: ignoreArchCheck, + quiet: quiet, + printer: printer + ) let arguments = Arguments( spec: spec, identifier: identifier, diff --git a/Sources/LucaCLI/Utils/Header.swift b/Sources/LucaCLI/Utils/Header.swift index da7281c..dc1262b 100644 --- a/Sources/LucaCLI/Utils/Header.swift +++ b/Sources/LucaCLI/Utils/Header.swift @@ -5,7 +5,7 @@ import LucaCore struct Header { - let printer: Printer + let printer: Printing func printHeader() { let asciiHeader = """ diff --git a/Sources/LucaCore/Core/Installer/Installer.swift b/Sources/LucaCore/Core/Installer/Installer.swift index a6bbdd3..0723113 100644 --- a/Sources/LucaCore/Core/Installer/Installer.swift +++ b/Sources/LucaCore/Core/Installer/Installer.swift @@ -1,6 +1,7 @@ // Installer.swift import Foundation +import Noora public struct Installer { @@ -26,12 +27,16 @@ public struct Installer { private let linkedToolsLister: LinkedToolsLister private let unlinker: Unlinker private let ignoreArchitectureCheck: Bool + private let quiet: Bool + private let noora: Noorable public init( fileManager: FileManaging, ignoreArchitectureCheck: Bool, + quiet: Bool = false, printer: Printing, - downloader: Downloading? = nil + downloader: Downloading? = nil, + noora: Noorable = Noora() ) { self.fileManager = fileManager self.printer = printer @@ -44,9 +49,52 @@ public struct Installer { self.linkedToolsLister = LinkedToolsLister(fileManager: fileManager) self.unlinker = Unlinker(fileManager: fileManager, printer: printer) self.ignoreArchitectureCheck = ignoreArchitectureCheck + self.quiet = quiet + self.noora = noora } public func install(installationType: InstallationType) async throws { + if quiet { + try await installQuietly(installationType: installationType) + } else { + try await installVerbose(installationType: installationType) + } + } + + // MARK: - Private + + private func installQuietly(installationType: InstallationType) async throws { + try await noora.progressStep( + message: "Installing tools", + successMessage: "Tools have been installed for the current project", + errorMessage: "Failed to install tools", + showSpinner: true + ) { updateMessage in + let dataDownloader = DataDownloader(session: .shared) + let releaseInfoProvider = ReleaseInfoProvider(dataDownloader: dataDownloader) + let specLoader = SpecLoader(fileManager: .default) + let toolFactory = ToolFactory(releaseInfoProvider: releaseInfoProvider, specLoader: specLoader) + + updateMessage("Detecting tools to install") + let tools = try await toolFactory.toolsForInstallationType(installationType) + + // Unlink orphaned tools only when installing from a spec + if case .spec = installationType { + try unlinkOrphanedTools(specTools: tools) + } + + for tool in tools { + updateMessage("Installing \(tool.name) \(tool.version)") + if isToolInstalled(tool) { + try reinstall(tool) + } else { + try await install(tool) + } + } + } + } + + private func installVerbose(installationType: InstallationType) async throws { let dataDownloader = DataDownloader(session: .shared) let releaseInfoProvider = ReleaseInfoProvider(dataDownloader: dataDownloader) let specLoader = SpecLoader(fileManager: .default) @@ -64,13 +112,7 @@ public struct Installer { printer.printFormatted("\(.info("🏃‍♂️ Installing tools for the current project."))") printer.printFormatted("") - - try await installTools(tools) - } - - // MARK: - Private - - private func installTools(_ tools: [Tool]) async throws { + for tool in tools { if isToolInstalled(tool) { try reinstall(tool) diff --git a/Sources/LucaCore/Core/Printer/QuietPrinter.swift b/Sources/LucaCore/Core/Printer/QuietPrinter.swift new file mode 100644 index 0000000..f9e50fe --- /dev/null +++ b/Sources/LucaCore/Core/Printer/QuietPrinter.swift @@ -0,0 +1,11 @@ +// QuietPrinter.swift + +import Foundation +import Noora + +public struct QuietPrinter: Printing { + + public init() {} + + public func printFormatted(_ terminalText: TerminalText) {} +} diff --git a/Tests/Core/InstallerTests.swift b/Tests/Core/InstallerTests.swift index 07ea044..582ed8c 100644 --- a/Tests/Core/InstallerTests.swift +++ b/Tests/Core/InstallerTests.swift @@ -15,18 +15,19 @@ struct InstallerTests { downloader = DownloaderMock(result: .fixture(Fixture(filename: "MockRelease", type: "zip"))) } - private func makeInstaller() -> Installer { + private func makeInstaller(quiet: Bool) -> Installer { Installer( fileManager: fileManager, ignoreArchitectureCheck: true, + quiet: quiet, printer: PrinterMock(), downloader: downloader ) } - @Test - func test_installIndividuals() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installIndividuals(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) let fixture = Fixture(filename: "Lucafile_mock", type: "yml") let bundle = Bundle.module @@ -73,9 +74,9 @@ struct InstallerTests { } } - @Test - func test_installSpec() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installSpec(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) let fixture = Fixture(filename: "Lucafile_mock", type: "yml") let bundle = Bundle.module @@ -109,9 +110,9 @@ struct InstallerTests { #expect(fileManager.fileExists(atPath: toolSymLink.path)) } - @Test - func test_installInvalid() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installInvalid(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) let fixture = Fixture(filename: "Lucafile_invalid", type: "yml") let bundle = Bundle.module @@ -122,9 +123,9 @@ struct InstallerTests { } } - @Test - func test_reinstallSpec() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_reinstallSpec(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) let fixture = Fixture(filename: "Lucafile_mock", type: "yml") let bundle = Bundle.module @@ -154,9 +155,9 @@ struct InstallerTests { } } - @Test - func test_installToolUpgradeVersion() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installToolUpgradeVersion(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) let lowVersionFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml") let highVersionFixture = Fixture(filename: "Lucafile_mock_highversion", type: "yml") @@ -188,9 +189,9 @@ struct InstallerTests { } } - @Test - func test_installToolDowngradeVersion() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installToolDowngradeVersion(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) let lowVersionFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml") let highVersionFixture = Fixture(filename: "Lucafile_mock_highversion", type: "yml") @@ -222,9 +223,9 @@ struct InstallerTests { } } - @Test - func test_installSpec_unlinksOrphanedTools() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installSpec_unlinksOrphanedTools(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) // First, install the full spec with all tools let fullFixture = Fixture(filename: "Lucafile_mock", type: "yml") @@ -258,9 +259,9 @@ struct InstallerTests { } } - @Test - func test_installIndividual_doesNotUnlinkExistingTools() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installIndividual_doesNotUnlinkExistingTools(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) // First, install a spec with multiple tools let fullFixture = Fixture(filename: "Lucafile_mock", type: "yml") @@ -303,9 +304,9 @@ struct InstallerTests { #expect(fileManager.fileExists(atPath: individualToolPath.path)) } - @Test - func test_installSpec_noOrphanedTools() async throws { - let installer = makeInstaller() + @Test(arguments: [true, false]) + func test_installSpec_noOrphanedTools(quiet: Bool) async throws { + let installer = makeInstaller(quiet: quiet) // Install a spec let fixture = Fixture(filename: "Lucafile_mock", type: "yml") @@ -327,12 +328,12 @@ struct InstallerTests { } } - @Test - func test_installSpec_sameToolDifferentVersion() async throws { + @Test(arguments: [true, false]) + func test_installSpec_sameToolDifferentVersion(quiet: Bool) async throws { // This test verifies that installing the same tool with different versions // correctly updates the symlink to point to the new version. - let installer = makeInstaller() + let installer = makeInstaller(quiet: quiet) // Install MockTool version 1.0.0 let lowVersionFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml") @@ -352,12 +353,12 @@ struct InstallerTests { #expect(fileManager.fileExists(atPath: symLink.path)) } - @Test - func test_installSpec_unlinksToolsByName() async throws { + @Test(arguments: [true, false]) + func test_installSpec_unlinksToolsByName(quiet: Bool) async throws { // This test verifies that orphan detection correctly identifies tools to unlink // by comparing tool names (from folder structure) against spec tool names. - let installer = makeInstaller() + let installer = makeInstaller(quiet: quiet) // First, install a spec with MockTool let mockToolFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml")