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
2 changes: 1 addition & 1 deletion .github/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ GitHub's config for this repo: CI, releases, Pages, dependabot, issue forms, PR

| Path | Purpose |
| --- | --- |
| `workflows/ci.yml` | push-main + PR gate. A `changes` job diffs the base against the head and gates every other job by area; `ci pass` aggregates them |
| `workflows/ci.yml` | push-main + PR gate. A `changes` job diffs the base against the head and gates the other jobs by area (`docs checks` always runs); `ci pass` aggregates them |
| `workflows/release.yml` | on `v*` tag: check the tag against `MARKETING_VERSION` and `CHANGELOG.md`, then (environment `release`) sign, notarize, write the Sparkle appcast, attest, publish. `workflow_dispatch` is a rehearsal that publishes nothing |
| `workflows/go-weekly.yml` | Monday run of the Go checks, so new govulncheck findings surface without a code change, plus `go test -fuzz` for 30 s per `Fuzz*` target (a failing input is uploaded as the `fuzz-failures` artifact); files or updates one `security` issue on failure |
| `workflows/github-pages.yml` | builds `_site` on PR, also deploys on push to main |
Expand Down
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ harness output: the summary lines and any failures, not the whole log
## Docs

- [ ] User-visible change: a line under `## [Unreleased]` in `CHANGELOG.md`, written for someone deciding whether to update. That section becomes the release notes verbatim
- [ ] Durable knowledge: `docs/knowledge/` or an `AGENTS.md`
- [ ] Internals changed: the docs that describe them updated (root `AGENTS.md`, "Docs")
- [ ] Defect fixed or accepted: its `docs/known-issues.md` line deleted or added
- [ ] Nothing doc-worthy
- [ ] `scripts/verify-docs.sh` passes

## UI changes

Expand Down
20 changes: 18 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ jobs:

