Repository: mtj0928/nest
Pull request #36: Add run command
Author: login: coffmark
Created: 2025-05-13T20:55:25Z | Merged: 2025-05-29T00:31:27Z

## Motivation

When `nestfile.yaml` was updated, version mismatches occurred when directly executing local binaries.

Therefore, I was using Git Hooks to run `$ nest bootstrap` to install the correct version of `nestfile.yaml`.

As an alternative to directly executing binaries, we can now use `$ nest run owner/repository` to execute binaries.

## Describe changes

I newly implemented `RunCommand.swift`.

Run command accepts the following arguments and options.

`$ nest help run`

```
OVERVIEW: Run executable file on a given nestfile. If not found, it will
attempt to install.

USAGE: nest run [--verbose] [--no-install] [--nestfile-path <nestfile-path>] <arguments> ...

ARGUMENTS:
  <arguments>

OPTIONS:
  -v, --verbose
  --no-install            Will not perform installation.
  --nestfile-path <nestfile-path>
                          A nestfile written in yaml. (default: nestfile.yaml)
  -h, --help              Show help information.
```

I used NestInfoController for manipulating NestInfo and NestfileController for manipulating Nestfile.


## Operation check

I used `realm/SwiftLint` as an example.

1. When `realm/SwiftLint` specified in `nestfile.yaml` is installed.

```
$ .nest/artifacts/realm_SwiftLint_github.com_https/0.55.0/SwiftLintBinary-macos/swiftlint lint Sources/NestKit
Linting Swift files at paths Sources/NestKit
// ...
Done linting! Found 44 violations, 0 serious in 34 files.
```

2. When `realm/SwiftLint` specified in `nestfile.yaml` is not installed

```
📦 Found an artifact bundle, SwiftLintBinary-macos.artifactbundle.zip, for SwiftLint.
🌐 Downloading the artifact bundle of SwiftLint...
✅ Success to download the artifact bundle of SwiftLint.
🪺 Success to install swiftlint version 0.55.0.
$ .nest/artifacts/realm_SwiftLint_github.com_https/0.55.0/SwiftLintBinary-macos/swiftlint lint Sources/NestKit
Linting Swift files at paths Sources/NestKit
// ...
Done linting! Found 44 violations, 0 serious in 34 files.
```

3. When `realm/SwiftLint` specified in `nestfile.yaml` is not installed and `--no-install` option is enabled

`$ nest run --no-install realm/SwiftLint lint Sources/NestKit`

```
Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.
```

## Issue ticket number and link
- Issue: #32 

## Checklist before requesting a review
- [x] I have performed a self-review of my code
- [x] I have added tests.


Diff:
diff --git a/README.md b/README.md
index 9f05835..4a7fac2 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,14 @@ $ nest update-nestfile nestfile.yaml --excludes realm/SwiftLint@0.58.1 realm/Swi
 
 `resolve-nestfile` is a similar command but it doesn't update the version when one is specified.
 
+### Execute the binary that matches `owner/repository` written in nestfile
+
+```sh
+$ nest run owner/repository
+```
+
+If a version matching the nestfile is not installed, it will attempt to install and run the matching version.
+
 ## Cache directory
 `nest` stores artifacts at `~/.nest` as a default. 
 If you want change the directory,
diff --git a/Sources/NestCLI/ArtifactBundleFetcher.swift b/Sources/NestCLI/ArtifactBundleFetcher.swift
index 1d9c31d..5c04c66 100644
--- a/Sources/NestCLI/ArtifactBundleFetcher.swift
+++ b/Sources/NestCLI/ArtifactBundleFetcher.swift
@@ -229,4 +229,18 @@ public enum ChecksumOption {
     case needsCheck(expected: String)
     case printActual(handler: (String) -> Void)
     case skip
+    
+    public init(isSkip: Bool = false, expectedChecksum: String?, logger: Logger) {
+        if isSkip {
+            self = .skip
+            return
+        }
+        if let expectedChecksum {
+            self = .needsCheck(expected: expectedChecksum)
+            return
+        }
+        self = .printActual { checksum in
+            logger.info("ℹ️ The checksum is \(checksum). Please add it to the nestfile to verify the downloaded file.")
+        }
+    }
 }
diff --git a/Sources/NestCLI/Executor/RunCommandExecutor.swift b/Sources/NestCLI/Executor/RunCommandExecutor.swift
new file mode 100644
index 0000000..9cf5662
--- /dev/null
+++ b/Sources/NestCLI/Executor/RunCommandExecutor.swift
@@ -0,0 +1,64 @@
+import NestKit
+import Logging
+
+public struct RunCommandExecutor {
+    /// `owner/repo` format
+    public let reference: String
+    public let subcommands: [String]
+    
+    public enum ParseError: Error {
+        case emptyArguments
+        case invalidFormat
+    }
+    
+    public init(arguments: [String]) throws {
+        guard !arguments.isEmpty else {
+            throw ParseError.emptyArguments
+        }
+        guard arguments[0].contains("/") else {
+            throw ParseError.invalidFormat
+        }
+        
+        self.reference = arguments[0]
+        self.subcommands = if arguments.count >= 2 {
+            Array(arguments[1...])
+        } else {
+            []
+        }
+    }
+    
+    /// Returns a relative path. If not, it will attempt to install.
+    public func resolveBinaryRelativePath(
+        noInstall: Bool,
+        reference: String,
+        version: String,
+        target: Nestfile.Target,
+        gitURL: GitURL,
+        gitVersion: GitVersion,
+        nestInfoController: NestInfoController,
+        executableBinaryPreparer: ExecutableBinaryPreparer,
+        artifactBundleManager: ArtifactBundleManager,
+        logger: Logger
+    ) async throws -> String? {
+        if let binaryRelativePath = nestInfoController.command(matchingTo: reference, version: version)?.binaryPath {
+            return binaryRelativePath
+        }
+        guard !noInstall else { return nil }
+
+        let executableBinaries = try await executableBinaryPreparer.fetchOrBuildBinariesFromGitRepository(
+            at: gitURL,
+            version: gitVersion,
+            artifactBundleZipFileName: target.assetName,
+            checksum: ChecksumOption(expectedChecksum: target.checksum, logger: logger)
+        )
+
+        for binary in executableBinaries {
+            try artifactBundleManager.install(binary)
+            logger.info("🪺 Success to install \(binary.commandName) version \(binary.version).")
+        }
+        guard let binaryRelativePath = nestInfoController.command(matchingTo: reference, version: version)?.binaryPath else {
+            return nil
+        }
+        return binaryRelativePath
+    }
+}
diff --git a/Sources/NestCLI/Nestfile.swift b/Sources/NestCLI/Nestfile.swift
index fa50fba..723c8a3 100644
--- a/Sources/NestCLI/Nestfile.swift
+++ b/Sources/NestCLI/Nestfile.swift
@@ -48,6 +48,30 @@ public struct Nestfile: Codable, Sendable {
             default: return false
             }
         }
