Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions .claude/skills/swiftyshell.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Before writing any code, follow this decision tree:
→ Use `Git`
2. Is this a git operation NOT covered by the typed `Git` API?
→ Use `Command("\1", arguments: ...)`
3. Is this a file-system operation covered by a typed wrapper (`Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`)?
3. Is this a file-system operation covered by a typed wrapper (`Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Rsync`)?
→ Use the typed wrapper
4. Is this an archive operation (`tar`, `zip`, or `unzip`)?
→ Use `Tar`, `Zip`, or `Unzip`
Expand Down Expand Up @@ -634,6 +634,60 @@ public struct Pwd: RunnableCommandFamily {
public func command() -> Command
public func run() async throws -> ShellOutput
}

public struct Rsync: RunnableCommandFamily {
public init(context: ShellContext = .init())
public func archive(_ enabled: Bool = true) -> Self // -a
public func recursive(_ enabled: Bool = true) -> Self // -r
public func compress(_ enabled: Bool = true) -> Self // -z
public func verbose(_ enabled: Bool = true) -> Self // -v
public func quiet(_ enabled: Bool = true) -> Self // -q
public func dryRun(_ enabled: Bool = true) -> Self // -n
public func checksum(_ enabled: Bool = true) -> Self // -c
public func update(_ enabled: Bool = true) -> Self // -u
public func delete(_ enabled: Bool = true) -> Self // --delete
public func deleteExcluded(_ enabled: Bool = true) -> Self
public func links(_ enabled: Bool = true) -> Self // -l
public func copyLinks(_ enabled: Bool = true) -> Self // -L
public func permissions(_ enabled: Bool = true) -> Self // -p
public func times(_ enabled: Bool = true) -> Self // -t
public func owner(_ enabled: Bool = true) -> Self // -o
public func group(_ enabled: Bool = true) -> Self // -g
public func hardLinks(_ enabled: Bool = true) -> Self // -H
public func sparse(_ enabled: Bool = true) -> Self // -S
public func oneFileSystem(_ enabled: Bool = true) -> Self // -x
public func itemizeChanges(_ enabled: Bool = true) -> Self // -i
public func humanReadable(_ enabled: Bool = true) -> Self // -h
public func progress(_ enabled: Bool = true) -> Self
public func partial(_ enabled: Bool = true) -> Self
public func existing(_ enabled: Bool = true) -> Self
public func ignoreExisting(_ enabled: Bool = true) -> Self
public func removeSourceFiles(_ enabled: Bool = true) -> Self
public func source(_ value: String) -> Self
public func sources(_ values: [String]) -> Self
public func destination(_ value: String) -> Self
public func exclude(_ pattern: String) -> Self
public func excludes(_ patterns: [String]) -> Self
public func include(_ pattern: String) -> Self
public func includes(_ patterns: [String]) -> Self
public func filter(_ rule: String) -> Self
public func filters(_ rules: [String]) -> Self
public func excludeFrom(_ path: String) -> Self
public func includeFrom(_ path: String) -> Self
public func filesFrom(_ path: String) -> Self
public func from0(_ enabled: Bool = true) -> Self
public func remoteShell(_ command: String) -> Self
public func remoteRsyncPath(_ path: String) -> Self
public func port(_ value: Int) -> Self
public func bandwidthLimit(_ rate: String) -> Self
public func maxSize(_ value: String) -> Self
public func minSize(_ value: String) -> Self
public func ioTimeout(_ seconds: Int) -> Self
public func option(_ value: String) -> Self
public func options(_ values: [String]) -> Self
public func command() -> Command
public func run() async throws -> ShellOutput
}
```

#### Archives (Tar / Zip / Unzip)
Expand Down Expand Up @@ -1044,6 +1098,16 @@ try await Brew(context: context).install("ripgrep").run()
// Homebrew — check outdated casks
let outdated = try await Brew(context: context).outdated().greedy().run()

// Rsync — mirror a project directory to a remote host
try await Rsync(context: context)
.archive()
.compress()
.delete()
.exclude(".build")
.source("/path/to/project/")
.destination("deploy@example.com:/srv/project/")
.run()

// Tar — create a gzip-compressed archive from a project directory
try await Tar(context: context)
.create()
Expand Down Expand Up @@ -1288,7 +1352,7 @@ SwiftyShell uses [SwiftPM Package Traits](https://github.com/swiftlang/swift-evo

Declared in `Package.swift`:

- **Per-family** — `Git`, `Brew`, `Grep`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Tar`, `Zip`, `Unzip`. One trait per family directory; for `Common/`, one trait per file.
- **Per-family** — `Git`, `Brew`, `Grep`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Rsync`, `Tar`, `Zip`, `Unzip`. One trait per family directory; for `Common/`, one trait per file.
- **Umbrellas** — `CommonUtilities` (every `Common/*` family), `All` (every command family).

Consumers select families with `traits:` on `.package(...)`:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Compute affected traits
id: compute
run: |
FULL='["", "Git", "Brew", "Grep", "Fzf", "Rg", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]'
FULL='["", "Git", "Brew", "Grep", "Fzf", "Rg", "Rsync", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]'

CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD")
echo "Changed files:"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/reusable-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:
Defaults to the full matrix when omitted.
required: false
type: string
default: '["", "Git", "Brew", "Grep", "Fzf", "Rg", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]'
default: '["", "Git", "Brew", "Grep", "Fzf", "Rg", "Rsync", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]'

jobs:
validate-traits:
Expand Down Expand Up @@ -209,10 +209,10 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Install zip, unzip, fzf, and ripgrep
- name: Install zip, unzip, fzf, ripgrep, and rsync
run: |
apt-get update
apt-get install -y --no-install-recommends zip unzip fzf ripgrep
apt-get install -y --no-install-recommends zip unzip fzf ripgrep rsync

- name: Cache SPM dependencies
uses: actions/cache@v5
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Typed wrapper for the Homebrew package manager: `Brew` and `BrewSubcommand`.

### `Sources/SwiftyShell/Common/`

Typed wrappers for frequently used shell utilities: `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `JqArgument`, `Tar`, `TarOperation`, `TarCompression`, `Zip`, `ZipCompressionLevel`, `Unzip`, and `UnzipEntry`. Each follows the same fluent builder conventions as all other command families.
Typed wrappers for frequently used shell utilities: `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `JqArgument`, `Rsync`, `Tar`, `TarOperation`, `TarCompression`, `Zip`, `ZipCompressionLevel`, `Unzip`, and `UnzipEntry`. Each follows the same fluent builder conventions as all other command families.

### `Sources/SwiftyShell/Internal/Execution/`

Expand Down Expand Up @@ -161,7 +161,7 @@ SwiftyShell uses [SwiftPM Package Traits](https://github.com/swiftlang/swift-evo

**Trait inventory (declared in `Package.swift`):**

- Per-family: `Git`, `Brew`, `Grep`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Tar`, `Zip`, `Unzip` (one trait per family directory; for `Common/`, one trait per file).
- Per-family: `Git`, `Brew`, `Grep`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Rsync`, `Tar`, `Zip`, `Unzip` (one trait per family directory; for `Common/`, one trait per file).
- Umbrellas: `CommonUtilities` (all `Common/*`), `All` (every family).

**The wiring contract** — enforced by `Scripts/validate-traits.swift` and CI:
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ let package = Package(
.trait(name: "Mv", description: "Typed wrapper for mv."),
.trait(name: "Pwd", description: "Typed wrapper for pwd."),
.trait(name: "Jq", description: "Typed wrapper for jq."),
.trait(name: "Rsync", description: "Typed wrapper for rsync file synchronization."),
.trait(name: "Tar", description: "Typed wrapper for tar archives."),
.trait(name: "Zip", description: "Typed wrapper for zip (Info-ZIP)."),
.trait(name: "Unzip", description: "Typed wrapper for unzip (Info-ZIP)."),
// Convenience umbrella that enables every Common/* utility family.
.trait(
name: "CommonUtilities",
description: "Enables all common file/directory utility families.",
enabledTraits: ["Ls", "Cp", "Mkdir", "Chmod", "Rm", "Mv", "Pwd", "Jq", "Tar", "Zip", "Unzip"]
enabledTraits: ["Ls", "Cp", "Mkdir", "Chmod", "Rm", "Mv", "Pwd", "Jq", "Rsync", "Tar", "Zip", "Unzip"]
),
// Convenience umbrella that enables every command family.
.trait(
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ SwiftyShell ships typed wrappers for common tools. Each family is gated behind a
| `Mv` | `mv` | `Mv` | Force |
| `Pwd` | `pwd` | `Pwd` | Physical and logical paths |
| `Jq` | `jq` | `Jq` | Filter expressions, `--arg` bindings, raw output |
| `Rsync` | `rsync` | `Rsync` | Archive/recursive sync, filters, remote shell, deletion, dry runs |
| `Tar` | `tar` | `Tar` | Portable tar archive creation, extraction, listing, compression |
| `Zip` | `zip` | `Zip` | Info-ZIP archive creation, compression, recursion, exclusions |
| `Unzip` | `unzip` | `Unzip` | Info-ZIP archive extraction and structured entry listing |
Expand Down
Loading