- run: shellcheck --version && shellcheck -x scripts/*.sh

# Unconditional: renaming or deleting any file can break a path a doc names,
# and the check takes under a second.
docs:
name: docs checks

runs-on: ubuntu-26.04

timeout-minutes: 5

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- run: scripts/verify-docs.sh

# The interop harness against a relay on localhost, never the public relays:
# those rate-limit a shared runner address and make the run flaky. The
# public-pool scenario stays a local run (docs/BUILDING.md).
Expand Down Expand Up @@ -152,7 +168,7 @@ jobs:
v=$(cd crocmobile && go list -m -f '{{.Version}}' github.com/schollz/croc/v11)
GOBIN="$RUNNER_TEMP/croc11" go install "github.com/schollz/croc/v11@$v"

# The R8 scenario's croc v10 peer.
# The croc v10 peer.
- run: go install "github.com/schollz/croc/v10@$(cat scripts/croc-v10.version)"

# Admission limits raised as a margin: every connection comes from
Expand Down Expand Up @@ -281,7 +297,7 @@ jobs:
pass:
name: ci pass

needs: [changes, swift-format, go, contrast, shellcheck, interop, build]
needs: [changes, swift-format, go, contrast, shellcheck, docs, interop, build]

if: ${{ always() }}

Expand Down
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ Native SwiftUI GUI for the croc file-transfer CLI.

Do every change on a new branch and land it through a pull request (merge rules: `.github/AGENTS.md`). GitHub deletes the remote branch when the PR merges. Then delete the local branch: `git switch main && git pull --ff-only && git branch -D BRANCH`. Use `-D`, because a squash merge leaves the branch's commits out of `main`, so `-d` refuses.

## Verification

A green build is not evidence a transfer works. A change to `crocmobile/session.go`, `CrocKit/Sources/` or `TransferController` needs its matching transfer harness run, with the output quoted (`docs/BUILDING.md`, "Verification harnesses"). Anything not run is reported as "not verified".

## Docs

`docs/README.md` indexes the contributor docs and `docs/AGENTS.md` holds their writing rules. A change is finished when the docs that describe it match it:

| You changed | Bring in step |
| --- | --- |
| `crocmobile/` or `CrocKit/` | `docs/knowledge/crocmobile-bridge.md`, `docs/ARCHITECTURE.md` |
| App models, views or settings | `docs/knowledge/app-ui-architecture.md`, `docs/knowledge/features.md` |
| Anything a user sees | `CHANGELOG.md` (`## [Unreleased]`), the docs site page in `web/docs/content/` |
| `web/landing/` | `docs/knowledge/landing-page.md` |
| Scripts, CI, toolchain pins | `docs/BUILDING.md`, `docs/knowledge/tooling.md` |
| Signing, Sparkle, store builds or listing | `docs/RELEASING.md`, `docs/knowledge/app-store-listing.md` |
| The croc version | `docs/knowledge/croc-upgrade-playbook.md` (it lists the rest) |
| Security-relevant behavior | `docs/THREAT-MODEL.md` |
| A defect found and left, or fixed | `docs/known-issues.md` |

Run `scripts/verify-docs.sh` after editing markdown; CI runs it.

## Design

Anything a user sees (app views, the landing page, the docs site, release artwork, store copy, UI strings) follows `design/`. Start at `design/AGENTS.md`. Run `scripts/verify-design.sh` after touching any of it; CI runs it.
Expand Down
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ extra sections where a release needs them, and the project follows

## [1.0.0] - 2026-10-15

CrocApp 1.0 is on three channels: the iOS and iPadOS App Store, the Mac App Store, and a notarized disk image for Mac. The App Store listings are linked from crocapp.dev. The other device needs croc 11 or newer.
CrocApp 1.0 is a notarized disk image for Mac. The iPhone, iPad and Mac App Store builds are in TestFlight and follow with the first App Store release, so the iPhone and iPad items below reach users then. The other device needs croc 11 or newer.

**iOS 26, iPadOS 26 or macOS 26 or later. The disk image is Apple silicon only.**
**macOS 26 or later, Apple silicon only.**

Verify a download with `gh attestation verify <file> --repo bakirgdev/CrocApp`: the release workflow attests the disk image, the update feed and the engine archive after stapling, so the attestation covers the bytes you download. `SHA256SUMS.txt` lists the SHA-256 checksums of the disk image and the engine archive.

### Added

- iPhone and iPad apps through the App Store, and a Mac App Store build. 0.9.9 was macOS only.
- iPhone and iPad apps and a Mac App Store build, in TestFlight until the first App Store release. 0.9.9 was macOS only.
- The macOS direct download is now signed with a Developer ID certificate, notarized and stapled, so it opens with only the standard first-launch confirmation.
- The direct download updates itself with Sparkle: choose "Check for Updates…" in the app menu. Sparkle installs an update only when its EdDSA signature matches the key built into the app, and the update check sends no system profile. Only this build has Sparkle; the App Store builds update through the App Store.
- Receiving a file whose name is already taken now offers Keep Both (saves the new one as `name (1).ext`) or Replace. With auto-accept on, the setting "When a file already exists" under Confirmation chooses; it defaults to Keep Both.
Expand All @@ -45,7 +45,7 @@ Verify a download with `gh attestation verify <file> --repo bakirgdev/CrocApp`:
- The transfer engine is croc v11.5.4. A croc 10 device can no longer complete a transfer with CrocApp, in either direction: the other device needs croc 11 or newer. Relays are picked from croc's public relay pool the way the croc 11 command line does, and a send fails with a clear message when no public relay answers.
- Failures say what went wrong: a wrong code, a refused transfer, a lost connection, a peer on an older croc, or no public relay available, instead of one generic message.
- History keeps less of a code: the first word of a code shaped like a generated one, only the first two characters of any other code, and only the server's host for Send as link. It never keeps the whole code.
- Every 1.0 screen works at the largest text sizes and with VoiceOver, including the transfer percentage.
- Every 1.0 screen works at the largest text sizes, including the transfer percentage.
- The QR code on the sender's waiting screen now encodes a `https://crocapp.dev/r/` link instead of `croc://<code>`, so a phone camera opens CrocApp, or a page that explains what to do when the app is missing. QR codes from 0.9.9 still scan in the app.

### Removed
Expand Down
42 changes: 39 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ git clone https://github.com/bakirgdev/CrocApp.git
cd CrocApp
```

Also needed: `croc` CLI (`brew install croc`) and outbound network access to the public relay.
Also needed: `croc` CLI (`brew install croc`; `croc --version` must match the pin in `crocmobile/go.mod`, otherwise `go install github.com/schollz/croc/v11@<pin>`) and outbound network access to the public relay.

The first build, the full toolchain table and every check are in [`docs/BUILDING.md`](docs/BUILDING.md). How the code fits together: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).

## Commits

Expand Down Expand Up @@ -86,7 +88,7 @@ govulncheck ./...

- **Comments explain why, never what.** Do not delete existing comments unless the code they describe is going away.
- **Stay in scope.** No drive-by refactors, no renaming unrelated symbols, no reformatting files your change does not touch.
- **No new third-party dependencies** without agreeing on them in an issue first. New dependency may end up shipped somewhere that moves people's files!
- **No new third-party dependencies** without agreeing on them in an issue first. A dependency ships inside an app that reads and writes people's files.

## Design rules

Expand All @@ -102,11 +104,45 @@ A failure means your change or `design/` is wrong. Fix one of them, never the ch

UI pull requests need **before and after screenshots in both light and dark**, and on both platforms if both are affected.

## Verification

A build that compiles proves nothing about a transfer. CI runs formatting, lint, the builds, the unit tests and `verify-interop.sh` against a local relay. Everything else that applies to your change is yours to run:

| What you changed | What to run |
|---|---|
| Swift under `app/` or `CrocKit/` | `scripts/format-swift.sh --lint`, the unit tests, a macOS and an iOS Simulator build |
| Anything in `crocmobile/` | `go test -race ./...`, `golangci-lint run ./...`, then `scripts/build-xcframework.sh` |
| `crocmobile/session.go`, `CrocKit/Sources/` or `TransferController` | the matching transfer harness, every time |
| Anything visual | `scripts/verify-design.sh` |
| Markdown | `scripts/verify-docs.sh` |

The commands, and what each harness covers and needs, are in [`docs/BUILDING.md`](docs/BUILDING.md) under "Format and lint" and "Verification harnesses".

### Tests

Non-UI logic gets unit tests: Go's `testing` in `crocmobile/`, Swift Testing in `CrocKit/Tests/` and `app/CrocAppTests/`. Tests that write settings or secrets use an injected store, never the real `UserDefaults` or Keychain. There is no XCUITest: gestures, the camera and taps are checked by hand, and transfers by the harnesses. A unit test never replaces a harness run.

**Paste the harness output into the pull request**, the summary lines and any failures. If you could not run something, write **"not verified"** and say why. That is an acceptable answer, and the maintainer runs it before merge. A claim that something passed when it never ran is not.

## Docs

Documentation is part of the change. Before you open the pull request:

| What happened | Where it goes |
|---|---|
| A user-visible change | A line under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md), written for someone deciding whether to update |
| You changed how something works inside | The doc that describes it: [`docs/AGENTS.md`](docs/AGENTS.md) says which file owns which fact |
| You fixed a listed defect | Delete its item from [`docs/known-issues.md`](docs/known-issues.md) |
| You found a defect and left it | Add an item to `docs/known-issues.md` |
| You noticed a stale or wrong doc | Fix or delete it in the same pull request |

Docs state what is true now, without dates or tracking IDs; [`docs/AGENTS.md`](docs/AGENTS.md) has the writing rules. Run `scripts/verify-docs.sh` after editing any markdown.

## AI-assisted contributions

Allowed, and used heavily here. Two conditions:

- **Disclose it.** Own the responsibility to report even if we live in the day and age where it may be considered as norm. Fill in the AI assistance line in the PR template. It tells the reviewer where to look harder, and nobody is judged for it.
- **Disclose it.** Fill in the AI assistance line in the PR template, even though AI use is common. You are responsible for reporting it. It tells the reviewer where to look harder, and nobody is judged for it.
- **You own the code.** You understand every line, you can explain why each one is there, and you ran all verifications and checks yourself. "The model said it works" is not verification.

Bulk-generated pull requests that the author has not read, does not understand, or cannot verify get closed without review.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<p align="center">
<strong>Send a file to any device, anywhere, with a code phrase.</strong>
<br>
A free, open-source native SwiftUI app for iPhone, iPad, and Mac.
A free, open-source native SwiftUI app for Mac, with iPhone and iPad arriving in the App Store release.
</p>

<p align="center">
Expand Down Expand Up @@ -32,7 +32,7 @@ It is a graphical front end for [croc](https://github.com/schollz/croc), the com
| Mac (Apple silicon only) | `CrocApp-<version>-arm64.dmg` from [GitHub Releases](https://github.com/bakirgdev/CrocApp/releases/latest) |
| Mac (Homebrew cask) | Not available yet. Homebrew accepts a cask once the repo shows [enough public interest](https://docs.brew.sh/Package-Acceptance-Policy#notability): 225 stars, 90 watchers, or 90 forks. Starring the repo helps |

The disk image is Developer ID signed, notarized, and updates itself through Sparkle (**Check for Updates…** in the app menu). Verify it with `gh attestation verify <file> --repo bakirgdev/CrocApp` ([`SECURITY.md`](SECURITY.md)). Install guide: [crocapp.dev/docs/getting-started/install](https://crocapp.dev/docs/getting-started/install/). Or [build from source](#build-from-source).
The disk image is Developer ID signed, notarized, and updates itself through Sparkle (**Check for Updates…** in the app menu). Verify it with `gh attestation verify <file> --repo bakirgdev/CrocApp` ([`SECURITY.md`](SECURITY.md)). Install guide: [crocapp.dev/docs/getting-started/install](https://crocapp.dev/docs/getting-started/install/). Or [build from source](docs/BUILDING.md).

## How it works

Expand Down Expand Up @@ -132,7 +132,7 @@ Transfers are end-to-end encrypted, the code phrase authenticates both devices (
| Exclude patterns, respect `.gitignore` | `--exclude --git` |
| Auto-accept transfers, off by default | `--yes` |
| Require confirmation on both sides | `--ask` |
| Connection: Automatic or Relay only | `--transport auto\|relay` |
| Connection: Automatic or Relay Only | `--transport auto\|relay` |

</details>

Expand Down
2 changes: 1 addition & 1 deletion app/CrocApp/Models/AdvancedSettingsValidation.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import Network

/// Checks for the Advanced fields (R25). Each mirrors what the engine accepts
/// Checks for the Advanced fields. Each mirrors what the engine accepts
/// (`crocmobile/network.go`, and croc's own parsing for the upload limit) and
/// returns plain-words text, or nil when the value is fine. Blank is always
/// fine: it means croc's default. Proxy texts never repeat the value, which can
Expand Down
2 changes: 1 addition & 1 deletion app/CrocApp/Models/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class AppRouter {
/// URLs waiting for SendView to pick up into its staged list.
var pendingSendURLs: [URL] = []
/// A receive link waiting for ReceiveView to prefill from it. Opening a
/// link never connects (R22): the user taps Receive.
/// link never connects: the user taps Receive.
var pendingReceive: TransferLink?

/// Weak: AppRouter.shared is constructed before TransferController
Expand Down
14 changes: 7 additions & 7 deletions app/CrocApp/Models/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import CrocKit
import Foundation
import Observation

/// Persisted power options (F13-F19). String relay fields store "" for
/// Persisted power options. String relay fields store "" for
/// "use croc default" and go to the engine as stored; the engine resolves
/// defaults and the public relay pool. `persist` gates UserDefaults writes so AutoVerify can override per-run
/// without contaminating real settings. didSet does not fire for the
/// assignments in init. `relayPassword` is the one exception to "UserDefaults":
/// it lives in the Keychain (see `KeychainStore`) and `persist` gates that
/// write too, so a harness run's `resetToDefaults()` can't blank the real
/// stored password. The Advanced proxies (R25) are the same: they can carry
/// stored password. The Advanced proxies are the same: they can carry
/// credentials, so they live in the Keychain and never in UserDefaults.
@MainActor
@Observable
Expand All @@ -24,7 +24,7 @@ final class AppSettings {
case publicDefault
case custom(String)
case localOnly
/// A receive that connects straight to the sender's IP (R25); no relay.
/// A receive that connects straight to the sender's IP; no relay.
case directToSender
}

Expand All @@ -50,15 +50,15 @@ final class AppSettings {
var transport: Transport {
didSet { save(transport.rawValue, "settings.transport") }
}
/// Where "Send as link" uploads (R21). The engine refuses `http` except
/// Where "Send as link" uploads. The engine refuses `http` except
/// on loopback.
var storedServer: String { didSet { save(storedServer, "settings.storedServer") } }
static let defaultStoredServer = StoredSendOptions().server
/// macOS menu-bar quick send (R26). On by default; the menu-bar scene is
/// macOS menu-bar quick send. On by default; the menu-bar scene is
/// inserted only while this is true.
var showInMenuBar: Bool { didSet { save(showInMenuBar, "settings.showInMenuBar") } }

// Advanced (R25). Defaults are croc's, read from `EngineOptions`.
// Advanced. Defaults are croc's, read from `EngineOptions`.
static let advancedDefaults = EngineOptions()

var curve: Curve { didSet { save(curve.rawValue, "settings.advanced.curve") } }
Expand Down Expand Up @@ -89,7 +89,7 @@ final class AppSettings {
didSet { persistProxy(httpProxy, account: Self.httpProxyAccount) }
}

/// Why `text` cannot be a stored-transfer server, or nil when it can (R12e).
/// Why `text` cannot be a stored-transfer server, or nil when it can.
/// Mirrors the engine: `https`, or `http` on loopback only, and nothing
/// after the host.
static func storedServerError(_ text: String) -> String? {
Expand Down
2 changes: 1 addition & 1 deletion app/CrocApp/Models/LoginItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ protocol LoginItemService {

extension SMAppService: LoginItemService {}

/// Open at login (R26): the toggle shows what the system reports, never a
/// Open at login: the toggle shows what the system reports, never a
/// stored copy of what we asked for, because the user can change it in System
/// Settings > General > Login Items at any time.
@MainActor
Expand Down
2 changes: 1 addition & 1 deletion app/CrocApp/Models/MenuBarQuickSend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import AppKit
import Observation

/// What the menu-bar window (R26) does on the user's behalf. A thin layer over
/// What the menu-bar window does on the user's behalf. A thin layer over
/// the one `TransferController`: it never holds transfer state of its own, so
/// the window shows a transfer started anywhere in the app.
@MainActor
Expand Down
2 changes: 1 addition & 1 deletion app/CrocApp/Models/OutputFolderStore.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import Observation

/// Receive destination folder (F7). Default: app Documents on iOS,
/// Receive destination folder. Default: app Documents on iOS,
/// Downloads/CrocApp on macOS. User override persists
/// via bookmark: security-scoped on macOS, plain on iOS (fileImporter URLs
/// are implicitly provider-scoped there; .withSecurityScope is macOS-only).
Expand Down
2 changes: 1 addition & 1 deletion app/CrocApp/Models/StoredTransferVault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import CrocKit
import Foundation
import OSLog

/// Receipts of this device's stored uploads (R19). Each carries the upload
/// Receipts of this device's stored uploads. Each carries the upload
/// token (the right to revoke) and the link (whose fragment is the decryption
/// key), so they live only in the Keychain. All receipts share one item as a
/// JSON array, because `SecretStore` cannot list accounts. A receipt leaves
Expand Down
Loading
Loading