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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ called out in a **Breaking** section. See

## [Unreleased]

### Breaking

- **`ferry export` and `ferry import` are now `ferry bundle export` and
`ferry bundle import`.** The two verbs move under a new `bundle` parent noun.
Their flags and behaviour are unchanged — only the command path moves. There
is no alias: the old top-level spellings now error.

### Fixed

- **Release automation self-heals a missing GitHub Release.** When a version's
Expand Down
24 changes: 24 additions & 0 deletions cmd/bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cmd

import (
"github.com/spf13/cobra"
)

var bundleCmd = &cobra.Command{
Use: "bundle",
Short: "Move the config repo offline as a portable bundle",
Long: `Companion commands for moving the config repo offline as a portable bundle.

When you can't clone the repo on the destination — a second user account on the
same machine, or a machine with no network path to the repo — a bundle carries
the setup across instead. "bundle export" writes a portable, secret-scanned .zip
of the repo's tracked shared files and prints its reproducible SHA256;
"bundle import <bundle>" validates that .zip fully and ingests it into a fresh
config repo on the other side. Secrets, ~/.ssh, and the per-machine local layer
are never carried unless you opt in with --include-local.`,
}

func init() {
bundleCmd.AddCommand(exportCmd, importCmd)
rootCmd.AddCommand(bundleCmd)
}
11 changes: 5 additions & 6 deletions cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ secret-scans every text file's content AND every path, and refuses ~/.ssh and
symlink entries. A tracked binary is scanned for embedded private-key markers
and withheld if any are found, otherwise bundled. The result is a self-contained
.zip you move to another account or
machine and ingest with "ferry import". Secrets and the per-machine local layer
are never included unless you pass --include-local. export prints the bundle's
reproducible SHA256 — exporting the same tracked sources always yields the same
digest — so you can verify the move with "ferry import --expect-sha256".`,
machine and ingest with "ferry bundle import". Secrets and the per-machine local
layer are never included unless you pass --include-local. export prints the
bundle's reproducible SHA256 — exporting the same tracked sources always yields
the same digest — so you can verify the move with "ferry bundle import --expect-sha256".`,
Args: cobra.NoArgs,
RunE: runExport,
}
Expand Down Expand Up @@ -153,7 +153,7 @@ func runExport(c *cobra.Command, _ []string) error {
fmt.Fprintf(out, "bundle sha256 (reproducible): %s\n", sha)
fmt.Fprintf(out, "summary: %d file(s) bundled, %d withheld\n", len(sources), len(withheld))
fmt.Fprintln(out, "this sha256 is reproducible: exporting the same tracked sources always yields the same digest (no timestamps or randomness are bundled).")
fmt.Fprintln(out, "convey the sha256 out-of-band; import with `ferry import --expect-sha256 <sha256>` to verify.")
fmt.Fprintln(out, "convey the sha256 out-of-band; import with `ferry bundle import --expect-sha256 <sha256>` to verify.")
return nil
}