+        
+        public var version: String? {
+            switch self {
+            case let .repository(repository):
+                return repository.version
+            case .zip, .deprecatedZIP:
+                return nil
+            }
+        }
+        
+        public var assetName: String? {
+            switch self {
+            case .repository(let repository): repository.assetName
+            case .zip, .deprecatedZIP: nil
+            }
+        }
+        
+        public var checksum: String? {
+            switch self {
+            case .repository(let repository): repository.checksum
+            case .zip(let zipURL): zipURL.checksum
+            case .deprecatedZIP: nil
+            }
+        }
     }
 
     public struct Repository: Codable, Equatable, Sendable {
diff --git a/Sources/NestCLI/NestfileController.swift b/Sources/NestCLI/NestfileController.swift
index 1c41eff..fc6e6f1 100644
--- a/Sources/NestCLI/NestfileController.swift
+++ b/Sources/NestCLI/NestfileController.swift
@@ -19,6 +19,20 @@ public struct NestfileController: Sendable {
         self.fileDownloader = fileDownloader
         self.checksumCalculator = checksumCalculator
     }
+    
+    /// Get the version that matches the `owner/repo`
+    /// - Parameters:
+    ///   - reference: `owner/repo` format
+    ///   - nestfile: Nestfile struct that defines nestfile.yaml
+    public func target(matchingTo reference: String, in nestfile: Nestfile) -> Nestfile.Target? {
+        return nestfile.targets
+            .first { target in
+                guard case let .repository(repository) = target,
+                      GitURL.parse(string: repository.reference)?.reference == reference
+                else { return false }
+                return true
+            }
+    }
 
     public func update(_ nestfile: Nestfile, excludedTargets: [ExcludedTarget]) async throws -> Nestfile {
         var nestfile = nestfile
diff --git a/Sources/NestKit/ArtifactBundleManager.swift b/Sources/NestKit/ArtifactBundleManager.swift
index 4defced..7fa6278 100644
--- a/Sources/NestKit/ArtifactBundleManager.swift
+++ b/Sources/NestKit/ArtifactBundleManager.swift
@@ -25,7 +25,7 @@ public struct ArtifactBundleManager: Sendable {
         commands = commands.filter { $0.version == version }
 
         for command in commands {
-            // Remove symboliklink
+            // Remove symbolic link
             if let linkedFilePath = try? self.linkedFilePath(commandName: name),
                linkedFilePath == command.binaryPath {
                 let resourceNames = command.resourcePaths.map { directory.url($0).lastPathComponent }
diff --git a/Sources/NestKit/Extensions/URL+extension.swift b/Sources/NestKit/Extensions/URL+extension.swift
index e524d31..5aba8d4 100644
--- a/Sources/NestKit/Extensions/URL+extension.swift
+++ b/Sources/NestKit/Extensions/URL+extension.swift
@@ -2,6 +2,15 @@ import Foundation
 import UniformTypeIdentifiers
 
 extension URL {
+    /// `owner/repo` format
+    /// `pathComponents[1]` must be specified as owner and `pathComponents[2]` must be specified as repository
+    public var reference: String? {
+        guard pathComponents.count >= 3 else { return nil }
+        let owner = pathComponents[1]
+        let repo = pathComponents[2].replacingOccurrences(of: ".\(pathExtension)", with: "")
+        return "\(owner)/\(repo)"
+    }
+    
     public var fileNameWithoutPathExtension: String {
         lastPathComponent.replacingOccurrences(of: ".\(pathExtension)", with: "")
     }
diff --git a/Sources/NestKit/Git/GitURL.swift b/Sources/NestKit/Git/GitURL.swift
index 0d7b923..7e613b5 100644
--- a/Sources/NestKit/Git/GitURL.swift
+++ b/Sources/NestKit/Git/GitURL.swift
@@ -55,6 +55,16 @@ public enum GitURL: Sendable, Hashable, Codable {
                 .replacingOccurrences(of: ".git", with: "")
         }
     }
+    
+    /// `owner/repo` format
+    public var reference: String? {
+        switch self {
+        case let .url(url):
+            return url.reference
+        case let .ssh(sshURL):
+            return sshURL.path.split(separator: ":").last?.replacingOccurrences(of: ".git", with: "")
+        }
+    }
 
     public var stringURL: String {
         switch self {
diff --git a/Sources/NestKit/NestInfoController.swift b/Sources/NestKit/NestInfoController.swift
index 6b4a6cf..cc88b00 100644
--- a/Sources/NestKit/NestInfoController.swift
+++ b/Sources/NestKit/NestInfoController.swift
@@ -19,6 +19,23 @@ public struct NestInfoController {
         return nestInfo
     }
 
+    /// Get NestInfo.Command from `owner/repository`
+    public func command(matchingTo reference: String, version: String) -> NestInfo.Command? {
+         return getInfo().commands
+            .first {
+                $0.value
+                    .contains {
+                        switch $0.manufacturer {
+                        case let .artifactBundle(sourceInfo):
+                            return sourceInfo.repository?.reference.reference == reference
+                        case let .localBuild(repository):
+                            return repository.reference.reference == reference
+                        }
+                    }
+            }?.value
+            .first { $0.version == version }
+    }
+
     func remove(command: String, version: String) throws {
         try updateInfo { info in
             info.commands[command] = info.commands[command]?.filter { $0.version != version }
diff --git a/Sources/NestKit/Utils/ProcessExecutor.swift b/Sources/NestKit/Utils/ProcessExecutor.swift
index 77cf655..c6eb5f4 100644
--- a/Sources/NestKit/Utils/ProcessExecutor.swift
+++ b/Sources/NestKit/Utils/ProcessExecutor.swift
@@ -19,10 +19,12 @@ extension ProcessExecutor {
 public struct NestProcessExecutor: ProcessExecutor {
     let currentDirectoryURL: URL?
     let logger: Logging.Logger
+    let logLevel: Logging.Logger.Level
 
-    public init(currentDirectory: URL? = nil, logger: Logging.Logger) {
+    public init(currentDirectory: URL? = nil, logger: Logging.Logger, logLevel: Logging.Logger.Level = .debug) {
         self.currentDirectoryURL = currentDirectory
         self.logger = logger
+        self.logLevel = logLevel
     }
 
     public func execute(command: String, _ arguments: [String]) async throws -> String {
@@ -36,7 +38,7 @@ public struct NestProcessExecutor: ProcessExecutor {
     }
 
     private func _execute(command: String, _ arguments: [String]) async throws -> [StreamElement] {
-        logger.debug("$ \(command) \(arguments.joined(separator: " "))")
+        logger.log(level: logLevel, "$ \(command) \(arguments.joined(separator: " "))")
         return try await withCheckedThrowingContinuation { continuous in
             let executableURL = URL(fileURLWithPath: command)
             do {
@@ -58,7 +60,7 @@ public struct NestProcessExecutor: ProcessExecutor {
                     else {
                         return
                     }
-                    logger.debug("\(string)")
+                    logger.log(level: logLevel, "\(string)")
                     results.withLock { $0 += [.output(string)] }
                 }
 
@@ -72,7 +74,7 @@ public struct NestProcessExecutor: ProcessExecutor {
                     else {
                         return
                     }
-                    logger.debug("\(string)", metadata: .color(.red))
+                    logger.log(level: logLevel, "\(string)", metadata: .color(.red))
                     results.withLock { $0 += [.error(string)] }
                 }
 
diff --git a/Sources/nest/Commands/BootstrapCommand.swift b/Sources/nest/Commands/BootstrapCommand.swift
index f028ae6..711c8a0 100644
--- a/Sources/nest/Commands/BootstrapCommand.swift
+++ b/Sources/nest/Commands/BootstrapCommand.swift
@@ -34,7 +34,7 @@ struct BootstrapCommand: AsyncParsableCommand {
         for targetInfo in nestfile.targets {
             let target: InstallTarget
             var version: GitVersion
-            let checksumOption = checksumOption(expectedChecksum: targetInfo.resolveChecksum(), logger: logger)
+            let checksumOption = ChecksumOption(isSkip: skipChecksumValidation, expectedChecksum: targetInfo.checksum, logger: logger)
 
             switch (targetInfo.resolveInstallTarget(), targetInfo.resolveVersion()) {
             case (.failure(let error), _):
@@ -54,7 +54,7 @@ struct BootstrapCommand: AsyncParsableCommand {
                 executableBinaries = try await executableBinaryPreparer.fetchOrBuildBinariesFromGitRepository(
                     at: gitURL,
                     version: version,
-                    artifactBundleZipFileName: targetInfo.resolveAssetName(),
+                    artifactBundleZipFileName: targetInfo.assetName,
                     checksum: checksumOption
                 )
             case .artifactBundle(let url):
@@ -68,18 +68,6 @@ struct BootstrapCommand: AsyncParsableCommand {
             }
         }
     }
-
-    private func checksumOption(expectedChecksum: String?, logger: Logger) -> ChecksumOption {
-        if skipChecksumValidation {
-            return .skip
-        }
-        if let expectedChecksum {
-            return .needsCheck(expected: expectedChecksum)
-        }
-        return .printActual { checksum in
-            logger.info("ℹ️ The checksum is \(checksum). Please add it to the nestfile to verify the downloaded file.")
-        }
-    }
 }
 
 extension Nestfile.Target {
@@ -115,21 +103,6 @@ extension Nestfile.Target {
             return nil
         }
     }
-
-    func resolveAssetName() -> String? {
-        switch self {
-        case .repository(let repository): repository.assetName
-        case .zip, .deprecatedZIP: nil
-        }
-    }
-
-    func resolveChecksum() -> String? {
-        switch self {
-        case .repository(let repository): repository.checksum
-        case .zip(let zipURL): zipURL.checksum
-        case .deprecatedZIP: nil
-        }
-    }
 }
 
 extension BootstrapCommand {
diff --git a/Sources/nest/Commands/RunCommand.swift b/Sources/nest/Commands/RunCommand.swift
new file mode 100644
index 0000000..d1b744d
--- /dev/null
+++ b/Sources/nest/Commands/RunCommand.swift
@@ -0,0 +1,122 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A path to nestfile")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        let executor: RunCommandExecutor
+        do {
+            executor = try RunCommandExecutor(arguments: arguments)
+        } catch let error as RunCommandExecutor.ParseError {
+            switch error {
+            case .emptyArguments:
+                logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            case .invalidFormat:
+                logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            }
+            return
+        }
+
+        guard let target = nestfileController.target(matchingTo: executor.reference, in: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+
+        guard let installTarget = InstallTarget(argument: executor.reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await executor.resolveBinaryRelativePath(
+            noInstall: noInstall,
+            reference: executor.reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                executor.subcommands
+            )
+    }
+}
+
+extension RunCommand {
+    private func setUp(nestfile: Nestfile) -> (
+        NestfileController,
+        ExecutableBinaryPreparer,
+        NestDirectory,
+        ArtifactBundleManager,
+        Logger
+    ) {
+        LoggingSystem.bootstrap()
+        let configuration = Configuration.make(
+            nestPath: nestfile.nestPath ?? ProcessInfo.processInfo.nestPath,
+            registryTokenEnvironmentVariableNames: nestfile.registries?.githubServerTokenEnvironmentVariableNames ?? [:],
+            logLevel: verbose ? .trace : .info
+        )
+        
+        let controller = NestfileController(
+            assetRegistryClientBuilder: AssetRegistryClientBuilder(
+                httpClient: configuration.httpClient,
+                registryConfigs: RegistryConfigs(github: GitHubRegistryConfigs.resolve(environmentVariableNames: nestfile.registries?.githubServerTokenEnvironmentVariableNames ?? [:])),
+                logger: configuration.logger
+            ),
+            fileSystem: configuration.fileSystem,
+            fileDownloader: configuration.fileDownloader,
+            checksumCalculator: SwiftChecksumCalculator(swift: SwiftCommand(
+                executor: NestProcessExecutor(logger: configuration.logger)
+            ))
+        )
+
+        return (
+            controller,
+            configuration.executableBinaryPreparer,
+            configuration.nestDirectory,
+            configuration.artifactBundleManager,
+            configuration.logger
+        )
+    }
+}
diff --git a/Sources/nest/nest.swift b/Sources/nest/nest.swift
index f90dbe3..ac68113 100644
--- a/Sources/nest/nest.swift
+++ b/Sources/nest/nest.swift
@@ -14,6 +14,7 @@ struct Nest: AsyncParsableCommand {
             ListCommand.self,
             SwitchCommand.self,
             BootstrapCommand.self,
+            RunCommand.self,
             GenerateNestfileCommand.self,
             UpdateNestfileCommand.self,
             ResolveNestfileCommand.self
diff --git a/Tests/NestCLITests/Executor/RunCommandExecutorTests.swift b/Tests/NestCLITests/Executor/RunCommandExecutorTests.swift
new file mode 100644
index 0000000..f0fafe4
--- /dev/null
+++ b/Tests/NestCLITests/Executor/RunCommandExecutorTests.swift
@@ -0,0 +1,26 @@
+import Testing
+import NestCLI
+
+struct RunCommandExecutorTests {
+    @Test(arguments: [
+        (["owner/repo"], "owner/repo", []),
+        (["owner/repo", "--option"], "owner/repo", ["--option"]),
+        (["owner/repo", "subcommand", "--option"], "owner/repo", ["subcommand", "--option"])
+    ])
+    func initialize(arguments: [String], expectedReference reference: String, expectedSubcommands subcommands: [String]?) throws {
+        let executor = try RunCommandExecutor(arguments: arguments)
+        #expect(executor.reference == reference)
+        #expect(executor.subcommands == subcommands)
+    }
+    
+    @Test(arguments: [
+        ([], RunCommandExecutor.ParseError.emptyArguments),
+        ([""], RunCommandExecutor.ParseError.invalidFormat),
+        (["ownerrepo"], RunCommandExecutor.ParseError.invalidFormat)
+    ])
+    func failedInitialize(arguments: [String], expectedError error: RunCommandExecutor.ParseError) throws {
+        #expect(throws: error, performing: {
+            try RunCommandExecutor(arguments: arguments)
+        })
+    }
+}
diff --git a/Tests/NestCLITests/NestfileControllerTests.swift b/Tests/NestCLITests/NestfileControllerTests.swift
index 3d04c25..288203d 100644
--- a/Tests/NestCLITests/NestfileControllerTests.swift
+++ b/Tests/NestCLITests/NestfileControllerTests.swift
@@ -26,6 +26,35 @@ struct NestfileControllerTests {
             ]
         ]
     }
+    
+    @Test
+    func fetchTarget() async throws {
+        let zipFileURL = try #require(URL(string: "https://example.com/foo.artifacatbundle.zip"))
+        let controller = NestfileController(
+            assetRegistryClientBuilder: AssetRegistryClientBuilder(
+                httpClient: httpClient,
+                registryConfigs: nil,
+                logger: Logger(label: "Test")
+            ),
+            fileSystem: fileSystem,
+            fileDownloader: NestFileDownloader(httpClient: httpClient),
+            checksumCalculator: SwiftChecksumCalculator(processExecutor: processExecutor)
+        )
+        let repositoryTarget = Nestfile.Target.repository(
+            Nestfile.Repository(
+                reference: "owner/foo",
+                version: "0.0.1",
+                assetName: nil,
+                checksum: nil
+            )
+        )
+        let nestfile = Nestfile(nestPath: "./.nest", targets: [
+            repositoryTarget,
+            .zip(Nestfile.ZIPURL(zipURL: zipFileURL.absoluteString, checksum: nil))
+        ])
+        #expect(controller.target(matchingTo: "owner/foo", in: nestfile) == repositoryTarget)
+        #expect(controller.target(matchingTo: "owner/undefined", in: nestfile) == nil)
+    }
 
     @Test
     func update() async throws {
diff --git a/Tests/NestKitTests/Extensions/URLTests.swift b/Tests/NestKitTests/Extensions/URLTests.swift
index 20a4479..c9a8e87 100644
--- a/Tests/NestKitTests/Extensions/URLTests.swift
+++ b/Tests/NestKitTests/Extensions/URLTests.swift
@@ -3,6 +3,17 @@ import Testing
 @testable import NestKit
 
 struct URLTests {
+    @Test(arguments: [
+        (URL(string: "https://github.com/owner/repo")!, "owner/repo"),
+        (URL(string: "https://github.com/owner/repo.git")!, "owner/repo"),
+        (URL(string: "https://github.com/owner/repo/releases/download/0.0.1/foo.artifactbundle.zip")!, "owner/repo"),
+        (URL(string: "https://foo.com/bar/owner/repo")!, "bar/owner"),
+        (URL(string: "https://foo.com/bar")!, nil)
+    ])
+    func testReference(url: URL, expected: String?) throws {
+        #expect(url.reference == expected)
+    }
+    
     @Test
     func testNeedsUnzipForZip() throws {
         let url = try #require(URL(string: "artifactBundle.zip"))
diff --git a/Tests/NestKitTests/Git/GitURLTests.swift b/Tests/NestKitTests/Git/GitURLTests.swift
index f01c27f..8ee6bf1 100644
--- a/Tests/NestKitTests/Git/GitURLTests.swift
+++ b/Tests/NestKitTests/Git/GitURLTests.swift
@@ -42,6 +42,17 @@ struct GitURLTests {
         let repository = GitURL.parse(string: parameter.name)
         #expect(repository?.repositoryName == parameter.expect)
     }
+    
+    @Test(arguments: [
+        ("owner/repo", expect: "owner/repo"),
+        ("https://github.com/owner/repo", expect: "owner/repo"),
+        ("github.com/owner/repo", "owner/repo"),
+        ("git@github.com:owner/repo.git", expect: "owner/repo")
+    ])
+    func referenceName(name: String, expected: String) {
+        let repository = GitURL.parse(string: name)
+        #expect(repository?.reference == expected)
+    }
 
     @Test(arguments:[
         ("owner/repo", expect: "https://github.com/owner/repo"),
diff --git a/Tests/NestKitTests/NestInfoControllerTests.swift b/Tests/NestKitTests/NestInfoControllerTests.swift
index 2a73f9d..e59f23b 100644
--- a/Tests/NestKitTests/NestInfoControllerTests.swift
+++ b/Tests/NestKitTests/NestInfoControllerTests.swift
@@ -19,6 +19,46 @@ struct NestInfoControllerTests {
             ]
         ]
     }
+    
+    @Test
+    func fetch() throws {
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: mockFileSystem)
+        let commands = [
+            try NestInfo.Command(
+                version: "0.0.1",
+                binaryPath: "a",
+                resourcePaths: ["b", "c"],
+                manufacturer: .artifactBundle(
+                    sourceInfo: ArtifactBundleSourceInfo(
+                        zipURL: #require(URL(string: "https://foo.com/bar.zip")),
+                        repository: Repository(
+                            reference: #require(.parse(string: "owner/foo")),
+                            version: "0.0.1"
+                        )
+                    )
+                )
+            ),
+            try NestInfo.Command(
+                version: "0.0.2",
+                binaryPath: "a",
+                resourcePaths: ["b", "c"],
+                manufacturer: .artifactBundle(
+                    sourceInfo: ArtifactBundleSourceInfo(
+                        zipURL: #require(URL(string: "https://foo.com/bar.zip")),
+                        repository: Repository(
+                            reference: #require(.parse(string: "owner/foo")),
+                            version: "0.0.2"
+                        )
+                    )
+                )
+            )
+        ]
+        for command in commands {
+            try nestInfoController.add(name: "foo", command: command)
+        }
+        #expect(nestInfoController.command(matchingTo: "owner/foo", version: "0.0.2") == commands[1])
+        #expect(nestInfoController.command(matchingTo: "owner/foo", version: "0.0.3") == nil)
+    }
 
     @Test
     func add() throws {


Reviews:
user: login: mtj0928
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-17T13:36:10Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T00:02:03Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T00:05:56Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T01:45:45Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T01:51:47Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T21:33:29Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T21:33:50Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T21:35:53Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T21:36:43Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T22:04:43Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T00:28:26Z
user: login: mtj0928
body: LGTM, except for the bug mentioned in a comment.
And, could you update README.md to explain the added command?
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-27T14:32:51Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T23:07:42Z
user: login: mtj0928
body: Basically, LGTM!
Let me modify the code a bit after merging this PR, for very personal preferences. 
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-29T00:31:03Z
user: login: coffmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T23:09:14Z
user: login: mtj0928
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-29T23:44:50Z
user: login: mtj0928
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-31T13:21:51Z

Review comments:
diff_hunk: @@ -19,6 +19,17 @@ public struct NestfileController: Sendable {
         self.fileDownloader = fileDownloader
         self.checksumCalculator = checksumCalculator
     }
+    
+    /// Get the version that matches the `owner/repo`
+    public func fetchTarget(reference: String, nestfile: Nestfile) -> Nestfile.Target? {
path: Sources/NestCLI/NestfileController.swift
user: login: mtj0928
body: [Naming] `fetch` sounds that the function get the target from a remote server.

How about this interface?
```swift
public func target(matchingTo reference: String, in nestfile: Nestfile) -> Nestfile.Target? 
```
created_at: 2025-05-17T12:50:48Z
author_association: OWNER
diff_hunk: @@ -19,6 +19,17 @@ public struct NestfileController: Sendable {
         self.fileDownloader = fileDownloader
         self.checksumCalculator = checksumCalculator
     }
+    
+    /// Get the version that matches the `owner/repo`
+    public func fetchTarget(reference: String, nestfile: Nestfile) -> Nestfile.Target? {
path: Sources/NestCLI/NestfileController.swift
user: login: mtj0928
body: And please write DocC that explains the reference can accept only `owner/repo` format.
created_at: 2025-05-17T12:51:25Z
author_association: OWNER
diff_hunk: @@ -19,6 +19,17 @@ public struct NestfileController: Sendable {
         self.fileDownloader = fileDownloader
         self.checksumCalculator = checksumCalculator
     }
+    
+    /// Get the version that matches the `owner/repo`
+    public func fetchTarget(reference: String, nestfile: Nestfile) -> Nestfile.Target? {
+        return nestfile.targets
+            .first { target in
+                guard case let .repository(repository) = target,
+                      repository.reference == reference || GitURL.parse(string: repository.reference)?.referenceName == reference
path: Sources/NestCLI/NestfileController.swift
user: login: mtj0928
body: Is not this enough?
```suggestion
                      GitURL.parse(string: repository.reference)?.referenceName == reference
```
created_at: 2025-05-17T12:51:51Z
author_association: OWNER
diff_hunk: @@ -2,6 +2,14 @@ import Foundation
 import UniformTypeIdentifiers
 
 extension URL {
+    /// `owner/repo` format
+    public var referenceName: String? {
+        guard pathComponents.count >= 3 else { return nil }
+        let owner = pathComponents[1]
+        let repo = pathComponents[2].replacingOccurrences(of: ".\(pathExtension)", with: "")
+        return "\(owner)/\(repo)"
+    }
path: Sources/NestKit/Extensions/URL+extension.swift
user: login: mtj0928
body: [Question] I'm not sure whether the logic always correct or not.
For example, is a URL like `https://for.com/bar/{owner}/{repo}` impossible?
created_at: 2025-05-17T12:59:51Z
author_association: OWNER
diff_hunk: @@ -19,6 +19,24 @@ public struct NestInfoController {
         return nestInfo
     }
 
+    /// Get NestInfo.Command from `owner/repository`
+    public func fetchCommand(reference: String, version: String) -> NestInfo.Command? {
path: Sources/NestKit/NestInfoController.swift
user: login: mtj0928
body: Ditto (naming)
created_at: 2025-05-17T13:02:47Z
author_association: OWNER
diff_hunk: @@ -19,6 +19,24 @@ public struct NestInfoController {
         return nestInfo
     }
 
+    /// Get NestInfo.Command from `owner/repository`
+    public func fetchCommand(reference: String, version: String) -> NestInfo.Command? {
+         return getInfo().commands
+            .first {
+                let command = $0.value
+                    .first {
+                        switch $0.manufacturer {
+                        case let .artifactBundle(sourceInfo):
+                            return sourceInfo.repository?.reference.referenceName == reference
+                        case let .localBuild(repository):
+                            return repository.reference.referenceName == reference
+                        }
+                    }
+                return command != nil
path: Sources/NestKit/NestInfoController.swift
user: login: mtj0928
body: This may be  straightforward.
```suggestion
                $0.value
                    .contains {
                        switch $0.manufacturer {
                        case let .artifactBundle(sourceInfo):
                            return sourceInfo.repository?.reference.referenceName == reference
                        case let .localBuild(repository):
                            return repository.reference.referenceName == reference
                        }
                    }
```
created_at: 2025-05-17T13:06:40Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,174 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A nestfile written in yaml.")
path: Sources/nest/Commands/RunCommand.swift
user: login: mtj0928
body: That nestfile is written in yaml  is obvious.
```suggestion
    @Option(help: "A path to nestfile")
```
created_at: 2025-05-17T13:09:18Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,174 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
path: Sources/nest/Commands/RunCommand.swift
user: login: mtj0928
body: `nest` module should be thin as much as possible.
So could you try to move the logic to other modules and reduce the logic in this module?

created_at: 2025-05-17T13:18:40Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,18 @@
+import NestCLI
+import Logging
+
+extension ChecksumOption {
+    init(isSkip: Bool = false, expectedChecksum: String?, logger: Logger) {
+        guard !isSkip else {
+            self = .skip
+            return
+        }
+        guard let expectedChecksum else {
+            self = .printActual { checksum in
+                logger.info("ℹ️ The checksum is \(checksum). Please add it to the nestfile to verify the downloaded file.")
+            }
+            return
+        }
+        self = .needsCheck(expected: expectedChecksum)
path: Sources/nest/Extensions/ArtifactBundleFetcher+extension.swift
user: login: mtj0928
body: I think `guard` statement is for error case.
```swift
guard foo else {
    // error case
}
// happy path
```

In this function, `skip` and `printActual` cases are not error cases.
So, could you use if statements instead?
created_at: 2025-05-17T13:29:32Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,174 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A nestfile written in yaml.")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        guard !arguments.isEmpty else {
+            logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            return
+        }
+        guard arguments[0].contains("/") else {
+            logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            return
+        }
+        
+        let reference = arguments[0]
+        let subcommands: [String] = if arguments.count >= 2 {
+            Array(arguments[1...])
+        } else {
+            []
+        }
+        guard let target = nestfileController.fetchTarget(reference: reference, nestfile: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+        
+        guard let installTarget = InstallTarget(argument: reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await resolveBinaryRelativePath(
+            didAttemptInstallation: false,
+            noInstall: noInstall,
+            reference: reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                subcommands
+            )
+    }
+
+    private func resolveBinaryRelativePath(
+        didAttemptInstallation: Bool,
+        noInstall: Bool,
+        reference: String,
+        version: String,
+        target: Nestfile.Target,
+        gitURL: GitURL,
+        gitVersion: GitVersion,
+        nestInfoController: NestInfoController,
+        executableBinaryPreparer: ExecutableBinaryPreparer,
+        artifactBundleManager: ArtifactBundleManager,
+        logger: Logger
+    ) async throws -> String? {
+        guard let binaryRelativePath = nestInfoController.fetchCommand(reference: reference, version: version)?.binaryPath
path: Sources/nest/Commands/RunCommand.swift
user: login: mtj0928
body: Use if statement instead of guard statement.
created_at: 2025-05-17T13:35:18Z
author_association: OWNER
diff_hunk: @@ -19,6 +19,17 @@ public struct NestfileController: Sendable {
         self.fileDownloader = fileDownloader
         self.checksumCalculator = checksumCalculator
     }
+    
+    /// Get the version that matches the `owner/repo`
+    public func fetchTarget(reference: String, nestfile: Nestfile) -> Nestfile.Target? {
path: Sources/NestCLI/NestfileController.swift
user: login: coffmark
body: Thanks! 
I've made the corrections here. (https://github.com/mtj0928/nest/pull/36/commits/297dda5eea831cad492a4f6e685812946589f09a)

created_at: 2025-05-20T00:02:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,17 @@ public struct NestfileController: Sendable {
         self.fileDownloader = fileDownloader
         self.checksumCalculator = checksumCalculator
     }
+    
+    /// Get the version that matches the `owner/repo`
+    public func fetchTarget(reference: String, nestfile: Nestfile) -> Nestfile.Target? {
+        return nestfile.targets
+            .first { target in
+                guard case let .repository(repository) = target,
+                      repository.reference == reference || GitURL.parse(string: repository.reference)?.referenceName == reference
path: Sources/NestCLI/NestfileController.swift
user: login: coffmark
body: Thanks!
I've made the corrections here. (https://github.com/mtj0928/nest/pull/36/commits/55718600306a8112a0623f52a925a6f7f8bf388e)
created_at: 2025-05-20T00:05:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,6 +2,14 @@ import Foundation
 import UniformTypeIdentifiers
 
 extension URL {
+    /// `owner/repo` format
+    public var referenceName: String? {
+        guard pathComponents.count >= 3 else { return nil }
+        let owner = pathComponents[1]
+        let repo = pathComponents[2].replacingOccurrences(of: ".\(pathExtension)", with: "")
+        return "\(owner)/\(repo)"
+    }
path: Sources/NestKit/Extensions/URL+extension.swift
user: login: coffmark
body: > is a URL like https://for.com/bar/{owner}/{repo} impossible?

In this case, `bar/owner` is output as a reference.
I added comments and tests (https://github.com/mtj0928/nest/pull/36/commits/174375cb0a7ae929d5f1a53f6a73aab43dfbc67e)
created_at: 2025-05-20T01:45:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,24 @@ public struct NestInfoController {
         return nestInfo
     }
 
+    /// Get NestInfo.Command from `owner/repository`
+    public func fetchCommand(reference: String, version: String) -> NestInfo.Command? {
path: Sources/NestKit/NestInfoController.swift
user: login: coffmark
body: Thanks!
I've made the corrections here. (https://github.com/mtj0928/nest/pull/36/commits/cbbe01596f2d30b35e16baa133f80bdf86343881)
created_at: 2025-05-20T01:51:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,24 @@ public struct NestInfoController {
         return nestInfo
     }
 
+    /// Get NestInfo.Command from `owner/repository`
+    public func fetchCommand(reference: String, version: String) -> NestInfo.Command? {
+         return getInfo().commands
+            .first {
+                let command = $0.value
+                    .first {
+                        switch $0.manufacturer {
+                        case let .artifactBundle(sourceInfo):
+                            return sourceInfo.repository?.reference.referenceName == reference
+                        case let .localBuild(repository):
+                            return repository.reference.referenceName == reference
+                        }
+                    }
+                return command != nil
path: Sources/NestKit/NestInfoController.swift
user: login: coffmark
body: Thanks!
I've made the corrections here. (https://github.com/mtj0928/nest/pull/36/commits/3b4f4650df3ac111992cd37b34ba792fa6605316)
created_at: 2025-05-20T21:33:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A nestfile written in yaml.")
path: Sources/nest/Commands/RunCommand.swift
user: login: coffmark
body: Thanks!
I've made the corrections here. (https://github.com/mtj0928/nest/pull/36/commits/6f71256e1c3c3b6fad544204e737fa9a90bbf34e)
created_at: 2025-05-20T21:33:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import NestCLI
+import Logging
+
+extension ChecksumOption {
+    init(isSkip: Bool = false, expectedChecksum: String?, logger: Logger) {
+        guard !isSkip else {
+            self = .skip
+            return
+        }
+        guard let expectedChecksum else {
+            self = .printActual { checksum in
+                logger.info("ℹ️ The checksum is \(checksum). Please add it to the nestfile to verify the downloaded file.")
+            }
+            return
+        }
+        self = .needsCheck(expected: expectedChecksum)
path: Sources/nest/Extensions/ArtifactBundleFetcher+extension.swift
user: login: coffmark
body: Thanks!
I've made the corrections here. (https://github.com/mtj0928/nest/pull/36/commits/bf4e6e26eec245f2952e933b0e303b6c93128521)
created_at: 2025-05-20T21:35:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A nestfile written in yaml.")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        guard !arguments.isEmpty else {
+            logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            return
+        }
+        guard arguments[0].contains("/") else {
+            logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            return
+        }
+        
+        let reference = arguments[0]
+        let subcommands: [String] = if arguments.count >= 2 {
+            Array(arguments[1...])
+        } else {
+            []
+        }
+        guard let target = nestfileController.fetchTarget(reference: reference, nestfile: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+        
+        guard let installTarget = InstallTarget(argument: reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await resolveBinaryRelativePath(
+            didAttemptInstallation: false,
+            noInstall: noInstall,
+            reference: reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                subcommands
+            )
+    }
+
+    private func resolveBinaryRelativePath(
+        didAttemptInstallation: Bool,
+        noInstall: Bool,
+        reference: String,
+        version: String,
+        target: Nestfile.Target,
+        gitURL: GitURL,
+        gitVersion: GitVersion,
+        nestInfoController: NestInfoController,
+        executableBinaryPreparer: ExecutableBinaryPreparer,
+        artifactBundleManager: ArtifactBundleManager,
+        logger: Logger
+    ) async throws -> String? {
+        guard let binaryRelativePath = nestInfoController.fetchCommand(reference: reference, version: version)?.binaryPath
path: Sources/nest/Commands/RunCommand.swift
user: login: coffmark
body: Thanks!
I fixed it together here. (https://github.com/mtj0928/nest/pull/36/commits/bf4e6e26eec245f2952e933b0e303b6c93128521)
created_at: 2025-05-20T21:36:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+import NestKit
+import Logging
+
+public struct RunCommandExecutor {
+    /// `owner/repo` format
+    public let reference: String
+    public let subcommands: [String]
+    
+    public enum ParseError: Error {
+        case emptyArguments
+        case invalidFormat
+    }
+    
+    public init(arguments: [String]) throws {
+        guard !arguments.isEmpty else {
+            throw ParseError.emptyArguments
+        }
+        guard arguments[0].contains("/") else {
+            throw ParseError.invalidFormat
+        }
+        
+        self.reference = arguments[0]
+        self.subcommands = if arguments.count >= 2 {
+            Array(arguments[1...])
+        } else {
+            []
+        }
+    }
+    
+    /// Returns a relative path. If not, it will attempt to install.
+    public func resolveBinaryRelativePath(
+        noInstall: Bool,
+        reference: String,
+        version: String,
+        target: Nestfile.Target,
+        gitURL: GitURL,
+        gitVersion: GitVersion,
+        nestInfoController: NestInfoController,
+        executableBinaryPreparer: ExecutableBinaryPreparer,
+        artifactBundleManager: ArtifactBundleManager,
+        logger: Logger
+    ) async throws -> String? {
path: Sources/NestCLI/Executor/RunCommandExecutor.swift
user: login: coffmark
body: Ideally, I would write tests, but there are no tests for the dependent component (`ExecutableBinaryPreparer`), so I decided not to write tests for the `resolveBinaryRelativePath` method.

(If it is required, I will write tests together with `ExecutableBinaryPreparer`!)
created_at: 2025-05-21T22:01:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,6 +48,30 @@ public struct Nestfile: Codable, Sendable {
             default: return false
             }
         }
+        
+        public var version: String? {
+            switch self {
+            case let .repository(repository):
+                return repository.version
+            case .zip, .deprecatedZIP:
+                return nil
+            }
+        }
+        
+        public var assetName: String? {
+            switch self {
+            case .repository(let repository): repository.assetName
+            case .zip, .deprecatedZIP: nil
+            }
+        }
+        
+        public var checksum: String? {
+            switch self {
+            case .repository(let repository): repository.checksum
+            case .zip(let zipURL): zipURL.checksum
+            case .deprecatedZIP: nil
+            }
+        }
path: Sources/NestCLI/Nestfile.swift
user: login: coffmark
body: I moved properties used by bootstrap and run commands.
created_at: 2025-05-21T22:03:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
path: Sources/nest/Commands/RunCommand.swift
user: login: coffmark
body: Thanks!
I tried to corrected here. (https://github.com/mtj0928/nest/pull/36/commits/1ee1a5f6d48e2a0a025a28c9b5ba50574e2e38b6, https://github.com/mtj0928/nest/pull/36/commits/8306591f62f77a728946a8859876a2966b324748)
created_at: 2025-05-26T00:28:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,122 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A path to nestfile")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        let executor: RunCommandExecutor
+        do {
+            executor = try RunCommandExecutor(arguments: arguments)
+        } catch let error as RunCommandExecutor.ParseError {
+            switch error {
+            case .emptyArguments:
+                logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            case .invalidFormat:
+                logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            }
+            return
+        }
+
+        guard let target = nestfileController.target(matchingTo: executor.reference, in: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+
+        guard let installTarget = InstallTarget(argument: executor.reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await executor.resolveBinaryRelativePath(
+            noInstall: noInstall,
+            reference: executor.reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                executor.subcommands
+            )
path: Sources/nest/Commands/RunCommand.swift
user: login: mtj0928
body: I noticed a bug that a user cannot input any texts to the command even if the command requires inputs because `NestProcessExecutor` does't support it.

It's OK to merge this PR without fixing the issue and fix it in another PR.
Of course, it's also welcome to fix it in this PR!

created_at: 2025-05-27T14:26:45Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,122 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A path to nestfile")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        let executor: RunCommandExecutor
+        do {
+            executor = try RunCommandExecutor(arguments: arguments)
+        } catch let error as RunCommandExecutor.ParseError {
+            switch error {
+            case .emptyArguments:
+                logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            case .invalidFormat:
+                logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            }
+            return
+        }
+
+        guard let target = nestfileController.target(matchingTo: executor.reference, in: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+
+        guard let installTarget = InstallTarget(argument: executor.reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await executor.resolveBinaryRelativePath(
+            noInstall: noInstall,
+            reference: executor.reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                executor.subcommands
+            )
path: Sources/nest/Commands/RunCommand.swift
user: login: coffmark
body: I would appreciate it if you could merge this PR as the differences are getting bigger.

As for the bugs, I will fix it and submit a PR. (If you need to create an Issue, I will create it.)
created_at: 2025-05-27T23:07:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,122 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A path to nestfile")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        let executor: RunCommandExecutor
+        do {
+            executor = try RunCommandExecutor(arguments: arguments)
+        } catch let error as RunCommandExecutor.ParseError {
+            switch error {
+            case .emptyArguments:
+                logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            case .invalidFormat:
+                logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            }
+            return
+        }
+
+        guard let target = nestfileController.target(matchingTo: executor.reference, in: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+
+        guard let installTarget = InstallTarget(argument: executor.reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await executor.resolveBinaryRelativePath(
+            noInstall: noInstall,
+            reference: executor.reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                executor.subcommands
+            )
path: Sources/nest/Commands/RunCommand.swift
user: login: coffmark
body: @mtj0928 
I tried writing a sample CLI tool to receive standard input, but could not reproduce it. (https://github.com/coffmark/InputText)

v0.0.1 -> only use readline method. ([source code](https://github.com/coffmark/InputText/blob/0.0.1/Sources/main.swift))
v0.0.2 -> use readline method with swift-argument-parser ([source code](https://github.com/coffmark/InputText/blob/0.0.2/Sources/main.swift))

Would it be possible for you to share the name of the CLI tool (if it is publicly available) or the steps to reproduce it?

https://github.com/user-attachments/assets/15550ad4-f16f-4d35-b250-f611d2323baf
created_at: 2025-05-29T23:09:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,122 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A path to nestfile")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        let executor: RunCommandExecutor
+        do {
+            executor = try RunCommandExecutor(arguments: arguments)
+        } catch let error as RunCommandExecutor.ParseError {
+            switch error {
+            case .emptyArguments:
+                logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            case .invalidFormat:
+                logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            }
+            return
+        }
+
+        guard let target = nestfileController.target(matchingTo: executor.reference, in: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+
+        guard let installTarget = InstallTarget(argument: executor.reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await executor.resolveBinaryRelativePath(
+            noInstall: noInstall,
+            reference: executor.reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                executor.subcommands
+            )
path: Sources/nest/Commands/RunCommand.swift
user: login: mtj0928
body: Oh, sorry.
I might misunderstood something.
Thank you for the investigation!
For now, it's fine to keep the current code.
created_at: 2025-05-29T23:44:50Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,122 @@
+import ArgumentParser
+import Foundation
+import NestKit
+import NestCLI
+import Logging
+
+struct RunCommand: AsyncParsableCommand {
+    static let configuration = CommandConfiguration(
+        commandName: "run",
+        abstract: "Run executable file on a given nestfile. If not found, it will attempt to install."
+    )
+
+    @Flag(name: .shortAndLong)
+    var verbose: Bool = false
+    
+    @Flag(help: "Will not perform installation.")
+    var noInstall: Bool = false
+    
+    @Option(help: "A path to nestfile")
+    var nestfilePath: String = "nestfile.yaml"
+    
+    @Argument(parsing: .captureForPassthrough)
+    var arguments: [String]
+    
+    mutating func run() async throws {
+        let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
+        let (nestfileController, executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp(nestfile: nestfile)
+        let nestInfoController = NestInfoController(directory: nestDirectory, fileSystem: FileManager.default)
+        
+        let executor: RunCommandExecutor
+        do {
+            executor = try RunCommandExecutor(arguments: arguments)
+        } catch let error as RunCommandExecutor.ParseError {
+            switch error {
+            case .emptyArguments:
+                logger.error("`owner/repository` is not specified.", metadata: .color(.red))
+            case .invalidFormat:
+                logger.error("Invalid format: \(arguments), expected owner/repository", metadata: .color(.red))
+            }
+            return
+        }
+
+        guard let target = nestfileController.target(matchingTo: executor.reference, in: nestfile),
+              let expectedVersion = target.version
+        else {
+            // While we could execute with the latest version, the bootstrap subcommand serves that purpose.
+            // Therefore, we return an error when no version is specified.
+            logger.error("Failed to find expected version in nestfile", metadata: .color(.red))
+            return
+        }
+
+        guard let installTarget = InstallTarget(argument: executor.reference),
+              case let .git(gitURL) = installTarget,
+              let gitVersion = GitVersion(argument: expectedVersion)
+        else {
+            return
+        }
+
+        guard let binaryRelativePath = try await executor.resolveBinaryRelativePath(
+            noInstall: noInstall,
+            reference: executor.reference,
+            version: expectedVersion,
+            target: target,
+            gitURL: gitURL,
+            gitVersion: gitVersion,
+            nestInfoController: nestInfoController,
+            executableBinaryPreparer: executableBinaryPreparer,
+            artifactBundleManager: artifactBundleManager,
+            logger: logger
+        ) else {
+            logger.error(
+                "Failed to find binary path, likely because it's not installed. Please try without the --no-install option or run the bootstrap command.",
+                metadata: .color(.red)
+            )
+            return
+        }
+
+        _ = try await NestProcessExecutor(logger: logger, logLevel: .info)
+            .execute(
+                command: "\(nestDirectory.rootDirectory.relativePath)\(binaryRelativePath)",
+                executor.subcommands
+            )
path: Sources/nest/Commands/RunCommand.swift
user: login: mtj0928
body: FYI: I've noticed that the input mechanism of [xcodes](https://github.com/XcodesOrg/xcodes)'s `select` doesn't work. #38

created_at: 2025-05-31T13:21:51Z
author_association: OWNER

PR comments:
user: login: coffmark
created_at: 2025-05-18T23:24:51Z
body: Thanks for your review!
I will contact you when the corrections are complete.
author_association: CONTRIBUTOR
user: login: coffmark
created_at: 2025-05-27T23:03:34Z
body: > LGTM, except for the bug mentioned in a comment. And, could you update README.md to explain the added command?

Thanks for your review!
I have corrected it here. (https://github.com/mtj0928/nest/pull/36/commits/ce62f3f3d75d02be8081385fb9dc911ccb15421b)
author_association: CONTRIBUTOR
user: login: coffmark
created_at: 2025-05-29T23:09:58Z
body: Thank you for merging!
author_association: CONTRIBUTOR