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
80 changes: 75 additions & 5 deletions .claude/skills/swiftyshell.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Before writing any code, follow this decision tree:
→ Use `Command("\1", arguments: ...)`
3. Is this a file-system operation covered by a typed wrapper (`Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`)?
→ Use the typed wrapper
4. Is this an archive operation (`zip` to create, `unzip` to extract or list)?
→ Use `Zip` or `Unzip`
4. Is this an archive operation (`tar`, `zip`, or `unzip`)?
→ Use `Tar`, `Zip`, or `Unzip`
5. Is this a `grep` or `jq` operation?
→ Use `Grep` or `Jq`
6. Is this a Homebrew operation (`brew install`, `brew upgrade`, `brew list`, ...)?
Expand Down Expand Up @@ -636,9 +636,68 @@ public struct Pwd: RunnableCommandFamily {
}
```

#### Archives (Zip / Unzip)
#### Archives (Tar / Zip / Unzip)

```swift
public enum TarCompression: Sendable, Equatable, Hashable {
case gzip
case bzip2
case xz
case auto
case custom(String)
}

public enum TarOperation: String, Sendable, Equatable, Hashable {
case create
case extract
case list
case append
case update
}

public struct Tar: RunnableCommandFamily {
public init(context: ShellContext = .init())
public func create() -> Self
public func extract() -> Self
public func list() -> Self
public func append() -> Self
public func update() -> Self
public func operation(_ value: TarOperation) -> Self
public func file(_ path: String) -> Self
public func path(_ value: String) -> Self
public func paths(_ values: [String]) -> Self
public func directory(_ path: String) -> Self
public func gzip() -> Self
public func bzip2() -> Self
public func xz() -> Self
public func autoCompress() -> Self
public func compression(_ value: TarCompression) -> Self
public func exclude(_ pattern: String) -> Self
public func excludes(_ patterns: [String]) -> Self
public func excludeFrom(_ path: String) -> Self
public func filesFrom(_ path: String) -> Self
public func nullTerminatedFiles(_ enabled: Bool = true) -> Self
public func stripComponents(_ count: Int) -> Self
public func toStdout(_ enabled: Bool = true) -> Self
public func verbose(_ enabled: Bool = true) -> Self
public func verify(_ enabled: Bool = true) -> Self
public func removeFilesAfterAdding(_ enabled: Bool = true) -> Self
public func dereferenceSymlinks(_ enabled: Bool = true) -> Self
public func absoluteNames(_ enabled: Bool = true) -> Self
public func preservePermissions(_ enabled: Bool = true) -> Self
public func sameOwner(_ enabled: Bool = true) -> Self
public func noSameOwner(_ enabled: Bool = true) -> Self
public func keepOldFiles(_ enabled: Bool = true) -> Self
public func skipOldFiles(_ enabled: Bool = true) -> Self
public func overwrite(_ enabled: Bool = true) -> Self
public func noRecursion(_ enabled: Bool = true) -> Self
public func oneFileSystem(_ enabled: Bool = true) -> Self
public func option(_ value: String) -> Self
public func options(_ values: [String]) -> Self
public func command() -> Command
public func run() async throws -> ShellOutput
}

public enum ZipCompressionLevel: Sendable, Equatable, Hashable {
case store // -0
case fastest // -1
Expand Down Expand Up @@ -728,7 +787,7 @@ public struct Unzip: RunnableCommandFamily {
}
```

`Zip` and `Unzip` wrap Info-ZIP binaries and behave identically on macOS (preinstalled) and Linux (`apt install zip unzip`). `Unzip.entries()` returns a single-use ``Workflow`` that parses the standard `unzip -l` table — paths with embedded spaces are preserved, missing timestamps surface as `nil`. SwiftyShell does not feed stdin to spawned processes, so unattended extracts should pass `overwrite()` or `neverOverwrite()` to avoid `unzip`'s overwrite prompt hanging the call.
`Tar` wraps the platform tar implementation for portable create, extract, and list workflows. `Zip` and `Unzip` wrap Info-ZIP binaries and behave identically on macOS (preinstalled) and Linux (`apt install zip unzip`). `Unzip.entries()` returns a single-use ``Workflow`` that parses the standard `unzip -l` table — paths with embedded spaces are preserved, missing timestamps surface as `nil`. SwiftyShell does not feed stdin to spawned processes, so unattended extracts should pass `overwrite()` or `neverOverwrite()` to avoid `unzip`'s overwrite prompt hanging the call.

#### Brew

Expand Down Expand Up @@ -985,6 +1044,17 @@ try await Brew(context: context).install("ripgrep").run()
// Homebrew — check outdated casks
let outdated = try await Brew(context: context).outdated().greedy().run()

// Tar — create a gzip-compressed archive from a project directory
try await Tar(context: context)
.create()
.gzip()
.file("/tmp/source.tar.gz")
.directory("/path/to/project")
.exclude(".build")
.path("Sources")
.path("Package.swift")
.run()

// Zip — create a recursive archive at maximum compression
try await Zip(context: context)
.recursive()
Expand Down Expand Up @@ -1218,7 +1288,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`, `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`, `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", "Zip", "Unzip", "CommonUtilities", "All"]'
FULL='["", "Git", "Brew", "Grep", "Fzf", "Rg", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]'

CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD")
echo "Changed files:"
Expand Down
2 changes: 1 addition & 1 deletion .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", "Zip", "Unzip", "CommonUtilities", "All"]'
default: '["", "Git", "Brew", "Grep", "Fzf", "Rg", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]'

jobs:
validate-traits:
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`, `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`, `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`, `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`, `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,13 +29,14 @@ 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: "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", "Zip", "Unzip"]
enabledTraits: ["Ls", "Cp", "Mkdir", "Chmod", "Rm", "Mv", "Pwd", "Jq", "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 |
| `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