diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..09c3f17 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,25 @@ +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuration-options +changelog: + exclude: + labels: + - ci + - ignore-for-release + authors: + - github-actions + - octocat + categories: + - title: Breaking Changes 🛠 + labels: + - semver/major + - breaking-change + - title: New Features 🎉 + labels: + - semver/minor + - enhancement + - title: Bug Fixes 🐛 + labels: + - semver/patch + - bug + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..2a98109 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,53 @@ +name: Release +on: + pull_request: + types: + - closed + branches: + - 'main' + +jobs: + test: + name: Test + if: github.event.pull_request.merged == true && github.head_ref == 'release' + uses: mobelux/Watcher/.github/workflows/test.yml@main + + release: + name: Release + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true && github.head_ref == 'release' + needs: test + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Get version + id: get-version + shell: bash + run: | + VERSION=$(grep -Eo '([0-9]+\.*)+' ${{ vars.VERSION_FILE_PATH }}) + echo "current-version=$VERSION" >> $GITHUB_ENV + + - name: Push tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/${{ env.current-version }}', + sha: '${{ github.sha }}' + }) + + - name: Create release + uses: actions/github-script@v7 + with: + script: | + github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: '${{ env.current-version }}', + generate_release_notes: true, + draft: false, + prerelease: false + }) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..e167d7b --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,38 @@ +name: Prepare Release +on: + workflow_dispatch: + inputs: + release_type: + description: Type of release + type: choice + required: true + options: + - patch + - minor + - major + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Bump Version file + id: bump + run: | + echo "version=$(swift package --allow-writing-to-package-directory version-file --target watcher --bump ${{ inputs.release_type }})" >> $GITHUB_OUTPUT + + - name: Create pull request + id: cpr + uses: peter-evans/create-pull-request@v4 + with: + commit-message: Bump Version.swift -> ${{ steps.bump.outputs.version }} + committer: GitHub + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + branch: release + delete-branch: true + title: '[CI] Prepare Version ${{ steps.bump.outputs.version }} Release' + body: | + Update `Version.swift` with bumped version number. + draft: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..bff8d6b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + workflow_call: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + +jobs: + unit_tests: + if: github.event.pull_request.draft == false + name: Run Tests + runs-on: macos-13 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build + run: swift build + + - name: Run tests + run: swift test diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..9ae1d31 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [watcher, WatcherCore] diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist index 4bec461..a286a36 100644 --- a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -5,7 +5,7 @@ FILEHEADER // ___FILENAME___ -// DirectoryWatcher +// Watcher // // Created by ___FULLUSERNAME___ on ___DATE___. // diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..66ee349 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mobelux LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index a88c96b..8eb4773 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ build: swift build -c release install: build - install .build/release/directory-watcher /usr/local/bin/directory-watcher + install .build/release/watcher /usr/local/bin/watcher clean: rm -rf .build diff --git a/Package.resolved b/Package.resolved index a3c049f..5edbac3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -54,6 +54,15 @@ "version" : "1.0.4" } }, + { + "identity" : "swift-version-file-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mobelux/swift-version-file-plugin", + "state" : { + "revision" : "b1f5cee4453f0c6e838d28b2ea8c25f0f9604407", + "version" : "0.2.0" + } + }, { "identity" : "yams", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 29b0f46..110b30f 100644 --- a/Package.swift +++ b/Package.swift @@ -4,13 +4,13 @@ import PackageDescription let package = Package( - name: "DirectoryWatcher", + name: "Watcher", platforms: [ .macOS(.v13), ], products: [ - .executable(name: "directory-watcher", targets: ["DirectoryWatcher"]), - .library(name: "DirectoryWatcherCore", targets: ["DirectoryWatcherCore"]) + .executable(name: "watcher", targets: ["watcher"]), + .library(name: "WatcherCore", targets: ["WatcherCore"]) ], dependencies: [ .package(url: "https://github.com/ChimeHQ/GlobPattern.git", from: "0.1.0"), @@ -18,18 +18,19 @@ let package = Package( .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), .package(url: "https://github.com/eonist/FileWatcher.git", from: "0.2.3"), .package(url: "https://github.com/johnsundell/shellout.git", from: "2.3.0"), - .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.4") + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.4"), + .package(url: "https://github.com/mobelux/swift-version-file-plugin", from: "0.2.0") ], targets: [ .executableTarget( - name: "DirectoryWatcher", + name: "watcher", dependencies: [ - "DirectoryWatcherCore", + "WatcherCore", .product(name: "ArgumentParser", package: "swift-argument-parser") ] ), .target( - name: "DirectoryWatcherCore", + name: "WatcherCore", dependencies: [ .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "FileWatcher", package: "FileWatcher"), @@ -39,8 +40,8 @@ let package = Package( ] ), .testTarget( - name: "DirectoryWatcherCoreTests", - dependencies: ["DirectoryWatcherCore"] + name: "WatcherCoreTests", + dependencies: ["WatcherCore"] ) ] ) diff --git a/README.md b/README.md index e2126a5..72c9ced 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# DirectoryWatcher +# Watcher Swift CLI tool to execute commands when watched directories are modified -## Installation +## 🖥 Installation -### Makefile +### 📄 Makefile You can use the [`Makefile`](Makefile) to build and install: @@ -12,19 +12,19 @@ You can use the [`Makefile`](Makefile) to build and install: make install ``` -### Manual +### 🛠️ Manual Clone this repo and build the executable: ``` -swift build -c release directory-watcher +swift build -c release watcher ``` -Copy the resulting binary at `.build/release/directory-watcher` to a location where it can be executed like `/usr/local/bin` +Copy the resulting binary at `.build/release/watcher` to a location where it can be executed like `/usr/local/bin` -## Configuration +## 🎛️ Configuration -DirectoryWatcher uses a `.watcher.yml` file at the root of the watched directory to define commands to execute when files matching a given glob -- and optionally, not matching an `exclude` glob -- are modified: +Watcher uses a `.watcher.yml` file at the root of the watched directory to define commands to execute when files matching a given glob -- and optionally, not matching an `exclude` glob -- are modified: ```yml - pattern: "/Sources/**/*.swift" @@ -37,10 +37,10 @@ DirectoryWatcher uses a `.watcher.yml` file at the root of the watched directory The optional `name` value is used for terminal output. -## Usage +## ⚙️ Usage ``` -USAGE: directory-watcher [--config ] [--throttle ] +USAGE: watcher [--config ] [--throttle ] OPTIONS: -c, --config The path to a configuration file. @@ -48,3 +48,7 @@ OPTIONS: The minimum interval, in seconds, between command execution in response to file changes. -h, --help Show help information. ``` + +## 🔄 Alternatives + +- [watchman](https://github.com/facebook/watchman) diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..ff9ca86 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +# Release Instructions + +This tool uses the [swift-version-file-plugin](https://github.com/Mobelux/swift-version-file-plugin) Swift Package Manager command plugin to maintain a source file--[`Sources/watcher/Version.swift`](Sources/watcher/Version.swift)--supplying the value that is returned when the tool is run with the `--version` option. To ensure that this is properly maintained, releases should only be created using the [`Prepare Release`](http://github.com/Mobelux/Watcher/actions/workflows/prepare-release.yml) workflow. Run the workflow using `workflow_dispatch` event trigger from the `main` branch with the appropriate release type. This will create a new PR on a `release` branch containing an update to the Version file. Add any additional changes related to the release, like updating a changelog, to this PR. Finally, merge the `release` branch into `main` to delete it and trigger the [`Create Release`](.github/workflows/create-release.yml) workflow. This will create a new tag corresponding to the value of the updated Version file and a new release. diff --git a/Sources/DirectoryWatcher/DirectoryWatcher.swift b/Sources/DirectoryWatcher/DirectoryWatcher.swift deleted file mode 100644 index e450256..0000000 --- a/Sources/DirectoryWatcher/DirectoryWatcher.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// DirectoryWatcher.swift -// DirectoryWatcher -// -// Created by Mathew Gacy on 6/22/23. -// - -import ArgumentParser -import Foundation -import DirectoryWatcherCore - -@main -struct DirectoryWatcher: AsyncParsableCommand { - @Option(name: .shortAndLong, help: "The path to a configuration file.") - var config: String? = nil - - @Option(name: .shortAndLong, help: "The minimum interval, in seconds, between command execution in response to file changes.") - var throttle: Int? - - mutating func run() async throws { - let watchTask = try DirectoryWatcherCore.watch( - configurationPath: config, - throttleInterval: throttle) - try await watchTask.value - } -} diff --git a/Sources/DirectoryWatcherCore/CommandConfiguration.swift b/Sources/WatcherCore/CommandConfiguration.swift similarity index 95% rename from Sources/DirectoryWatcherCore/CommandConfiguration.swift rename to Sources/WatcherCore/CommandConfiguration.swift index e5ed659..5c340d3 100644 --- a/Sources/DirectoryWatcherCore/CommandConfiguration.swift +++ b/Sources/WatcherCore/CommandConfiguration.swift @@ -1,6 +1,6 @@ // // Configuration.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // diff --git a/Sources/DirectoryWatcherCore/Constants.swift b/Sources/WatcherCore/Constants.swift similarity index 93% rename from Sources/DirectoryWatcherCore/Constants.swift rename to Sources/WatcherCore/Constants.swift index 1bf8535..d47895d 100644 --- a/Sources/DirectoryWatcherCore/Constants.swift +++ b/Sources/WatcherCore/Constants.swift @@ -1,6 +1,6 @@ // // Constants.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // diff --git a/Sources/DirectoryWatcherCore/DirectoryEvent.swift b/Sources/WatcherCore/DirectoryEvent.swift similarity index 56% rename from Sources/DirectoryWatcherCore/DirectoryEvent.swift rename to Sources/WatcherCore/DirectoryEvent.swift index a3cd9c4..63684a3 100644 --- a/Sources/DirectoryWatcherCore/DirectoryEvent.swift +++ b/Sources/WatcherCore/DirectoryEvent.swift @@ -1,6 +1,6 @@ // // DirectoryEvent.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // @@ -8,10 +8,18 @@ import FileWatcher import Foundation +/// An event that occurred in a directory being watched. public struct DirectoryEvent: Sendable { + /// The path to the directory that was changed. public let path: String + /// A description of the event. public let description: String + /// Initialize a new `DirectoryEvent`. + /// + /// - Parameters: + /// - path: The path to the directory that was changed. + /// - description: A description of the event. public init(path: String, description: String) { self.path = path self.description = description @@ -19,6 +27,9 @@ public struct DirectoryEvent: Sendable { } extension DirectoryEvent { + /// Initialize a new `DirectoryEvent` from a `FileWatcherEvent`. + /// + /// - Parameter fileWatcherEvent: The `FileWatcherEvent` to use. init?(_ fileWatcherEvent: FileWatcherEvent) { guard fileWatcherEvent.dirChanged || fileWatcherEvent.fileChanged else { return nil diff --git a/Sources/WatcherCore/Documentation.docc/WatcherCore.md b/Sources/WatcherCore/Documentation.docc/WatcherCore.md new file mode 100644 index 0000000..e980920 --- /dev/null +++ b/Sources/WatcherCore/Documentation.docc/WatcherCore.md @@ -0,0 +1,11 @@ +# ``WatcherCore`` + +Invoke the core logic of the Watcher tool. + +## Overview + +This library contains the core logic powering the Watcher command-line tool. + +## Topics + +### Essentials diff --git a/Sources/DirectoryWatcherCore/EventStreamGenerator.swift b/Sources/WatcherCore/EventStreamGenerator.swift similarity index 75% rename from Sources/DirectoryWatcherCore/EventStreamGenerator.swift rename to Sources/WatcherCore/EventStreamGenerator.swift index 0cebb03..5299892 100644 --- a/Sources/DirectoryWatcherCore/EventStreamGenerator.swift +++ b/Sources/WatcherCore/EventStreamGenerator.swift @@ -1,6 +1,6 @@ // // EventStreamGenerator.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // @@ -8,7 +8,12 @@ import FileWatcher import Foundation +/// A type that can generate a stream of Directory Events. public struct EventStreamGenerator { + /// Returns a stream of ``DirectoryEvent``s for the specified paths. + /// + /// - Parameter paths: The paths to watch. + /// - Returns: A stream of events. public static func changes( on paths: [String] ) -> AsyncThrowingStream { diff --git a/Sources/DirectoryWatcherCore/Extensions/FileWatcherEvent+Utils.swift b/Sources/WatcherCore/Extensions/FileWatcherEvent+Utils.swift similarity index 94% rename from Sources/DirectoryWatcherCore/Extensions/FileWatcherEvent+Utils.swift rename to Sources/WatcherCore/Extensions/FileWatcherEvent+Utils.swift index 9f85502..684b298 100644 --- a/Sources/DirectoryWatcherCore/Extensions/FileWatcherEvent+Utils.swift +++ b/Sources/WatcherCore/Extensions/FileWatcherEvent+Utils.swift @@ -1,6 +1,6 @@ // // FileWatcherEvent+Utils.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // diff --git a/Sources/DirectoryWatcherCore/Extensions/String+Utils.swift b/Sources/WatcherCore/Extensions/String+Utils.swift similarity index 95% rename from Sources/DirectoryWatcherCore/Extensions/String+Utils.swift rename to Sources/WatcherCore/Extensions/String+Utils.swift index 438c172..fe416e2 100644 --- a/Sources/DirectoryWatcherCore/Extensions/String+Utils.swift +++ b/Sources/WatcherCore/Extensions/String+Utils.swift @@ -1,6 +1,6 @@ // // String+Utils.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // diff --git a/Sources/DirectoryWatcherCore/Logger.swift b/Sources/WatcherCore/Logger.swift similarity index 87% rename from Sources/DirectoryWatcherCore/Logger.swift rename to Sources/WatcherCore/Logger.swift index 68e441a..3bca0f9 100644 --- a/Sources/DirectoryWatcherCore/Logger.swift +++ b/Sources/WatcherCore/Logger.swift @@ -1,6 +1,6 @@ // // Logger.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // diff --git a/Sources/DirectoryWatcherCore/WatchCommandProvider.swift b/Sources/WatcherCore/WatchCommandProvider.swift similarity index 99% rename from Sources/DirectoryWatcherCore/WatchCommandProvider.swift rename to Sources/WatcherCore/WatchCommandProvider.swift index bf5ba85..87135a0 100644 --- a/Sources/DirectoryWatcherCore/WatchCommandProvider.swift +++ b/Sources/WatcherCore/WatchCommandProvider.swift @@ -1,6 +1,6 @@ // // WatchCommandProvider.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // diff --git a/Sources/DirectoryWatcherCore/DirectoryWatcherCore.swift b/Sources/WatcherCore/WatcherCore.swift similarity index 66% rename from Sources/DirectoryWatcherCore/DirectoryWatcherCore.swift rename to Sources/WatcherCore/WatcherCore.swift index 8b3f987..b4a8f4c 100644 --- a/Sources/DirectoryWatcherCore/DirectoryWatcherCore.swift +++ b/Sources/WatcherCore/WatcherCore.swift @@ -1,6 +1,6 @@ // -// DirectoryWatcherCore.swift -// DirectoryWatcher +// WatcherCore.swift +// Watcher // // Created by Mathew Gacy on 6/22/23. // @@ -8,7 +8,15 @@ import AsyncAlgorithms import Foundation -public struct DirectoryWatcherCore { +/// The entry point for the `Watcher` command-line tool. +public struct WatcherCore { + /// Watch the given directory for changes and execute commands in response. + /// + /// - Parameters: + /// - path: The path of the directory to watch. + /// - configurationPath: The path of the tool's configuration file. + /// - throttleInterval: The minimum interval between command executions. + /// - Returns: A reference to the task. public static func watch( path: String? = nil, configurationPath: String? = nil, @@ -21,7 +29,7 @@ public struct DirectoryWatcherCore { ?? URL(fileURLWithPath: watchedPath).appendingPathComponent(Constants.defaultConfigurationPath) guard let configs: [CommandConfiguration] = try YAMLReader.live.read(at: configURL) else { - throw DirectoryWatcherError.custom("Unable to read expected config at `\(configURL)`") + throw WatcherError.custom("Unable to read expected config at `\(configURL)`") } // Make commands @@ -38,7 +46,14 @@ public struct DirectoryWatcherCore { } } -extension DirectoryWatcherCore { +extension WatcherCore { + /// Runs the given operation asynchronously. + /// + /// - Parameters: + /// - paths: The paths to watch. + /// - throttleInterval: The minumum interval between command executions. + /// - operation: The operation to perform. + /// - Returns: A reference to the task. static func makeTask( watching paths: [String], throttleInterval: Int, diff --git a/Sources/DirectoryWatcherCore/DirectoryWatcherError.swift b/Sources/WatcherCore/WatcherError.swift similarity index 61% rename from Sources/DirectoryWatcherCore/DirectoryWatcherError.swift rename to Sources/WatcherCore/WatcherError.swift index 83a22b7..32a1568 100644 --- a/Sources/DirectoryWatcherCore/DirectoryWatcherError.swift +++ b/Sources/WatcherCore/WatcherError.swift @@ -1,13 +1,15 @@ // -// DirectoryWatcherError.swift -// DirectoryWatcher +// WatcherError.swift +// Watcher // // Created by Mathew Gacy on 6/23/23. // import Foundation -public enum DirectoryWatcherError: LocalizedError { +/// Errors thrown by Watcher. +public enum WatcherError: LocalizedError { + /// An error with a custom description. case custom(String) public var errorDescription: String? { diff --git a/Sources/DirectoryWatcherCore/YAMLReader.swift b/Sources/WatcherCore/YAMLReader.swift similarity index 60% rename from Sources/DirectoryWatcherCore/YAMLReader.swift rename to Sources/WatcherCore/YAMLReader.swift index a311501..e492cde 100644 --- a/Sources/DirectoryWatcherCore/YAMLReader.swift +++ b/Sources/WatcherCore/YAMLReader.swift @@ -1,6 +1,6 @@ // // YAMLReader.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // @@ -8,14 +8,23 @@ import Foundation import Yams +/// A type that can read a YAML file. public struct YAMLReader { private let decoder: YAMLDecoder = .init() private let read: (URL) throws -> String + /// Initialize a new `YAMLReader`. + /// + /// - Parameter read: A closure that reads a file at the given URL and returns its contents. + /// as a `String`. public init(read: @escaping (URL) throws -> String) { self.read = read } + /// Returns the decoded contents of the file at the specified URL. + /// + /// - Parameter url: The `URL` of the file to read. + /// - Returns: The decoded contents of the file. public func read(at url: URL) throws -> T? { do { return try decoder.decode( @@ -28,6 +37,7 @@ public struct YAMLReader { } public extension YAMLReader { + /// The live implementation of the reader. static var live: Self { .init(read: { try String(contentsOf: $0) }) } diff --git a/Sources/watcher/Documentation.docc/Articles/Configuring-Watcher.md b/Sources/watcher/Documentation.docc/Articles/Configuring-Watcher.md new file mode 100644 index 0000000..4301490 --- /dev/null +++ b/Sources/watcher/Documentation.docc/Articles/Configuring-Watcher.md @@ -0,0 +1,22 @@ +# Configuring Watcher + +Create a configuration file to control the behavior of Watcher. + +## Overview + +Watcher requires a configuration file that controls which directories are watched and the commands that are executed when they change. + +### Configure watched directories and commands + +Watcher uses a `.watcher.yml` file at the root of the watched directory to define commands to execute when files matching a given glob -- and optionally, not matching an `exclude` glob -- are modified: + +```yml +- pattern: "/Sources/**/*.swift" + command: swift run + exclude: "/**/ignore.swift" + name: Regenerate site +- pattern: "/src/scss/**/*.scss" + command: echo "compile Sass" +``` + +The optional `name` value is used for terminal output. diff --git a/Sources/watcher/Documentation.docc/Articles/Getting-started.md b/Sources/watcher/Documentation.docc/Articles/Getting-started.md new file mode 100644 index 0000000..6dc184c --- /dev/null +++ b/Sources/watcher/Documentation.docc/Articles/Getting-started.md @@ -0,0 +1,25 @@ +# Getting started + +Install Watcher. + +## Overview + +Watcher supports multiple installation methods. + +### Makefile + +You can use the `Makefile` to build and install: + +``` +make install +``` + +### Manual + +Clone this repo and build the executable: + +``` +swift build -c release watcher +``` + +Copy the resulting binary at `.build/release/watcher` to a location where it can be executed like `/usr/local/bin`. diff --git a/Sources/watcher/Documentation.docc/Watcher.md b/Sources/watcher/Documentation.docc/Watcher.md new file mode 100644 index 0000000..1304172 --- /dev/null +++ b/Sources/watcher/Documentation.docc/Watcher.md @@ -0,0 +1,14 @@ +# ``watcher`` + +Execute commands when watched directories are modified. + +## Overview + +`Watcher` allows you to configure commands to execute when watched directories are modified. + +## Topics + +### Essentials + +- +- diff --git a/Sources/watcher/Version.swift b/Sources/watcher/Version.swift new file mode 100644 index 0000000..7e54b4b --- /dev/null +++ b/Sources/watcher/Version.swift @@ -0,0 +1,5 @@ +// This file was generated by the `VersionFile` package plugin. + +enum Version { + static let number = "0.0.1" +} diff --git a/Sources/watcher/Watcher.swift b/Sources/watcher/Watcher.swift new file mode 100644 index 0000000..81d0fc3 --- /dev/null +++ b/Sources/watcher/Watcher.swift @@ -0,0 +1,35 @@ +// +// Watcher.swift +// Watcher +// +// Created by Mathew Gacy on 6/22/23. +// + +import ArgumentParser +import Foundation +import WatcherCore + +/// The entry point for the `Watcher` command-line tool. +@main +struct Watcher: AsyncParsableCommand { + /// Configuration for this command. + static let configuration = CommandConfiguration( + abstract: "Execute commands when watched directories are modified.", + version: Version.number) + + /// The path to a configuration file. + @Option(name: .shortAndLong, help: "The path to a configuration file.") + var config: String? = nil + + /// The minimum interval, in seconds, between command execution in response to file changes. + @Option(name: .shortAndLong, help: "The minimum interval, in seconds, between command execution in response to file changes.") + var throttle: Int? + + /// Runs the command. + mutating func run() async throws { + let watchTask = try WatcherCore.watch( + configurationPath: config, + throttleInterval: throttle) + try await watchTask.value + } +} diff --git a/Tests/DirectoryWatcherCoreTests/DirectoryWatcherCoreTests.swift b/Tests/DirectoryWatcherCoreTests/DirectoryWatcherCoreTests.swift deleted file mode 100644 index 573d749..0000000 --- a/Tests/DirectoryWatcherCoreTests/DirectoryWatcherCoreTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// DirectoryWatcherCoreTests.swift -// DirectoryWatcher -// -// Created by Mathew Gacy on 6/22/23. -// - -@testable import DirectoryWatcherCore -import Foundation -import XCTest - -final class DirectoryMonitorCoreTests: XCTestCase { - -} diff --git a/Tests/DirectoryWatcherCoreTests/Helpers/Mock.swift b/Tests/WatcherCoreTests/Helpers/Mock.swift similarity index 96% rename from Tests/DirectoryWatcherCoreTests/Helpers/Mock.swift rename to Tests/WatcherCoreTests/Helpers/Mock.swift index ab547db..8c78b98 100644 --- a/Tests/DirectoryWatcherCoreTests/Helpers/Mock.swift +++ b/Tests/WatcherCoreTests/Helpers/Mock.swift @@ -1,11 +1,11 @@ // // Mock.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/23/23. // -@testable import DirectoryWatcherCore +@testable import WatcherCore import Foundation enum Mock {} diff --git a/Tests/DirectoryWatcherCoreTests/Helpers/WatchCommandProvider+Mock.swift b/Tests/WatcherCoreTests/Helpers/WatchCommandProvider+Mock.swift similarity index 91% rename from Tests/DirectoryWatcherCoreTests/Helpers/WatchCommandProvider+Mock.swift rename to Tests/WatcherCoreTests/Helpers/WatchCommandProvider+Mock.swift index dfafa56..458538c 100644 --- a/Tests/DirectoryWatcherCoreTests/Helpers/WatchCommandProvider+Mock.swift +++ b/Tests/WatcherCoreTests/Helpers/WatchCommandProvider+Mock.swift @@ -1,11 +1,11 @@ // // WatchCommandProvider+Mock.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 7/6/23. // -@testable import DirectoryWatcherCore +@testable import WatcherCore import Foundation final class CommandExecutor: @unchecked Sendable { diff --git a/Tests/DirectoryWatcherCoreTests/Helpers/YAMLReader+Mock.swift b/Tests/WatcherCoreTests/Helpers/YAMLReader+Mock.swift similarity index 78% rename from Tests/DirectoryWatcherCoreTests/Helpers/YAMLReader+Mock.swift rename to Tests/WatcherCoreTests/Helpers/YAMLReader+Mock.swift index a16e0e5..120d577 100644 --- a/Tests/DirectoryWatcherCoreTests/Helpers/YAMLReader+Mock.swift +++ b/Tests/WatcherCoreTests/Helpers/YAMLReader+Mock.swift @@ -1,11 +1,11 @@ // // YAMLReader+Mock.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 7/6/23. // -@testable import DirectoryWatcherCore +@testable import WatcherCore import Foundation extension YAMLReader { diff --git a/Tests/DirectoryWatcherCoreTests/WatchCommandProviderTests.swift b/Tests/WatcherCoreTests/WatchCommandProviderTests.swift similarity index 95% rename from Tests/DirectoryWatcherCoreTests/WatchCommandProviderTests.swift rename to Tests/WatcherCoreTests/WatchCommandProviderTests.swift index 45394a2..b36fdbf 100644 --- a/Tests/DirectoryWatcherCoreTests/WatchCommandProviderTests.swift +++ b/Tests/WatcherCoreTests/WatchCommandProviderTests.swift @@ -1,11 +1,11 @@ // // WatchCommandProviderTests.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/24/23. // -@testable import DirectoryWatcherCore +@testable import WatcherCore import Foundation import XCTest diff --git a/Tests/WatcherCoreTests/WatcherCoreTests.swift b/Tests/WatcherCoreTests/WatcherCoreTests.swift new file mode 100644 index 0000000..90fc535 --- /dev/null +++ b/Tests/WatcherCoreTests/WatcherCoreTests.swift @@ -0,0 +1,14 @@ +// +// WatcherCoreTests.swift +// Watcher +// +// Created by Mathew Gacy on 6/22/23. +// + +@testable import WatcherCore +import Foundation +import XCTest + +final class WatcherCoreTests: XCTestCase { + +} diff --git a/Tests/DirectoryWatcherCoreTests/YAMLReaderTests.swift b/Tests/WatcherCoreTests/YAMLReaderTests.swift similarity index 73% rename from Tests/DirectoryWatcherCoreTests/YAMLReaderTests.swift rename to Tests/WatcherCoreTests/YAMLReaderTests.swift index 29f77fe..d99c532 100644 --- a/Tests/DirectoryWatcherCoreTests/YAMLReaderTests.swift +++ b/Tests/WatcherCoreTests/YAMLReaderTests.swift @@ -1,15 +1,15 @@ // // YAMLReaderTests.swift -// DirectoryWatcher +// Watcher // // Created by Mathew Gacy on 6/24/23. // -@testable import DirectoryWatcherCore +@testable import WatcherCore import Foundation import XCTest -final class YAMLReaderTests: XCTest { +final class YAMLReaderTests: XCTestCase { let configURL = URL(fileURLWithPath: "\(Mock.commandConfigurations)\(".watcher.yml")") func testReadValidConfig() throws { @@ -27,9 +27,9 @@ final class YAMLReaderTests: XCTest { func testReadInvalidValidConfig() throws { let sut = YAMLReader(read: { _ in "foo" }) - XCTAssertThrowsError({ - let config: [CommandConfiguration]? = try sut.read(at: self.configURL) - _ = config - }) + let read: () throws -> [CommandConfiguration]? = { + try sut.read(at: self.configURL) + } + XCTAssertThrowsError(try read()) } }