Expand Down Expand Up @@ -301,5 +301,4 @@ func isProbablyText(data []byte) bool {
func init() {
exportCmd.Flags().String("out", "", "path to write the bundle (default ./ferry-bundle.zip; must be OUTSIDE the repo)")
exportCmd.Flags().Bool("include-local", false, "also bundle the per-machine local layer (local/**, ferry.local.toml)")
rootCmd.AddCommand(exportCmd)
}
1 change: 0 additions & 1 deletion cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,5 +252,4 @@ func init() {
importCmd.Flags().String("out", "", "target repo directory (default ~/.config/ferry/repo; must be empty/absent)")
importCmd.Flags().String("expect-sha256", "", "verify the bundle's overall SHA256 before importing (out-of-band tamper check)")
importCmd.Flags().Bool("include-local", false, "also import the bundle's local layer (only if it was exported --include-local)")
rootCmd.AddCommand(importCmd)
}
3 changes: 1 addition & 2 deletions docs/reference/cli/ferry.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ ferry [flags]

* [ferry agents](ferry_agents.md) - Onboard project repos and migrate agent-instruction setups
* [ferry apply](ferry_apply.md) - Reconcile this machine to the repo (deploy dotfiles, terminal settings)
* [ferry bundle](ferry_bundle.md) - Move the config repo offline as a portable bundle
* [ferry capture](ferry_capture.md) - Pull local changes back into the repo (interactive, selective)
* [ferry diff](ferry_diff.md) - Preview what apply would change
* [ferry doctor](ferry_doctor.md) - Report machine/tool health
* [ferry export](ferry_export.md) - Write a portable, secret-scanned bundle of the config repo
* [ferry import](ferry_import.md) - Ingest a ferry bundle into a fresh config repo
* [ferry init](ferry_init.md) - First-run setup: locate/clone the config repo and write ferry's config
* [ferry restore](ferry_restore.md) - Reverse ferry's changes, returning to the pre-ferry state from backup
* [ferry status](ferry_status.md) - Report config drift (what changed on this machine)
Expand Down
28 changes: 28 additions & 0 deletions docs/reference/cli/ferry_bundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## ferry bundle

Move the config repo offline as a portable bundle

### Synopsis

Companion commands for moving the config repo offline as a portable bundle.

When you can't clone the repo on the destination — a second user account on the
same machine, or a machine with no network path to the repo — a bundle carries
the setup across instead. "bundle export" writes a portable, secret-scanned .zip
of the repo's tracked shared files and prints its reproducible SHA256;
"bundle import <bundle>" validates that .zip fully and ingests it into a fresh
config repo on the other side. Secrets, ~/.ssh, and the per-machine local layer
are never carried unless you opt in with --include-local.

### Options

```
-h, --help help for bundle
```

### SEE ALSO

* [ferry](ferry.md) - Carries your terminal, dotfiles, and dependencies across machines
* [ferry bundle export](ferry_bundle_export.md) - Write a portable, secret-scanned bundle of the config repo
* [ferry bundle import](ferry_bundle_import.md) - Ingest a ferry bundle into a fresh config repo

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## ferry export
## ferry bundle export

Write a portable, secret-scanned bundle of the config repo

Expand All @@ -11,13 +11,13 @@ secret-scans every text file's content AND every path, and refuses ~/.ssh and
symlink entries. A tracked binary is scanned for embedded private-key markers
and withheld if any are found, otherwise bundled. The result is a self-contained
.zip you move to another account or
machine and ingest with "ferry import". Secrets and the per-machine local layer
are never included unless you pass --include-local. export prints the bundle's
reproducible SHA256 — exporting the same tracked sources always yields the same
digest — so you can verify the move with "ferry import --expect-sha256".
machine and ingest with "ferry bundle import". Secrets and the per-machine local
layer are never included unless you pass --include-local. export prints the
bundle's reproducible SHA256 — exporting the same tracked sources always yields
the same digest — so you can verify the move with "ferry bundle import --expect-sha256".

```
ferry export [flags]
ferry bundle export [flags]
```

### Options
Expand All @@ -30,5 +30,5 @@ ferry export [flags]

### SEE ALSO

* [ferry](ferry.md) - Carries your terminal, dotfiles, and dependencies across machines
* [ferry bundle](ferry_bundle.md) - Move the config repo offline as a portable bundle

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## ferry import
## ferry bundle import

Ingest a ferry bundle into a fresh config repo

Expand All @@ -14,7 +14,7 @@ Pass --expect-sha256 <hash> to verify the bundle against the SHA256 export
printed. Run "ferry apply" afterwards to reconcile this machine.

```
ferry import <bundle> [flags]
ferry bundle import <bundle> [flags]
```

### Options
Expand All @@ -28,5 +28,5 @@ ferry import <bundle> [flags]

### SEE ALSO

* [ferry](ferry.md) - Carries your terminal, dotfiles, and dependencies across machines
* [ferry bundle](ferry_bundle.md) - Move the config repo offline as a portable bundle

5 changes: 3 additions & 2 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ Every command is run as `ferry <command>` (e.g. `ferry init`).
| `doctor` | Report machine/tool health, and observe ferry's managed-target invariants read-only: no deployed target is a symlink (ferry deploys regular-file copies), none resolves under `~/.ssh`, and every target resolves inside `$HOME`. A genuine breach is reported `[fail]` and exits non-zero; a machine with nothing managed yet is advisory. |
| `diff` | Preview what `apply` would change. |
| `restore` | Reverse ferry's changes, returning the machine to its pre-ferry state from an automatic backup. |
| `export` | Write a portable, secret-scanned `.zip` bundle of the repo's tracked shared files for an offline move. Prints the bundle's reproducible SHA256 — exporting the same tracked sources always yields the same digest (no timestamps or randomness are bundled), so the digest can be recomputed and verified with `import --expect-sha256`. Never includes secrets, `~/.ssh`, or the local layer (unless `--include-local`). |
| `import` | Ingest a bundle into a fresh config repo (`~/.config/ferry/repo` by default), validate it fully, then write ferry's config. Refuses a non-empty target. `--expect-sha256 <hash>` verifies integrity. |
| `bundle` | Parent noun for moving the config repo offline as a portable bundle. Its subcommands are `bundle export` and `bundle import`. |
| `bundle export` | Write a portable, secret-scanned `.zip` bundle of the repo's tracked shared files for an offline move. Prints the bundle's reproducible SHA256 — exporting the same tracked sources always yields the same digest (no timestamps or randomness are bundled), so the digest can be recomputed and verified with `bundle import --expect-sha256`. Never includes secrets, `~/.ssh`, or the local layer (unless `--include-local`). |
| `bundle import` | Ingest a bundle into a fresh config repo (`~/.config/ferry/repo` by default), validate it fully, then write ferry's config. Refuses a non-empty target. `--expect-sha256 <hash>` verifies integrity. |
| `agents scaffold` | Set up a project repo for AI-agent work from the templates in the config repo's `agents/` area: an `AGENTS.md` router, `CLAUDE.md`/`GEMINI.md` bridges, and committed `.abcd/work/` memory files. `--private` instead creates a `.abcd/.work.local/` layer hidden via `.git/info/exclude` — for repos you don't own, it leaves zero tracked trace. `--attribution` (mutually exclusive with `--private`) instead installs a `prepare-commit-msg` hook that appends a kernel-style `Assisted-by:` trailer to agent-authored commits, for repos that require AI disclosure. Idempotent; never overwrites or repoints anything it didn't create. Works in linked worktrees and submodules. |
| `agents adopt` | One-time migration of an existing symlink-based instruction setup into the config repo: imports the source files (never modifying the source directory), then swaps each `$HOME` bridge symlink for a ferry-managed copy in a single journalled transaction — any failure rolls back and the symlinks return. Refuses directory-level bridges with exact instructions rather than writing through them. |
| `version` | Print the version; `--verbose` adds the Go version and platform. |
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,22 +480,22 @@ missing refs, and reports any gate block read-only (populate the store, then re-

## What a bundle contains

`ferry export` produces a portable `.zip` for an [offline move](../tutorials/getting-started.md#move-to-another-account-or-machine-offline).
`ferry bundle export` produces a portable `.zip` for an [offline move](../tutorials/getting-started.md#move-to-another-account-or-machine-offline).
It carries the **shared layer only**: the repo's git-tracked shared files (never
untracked, ignored, or editor-backup files). It never carries secrets, anything under
`~/.ssh/`, or the per-machine `.local` layer.

The per-machine local layer (`local/**`, `ferry.local.toml`) is **gitignored by
design**, so it is not tracked and does not enter the bundle. `--include-local` adds
back the local-layer files that are actually **tracked** in the repo (the same
`git ls-files` set export collects from) — so it bundles a local-layer file only if you
`git ls-files` set `bundle export` collects from) — so it bundles a local-layer file only if you
have force-tracked it. The local layer lands on the other side only when you pass
`--include-local` on **both** export and import (a double opt-in).
`--include-local` on **both** `bundle export` and `bundle import` (a double opt-in).

Every included text file's content is secret-scanned, and every entry path is scanned
for secret-shaped components; a binary is scanned for embedded private-key markers.
Anything that trips these is withheld and reported, not bundled — symmetrically on
import, which re-runs the same checks.
`bundle import`, which re-runs the same checks.

## Related

Expand Down
16 changes: 8 additions & 8 deletions docs/tutorials/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,27 +188,27 @@ or run `ferry capture` to save the current file into the repo before applying.

When you can't clone the repo on the destination: a second user account on the same Mac,
or a machine with no network path to your repo: move a self-contained bundle instead.
`ferry export` writes a secret-scanned `.zip` of the repo's tracked shared files;
`ferry import` ingests it into a fresh config repo on the other side.
`ferry bundle export` writes a secret-scanned `.zip` of the repo's tracked shared files;
`ferry bundle import` ingests it into a fresh config repo on the other side.

```bash
# On the source machine/account:
ferry export --out /Users/Shared/ferry-bundle.zip # prints the bundle path + SHA256
ferry bundle export --out /Users/Shared/ferry-bundle.zip # prints the bundle path + SHA256

# On the destination machine/account, after moving the zip there:
ferry import --expect-sha256 <sha256> /Users/Shared/ferry-bundle.zip
ferry bundle import --expect-sha256 <sha256> /Users/Shared/ferry-bundle.zip
ferry apply # reconcile the destination
```

**Same-Mac, two accounts:** account B usually cannot read account A's home directory, so
write the bundle somewhere both accounts can reach: `--out /Users/Shared/ferry-bundle.zip`,
or move it via a USB drive or AirDrop. Convey the SHA256 that `export` printed
or move it via a USB drive or AirDrop. Convey the SHA256 that `bundle export` printed
separately (a message, not the same channel as the file) and pass it to
`import --expect-sha256` so a tampered or corrupt bundle is refused. `import` writes into
`~/.config/ferry/repo` by default and refuses a non-empty target.
`bundle import --expect-sha256` so a tampered or corrupt bundle is refused. `bundle import`
writes into `~/.config/ferry/repo` by default and refuses a non-empty target.

The bundle never contains secrets, anything under `~/.ssh/`, or the per-machine local
layer (unless you pass `--include-local` on **both** export and import). SSH keys stay a
layer (unless you pass `--include-local` on **both** `bundle export` and `bundle import`). SSH keys stay a
manual copy you make yourself: see [Move SSH keys yourself](../how-to/move-ssh-keys.md).

---
Expand Down
Loading
Loading