diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 548b841..8c5da07 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: true contact_links: - name: Troubleshooting - url: https://github.com/ado11231/ss/blob/master/docs/TROUBLESHOOTING.md + url: https://github.com/ado11231/ally/blob/master/docs/TROUBLESHOOTING.md about: Most error messages are explained here, with the fix. diff --git a/.github/ISSUE_TEMPLATE/problem.md b/.github/ISSUE_TEMPLATE/problem.md index c15736e..3a8ae0c 100644 --- a/.github/ISSUE_TEMPLATE/problem.md +++ b/.github/ISSUE_TEMPLATE/problem.md @@ -24,4 +24,4 @@ Paste the full output here. * Client operating system: * Agent operating system: * How they connect: same network, tailnet, or different networks -* Output of `slingshot health`, if the Agent can be reached: +* Output of `ally health`, if the Agent can be reached: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7f8b7cf..799fec2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,7 +12,7 @@ ## Docs -- [ ] [USAGE.md](https://github.com/ado11231/ss/blob/master/docs/USAGE.md) for a new or changed command or option. -- [ ] [TROUBLESHOOTING.md](https://github.com/ado11231/ss/blob/master/docs/TROUBLESHOOTING.md) for a new or changed message. -- [ ] The [file reference](https://github.com/ado11231/ss/blob/master/docs/ARCHITECTURE.md#file-reference) for a new or moved file. -- [ ] [ROADMAP.md](https://github.com/ado11231/ss/blob/master/docs/ROADMAP.md) for what now works, with the date. +- [ ] [USAGE.md](https://github.com/ado11231/ally/blob/master/docs/USAGE.md) for a new or changed command or option. +- [ ] [TROUBLESHOOTING.md](https://github.com/ado11231/ally/blob/master/docs/TROUBLESHOOTING.md) for a new or changed message. +- [ ] The [file reference](https://github.com/ado11231/ally/blob/master/docs/ARCHITECTURE.md#file-reference) for a new or moved file. +- [ ] [ROADMAP.md](https://github.com/ado11231/ally/blob/master/docs/ROADMAP.md) for what now works, with the date. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3714a9..80c8165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,4 +33,4 @@ jobs: run: cargo test --workspace - name: Menu bar app if: runner.os == 'macOS' - run: swift build -c release --package-path crates/slingshot-cli/menubar + run: swift build -c release --package-path crates/ally-cli/menubar diff --git a/.gitignore b/.gitignore index 9a3277e..158e3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target -/crates/slingshot-cli/menubar/.build +/crates/ally-cli/menubar/.build diff --git a/AGENTS.md b/AGENTS.md index 479b07d..8253070 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ 1. [Accountability](#1-accountability) 2. [Read First](#2-read-first) -3. [What Slingshot Is](#3-what-slingshot-is) +3. [What Ally Is](#3-what-ally-is) 4. [Rules That Are Never Broken](#4-rules-that-are-never-broken) 5. [Out Of Scope](#5-out-of-scope) 6. [Before Writing Code](#6-before-writing-code) @@ -51,13 +51,13 @@ --- -## 3. What Slingshot Is +## 3. What Ally Is -* Slingshot lets a light machine use the CPU, RAM, and GPU of a powerful one. +* Ally lets a light machine use the CPU, RAM, and GPU of a powerful one. * The user keeps their own editor, terminal, and browser. Builds, servers, databases, containers, coding agents, and small AI models run on the powerful machine. * It must feel local. There is no virtual machine and no remote desktop. * **Goal:** a public open source tool a stranger can install and set up in minutes. Setup friction is a bug. -* **Name:** Slingshot. The command is `slingshot`. It was called borrow until September 22, 2026. +* **Name:** Ally. The command is `ally`. It was called Slingshot until October 16, 2026, and borrow until September 22, 2026. * **Languages:** Rust, except the macOS menu bar app, which is SwiftUI. * **Roles:** the **Client** is the machine the user works on. The **Agent** is the machine with the resources. @@ -67,7 +67,7 @@ 1. **The Client stays light.** Never move heavy work onto the Client. If a change makes the Client do real work, the change is wrong. 2. **Always state where work runs.** Every remote command prints its location, such as `▶ Running on archbox via tailnet`. -3. **Wrap existing tools. Never rebuild them.** Slingshot wraps `ssh`, `rsync`, `tmux`, `docker`, `ollama`, `nvidia-smi`, and the `iroh` library. Never write your own ssh, file transfer, terminal multiplexer, container engine, inference engine, relay server, or NAT traversal. +3. **Wrap existing tools. Never rebuild them.** Ally wraps `ssh`, `rsync`, `tmux`, `docker`, `ollama`, `nvidia-smi`, and the `iroh` library. Never write your own ssh, file transfer, terminal multiplexer, container engine, inference engine, relay server, or NAT traversal. 4. **Keep setup simple.** One program per machine, one pairing step, one command to use. Reject designs that add setup steps. 5. **Stay platform neutral.** Describe everything as Client and Agent, never as Mac and Linux. Mac to Linux and Linux to Linux must both work. Only the menu bar app may be macOS specific. 6. **Design for a stranger.** Anything easy only on the author's machine becomes a setup check or an installer step. Every failed check prints the exact fix. Detect and instruct. Never install without asking: show exactly what will run, then ask once. @@ -87,7 +87,7 @@ | Treating environment files as source | `.env`, `.env.*`, `*.env`, and `.envrc` never enter a sync, and their contents never appear in arguments. | | Assuming a package manager | The main test Agent runs Arch Linux, which uses `pacman`, not `apt`. Detect and instruct. | | Fixed system paths in shared code | Never write `/Users/...` or `/home/...`. Use the `directories` crate. | -| New operations on the pairing port | The TCP port only pairs. Every other request uses `slingshot internal-control` over ssh. | +| New operations on the pairing port | The TCP port only pairs. Every other request uses `ally internal-control` over ssh. | | A custom way to run commands | ssh carries all work. `run`, `attach`, and `rsync` always go through it. | --- @@ -97,7 +97,7 @@ 1. **Understand the request.** If it is unclear or has more than one meaning, ask. Never guess about anything that changes behavior, deletes data, or affects security. 2. **Read the code you will change, and its callers.** Find both in the [file reference](docs/ARCHITECTURE.md#file-reference). Never edit a file you have not read. 3. **Look for existing helpers.** Search before writing anything new. See the table below. -4. **Choose the right crate.** Rules both machines apply go in `slingshot-core`, Agent only code in `slingshot-agent`, and commands and Client code in `slingshot-cli`. `slingshot-cli` uses the other two, `slingshot-agent` uses `slingshot-core`, and `slingshot-core` uses neither. +4. **Choose the right crate.** Rules both machines apply go in `ally-core`, Agent only code in `ally-agent`, and commands and Client code in `ally-cli`. `ally-cli` uses the other two, `ally-agent` uses `ally-core`, and `ally-core` uses neither. 5. **Plan the smallest change** that solves the problem and keeps everything working end to end. | Need | Existing Helper | @@ -130,7 +130,7 @@ * Match the surrounding code's names, structure, and idioms. * Use plain English names. Never names like `mgr`, `ctx2`, `tmp_data`, or `handle_stuff`. * Errors use `anyhow`. Every visible error says what went wrong and what to do, in one sentence, with the exact command when there is one. -* A failing user command is not a Slingshot error. Report its exit code faithfully. +* A failing user command is not an Ally error. Report its exit code faithfully. * Never use `unwrap()` outside tests. Use `?`, or `expect("reason this cannot fail")` for true invariants only. * Never use `unsafe` unless existing code already does the same thing, and explain why. * Never add `#[allow(...)]` to silence a warning. Fix the cause, or ask. @@ -149,9 +149,9 @@ ### Formats And Versions -* **Control messages:** any change to a `Request` or `Response` shape in `slingshot-core/src/control.rs` raises `control::VERSION`, currently 14. -* **Menu bar lines:** if a field in `slingshot-cli/src/watch/event.rs` changes meaning or is removed, raise `watch::event::VERSION` and change `slingshot-cli/menubar/Sources/Model.swift` in the same commit. -* **Pairing messages:** a change to the pairing messages in `slingshot-core/src/protocol.rs` means both machines must update before the next link. A change to `Specs` or `Health`, which are saved or sent after pairing, forces every user to link again. Say which one explicitly. +* **Control messages:** any change to a `Request` or `Response` shape in `ally-core/src/control.rs` raises `control::VERSION`, currently 14. +* **Menu bar lines:** if a field in `ally-cli/src/watch/event.rs` changes meaning or is removed, raise `watch::event::VERSION` and change `ally-cli/menubar/Sources/Model.swift` in the same commit. +* **Pairing messages:** a change to the pairing messages in `ally-core/src/protocol.rs` means both machines must update before the next link. A change to `Specs` or `Health`, which are saved or sent after pairing, forces every user to link again. Say which one explicitly. ### Security @@ -169,7 +169,7 @@ ### The Menu Bar App -* Swift only displays information and posts notifications. All thresholds, rules, and wording live in Rust, in `crates/slingshot-cli/src/watch/`. +* Swift only displays information and posts notifications. All thresholds, rules, and wording live in Rust, in `crates/ally-cli/src/watch/`. * Confirm an SF Symbol name exists before using it. A wrong name draws nothing and reports no error. * macOS only Rust code sits behind `#[cfg(target_os = "macos")]`, with a clear message on other systems. @@ -193,12 +193,12 @@ ## 8. Output Style -* All output goes through `slingshot-core/src/presentation.rs` and `slingshot-core/src/step.rs`. +* All output goes through `ally-core/src/presentation.rs` and `ally-core/src/step.rs`. * Slow work is a step: a spinner in a terminal, then a line such as `✓ Synced 3 files 0.4s`. * Status symbols: `✓`, `!`, `✗`, and `▶`. * Color marks only what needs attention: symbols, the Agent and path in `▶` lines, and warning or failing values. Healthy values stay plain, headings are bold, and details are dimmed. * Messages use sentence case. CPU, RAM, GPU, and VRAM are always capitals. -* `--color auto|always|never` controls color. Slingshot's options come before `run`. Everything after `run` belongs to the user's command. +* `--color auto|always|never` controls color. Ally's options come before `run`. Everything after `run` belongs to the user's command. --- @@ -215,7 +215,7 @@ cargo test --workspace * If you changed the menu bar app, also run: ```sh -swift build -c release --package-path crates/slingshot-cli/menubar +swift build -c release --package-path crates/ally-cli/menubar ``` * Every rule or decision you add or change gets a unit test in the same file's `tests` module. @@ -255,7 +255,7 @@ swift build -c release --package-path crates/slingshot-cli/menubar * Commit messages follow `type: summary`, with type `feat`, `fix`, `refactor`, `docs`, or `test`. The body explains why. * Make small commits that each build and pass tests. * **No AI attribution.** Never add `Co-Authored-By` lines, "Generated with" lines, or tool names. Commits belong to the person who asked for them. -* Never commit secrets, build output, or anything in `crates/slingshot-cli/menubar/.build/`. +* Never commit secrets, build output, or anything in `crates/ally-cli/menubar/.build/`. --- @@ -310,6 +310,6 @@ swift build -c release --package-path crates/slingshot-cli/menubar * **Phases 1, 3, and 4 are complete.** Phase 5 is in progress: the menu bar app, notifications, and the fixes from the October 7, 2026 fresh install test are built, and automatic port forwarding is not. * **Next:** port forwarding (#9), then local models (#20, #38 to #40). Every finding from the October 7, 2026 fresh install test is fixed. * **All tests, formatting, and Clippy pass on `master`.** CI checks every pull request on Linux and macOS. -* **The rename from borrow has no migration.** Both machines must run `slingshot link` again after updating. +* **The renames from borrow and Slingshot have no migration.** Both machines must run `slingshot uninstall`, install Ally, then run `ally link`. * Everything under [known limitations](docs/ROADMAP.md#known-limitations) does not work yet. Never describe it as working. * The full [test record](docs/ROADMAP.md#test-record) is in the roadmap. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 43c994c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8782e14..b47818c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -* How to set up Slingshot, make a change, and get it merged. +* How to set up Ally, make a change, and get it merged. ## Contents @@ -26,20 +26,20 @@ | Tool | Machine | Purpose | | --- | --- | --- | -| Rust, latest stable | Both | Builds Slingshot. | +| Rust, latest stable | Both | Builds Ally. | | `rsync` | Both | Copies project files. | | An ssh server | Agent | Carries all work and requests. | | `tmux` | Agent | Keeps sessions running. | | Xcode command line tools | Mac Client | Builds the menu bar app. | ```sh -git clone https://github.com/ado11231/ss.git slingshot -cd slingshot -cargo install --path crates/slingshot-cli -slingshot menubar +git clone https://github.com/ado11231/ally.git ally +cd ally +cargo install --path crates/ally-cli +ally menubar ``` -* `cargo install --path` installs your build as `slingshot`. `slingshot menubar` builds the app on a Mac, and rebuilds it when its source changed. +* `cargo install --path` installs your build as `ally`. `ally menubar` builds the app on a Mac, and rebuilds it when its source changed. ## Building And Testing @@ -51,24 +51,24 @@ cargo clippy --workspace --all-targets cargo test --workspace ``` -* After changing the menu bar app, also run `swift build -c release --package-path crates/slingshot-cli/menubar`. +* After changing the menu bar app, also run `swift build -c release --package-path crates/ally-cli/menubar`. * CI runs the same checks on Linux and macOS for every pull request. -* Unit tests sit in a `tests` module at the bottom of the file they test. Tests of the finished program are in `crates/slingshot-cli/tests/`. +* Unit tests sit in a `tests` module at the bottom of the file they test. Tests of the finished program are in `crates/ally-cli/tests/`. * Every rule or decision needs a test, such as which files are source, what a sync copies, when a notification is sent, or how an error is worded. Code that only starts another program, such as `ssh`, is tested on real machines. ## Testing On Two Machines * Test changes to running, syncing, sessions, or connections on two real machines: -1. `slingshot start` on the Agent, and `slingshot link ` on the Client. -2. Use the changed feature, such as `slingshot run cargo build`, `slingshot sync`, or `slingshot attach`. -3. Test failures: stop `slingshot start`, drop the network, or restart the Agent. +1. `ally start` on the Agent, and `ally link ` on the Client. +2. Use the changed feature, such as `ally run cargo build`, `ally sync`, or `ally attach`. +3. Test failures: stop `ally start`, drop the network, or restart the Agent. * List exactly what you ran and what happened in the pull request. Never call something working unless you ran it. ## Making A Change -1. **Find the place** in the [file reference](docs/ARCHITECTURE.md#file-reference). Shared rules go in `slingshot-core`, Agent only code in `slingshot-agent`, and commands and Client code in `slingshot-cli`. +1. **Find the place** in the [file reference](docs/ARCHITECTURE.md#file-reference). Shared rules go in `ally-core`, Agent only code in `ally-agent`, and commands and Client code in `ally-cli`. 2. **Keep it focused.** The smallest change that solves the problem. Unrelated improvements go in another pull request. 3. **Match what is there:** naming, structure, and comment style. 4. **Test it,** and run the changed command at least once. @@ -91,22 +91,22 @@ cargo test --workspace ## Code Standards -* `slingshot-core` uses neither other crate. `slingshot-agent` uses `slingshot-core`. `slingshot-cli` uses both. +* `ally-core` uses neither other crate. `ally-agent` uses `ally-core`. `ally-cli` uses both. * Wrap existing tools such as `ssh`, `rsync`, `tmux`, `docker`, and `iroh`. Never write your own. * Say Client and Agent, never Mac and Linux. Only the menu bar app may be macOS specific. * No fixed paths such as `/Users/...`. Use the `directories` crate. * Never assume a package manager. Detect, print the install command, and ask once before running anything. -* Errors use `anyhow`, and every visible error says how to fix it. A failing user command is not a Slingshot error; pass its exit code through. +* Errors use `anyhow`, and every visible error says how to fix it. A failing user command is not an Ally error; pass its exit code through. * Use `tokio` for async, `tracing` for logs, and `clap` for arguments. No `unwrap` outside tests. * Quote each argument sent to the Agent on its own. Never stop a process by ID alone; check its start time. * Changing a control message raises `control::VERSION`. Changing the menu bar line format raises `watch::event::VERSION`, and `Sources/Model.swift` changes with `watch/event.rs`. -* Swift only displays and notifies. Thresholds and rules stay in `crates/slingshot-cli/src/watch/`. +* Swift only displays and notifies. Thresholds and rules stay in `crates/ally-cli/src/watch/`. * Comments go above an item, only when its purpose is not obvious, and say why. No comments in function bodies, commented out code, or `TODO` notes. * Explain every new crate in the pull request, and use its latest version. ## Output Standards -* Use `slingshot-core/src/presentation.rs` and `step.rs`. Never raw color codes. +* Use `ally-core/src/presentation.rs` and `step.rs`. Never raw color codes. * Slow work shows a spinner, then a line such as `✓ Synced 3 files 0.4s`. * Symbols: `✓` success, `!` warning, `✗` failure, `▶` remote work. Every remote command says where it runs, such as `▶ Running on archbox via tailnet`. * Color marks only what needs attention. Headings are bold, and details dimmed. @@ -136,12 +136,12 @@ cargo test --workspace | --- | --- | | Sharing RAM over the network | A network is about a hundred thousand times slower than RAM. | | Remote desktop or screen streaming | Sunshine and Moonlight already do this. | -| Copying or mounting build output | `target`, `node_modules`, virtual environments, and caches stay on the Agent. Copying them makes Slingshot too slow. | +| Copying or mounting build output | `target`, `node_modules`, virtual environments, and caches stay on the Agent. Copying them makes Ally too slow. | | Syncing environment files as source | `.env`, `.env.*`, `*.env`, and `.envrc` often hold secrets. | | Requiring an account or VPN | A VPN is used when present, never required. iroh needs no account and no open router port. | -| Custom `ssh`, `rsync`, `tmux`, or relay servers | Slingshot wraps these tools and never replaces them. | +| Custom `ssh`, `rsync`, `tmux`, or relay servers | Ally wraps these tools and never replaces them. | ## Reporting A Problem 1. Check [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) first. -2. Open an issue with the Problem template. It asks for the exact command, what you expected, the full output, each machine's system and how they connect, and `slingshot health` output if the Agent answers. +2. Open an issue with the Problem template. It asks for the exact command, what you expected, the full output, each machine's system and how they connect, and `ally health` output if the Agent answers. diff --git a/Cargo.lock b/Cargo.lock index 0ccb5c7..1fd3623 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,62 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ally-agent" +version = "0.1.0" +dependencies = [ + "ally-core", + "anyhow", + "directories", + "iroh", + "libc", + "rand", + "serde", + "serde_json", + "shell-words", + "sysinfo", + "tokio", + "tracing", +] + +[[package]] +name = "ally-cli" +version = "0.1.0" +dependencies = [ + "ally-agent", + "ally-core", + "anyhow", + "clap", + "crossterm", + "directories", + "iroh", + "serde", + "serde_json", + "shell-words", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "ally-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "directories", + "hmac 0.13.0", + "ignore", + "indicatif", + "iroh", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "spake2", + "sysinfo", + "tokio", + "toml", +] + [[package]] name = "android_system_properties" version = "0.1.6" @@ -3124,62 +3180,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "slingshot-agent" -version = "0.1.0" -dependencies = [ - "anyhow", - "directories", - "iroh", - "libc", - "rand", - "serde", - "serde_json", - "shell-words", - "slingshot-core", - "sysinfo", - "tokio", - "tracing", -] - -[[package]] -name = "slingshot-cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "crossterm", - "directories", - "iroh", - "serde", - "serde_json", - "shell-words", - "slingshot-agent", - "slingshot-core", - "tokio", - "tracing-subscriber", -] - -[[package]] -name = "slingshot-core" -version = "0.1.0" -dependencies = [ - "anyhow", - "directories", - "hmac 0.13.0", - "ignore", - "indicatif", - "iroh", - "rand", - "serde", - "serde_json", - "sha2 0.11.0", - "spake2", - "sysinfo", - "tokio", - "toml", -] - [[package]] name = "smallvec" version = "1.16.0" diff --git a/Cargo.toml b/Cargo.toml index 4d9c457..2ff5357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,8 @@ resolver = "3" version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" -repository = "https://github.com/ado11231/ss" -homepage = "https://github.com/ado11231/ss" +repository = "https://github.com/ado11231/ally" +homepage = "https://github.com/ado11231/ally" keywords = ["remote", "ssh", "gpu", "development", "cli"] categories = ["command-line-utilities", "development-tools"] diff --git a/README.md b/README.md index 66481df..463d802 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -

slingshot

+

ally

-

Slingshot logo, a pixel art slingshot

+

Ally logo, a small blue pixel art creature

- CI - crates.io + CI + crates.io macOS and Linux

-

Use the CPU, RAM, and GPU of a more powerful computer, right from your laptop. Local AI models are coming next.

+

run terminal processes and local models on your stronger computer. use your gaming PC or mini computer as a server.

-

Starting Slingshot on the powerful machine, linking the laptop, and opening a session

+

Starting Ally on the powerful machine, linking the laptop, and opening a session

## What It Does @@ -31,7 +31,7 @@ ### On The Agent -**1. Install what Slingshot needs.** Open your system and run the commands. +**1. Install what Ally needs.** Open your system and run the commands.
Arch Linux @@ -91,36 +91,36 @@ Turn on Remote Login in System Settings, General, Sharing.
-**2. Install Slingshot.** +**2. Install Ally.** ```sh -cargo install --locked --git https://github.com/ado11231/ss slingshot-cli -slingshot --version +cargo install --locked --git https://github.com/ado11231/ally ally-cli +ally --version ``` -If `command not found` shows, your terminal can't find Slingshot yet. Add it to your path with this line, then open a new terminal. On zsh, use `~/.zshrc` instead. +If `command not found` shows, your terminal can't find Ally yet. Add it to your path with this line, then open a new terminal. On zsh, use `~/.zshrc` instead. ```sh echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc ``` -**3. Start Slingshot.** +**3. Start Ally.** ```sh -slingshot start +ally start ``` Checks the machine and shows how to fix anything missing. -Asks whether to start Slingshot when the computer turns on. Answer yes to keep the Agent ready after a restart. +Asks whether to start Ally when the computer turns on. Answer yes to keep the Agent ready after a restart. -Prints a line such as `slingshot link 192.168.1.9:7433:K7QW9ZR2` for the Client. +Prints a line such as `ally link 192.168.1.9:7433:K7QW9ZR2` for the Client. Each line works once and expires after 10 minutes. Press Enter for a new one. ### On The Client -**1. Install what Slingshot needs.** Open your system and run the commands. +**1. Install what Ally needs.** Open your system and run the commands.
macOS @@ -172,45 +172,43 @@ When asked, press Enter to install Rust, then open a new terminal.
-**2. Install Slingshot**, the same way as on the Agent. +**2. Install Ally**, the same way as on the Agent. ```sh -cargo install --locked --git https://github.com/ado11231/ss slingshot-cli -slingshot --version +cargo install --locked --git https://github.com/ado11231/ally ally-cli +ally --version ``` -If `command not found` shows, add Slingshot to your path the same way as on the Agent. +If `command not found` shows, add Ally to your path the same way as on the Agent. **3. Link to the Agent.** Run the line the Agent printed. ```sh -slingshot link 192.168.1.9:7433:K7QW9ZR2 +ally link 192.168.1.9:7433:K7QW9ZR2 ``` -Both machines need the same network or Tailscale for this step only. After that, Slingshot works from anywhere. +Both machines need the same network or Tailscale for this step only. After that, Ally works from anywhere. Offers to set up your tools on the Agent, such as Claude Code, and the menu bar app on a Mac. Asks before each one. **4. Try it.** Open a terminal in your project folder, then run: ```sh -slingshot run cargo build +ally run cargo build ``` Any command works, such as `npm run dev` or `python main.py`. -`sling` is the short name: `sling run cargo build`. - ## Commands | Command | Does | | --- | --- | -| `slingshot run ` | Copies your changes, then runs the command on the Agent. | -| `slingshot attach` | Opens a terminal on the Agent that keeps running when you disconnect. | -| `slingshot sync --pull` | Brings edits made on the Agent back to the Client. | -| `slingshot health` | Shows the Agent's hardware and use. Add `--watch` to keep it live. | -| `slingshot ps` | Lists running jobs. `slingshot stop ` stops one. | -| `slingshot projects` | Lists the projects on the Agent and their size. `clean` frees build output. | +| `ally run ` | Copies your changes, then runs the command on the Agent. | +| `ally attach` | Opens a terminal on the Agent that keeps running when you disconnect. | +| `ally sync --pull` | Brings edits made on the Agent back to the Client. | +| `ally health` | Shows the Agent's hardware and use. Add `--watch` to keep it live. | +| `ally ps` | Lists running jobs. `ally stop ` stops one. | +| `ally projects` | Lists the projects on the Agent and their size. `clean` frees build output. | Every command and option is in [USAGE.md](docs/USAGE.md). @@ -218,4 +216,4 @@ Every command and option is in [USAGE.md](docs/USAGE.md). On a Mac, `link` offers a menu bar app showing how busy the Agent is and when a run finishes. -

The Slingshot menu bar panel

+

The Ally menu bar panel

diff --git a/crates/slingshot-agent/Cargo.toml b/crates/ally-agent/Cargo.toml similarity index 76% rename from crates/slingshot-agent/Cargo.toml rename to crates/ally-agent/Cargo.toml index 674f5f0..9b2ae3a 100644 --- a/crates/slingshot-agent/Cargo.toml +++ b/crates/ally-agent/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "slingshot-agent" -description = "The Agent side of Slingshot: the daemon on the powerful machine" +name = "ally-agent" +description = "The Agent side of Ally: the daemon on the powerful machine" version.workspace = true edition.workspace = true license.workspace = true @@ -10,7 +10,7 @@ keywords.workspace = true categories.workspace = true [dependencies] -slingshot-core = { path = "../slingshot-core", version = "0.1.0" } +ally-core = { path = "../ally-core", version = "0.1.0" } anyhow = { workspace = true } iroh = { workspace = true } directories = { workspace = true } diff --git a/crates/slingshot-agent/LICENSE-APACHE b/crates/ally-agent/LICENSE-APACHE similarity index 100% rename from crates/slingshot-agent/LICENSE-APACHE rename to crates/ally-agent/LICENSE-APACHE diff --git a/crates/slingshot-agent/LICENSE-MIT b/crates/ally-agent/LICENSE-MIT similarity index 100% rename from crates/slingshot-agent/LICENSE-MIT rename to crates/ally-agent/LICENSE-MIT diff --git a/crates/slingshot-agent/src/awake.rs b/crates/ally-agent/src/awake.rs similarity index 93% rename from crates/slingshot-agent/src/awake.rs rename to crates/ally-agent/src/awake.rs index 0bbbc98..d440d71 100644 --- a/crates/slingshot-agent/src/awake.rs +++ b/crates/ally-agent/src/awake.rs @@ -1,4 +1,4 @@ -//! Keeps the Agent from sleeping on its own while `slingshot start` runs, because a sleeping +//! Keeps the Agent from sleeping on its own while `ally start` runs, because a sleeping //! Agent cannot be reached from anywhere. Wraps the system's own tool, and ties its lock to //! this process so the lock ends with `start`, even when `start` is killed outright. @@ -38,7 +38,7 @@ fn masked(stdout: &[u8]) -> bool { /// The polkit rule that lets this account hold the sleep lock with nobody logged in, as a /// boot service must. Without it, polkit asks for an admin password outside a session at /// the machine, so the lock is refused and an idle login screen can suspend the Agent. -pub const SLEEP_RULE: &str = "/etc/polkit-1/rules.d/50-slingshot.rules"; +pub const SLEEP_RULE: &str = "/etc/polkit-1/rules.d/50-ally.rules"; /// Saved in the Agent's folder once the rule is written, since the rules folder is /// readable only by root. @@ -51,7 +51,7 @@ pub fn rule_written(root: &Path) -> bool { pub fn mark_rule(root: &Path, written: bool) -> anyhow::Result<()> { let mark = root.join(SLEEP_RULE_MARK); match written { - true => slingshot_core::storage::write_bytes(&mark, b""), + true => ally_core::storage::write_bytes(&mark, b""), false => match std::fs::remove_file(&mark) { Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e.into()), _ => Ok(()), @@ -68,8 +68,8 @@ pub fn sleep_rule(user: &str) -> Option { .all(|c| c.is_ascii_alphanumeric() || "_.-".contains(c)); plain.then(|| { format!( - "// Written by Slingshot: lets {user} keep this machine awake while Slingshot runs,\n\ - // even with nobody logged in. slingshot start --remove deletes it.\n\ + "// Written by Ally: lets {user} keep this machine awake while Ally runs,\n\ + // even with nobody logged in. ally start --remove deletes it.\n\ polkit.addRule(function(action, subject) {{\n\ \x20 if ((action.id == \"org.freedesktop.login1.inhibit-block-sleep\" ||\n\ \x20 action.id == \"org.freedesktop.login1.inhibit-block-idle\") &&\n\ @@ -104,7 +104,7 @@ fn systemd_inhibit() -> Tool { command.args([ "--no-ask-password", "--what=idle:sleep", - "--who=Slingshot", + "--who=Ally", "--why=Reachable by paired Clients", "--mode=block", "cat", diff --git a/crates/slingshot-agent/src/boot.rs b/crates/ally-agent/src/boot.rs similarity index 86% rename from crates/slingshot-agent/src/boot.rs rename to crates/ally-agent/src/boot.rs index aaf5df2..d8e1b34 100644 --- a/crates/slingshot-agent/src/boot.rs +++ b/crates/ally-agent/src/boot.rs @@ -1,19 +1,19 @@ //! Starting the Agent at boot. Wraps the system's own service manager: a systemd user //! service on Linux, with linger so it starts before anyone logs in, and a launchd agent on -//! macOS, which starts at login. Either one runs `slingshot internal-daemon`. +//! macOS, which starts at login. Either one runs `ally internal-daemon`. use crate::{awake, service}; +use ally_core::presentation::{self, home_path}; +use ally_core::storage; use anyhow::{Context, bail}; -use slingshot_core::presentation::{self, home_path}; -use slingshot_core::storage; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -const UNIT: &str = "slingshot.service"; -const LABEL: &str = "dev.slingshot.agent"; +const UNIT: &str = "ally.service"; +const LABEL: &str = "dev.ally.agent"; -/// Saved when the person at `slingshot start` says no, so the question is asked only once. +/// Saved when the person at `ally start` says no, so the question is asked only once. const DECLINED: &str = "boot-declined"; /// What starting at boot writes and runs on this system. @@ -58,10 +58,10 @@ pub fn decline(root: &Path) -> anyhow::Result<()> { /// Work out the file and commands for the Agent `name`. The service keeps this shell's PATH, /// because tools such as Homebrew's tmux are not on the short PATH a service starts with. pub fn plan(root: &Path, name: &str, user: &str) -> anyhow::Result { - let program = std::env::current_exe().context("Could not find the slingshot program")?; + let program = std::env::current_exe().context("Could not find the ally program")?; let program = program .to_str() - .context("The slingshot program's path is not valid text. Move it to a plain path")?; + .context("The ally program's path is not valid text. Move it to a plain path")?; let path = std::env::var("PATH").unwrap_or_default(); let file = file()?; if cfg!(target_os = "macos") { @@ -82,7 +82,7 @@ pub fn plan(root: &Path, name: &str, user: &str) -> anyhow::Result { }); } if !cfg!(target_os = "linux") { - bail!("Starting at boot works on Linux and macOS. Run slingshot start by hand here"); + bail!("Starting at boot works on Linux and macOS. Run ally start by hand here"); } Ok(Plan { contents: systemd(program, name, &path)?, @@ -143,13 +143,13 @@ pub fn install(plan: &Plan) -> anyhow::Result<()> { for command in &plan.commands { run(command)?; } - presentation::success(format!("Slingshot starts {}", plan.when)); + presentation::success(format!("Ally starts {}", plan.when)); presentation::detail("Logs", &plan.logs); if let Some(linger) = &plan.linger && run(linger).is_err() { presentation::warning(format!( - "Slingshot starts when you log in, not at boot. To start it at boot, run sudo {}", + "Ally starts when you log in, not at boot. To start it at boot, run sudo {}", shell_words::join(linger) )); } @@ -160,7 +160,7 @@ pub fn install(plan: &Plan) -> anyhow::Result<()> { presentation::success("This machine stays awake with nobody logged in"); } Err(error) => presentation::warning(format!( - "{error:#}. This machine may sleep with nobody logged in. Run slingshot start --boot in a terminal to try again" + "{error:#}. This machine may sleep with nobody logged in. Run ally start --boot in a terminal to try again" )), } } @@ -188,7 +188,7 @@ fn write_sleep_rule(rule: &str) -> anyhow::Result<()> { Ok(()) } -/// Delete the polkit rule, if Slingshot wrote one. A failure leaves the exact command. +/// Delete the polkit rule, if Ally wrote one. A failure leaves the exact command. fn remove_sleep_rule() -> anyhow::Result<()> { let root = service::root()?; if !awake::rule_written(&root) { @@ -211,16 +211,16 @@ fn remove_sleep_rule() -> anyhow::Result<()> { } } -/// `slingshot start --remove`: show what will happen, ask, then stop the service and delete +/// `ally start --remove`: show what will happen, ask, then stop the service and delete /// its file. Linger stays on, because other services of this account may rely on it. pub async fn remove() -> anyhow::Result { let file = file()?; if !file.exists() { - presentation::success("Slingshot does not start by itself"); + presentation::success("Ally does not start by itself"); return Ok(0); } let command = removal(); - eprintln!("\n This stops Slingshot and stops it starting by itself:"); + eprintln!("\n This stops Ally and stops it starting by itself:"); presentation::detail("Runs", shell_words::join(&command)); presentation::detail("Deletes", home_path(&file)); if awake::rule_written(&service::root()?) { @@ -228,13 +228,13 @@ pub async fn remove() -> anyhow::Result { } anyhow::ensure!( crate::interactive(), - "Run slingshot start --remove in a terminal to confirm" + "Run ally start --remove in a terminal to confirm" ); if !presentation::confirm("Remove the boot service?".to_string()).await? { return Ok(0); } remove_service()?; - presentation::success("Slingshot no longer starts by itself"); + presentation::success("Ally no longer starts by itself"); Ok(0) } @@ -257,7 +257,7 @@ fn run(command: &[String]) -> anyhow::Result<()> { .with_context(|| format!("Could not start {}", command[0]))?; if !output.status.success() { bail!( - "{shown} failed: {}. Fix that, then run slingshot start --boot", + "{shown} failed: {}. Fix that, then run ally start --boot", String::from_utf8_lossy(&output.stderr).trim() ); } @@ -283,7 +283,7 @@ fn words(parts: &[&str]) -> Vec { fn systemd(program: &str, name: &str, path: &str) -> anyhow::Result { Ok(format!( "[Unit]\n\ - Description=Slingshot Agent\n\ + Description=Ally Agent\n\ \n\ [Service]\n\ ExecStart={} internal-daemon --name {}\n\ @@ -303,7 +303,7 @@ fn systemd(program: &str, name: &str, path: &str) -> anyhow::Result { /// variable only in command lines. fn systemd_word(value: &str, command: bool) -> anyhow::Result { if value.contains(['\n', '\r']) { - bail!("A line break cannot go in a systemd unit. Rename the Agent or move slingshot"); + bail!("A line break cannot go in a systemd unit. Rename the Agent or move ally"); } let mut quoted = value .replace('\\', "\\\\") @@ -369,8 +369,8 @@ mod tests { #[test] fn unit_values_are_quoted_so_systemd_reads_them_back_unchanged() { assert_eq!( - systemd_word(r#"/opt/my "tools"\slingshot"#, true).unwrap(), - r#""/opt/my \"tools\"\\slingshot""# + systemd_word(r#"/opt/my "tools"\ally"#, true).unwrap(), + r#""/opt/my \"tools\"\\ally""# ); assert_eq!( systemd_word("box%h$HOME", true).unwrap(), @@ -382,10 +382,8 @@ mod tests { #[test] fn the_unit_runs_the_daemon_under_the_chosen_name_and_restarts_it() { - let unit = systemd("/usr/bin/slingshot", "archbox", "/usr/bin:/bin").unwrap(); - assert!( - unit.contains(r#"ExecStart="/usr/bin/slingshot" internal-daemon --name "archbox""#) - ); + let unit = systemd("/usr/bin/ally", "archbox", "/usr/bin:/bin").unwrap(); + assert!(unit.contains(r#"ExecStart="/usr/bin/ally" internal-daemon --name "archbox""#)); assert!(unit.contains(r#"Environment="PATH=/usr/bin:/bin""#)); assert!(unit.contains("Restart=on-failure")); assert!(unit.contains("WantedBy=default.target")); @@ -393,8 +391,8 @@ mod tests { #[test] fn the_launch_agent_escapes_its_values_and_restarts_only_after_a_failure() { - let plist = launchd("/opt/a&b/slingshot", "", "/usr/bin", "/tmp/daemon.log"); - assert!(plist.contains("/opt/a&b/slingshot")); + let plist = launchd("/opt/a&b/ally", "", "/usr/bin", "/tmp/daemon.log"); + assert!(plist.contains("/opt/a&b/ally")); assert!(plist.contains("<box>")); assert!(plist.contains("SuccessfulExit\n ")); assert!(plist.contains("/tmp/daemon.log")); diff --git a/crates/slingshot-agent/src/clients.rs b/crates/ally-agent/src/clients.rs similarity index 98% rename from crates/slingshot-agent/src/clients.rs rename to crates/ally-agent/src/clients.rs index af0da86..3e73fc6 100644 --- a/crates/slingshot-agent/src/clients.rs +++ b/crates/ally-agent/src/clients.rs @@ -1,10 +1,10 @@ //! Which Clients may reach this Agent over iroh. Pairing adds a Client's key and unlink //! removes it. ssh still decides who logs in; this only decides who reaches sshd at all. +use ally_core::storage; +use ally_core::tunnel; use iroh::PublicKey; use serde::{Deserialize, Serialize}; -use slingshot_core::storage; -use slingshot_core::tunnel; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; diff --git a/crates/slingshot-agent/src/jobs.rs b/crates/ally-agent/src/jobs.rs similarity index 95% rename from crates/slingshot-agent/src/jobs.rs rename to crates/ally-agent/src/jobs.rs index 47931d2..14b8ee7 100644 --- a/crates/slingshot-agent/src/jobs.rs +++ b/crates/ally-agent/src/jobs.rs @@ -1,12 +1,12 @@ -//! Slingshot runs and sessions on the Agent: persistent records, reconciliation after a +//! Ally runs and sessions on the Agent: persistent records, reconciliation after a //! restart, persistent tmux sessions, and stopping work without trusting stale PIDs. use crate::projects; +use ally_core::control::{Job, JobKind, JobState, SessionInfo}; +use ally_core::preflight; +use ally_core::storage; +use ally_core::tools; use anyhow::{Context, bail, ensure}; -use slingshot_core::control::{Job, JobKind, JobState, SessionInfo}; -use slingshot_core::preflight; -use slingshot_core::storage; -use slingshot_core::tools; use std::collections::HashMap; use std::fs; use std::os::unix::fs::MetadataExt; @@ -15,7 +15,7 @@ use std::process::Command; use std::time::{Duration, Instant}; use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind}; -/// Completed records kept for `slingshot ps --all`. +/// Completed records kept for `ally ps --all`. const HISTORY: usize = 100; /// How long stopped work gets to exit on its own before it is killed. @@ -160,7 +160,7 @@ fn reconcile(root: &Path, job: &Job) -> Option { fn find_active(root: &Path, query: &str) -> anyhow::Result { ensure!( query.len() >= 4 && query.bytes().all(|b| b.is_ascii_hexdigit()), - "Pass the job ID shown by slingshot ps" + "Pass the job ID shown by ally ps" ); let query = query.to_ascii_lowercase(); let matches: Vec = list(root, false)? @@ -168,7 +168,7 @@ fn find_active(root: &Path, query: &str) -> anyhow::Result { .filter(|job| job.id.starts_with(&query)) .collect(); match matches.len() { - 0 => bail!("No running Slingshot job matches {query}. See slingshot ps"), + 0 => bail!("No running Ally job matches {query}. See ally ps"), 1 => Ok(matches .into_iter() .next() @@ -191,7 +191,7 @@ fn tmux_program() -> Option { (out.status.success() && path.starts_with('/')).then_some(path) } -/// tmux on Slingshot's own private socket, so personal tmux sessions are never touched. +/// tmux on Ally's own private socket, so personal tmux sessions are never touched. fn tmux(root: &Path) -> Command { let mut command = Command::new("tmux"); command.arg("-S").arg(tmux_socket(root)); @@ -205,7 +205,7 @@ fn session_exists(root: &Path, id: &str) -> bool { .is_ok_and(|out| out.status.success()) } -/// The shell, terminal, and input settings every Slingshot session gets. Sessions start +/// The shell, terminal, and input settings every Ally session gets. Sessions start /// from the daemon, so nothing here may depend on the daemon's own environment. /// The wheel goes to a program only when it asked for the mouse. Otherwise it scrolls the /// session's history, because tmux would turn it into arrow keys for a full screen program, @@ -234,7 +234,7 @@ fn tmux_config(shell: &str, terminal: &str) -> String { "bind k send-keys -R \\; clear-history \\; send-keys C-l".to_string(), "set -g status-style \"bg=default,fg=colour245\"".to_string(), "set -g status-left-length 80".to_string(), - "set -g status-left \"#{@slingshot} \"".to_string(), + "set -g status-left \"#{@ally} \"".to_string(), "set -g status-right \"#[fg=default,bold]Ctrl B, D#[default] to detach \"".to_string(), "set -g window-status-format \"\"".to_string(), "set -g window-status-current-format \"\"".to_string(), @@ -244,7 +244,7 @@ fn tmux_config(shell: &str, terminal: &str) -> String { } /// The account's login shell from the user database, so a session loads the same startup -/// files as a normal login even when `slingshot start` ran without `SHELL` set. +/// files as a normal login even when `ally start` ran without `SHELL` set. pub fn login_shell(root: &Path) -> String { let from_database = fs::metadata(root).ok().and_then(|meta| { let out = Command::new("getent") @@ -321,12 +321,12 @@ fn prepare_server(root: &Path) -> anyhow::Result { Ok(config) } -/// Where a session or run starts: `~/Slingshot/`, a link to the project copy, so -/// the prompt shows the project rather than Slingshot's storage path, and Docker Compose +/// Where a session or run starts: `~/Ally/`, a link to the project copy, so +/// the prompt shows the project rather than Ally's storage path, and Docker Compose /// names the project after it instead of after the copy's `source` folder. An existing /// file, or a link to another copy, is never replaced; the short ID is added instead. pub fn project_link(home: &Path, name: &str, id: &str, target: &Path) -> anyhow::Result { - let folder = home.join("Slingshot"); + let folder = home.join("Ally"); fs::create_dir_all(&folder)?; let clean: String = name .chars() @@ -442,7 +442,7 @@ pub fn session(root: &Path, agent: &str, project: Option<&str>) -> anyhow::Resul let created = command.output(); let place = owner.map(|(_, name)| name).unwrap_or("home"); let _ = tmux(root) - .args(["set-option", "-t", &job.id, "@slingshot"]) + .args(["set-option", "-t", &job.id, "@ally"]) .arg(session_label(agent, place)) .output(); let failure = match &created { @@ -646,7 +646,7 @@ mod tests { let copy = home.0.join("copy"); fs::create_dir(©).unwrap(); let link = project_link(&home.0, "my app", "abcdef0123456789", ©).unwrap(); - assert_eq!(link, home.0.join("Slingshot").join("my_app")); + assert_eq!(link, home.0.join("Ally").join("my_app")); assert_eq!(fs::read_link(&link).unwrap(), copy); assert_eq!( project_link(&home.0, "my app", "abcdef0123456789", ©).unwrap(), @@ -659,17 +659,17 @@ mod tests { assert_ne!(second, link); assert_eq!(fs::read_link(&second).unwrap(), other); - fs::write(home.0.join("Slingshot").join("notes"), "mine").unwrap(); + fs::write(home.0.join("Ally").join("notes"), "mine").unwrap(); let beside = project_link(&home.0, "notes", "fedcba9876543210", ©).unwrap(); - assert_ne!(beside, home.0.join("Slingshot").join("notes")); + assert_ne!(beside, home.0.join("Ally").join("notes")); assert_eq!( - fs::read_to_string(home.0.join("Slingshot").join("notes")).unwrap(), + fs::read_to_string(home.0.join("Ally").join("notes")).unwrap(), "mine" ); assert!( project_link(&home.0, "../..", "0011223344556677", ©) .unwrap() - .starts_with(home.0.join("Slingshot")) + .starts_with(home.0.join("Ally")) ); } diff --git a/crates/slingshot-agent/src/lib.rs b/crates/ally-agent/src/lib.rs similarity index 89% rename from crates/slingshot-agent/src/lib.rs rename to crates/ally-agent/src/lib.rs index 95d1473..f4b5213 100644 --- a/crates/slingshot-agent/src/lib.rs +++ b/crates/ally-agent/src/lib.rs @@ -11,14 +11,14 @@ pub mod service; pub mod tunnel; pub mod uninstall; +use ally_core::keys; +use ally_core::network::Network; +use ally_core::preflight; +use ally_core::presentation::{self, Style, Tone}; +use ally_core::protocol::{Handshake, Joining, Paired, Request, Response, Side}; +use ally_core::step; +use ally_core::telemetry; use anyhow::Context; -use slingshot_core::keys; -use slingshot_core::network::Network; -use slingshot_core::preflight; -use slingshot_core::presentation::{self, Style, Tone}; -use slingshot_core::protocol::{Handshake, Joining, Paired, Request, Response, Side}; -use slingshot_core::step; -use slingshot_core::telemetry; use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; @@ -48,7 +48,8 @@ const PAIRING_TIMEOUT: Duration = Duration::from_secs(10); const WRONG_CODE_LIMIT: u32 = 3; /// What an older Client sees, since it cannot take part in the handshake. -const UPDATE_CLIENT: &str = "This Agent needs a newer Slingshot. Update Slingshot on this machine, then run slingshot link again"; +const UPDATE_CLIENT: &str = + "This Agent needs a newer Ally. Update Ally on this machine, then run ally link again"; /// The one time token that lets a Client install its key. Single use: pairing /// consumes it, and there is no way to ask the daemon what it was. @@ -81,8 +82,8 @@ struct Agent { exchanging: AtomicBool, } -/// What Ctrl C does where `slingshot start` runs: stop the daemon this process runs, or -/// only leave, when the daemon belongs to another `slingshot start` on this account. +/// What Ctrl C does where `ally start` runs: stop the daemon this process runs, or +/// only leave, when the daemon belongs to another `ally start` on this account. #[derive(Debug, Clone, Copy, PartialEq)] enum Mode { Daemon, @@ -93,17 +94,17 @@ impl Mode { fn ctrl_c(self) -> &'static str { match self { Mode::Daemon => "Ctrl C to stop", - Mode::Beside => "Ctrl C to leave while Slingshot keeps running", + Mode::Beside => "Ctrl C to leave while Ally keeps running", } } } const REACHABLE: &str = "Reachable from other networks through iroh"; -/// What `slingshot start` does about starting at boot. +/// What `ally start` does about starting at boot. #[derive(Debug, Clone, Copy, PartialEq)] pub enum AtBoot { - /// Ask once, the first time `slingshot start` runs in a terminal. + /// Ask once, the first time `ally start` runs in a terminal. Ask, /// `--boot`: set it up without asking, even after an earlier no. Install, @@ -129,7 +130,7 @@ pub async fn start(name: Option, port: u16, at_boot: AtBoot) -> anyhow:: .and_then(|user| preflight::docker_check(&user)), ); if preflight::report(&checks) { - anyhow::bail!("Fix the reported errors, then run slingshot start again"); + anyhow::bail!("Fix the reported errors, then run ally start again"); } let root = service::root()?; @@ -137,7 +138,7 @@ pub async fn start(name: Option, port: u16, at_boot: AtBoot) -> anyhow:: if service::running(&root)? { if at_boot == AtBoot::Install && !installed { anyhow::bail!( - "Slingshot is already running for this account. Stop the other slingshot start, then run slingshot start --boot again" + "Ally is already running for this account. Stop the other ally start, then run ally start --boot again" ); } return beside(name, port, root).await; @@ -146,7 +147,7 @@ pub async fn start(name: Option, port: u16, at_boot: AtBoot) -> anyhow:: let name = agent_name(name); if installed { presentation::warning(format!( - "Slingshot is set to start by itself but is not running. Its file is {}. Run slingshot start --remove, then slingshot start --boot", + "Ally is set to start by itself but is not running. Its file is {}. Run ally start --remove, then ally start --boot", presentation::home_path(&boot::file()?) )); } else if wants_boot(&root, &name, at_boot).await? { @@ -154,7 +155,7 @@ pub async fn start(name: Option, port: u16, at_boot: AtBoot) -> anyhow:: } let online = Arc::new(AtomicBool::new(false)); let _service = service::start(name.clone(), Arc::clone(&online))?; - let identity = slingshot_core::tunnel::identity(&root)?; + let identity = ally_core::tunnel::identity(&root)?; let linked = clients::names(&root)?; let (agent, addresses) = pairing( name.clone(), @@ -188,7 +189,7 @@ pub async fn start(name: Option, port: u16, at_boot: AtBoot) -> anyhow:: endpoint.close().await; if stop != Stop::TerminalClosed { eprintln!(); - presentation::success(format!("Stopped Slingshot on {name}")); + presentation::success(format!("Stopped Ally on {name}")); } Ok(0) @@ -196,18 +197,18 @@ pub async fn start(name: Option, port: u16, at_boot: AtBoot) -> anyhow:: /// What a boot service runs: the control socket, iroh, and the lock that keeps the machine /// awake. It never opens the pairing port, so no code exists until someone runs -/// `slingshot start`, which pairs beside it. The checks are left to that command, because +/// `ally start`, which pairs beside it. The checks are left to that command, because /// nobody is here to read them, and at boot sshd may not be up yet. pub async fn daemon(name: Option) -> anyhow::Result { let root = service::root()?; let name = agent_name(name); let online = Arc::new(AtomicBool::new(false)); let _service = service::start(name.clone(), Arc::clone(&online))?; - let identity = slingshot_core::tunnel::identity(&root)?; + let identity = ally_core::tunnel::identity(&root)?; let endpoint = tunnel::start(identity, root).await?; tokio::spawn(mark_online(endpoint.clone(), online)); let _awake = keep_awake(); - presentation::success(format!("Slingshot is running on {name}")); + presentation::success(format!("Ally is running on {name}")); stopped().await?; endpoint.close().await; @@ -221,17 +222,17 @@ async fn wants_boot(root: &std::path::Path, name: &str, at_boot: AtBoot) -> anyh if ask && (!interactive() || boot::declined(root)) { return Ok(false); } - let user = whoami().context("Could not work out which user is running Slingshot")?; + let user = whoami().context("Could not work out which user is running Ally")?; let plan = boot::plan(root, name, &user)?; if ask { eprintln!( - "\n Slingshot can start {} by itself, so linked Clients reach this machine without anyone here:", + "\n Ally can start {} by itself, so linked Clients reach this machine without anyone here:", plan.when ); boot::show(&plan); - if !presentation::confirm(format!("Start Slingshot {}?", plan.when)).await? { + if !presentation::confirm(format!("Start Ally {}?", plan.when)).await? { boot::decline(root)?; - presentation::detail("Later", "slingshot start --boot"); + presentation::detail("Later", "ally start --boot"); return Ok(false); } } else { @@ -239,7 +240,7 @@ async fn wants_boot(root: &std::path::Path, name: &str, at_boot: AtBoot) -> anyh } boot::install(&plan)?; - let step = step::start("Starting Slingshot"); + let step = step::start("Starting Ally"); let started = Instant::now(); let mut answered = false; loop { @@ -254,14 +255,14 @@ async fn wants_boot(root: &std::path::Path, name: &str, at_boot: AtBoot) -> anyh } step.clear(); anyhow::bail!( - "Slingshot was set up to start {} but did not start. Its file is {}", + "Ally was set up to start {} but did not start. Its file is {}", plan.when, presentation::home_path(&plan.file) ); } tokio::time::sleep(Duration::from_millis(250)).await; } - step.done("Slingshot started"); + step.done("Ally started"); Ok(true) } @@ -286,23 +287,23 @@ fn keep_awake() -> Option { } let held = awake::hold(); match held { - Some(_) => presentation::success("Keeping this machine awake while Slingshot runs"), + Some(_) => presentation::success("Keeping this machine awake while Ally runs"), None => presentation::warning(sleep_warning(std::env::var_os("SSH_CONNECTION").is_some())), } held } -/// Pair beside a daemon another `slingshot start` runs, such as one started at boot. Pairing +/// Pair beside a daemon another `ally start` runs, such as one started at boot. Pairing /// only writes files the daemon reads fresh, so the code never has to reach the daemon. async fn beside(name: Option, port: u16, root: PathBuf) -> anyhow::Result { let status = service::status(&root).await?; if let Some(asked) = name.filter(|asked| *asked != status.name) { anyhow::bail!( - "Slingshot is already running here as {}, not {asked}. Run slingshot start without --name", + "Ally is already running here as {}, not {asked}. Run ally start without --name", status.name ); } - let identity = slingshot_core::tunnel::identity(&root)?; + let identity = ally_core::tunnel::identity(&root)?; let linked = clients::names(&root)?; let (agent, addresses) = pairing( status.name.clone(), @@ -328,7 +329,7 @@ async fn beside(name: Option, port: u16, root: PathBuf) -> anyhow::Resul if stopped().await? != Stop::TerminalClosed { eprintln!(); - presentation::success(format!("Slingshot keeps running on {}", status.name)); + presentation::success(format!("Ally keeps running on {}", status.name)); } Ok(0) } @@ -351,7 +352,7 @@ async fn pairing( let (chosen, listeners) = listen(port).await?; if chosen != port { presentation::warning(format!( - "Port {port} is in use, perhaps by another slingshot start, so this one uses {chosen}" + "Port {port} is in use, perhaps by another ally start, so this one uses {chosen}" )); } let addresses: Vec = listeners @@ -408,11 +409,11 @@ async fn stopped() -> anyhow::Result { } /// Why the machine may sleep. Over ssh the system only allows the lock with a password, -/// which Slingshot never asks for, so the fix is to start it at the machine. +/// which Ally never asks for, so the fix is to start it at the machine. fn sleep_warning(over_ssh: bool) -> &'static str { match over_ssh { true => { - "Started over ssh, so this machine may still sleep. Run slingshot start at the machine to keep it awake" + "Started over ssh, so this machine may still sleep. Run ally start at the machine to keep it awake" } false => { "Could not stop this machine from sleeping. If it sleeps, other machines cannot reach it until it wakes" @@ -429,7 +430,7 @@ async fn report_reach(endpoint: &iroh::Endpoint) { false => { step.clear(); presentation::warning( - "No iroh relay answered, so other networks cannot reach this machine yet. The same network still works, and Slingshot keeps trying", + "No iroh relay answered, so other networks cannot reach this machine yet. The same network still works, and Ally keeps trying", ); } } @@ -484,7 +485,7 @@ async fn handle(stream: TcpStream, peer: SocketAddr, agent: Arc) -> anyho }, Ok(Request::Info | Request::Health | Request::Pair {}) => error(UPDATE_CLIENT), Ok(Request::Join { .. }) => { - error("Pairing must begin with a handshake. Run slingshot link again") + error("Pairing must begin with a handshake. Run ally link again") } }; @@ -546,7 +547,7 @@ fn open(agent: &Arc) -> Result<(String, Handshake, Vec, Exchange), St Ok((token, handshake, ours, exchange)) } -const EXPIRED: &str = "That pairing code has expired or was already used. Press Enter where slingshot start is running for a new one"; +const EXPIRED: &str = "That pairing code has expired or was already used. Press Enter where ally start is running for a new one"; /// Check the Client's proof, then use up the code and install the Client's keys. A wrong /// proof counts against the code, and the last allowed one burns it. @@ -559,11 +560,11 @@ fn join( peer: SocketAddr, ) -> Response { let Request::Join { details, proof } = request else { - return error("Pairing stopped halfway. Run slingshot link again"); + return error("Pairing stopped halfway. Run ally link again"); }; let key = match handshake.finish(theirs) { Ok(key) => key, - Err(e) => return error(format!("{e:#}. Run slingshot link again")), + Err(e) => return error(format!("{e:#}. Run ally link again")), }; let right = key.check(Side::Client, &details, &proof); @@ -658,7 +659,7 @@ fn whoami() -> anyhow::Result { } /// How many ports after the requested one to try, so several accounts on one machine can -/// each run slingshot start. +/// each run ally start. const PORTS_TO_TRY: u16 = 10; /// Listen on every pairing address at the first port where none is taken. An address that @@ -686,7 +687,7 @@ async fn listen(port: u16) -> anyhow::Result<(u16, Vec)> { } } anyhow::bail!( - "Ports {port} to {} are all in use. Choose another with: slingshot start --port ", + "Ports {port} to {} are all in use. Choose another with: ally start --port ", port.saturating_add(PORTS_TO_TRY - 1) ) } @@ -726,7 +727,7 @@ fn show(agent: &Agent, addresses: &[SocketAddr], release: &str, linked: &[String let style = Style::stderr(); eprintln!(); eprintln!( - "{} Slingshot {} is running on {}", + "{} Ally {} is running on {}", style.paint("▶", Tone::Info), style.dim(release), style.paint(&agent.name, Tone::Info) @@ -789,7 +790,7 @@ fn announce(addresses: &[SocketAddr], token: &str, mode: Mode) { .iter() .map(|addr| { ( - format!("slingshot link {}:{}:{token}", addr.ip(), addr.port()), + format!("ally link {}:{}:{token}", addr.ip(), addr.port()), Network::of(&addr.ip().to_string()).name(), ) }) @@ -887,12 +888,12 @@ mod tests { #[test] fn leaving_beside_the_daemon_says_it_keeps_running() { assert!(code_footer(Mode::Daemon).ends_with("Ctrl C to stop")); - assert!(ask_for_code(Mode::Beside).ends_with("Slingshot keeps running")); + assert!(ask_for_code(Mode::Beside).ends_with("Ally keeps running")); } #[test] fn the_sleep_warning_says_what_to_do_over_ssh() { - assert!(sleep_warning(true).contains("Run slingshot start at the machine")); + assert!(sleep_warning(true).contains("Run ally start at the machine")); assert!(sleep_warning(false).contains("Could not stop this machine from sleeping")); } @@ -975,11 +976,9 @@ pub(crate) mod testing { impl Root { pub fn new() -> Root { - let path = std::env::temp_dir().join(format!( - "slingshot-agent-{}", - slingshot_core::storage::new_id() - )); - slingshot_core::storage::private_dir(&path).unwrap(); + let path = + std::env::temp_dir().join(format!("ally-agent-{}", ally_core::storage::new_id())); + ally_core::storage::private_dir(&path).unwrap(); Root(path.canonicalize().unwrap()) } } diff --git a/crates/slingshot-agent/src/projects.rs b/crates/ally-agent/src/projects.rs similarity index 96% rename from crates/slingshot-agent/src/projects.rs rename to crates/ally-agent/src/projects.rs index f05ea41..4f2095c 100644 --- a/crates/slingshot-agent/src/projects.rs +++ b/crates/ally-agent/src/projects.rs @@ -2,15 +2,15 @@ //! environment files. Each project ID gets separate folders for each kind of data. use crate::jobs; -use anyhow::{Context, bail, ensure}; -use serde::{Deserialize, Serialize}; -use slingshot_core::artifacts::{self, Layout}; -use slingshot_core::control::{ +use ally_core::artifacts::{self, Layout}; +use ally_core::control::{ Job, JobKind, MAX_ENVIRONMENT_FILE, ProjectInfo, ProjectRef, ProjectSummary, Snapshot, }; -use slingshot_core::source::{self, Manifest, Rules}; -use slingshot_core::sync::{self, StateDir}; -use slingshot_core::{stack, storage}; +use ally_core::source::{self, Manifest, Rules}; +use ally_core::sync::{self, StateDir}; +use ally_core::{stack, storage}; +use anyhow::{Context, bail, ensure}; +use serde::{Deserialize, Serialize}; use std::fs::{self, File}; use std::path::{Path, PathBuf}; @@ -124,7 +124,7 @@ pub fn load(root: &Path, id: &str) -> anyhow::Result<(Paths, Metadata)> { let paths = Paths::new(root, id)?; let metadata: Option = storage::read_json(&paths.metadata())?; let metadata = - metadata.context("This project has no copy on the Agent yet. Run slingshot sync first")?; + metadata.context("This project has no copy on the Agent yet. Run ally sync first")?; Ok((paths, metadata)) } @@ -158,7 +158,7 @@ pub fn acquire( }; if let Some(job) = blocker(&active, blocking) { bail!( - "{} has {} in progress (job {}). Stop it with slingshot stop {} or wait for it to finish", + "{} has {} in progress (job {}). Stop it with ally stop {} or wait for it to finish", metadata.name, describe(job.kind), storage::short_id(&job.id), @@ -184,12 +184,12 @@ pub fn ensure_ready(paths: &Paths, metadata: &Metadata) -> anyhow::Result<()> { let state = paths.sync_state(); ensure!( !state.interrupted(), - "An interrupted sync of {} needs recovery. Run slingshot sync to recover it", + "An interrupted sync of {} needs recovery. Run ally sync to recover it", metadata.name ); ensure!( state.baseline_file().exists(), - "{} has not been copied to the Agent yet. Run slingshot sync first", + "{} has not been copied to the Agent yet. Run ally sync first", metadata.name ); Ok(()) @@ -264,7 +264,7 @@ pub fn inspect(root: &Path, id: &str, excludes: &[String]) -> anyhow::Result { ensure!( meta.file_type().is_symlink() && fs::read_link(&link)? == stored, - "A file Slingshot does not manage already exists at {target} on the Agent" + "A file Ally does not manage already exists at {target} on the Agent" ); true } @@ -447,7 +447,7 @@ pub fn env_remove(root: &Path, id: &str, target: &str) -> anyhow::Result<()> { remove_environment(&paths, target) } -/// Remove the link only when it is still Slingshot's own, then the stored copy. +/// Remove the link only when it is still Ally's own, then the stored copy. fn remove_environment(paths: &Paths, target: &str) -> anyhow::Result<()> { let stored = paths.environment.join(stored_name(target)); if let Ok(link) = storage::safe_path(&paths.source, target) @@ -569,11 +569,11 @@ pub fn clean(root: &Path, id: &str) -> anyhow::Result { Ok(bytes) } -/// Delete a project from the Agent, with its `~/Slingshot` link. Waits for every job. +/// Delete a project from the Agent, with its `~/Ally` link. Waits for every job. pub fn remove(root: &Path, home: Option<&Path>, id: &str) -> anyhow::Result<()> { let (paths, metadata) = load(root, id)?; let _lock = acquire(root, &paths, &metadata, Blocking::AllJobs)?; - if let Some(Ok(items)) = home.map(|home| fs::read_dir(home.join("Slingshot"))) { + if let Some(Ok(items)) = home.map(|home| fs::read_dir(home.join("Ally"))) { for item in items.flatten() { if fs::read_link(item.path()).is_ok_and(|target| target == paths.source) { fs::remove_file(item.path())?; @@ -891,7 +891,7 @@ mod tests { let home = Root::new(); let id = project(&root.0); let paths = initialize(&root.0, &id); - let folder = home.0.join("Slingshot"); + let folder = home.0.join("Ally"); fs::create_dir(&folder).unwrap(); std::os::unix::fs::symlink(&paths.source, folder.join("app")).unwrap(); std::os::unix::fs::symlink(&home.0, folder.join("other")).unwrap(); @@ -917,7 +917,7 @@ mod tests { impl TempDirLike { fn with(files: &[(&str, &str)]) -> TempDirLike { - let path = std::env::temp_dir().join(format!("slingshot-client-{}", storage::new_id())); + let path = std::env::temp_dir().join(format!("ally-client-{}", storage::new_id())); for (name, body) in files { let file = path.join(name); fs::create_dir_all(file.parent().unwrap()).unwrap(); diff --git a/crates/slingshot-agent/src/runner.rs b/crates/ally-agent/src/runner.rs similarity index 95% rename from crates/slingshot-agent/src/runner.rs rename to crates/ally-agent/src/runner.rs index 45c0856..030f31e 100644 --- a/crates/slingshot-agent/src/runner.rs +++ b/crates/ally-agent/src/runner.rs @@ -1,15 +1,15 @@ -//! Foreground runs started over SSH by `slingshot run`. +//! Foreground runs started over SSH by `ally run`. //! -//! The command runs in its own process group so Slingshot can stop all of it. When SSH +//! The command runs in its own process group so Ally can stop all of it. When SSH //! provides a terminal, that group becomes the terminal's foreground group, so Ctrl C, //! resizing, and interactive input reach the command exactly as they would locally. use crate::{jobs, projects, service}; +use ally_core::control::{JobKind, JobState}; +use ally_core::presentation::{Style, Tone}; +use ally_core::storage; +use ally_core::tools; use anyhow::{Context, bail}; -use slingshot_core::control::{JobKind, JobState}; -use slingshot_core::presentation::{Style, Tone}; -use slingshot_core::storage; -use slingshot_core::tools; use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::signal::unix::{SignalKind, signal}; @@ -50,7 +50,7 @@ pub async fn run( )?; if !directory.is_dir() { bail!( - "{} does not exist in the Agent copy. Run slingshot sync if it was just created", + "{} does not exist in the Agent copy. Run ally sync if it was just created", cwd.unwrap_or_default() ); } @@ -154,7 +154,7 @@ pub async fn run( /// A command that could not start exits the way a shell reports it: 127 when it does not /// exist, 126 when it cannot be run, so scripts see the same codes as locally. -/// Where a run in a project starts: the same `~/Slingshot/` link a session uses, +/// Where a run in a project starts: the same `~/Ally/` link a session uses, /// so tools that name things after the folder, such as Docker Compose, see the project /// rather than the copy's `source` folder. The copy itself is the fallback. fn start_folder( @@ -282,7 +282,7 @@ mod tests { std::fs::create_dir_all(source.join("api")).unwrap(); let start = |cwd| start_folder(Some(&home.0), "web app", "abcdef0123456789", &source, cwd); - let folder = home.0.join("Slingshot").join("web_app"); + let folder = home.0.join("Ally").join("web_app"); assert_eq!(start(None).unwrap(), folder); assert_eq!(start(Some("api")).unwrap(), folder.join("api")); assert!(start(Some("../outside")).is_err()); diff --git a/crates/slingshot-agent/src/service.rs b/crates/ally-agent/src/service.rs similarity index 92% rename from crates/slingshot-agent/src/service.rs rename to crates/ally-agent/src/service.rs index 1218e24..9b00ffb 100644 --- a/crates/slingshot-agent/src/service.rs +++ b/crates/ally-agent/src/service.rs @@ -1,13 +1,13 @@ //! The private Agent control socket. //! -//! SSH authenticates every caller. A hidden `slingshot internal-control` helper, started +//! SSH authenticates every caller. A hidden `ally internal-control` helper, started //! through that SSH login, relays bytes between the connection and a Unix socket that //! only the Agent account can open. The Agent OS account is the trust boundary. use crate::{jobs, projects}; +use ally_core::control::{self, AgentStatus, AgentTools, Request, Response}; +use ally_core::{preflight, storage, telemetry, tools}; use anyhow::{Context, bail}; -use slingshot_core::control::{self, AgentStatus, AgentTools, Request, Response}; -use slingshot_core::{preflight, storage, telemetry, tools}; use std::fs::File; use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf}; @@ -36,12 +36,12 @@ pub fn socket(root: &Path) -> PathBuf { root.join("control.sock") } -/// Keeps the daemon lock for as long as `slingshot start` runs. +/// Keeps the daemon lock for as long as `ally start` runs. pub struct Service { _guard: File, } -/// Whether the daemon is already running for this account, held by another `slingshot start`. +/// Whether the daemon is already running for this account, held by another `ally start`. pub fn running(root: &Path) -> anyhow::Result { storage::private_dir(root)?; Ok(storage::try_lock(&root.join("daemon.lock"))?.is_none()) @@ -52,11 +52,11 @@ pub fn start(name: String, online: Arc) -> anyhow::Result { let root = root()?; storage::private_dir(&root)?; let guard = storage::try_lock(&root.join("daemon.lock"))? - .context("Another slingshot start is already running for this account")?; + .context("Another ally start is already running for this account")?; let path = socket(&root); if path.as_os_str().len() >= 100 { bail!( - "The Slingshot storage path is too long for a control socket: {}. Use an account with a shorter home directory", + "The Ally storage path is too long for a control socket: {}. Use an account with a shorter home directory", path.display() ); } @@ -285,7 +285,7 @@ fn respond(root: &Path, name: &str, request: Request) -> anyhow::Result anyhow::Result { let shell = jobs::login_shell(root); let output = std::process::Command::new(&shell) @@ -305,9 +305,9 @@ fn agent_tools(root: &Path) -> anyhow::Result { }) } -/// Ask the daemon another `slingshot start` runs what it calls itself, for pairing beside it. +/// Ask the daemon another `ally start` runs what it calls itself, for pairing beside it. pub async fn status(root: &Path) -> anyhow::Result { - const NO_ANSWER: &str = "Slingshot is running for this account but did not answer. Stop the other slingshot start, then run slingshot start again"; + const NO_ANSWER: &str = "Ally is running for this account but did not answer. Stop the other ally start, then run ally start again"; let mut stream = UnixStream::connect(socket(root)).await.context(NO_ANSWER)?; let asked = async { control::write_frame(&mut stream, &Request::Status).await?; @@ -319,7 +319,7 @@ pub async fn status(root: &Path) -> anyhow::Result { { Ok(Some(Response::Status(status))) => Ok(status), Err(error) if error.downcast_ref::().is_some() => bail!( - "A different version of Slingshot is already running for this account. Stop the other slingshot start, then run slingshot start again" + "A different version of Ally is already running for this account. Stop the other ally start, then run ally start again" ), _ => bail!(NO_ANSWER), } @@ -331,7 +331,7 @@ pub async fn bridge() -> anyhow::Result { let path = socket(&root()?); let stream = UnixStream::connect(&path) .await - .context("Slingshot is not running on the Agent. Run slingshot start there")?; + .context("Ally is not running on the Agent. Run ally start there")?; let (mut from_agent, mut to_agent) = stream.into_split(); let upload = async { let mut stdin = tokio::io::stdin(); @@ -356,7 +356,7 @@ pub async fn bridge() -> anyhow::Result { mod tests { use super::*; use crate::testing::Root; - use slingshot_core::control::ProjectRef; + use ally_core::control::ProjectRef; async fn call(stream: &mut UnixStream, request: Request) -> Response { control::write_frame(stream, &request).await.unwrap(); @@ -365,7 +365,7 @@ mod tests { /// Socket paths have a small length limit, so tests use a short name under /tmp. fn serve(root: &Root) -> PathBuf { - let path = PathBuf::from(format!("/tmp/slingshot-{}.sock", &storage::new_id()[..12])); + let path = PathBuf::from(format!("/tmp/ally-{}.sock", &storage::new_id()[..12])); let listener = UnixListener::bind(&path).unwrap(); tokio::spawn(accept( listener, @@ -462,7 +462,7 @@ mod tests { stream.write_u32(body.len() as u32).await.unwrap(); stream.write_all(&body).await.unwrap(); let reply: Response = control::read_frame(&mut stream).await.unwrap().unwrap(); - assert!(matches!(reply, Response::Error(message) if message.contains("Update Slingshot"))); + assert!(matches!(reply, Response::Error(message) if message.contains("Update Ally"))); } #[tokio::test] diff --git a/crates/slingshot-agent/src/tunnel.rs b/crates/ally-agent/src/tunnel.rs similarity index 97% rename from crates/slingshot-agent/src/tunnel.rs rename to crates/ally-agent/src/tunnel.rs index cdf26ec..5921a50 100644 --- a/crates/slingshot-agent/src/tunnel.rs +++ b/crates/ally-agent/src/tunnel.rs @@ -3,17 +3,17 @@ //! only ever carries ssh, which still decides who logs in. use crate::clients; +use ally_core::tunnel::{ALPN, NOT_PAIRED}; use anyhow::Context; use iroh::endpoint::{Incoming, RecvStream, SendStream, presets}; use iroh::{Endpoint, SecretKey}; -use slingshot_core::tunnel::{ALPN, NOT_PAIRED}; use std::net::{Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::time::Duration; use tokio::net::TcpStream; use tracing::{debug, info, warn}; -/// The Agent's own sshd. Slingshot already requires it on the usual port. +/// The Agent's own sshd. Ally already requires it on the usual port. const SSHD: SocketAddr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 22); /// How long `start` waits to hear from a relay before saying other networks cannot reach diff --git a/crates/slingshot-agent/src/uninstall.rs b/crates/ally-agent/src/uninstall.rs similarity index 90% rename from crates/slingshot-agent/src/uninstall.rs rename to crates/ally-agent/src/uninstall.rs index b2136ec..6854226 100644 --- a/crates/slingshot-agent/src/uninstall.rs +++ b/crates/ally-agent/src/uninstall.rs @@ -1,16 +1,16 @@ -//! What `slingshot uninstall` removes when this machine is an Agent: the boot service, its -//! running work, the access it gave Clients, the `~/Slingshot` links, and its data. +//! What `ally uninstall` removes when this machine is an Agent: the boot service, its +//! running work, the access it gave Clients, the `~/Ally` links, and its data. use crate::projects::size; use crate::{boot, jobs, service}; +use ally_core::control::{Job, JobKind}; +use ally_core::keys; +use ally_core::presentation; use anyhow::Context; -use slingshot_core::control::{Job, JobKind}; -use slingshot_core::keys; -use slingshot_core::presentation; use std::fs; use std::path::{Path, PathBuf}; -/// Everything Slingshot keeps on this machine as an Agent. +/// Everything Ally keeps on this machine as an Agent. pub struct Found { pub service: Option, pub active: Vec, @@ -28,7 +28,7 @@ impl Found { let service = Some(boot::file()?).filter(|file| file.exists()); anyhow::ensure!( service.is_some() || !root.exists() || !service::running(&root)?, - "Slingshot is running in a terminal on this machine. Stop it with Ctrl C there, then run slingshot uninstall again" + "Ally is running in a terminal on this machine. Stop it with Ctrl C there, then run ally uninstall again" ); let data = root.exists().then(|| { let projects = fs::read_dir(root.join("projects")) @@ -37,7 +37,7 @@ impl Found { (root.clone(), projects, size(&root)) }); let links = directories::BaseDirs::new() - .map(|dirs| dirs.home_dir().join("Slingshot")) + .map(|dirs| dirs.home_dir().join("Ally")) .filter(|folder| folder.exists()); Ok(Found { service, @@ -104,7 +104,7 @@ impl Found { pub fn remove(&self) -> anyhow::Result<()> { if self.service.is_some() { boot::remove_service()?; - presentation::success("Stopped Slingshot and its boot service"); + presentation::success("Stopped Ally and its boot service"); } if let Some((root, _, _)) = &self.data { for job in &self.active { @@ -176,7 +176,7 @@ fn running(active: &[Job]) -> String { } } -/// Remove Slingshot's own links, then the folder if nothing else is in it. A file the owner +/// Remove Ally's own links, then the folder if nothing else is in it. A file the owner /// put there is kept and named. fn remove_links(folder: &Path) -> anyhow::Result<()> { for item in fs::read_dir(folder)?.flatten() { @@ -206,7 +206,7 @@ mod tests { project: None, project_name: None, command: String::new(), - state: slingshot_core::control::JobState::Running, + state: ally_core::control::JobState::Running, started: 0, ended: None, exit_code: None, @@ -238,9 +238,9 @@ mod tests { } #[test] - fn only_slingshots_links_are_removed_from_the_folder() { + fn only_ally_links_are_removed_from_the_folder() { let home = Root::new(); - let folder = home.0.join("Slingshot"); + let folder = home.0.join("Ally"); fs::create_dir(&folder).unwrap(); std::os::unix::fs::symlink(&home.0, folder.join("app")).unwrap(); remove_links(&folder).unwrap(); diff --git a/crates/slingshot-cli/Cargo.toml b/crates/ally-cli/Cargo.toml similarity index 65% rename from crates/slingshot-cli/Cargo.toml rename to crates/ally-cli/Cargo.toml index 70426f7..80838c9 100644 --- a/crates/slingshot-cli/Cargo.toml +++ b/crates/ally-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slingshot-cli" +name = "ally-cli" description = "Use the CPU, RAM, and GPU of a powerful machine from your laptop, in your own editor and terminal" readme = "../../README.md" version.workspace = true @@ -10,19 +10,14 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true -# The package is slingshot-cli, but the program it produces is called slingshot, with sling -# as its short name. Both run the code in lib.rs. +# The package is ally-cli, but the program it produces is called ally. [[bin]] -name = "slingshot" +name = "ally" path = "src/main.rs" -[[bin]] -name = "sling" -path = "src/bin/sling.rs" - [dependencies] -slingshot-core = { path = "../slingshot-core", version = "0.1.0" } -slingshot-agent = { path = "../slingshot-agent", version = "0.1.0" } +ally-core = { path = "../ally-core", version = "0.1.0" } +ally-agent = { path = "../ally-agent", version = "0.1.0" } anyhow = { workspace = true } iroh = { workspace = true } clap = { workspace = true } diff --git a/crates/slingshot-cli/LICENSE-APACHE b/crates/ally-cli/LICENSE-APACHE similarity index 100% rename from crates/slingshot-cli/LICENSE-APACHE rename to crates/ally-cli/LICENSE-APACHE diff --git a/crates/slingshot-cli/LICENSE-MIT b/crates/ally-cli/LICENSE-MIT similarity index 100% rename from crates/slingshot-cli/LICENSE-MIT rename to crates/ally-cli/LICENSE-MIT diff --git a/crates/slingshot-cli/build.rs b/crates/ally-cli/build.rs similarity index 84% rename from crates/slingshot-cli/build.rs rename to crates/ally-cli/build.rs index 62ec0ce..23e6be6 100644 --- a/crates/slingshot-cli/build.rs +++ b/crates/ally-cli/build.rs @@ -1,4 +1,4 @@ -//! Puts the commit Slingshot was built from into `slingshot --version`, so an old build is +//! Puts the commit Ally was built from into `ally --version`, so an old build is //! easy to spot. Without git, such as a build from a published package, only the version //! shows. @@ -10,7 +10,7 @@ fn main() { Some(commit) => format!("{version} ({commit})"), None => version, }; - println!("cargo:rustc-env=SLINGSHOT_VERSION={shown}"); + println!("cargo:rustc-env=ALLY_VERSION={shown}"); if let Some(dir) = git(&["rev-parse", "--absolute-git-dir"]) { println!("cargo:rerun-if-changed={dir}/HEAD"); println!("cargo:rerun-if-changed={dir}/logs/HEAD"); diff --git a/crates/ally-cli/menubar/AppIcon.icns b/crates/ally-cli/menubar/AppIcon.icns new file mode 100644 index 0000000..16ef34c Binary files /dev/null and b/crates/ally-cli/menubar/AppIcon.icns differ diff --git a/crates/slingshot-cli/menubar/Info.plist b/crates/ally-cli/menubar/Info.plist similarity index 84% rename from crates/slingshot-cli/menubar/Info.plist rename to crates/ally-cli/menubar/Info.plist index 3f50043..5922660 100644 --- a/crates/slingshot-cli/menubar/Info.plist +++ b/crates/ally-cli/menubar/Info.plist @@ -3,15 +3,15 @@ CFBundleExecutable - Slingshot + Ally CFBundleIconFile AppIcon CFBundleIdentifier - dev.slingshot.menubar + dev.ally.menubar CFBundleName - Slingshot + Ally CFBundleDisplayName - Slingshot + Ally CFBundlePackageType APPL CFBundleShortVersionString diff --git a/crates/ally-cli/menubar/Logo.png b/crates/ally-cli/menubar/Logo.png new file mode 100644 index 0000000..9194fce Binary files /dev/null and b/crates/ally-cli/menubar/Logo.png differ diff --git a/crates/slingshot-cli/menubar/Package.swift b/crates/ally-cli/menubar/Package.swift similarity index 51% rename from crates/slingshot-cli/menubar/Package.swift rename to crates/ally-cli/menubar/Package.swift index 48ff699..c0e9b82 100644 --- a/crates/slingshot-cli/menubar/Package.swift +++ b/crates/ally-cli/menubar/Package.swift @@ -1,13 +1,13 @@ // swift-tools-version:5.9 -// The Slingshot menu bar app. It only draws what `slingshot internal-watch` prints, so all +// The Ally menu bar app. It only draws what `ally internal-watch` prints, so all // polling and every decision to notify stays in the Rust program. import PackageDescription let package = Package( - name: "SlingshotMenuBar", + name: "AllyMenuBar", platforms: [.macOS(.v14)], targets: [ - .executableTarget(name: "SlingshotMenuBar", path: "Sources") + .executableTarget(name: "AllyMenuBar", path: "Sources") ] ) diff --git a/crates/slingshot-cli/menubar/Sources/App.swift b/crates/ally-cli/menubar/Sources/App.swift similarity index 92% rename from crates/slingshot-cli/menubar/Sources/App.swift rename to crates/ally-cli/menubar/Sources/App.swift index 0be8463..f88ed66 100644 --- a/crates/slingshot-cli/menubar/Sources/App.swift +++ b/crates/ally-cli/menubar/Sources/App.swift @@ -2,7 +2,7 @@ import AppKit import SwiftUI @main -struct SlingshotApp: App { +struct AllyApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate var body: some Scene { @@ -32,7 +32,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { LoginItem.enable() } - /// `slingshot menubar` opens the app again after saving a new program path or Agent. + /// `ally menubar` opens the app again after saving a new program path or Agent. func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { watcher.start() return false @@ -57,7 +57,7 @@ struct MenuBarLabel: View { /// A bundle without the logo falls back to a system symbol rather than an empty space. private static let logo: NSImage = { guard let source = Bundle.main.image(forResource: "Logo") else { - return NSImage(systemSymbolName: "server.rack", accessibilityDescription: "Slingshot") ?? NSImage() + return NSImage(systemSymbolName: "server.rack", accessibilityDescription: "Ally") ?? NSImage() } let logo = NSImage(size: NSSize(width: 18, height: 18), flipped: false) { rect in NSGraphicsContext.current?.imageInterpolation = .none diff --git a/crates/slingshot-cli/menubar/Sources/Bars.swift b/crates/ally-cli/menubar/Sources/Bars.swift similarity index 100% rename from crates/slingshot-cli/menubar/Sources/Bars.swift rename to crates/ally-cli/menubar/Sources/Bars.swift diff --git a/crates/slingshot-cli/menubar/Sources/LoginItem.swift b/crates/ally-cli/menubar/Sources/LoginItem.swift similarity index 74% rename from crates/slingshot-cli/menubar/Sources/LoginItem.swift rename to crates/ally-cli/menubar/Sources/LoginItem.swift index f4b87d2..531a315 100644 --- a/crates/slingshot-cli/menubar/Sources/LoginItem.swift +++ b/crates/ally-cli/menubar/Sources/LoginItem.swift @@ -2,7 +2,7 @@ import Foundation import ServiceManagement /// Starting at login is what makes installing once enough, so the app is always a login -/// item. `slingshot menubar --remove` starts the app with `--remove` to take it off. +/// item. `ally menubar --remove` starts the app with `--remove` to take it off. enum LoginItem { static func enable() { guard SMAppService.mainApp.status != .enabled else { return } @@ -17,7 +17,7 @@ enum LoginItem { try SMAppService.mainApp.unregister() } } catch { - NSLog("Slingshot could not change the login item: \(error.localizedDescription)") + NSLog("Ally could not change the login item: \(error.localizedDescription)") } } } diff --git a/crates/slingshot-cli/menubar/Sources/Model.swift b/crates/ally-cli/menubar/Sources/Model.swift similarity index 100% rename from crates/slingshot-cli/menubar/Sources/Model.swift rename to crates/ally-cli/menubar/Sources/Model.swift diff --git a/crates/slingshot-cli/menubar/Sources/Notifier.swift b/crates/ally-cli/menubar/Sources/Notifier.swift similarity index 100% rename from crates/slingshot-cli/menubar/Sources/Notifier.swift rename to crates/ally-cli/menubar/Sources/Notifier.swift diff --git a/crates/slingshot-cli/menubar/Sources/PopoverView.swift b/crates/ally-cli/menubar/Sources/PopoverView.swift similarity index 96% rename from crates/slingshot-cli/menubar/Sources/PopoverView.swift rename to crates/ally-cli/menubar/Sources/PopoverView.swift index a439b82..e9b0799 100644 --- a/crates/slingshot-cli/menubar/Sources/PopoverView.swift +++ b/crates/ally-cli/menubar/Sources/PopoverView.swift @@ -71,7 +71,7 @@ struct PopoverView: View { Circle() .fill(watcher.status?.online == true ? Color.green : Color.secondary) .frame(width: 8, height: 8) - Text(watcher.status?.agent.nonEmpty ?? "Slingshot") + Text(watcher.status?.agent.nonEmpty ?? "Ally") .font(.headline) Spacer() if watcher.status?.online == true, let path = watcher.status?.path { @@ -169,20 +169,20 @@ struct PopoverView: View { private var offline: some View { if let missing = watcher.missingProgram { ProblemView( - title: "Can't find the slingshot program", + title: "Can't find the ally program", detail: missing.isEmpty - ? "The app has not been told where slingshot is yet." + ? "The app has not been told where ally is yet." : "It was at \(missing), which no longer exists.", - fix: Fix(machine: "client", command: "slingshot menubar"), + fix: Fix(machine: "client", command: "ally menubar"), agent: nil, retrying: false, retry: nil ) } else if let problem = watcher.problem { ProblemView( - title: "Slingshot could not start", + title: "Ally could not start", detail: problem, - fix: Fix(machine: "client", command: "slingshot menubar"), + fix: Fix(machine: "client", command: "ally menubar"), agent: nil, retrying: watcher.retrying, retry: watcher.retry @@ -208,7 +208,7 @@ struct PopoverView: View { private var footer: some View { VStack(spacing: 2) { - MenuRow(icon: "power", title: "Quit Slingshot") { + MenuRow(icon: "power", title: "Quit Ally") { NSApplication.shared.terminate(nil) } trailing: { Text("⌘Q") diff --git a/crates/slingshot-cli/menubar/Sources/Watcher.swift b/crates/ally-cli/menubar/Sources/Watcher.swift similarity index 86% rename from crates/slingshot-cli/menubar/Sources/Watcher.swift rename to crates/ally-cli/menubar/Sources/Watcher.swift index 32e3e3f..376480b 100644 --- a/crates/slingshot-cli/menubar/Sources/Watcher.swift +++ b/crates/ally-cli/menubar/Sources/Watcher.swift @@ -1,7 +1,7 @@ import Foundation import Observation -/// Runs `slingshot internal-watch` and keeps what it reports. Closing the helper's input is what stops it, so it never outlives the app. +/// Runs `ally internal-watch` and keeps what it reports. Closing the helper's input is what stops it, so it never outlives the app. @MainActor @Observable final class Watcher { @@ -11,7 +11,7 @@ final class Watcher { private(set) var retrying = false /// Why there is nothing to show, such as a helper that keeps failing. private(set) var problem: String? - /// Set when the app cannot find the slingshot program: the saved path that is gone, or an + /// Set when the app cannot find the ally program: the saved path that is gone, or an /// empty string when no path was ever saved. Trying again cannot fix this. private(set) var missingProgram: String? @@ -27,10 +27,10 @@ final class Watcher { private static let backoff: [TimeInterval] = [2, 5, 15, 30] - /// Start the helper, or restart it when `slingshot menubar` saved a new program or Agent. + /// Start the helper, or restart it when `ally menubar` saved a new program or Agent. func start() { let defaults = UserDefaults.standard - let saved = defaults.string(forKey: "slingshotPath") + let saved = defaults.string(forKey: "allyPath") guard let program = saved, FileManager.default.isExecutableFile(atPath: program) else { missingProgram = saved ?? "" return @@ -93,7 +93,7 @@ final class Watcher { do { try process.run() } catch { - problem = "Could not start Slingshot at \(program): \(error.localizedDescription)" + problem = "Could not start Ally at \(program): \(error.localizedDescription)" return } self.process = process @@ -111,7 +111,7 @@ final class Watcher { case .status(let status): apply(status) case .notice(let notice): onNotice?(notice) case .unsupported(let version): - problem = "Slingshot speaks format \(version), and this app speaks format \(supportedVersion)." + problem = "Ally speaks format \(version), and this app speaks format \(supportedVersion)." } } } @@ -129,7 +129,7 @@ final class Watcher { } } - /// The helper exits cleanly when Slingshot was updated, so the new program takes over at + /// The helper exits cleanly when Ally was updated, so the new program takes over at /// once. Any other exit means something is wrong, so show why and try again. private func ended(_ ended: Process) { guard ended === process, !stopping else { return } @@ -142,7 +142,7 @@ final class Watcher { .split(separator: "\n") .last .map { $0.replacingOccurrences(of: "✗ ", with: "") } - problem = message ?? "Slingshot stopped unexpectedly." + problem = message ?? "Ally stopped unexpectedly." process = nil let wait = Self.backoff[min(restarts, Self.backoff.count - 1)] restarts += 1 diff --git a/crates/slingshot-cli/src/client.rs b/crates/ally-cli/src/client.rs similarity index 87% rename from crates/slingshot-cli/src/client.rs rename to crates/ally-cli/src/client.rs index aa6bd19..9585e91 100644 --- a/crates/slingshot-cli/src/client.rs +++ b/crates/ally-cli/src/client.rs @@ -4,12 +4,12 @@ use crate::route::{self, Route}; use crate::ssh::RemoteCommand; use crate::tunnel; +use ally_core::config::Agent; +use ally_core::control::{self, Request, Response}; +use ally_core::presentation; +use ally_core::protocol; +use ally_core::step::{self, Step}; use anyhow::Context; -use slingshot_core::config::Agent; -use slingshot_core::control::{self, Request, Response}; -use slingshot_core::presentation; -use slingshot_core::protocol; -use slingshot_core::step::{self, Step}; use std::process::Stdio; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; @@ -27,8 +27,8 @@ pub async fn pair( ) -> anyhow::Result { let stream = tokio::time::timeout(Duration::from_secs(10), TcpStream::connect((host, port))) .await - .with_context(|| format!("Timed out reaching the Agent at {host}:{port}. Check that slingshot start is running there"))? - .with_context(|| format!("Could not reach the Agent at {host}:{port}. Check that slingshot start is running there"))?; + .with_context(|| format!("Timed out reaching the Agent at {host}:{port}. Check that ally start is running there"))? + .with_context(|| format!("Could not reach the Agent at {host}:{port}. Check that ally start is running there"))?; let (reader, mut writer) = stream.into_split(); let mut reader = BufReader::new(reader.take(1024 * 1024)); @@ -38,7 +38,7 @@ pub async fn pair( protocol::Response::Start { spake } => spake, protocol::Response::Error { message } if message.starts_with("Could not understand") => { anyhow::bail!( - "The Agent at {host} runs an older Slingshot. Update Slingshot there, restart slingshot start, then link again" + "The Agent at {host} runs an older Ally. Update Ally there, restart ally start, then link again" ) } other => return Err(unexpected_pairing(other)), @@ -82,7 +82,7 @@ async fn answer( .context("The Agent did not answer the pairing request")? .context("The Agent closed the connection without answering")?; serde_json::from_str(reply.trim()) - .context("Could not understand the Agent's answer. Update Slingshot on both machines") + .context("Could not understand the Agent's answer. Update Ally on both machines") } /// An Agent error passes through as written. Anything else means the two sides disagree @@ -91,7 +91,7 @@ fn unexpected_pairing(response: protocol::Response) -> anyhow::Error { match response { protocol::Response::Error { message } => anyhow::anyhow!("{message}"), _ => anyhow::anyhow!( - "The Agent answered pairing out of order. Update Slingshot on both machines, then link again" + "The Agent answered pairing out of order. Update Ally on both machines, then link again" ), } } @@ -191,7 +191,7 @@ impl Control { } Ok(Err(error)) if matches!(self.child.try_wait(), Ok(None)) => { return Err(error.context(format!( - "Could not understand {}. Update Slingshot on both machines", + "Could not understand {}. Update Ally on both machines", self.name ))); } @@ -230,10 +230,10 @@ impl Control { .unwrap_or("the SSH connection closed"); let hint = match detail { d if d.contains("command not found") || d.contains("No such file") => { - ". Slingshot was not found on the Agent. Run slingshot link again after installing it there" + ". Ally was not found on the Agent. Run ally link again after installing it there" } d if d.contains("unrecognized subcommand") => { - ". Slingshot versions differ between the machines. Update Slingshot on both machines" + ". Ally versions differ between the machines. Update Ally on both machines" } _ => "", }; @@ -250,9 +250,9 @@ impl Control { } } -/// The command that installs or updates Slingshot. It builds from GitHub until Slingshot is +/// The command that installs or updates Ally. It builds from GitHub until Ally is /// published to crates.io (#17). -const INSTALL: &str = "cargo install --locked --git https://github.com/ado11231/ss slingshot-cli"; +const INSTALL: &str = "cargo install --locked --git https://github.com/ado11231/ally ally-cli"; /// How to update after a protocol mismatch, naming the machine that runs the older build. fn update_advice(name: &str, mismatch: &control::Mismatch) -> String { @@ -263,11 +263,11 @@ fn update_advice(name: &str, mismatch: &control::Mismatch) -> String { .unwrap_or_default(); match mismatch.other_is_older() { true => format!( - "{name} runs an older Slingshot{theirs} than this machine ({}). Update it there with: {INSTALL}, then restart slingshot start", + "{name} runs an older Ally{theirs} than this machine ({}). Update it there with: {INSTALL}, then restart ally start", control::RELEASE ), false => format!( - "This machine runs an older Slingshot ({}) than {name}{theirs}. Update it with: {INSTALL}", + "This machine runs an older Ally ({}) than {name}{theirs}. Update it with: {INSTALL}", control::RELEASE ), } @@ -278,7 +278,7 @@ fn update_advice(name: &str, mismatch: &control::Mismatch) -> String { fn release_differs(name: &str, theirs: Option<&str>) -> Option { let theirs = theirs.filter(|theirs| *theirs != control::RELEASE)?; Some(format!( - "{name} runs Slingshot {theirs} and this machine runs {}. Update the older one with: {INSTALL}", + "{name} runs Ally {theirs} and this machine runs {}. Update the older one with: {INSTALL}", control::RELEASE )) } @@ -311,7 +311,7 @@ pub async fn request(agent: &Agent, request: Request) -> anyhow::Result anyhow::Error { - anyhow::anyhow!("The Agent returned an unexpected response. Update Slingshot on both machines") + anyhow::anyhow!("The Agent returned an unexpected response. Update Ally on both machines") } #[cfg(test)] @@ -326,10 +326,10 @@ mod tests { }; let advice = update_advice("archbox", &older); assert!( - advice.starts_with("archbox runs an older Slingshot 0.0.9 than this machine"), + advice.starts_with("archbox runs an older Ally 0.0.9 than this machine"), "{advice}" ); - assert!(advice.contains("restart slingshot start")); + assert!(advice.contains("restart ally start")); let newer = control::Mismatch { theirs: control::VERSION + 1, @@ -337,7 +337,7 @@ mod tests { }; let advice = update_advice("archbox", &newer); assert!( - advice.starts_with("This machine runs an older Slingshot"), + advice.starts_with("This machine runs an older Ally"), "{advice}" ); assert!( @@ -351,6 +351,6 @@ mod tests { assert_eq!(release_differs("archbox", Some(control::RELEASE)), None); assert_eq!(release_differs("archbox", None), None); let warning = release_differs("archbox", Some("0.0.1")).unwrap(); - assert!(warning.starts_with("archbox runs Slingshot 0.0.1 and this machine runs")); + assert!(warning.starts_with("archbox runs Ally 0.0.1 and this machine runs")); } } diff --git a/crates/slingshot-cli/src/commands/attach.rs b/crates/ally-cli/src/commands/attach.rs similarity index 94% rename from crates/slingshot-cli/src/commands/attach.rs rename to crates/ally-cli/src/commands/attach.rs index 4adbce8..927b7ad 100644 --- a/crates/slingshot-cli/src/commands/attach.rs +++ b/crates/ally-cli/src/commands/attach.rs @@ -1,4 +1,4 @@ -//! `slingshot attach [path]`: open or rejoin a persistent session on the Agent, in the +//! `ally attach [path]`: open or rejoin a persistent session on the Agent, in the //! current project's copy, or in the Agent's home folder when there is no project. use crate::client::{self, Refused, unexpected}; @@ -6,16 +6,16 @@ use crate::project::{self, Local}; use crate::route; use crate::ssh::{self, RemoteCommand}; use crate::transfer::{self, Conflicts, Direction}; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::control::{Request, Response, SessionInfo}; -use slingshot_core::presentation::{self, Style}; +use ally_core::config::{Agent, Config}; +use ally_core::control::{Request, Response, SessionInfo}; +use ally_core::presentation::{self, Style}; pub async fn attach( agent: Option, path: Option, ) -> anyhow::Result { if !ssh::wants_terminal() { - anyhow::bail!("slingshot attach needs an interactive terminal"); + anyhow::bail!("ally attach needs an interactive terminal"); } let config = Config::load()?; let target = config.resolve(agent.as_deref())?; @@ -64,7 +64,7 @@ pub async fn attach( let lost = format!( "Lost connection to {}. The session keeps running there\n{}", target.name, - presentation::row("Return", "slingshot attach").trim_end() + presentation::row("Return", "ally attach").trim_end() ); let code = super::run::interact(target, &remote, lost).await?; if pulls_after(code) @@ -99,7 +99,7 @@ fn tmux_args(socket: &str, id: &str, label: &str, returning: bool) -> Vec presentation::success(format!("The session on {} ended", target.name)), } @@ -154,7 +154,7 @@ fn pulls_after(code: i32) -> bool { } /// Say whether the session was left or ended, then bring back what changed on the Agent -/// during it, such as a coding agent's edits, as `slingshot sync --pull` does. A failure is +/// during it, such as a coding agent's edits, as `ally sync --pull` does. A failure is /// a warning, because the session itself ended well and the edits wait safely on the Agent. async fn after_leaving(target: &Agent, local: Option<&Local>, session: &str) -> anyhow::Result<()> { let Some(local) = local else { @@ -238,7 +238,7 @@ async fn open_session( #[cfg(test)] mod tests { use super::*; - use slingshot_core::control::{Job, JobKind, JobState}; + use ally_core::control::{Job, JobKind, JobState}; #[test] fn a_session_counts_as_ended_only_when_the_agent_no_longer_lists_it() { @@ -293,7 +293,7 @@ mod tests { "set-option", "-t", "=j1:", - "@slingshot", + "@ally", "L", ";", "attach-session", diff --git a/crates/slingshot-cli/src/commands/env.rs b/crates/ally-cli/src/commands/env.rs similarity index 88% rename from crates/slingshot-cli/src/commands/env.rs rename to crates/ally-cli/src/commands/env.rs index 8d97ed9..85cbe62 100644 --- a/crates/slingshot-cli/src/commands/env.rs +++ b/crates/ally-cli/src/commands/env.rs @@ -1,12 +1,12 @@ -//! `slingshot env`: environment files kept on the Agent, outside source copies. +//! `ally env`: environment files kept on the Agent, outside source copies. use crate::client::{fetch, unexpected}; use crate::project::{self, Local}; +use ally_core::config::{Agent, Config}; +use ally_core::control::{MAX_ENVIRONMENT_FILE, Request, Response}; +use ally_core::presentation::{self, Style, Tone}; +use ally_core::source; use anyhow::Context; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::control::{MAX_ENVIRONMENT_FILE, Request, Response}; -use slingshot_core::presentation::{self, Style, Tone}; -use slingshot_core::source; use std::path::{Path, PathBuf}; /// The Agent, the project folder, and the project's ID there. Every env subcommand needs @@ -61,7 +61,7 @@ pub async fn add( } /// Where a file lands when no target is given: its own place in the project, so -/// `slingshot env add api/.env` fills `api/.env` on the Agent. A file from outside the +/// `ally env add api/.env` fills `api/.env` on the Agent. A file from outside the /// project keeps only its name. fn default_target(file: &Path, root: &Path) -> anyhow::Result { let full = std::fs::canonicalize(file) @@ -95,7 +95,7 @@ pub async fn list(agent: Option) -> anyhow::Result { if names.is_empty() { println!( " None set. Add one with {}", - style.paint("slingshot env add .env", Tone::Info) + style.paint("ally env add .env", Tone::Info) ); } for name in names { @@ -130,12 +130,12 @@ mod tests { #[test] fn a_file_without_a_target_keeps_its_place_in_the_project() { let project = - std::env::temp_dir().join(format!("env-target-{}", slingshot_core::storage::new_id())); + std::env::temp_dir().join(format!("env-target-{}", ally_core::storage::new_id())); std::fs::create_dir_all(project.join("api")).unwrap(); std::fs::write(project.join(".env"), "A=1").unwrap(); std::fs::write(project.join("api/.env.local"), "B=2").unwrap(); let outside = - std::env::temp_dir().join(format!("prod-{}.env", slingshot_core::storage::new_id())); + std::env::temp_dir().join(format!("prod-{}.env", ally_core::storage::new_id())); std::fs::write(&outside, "C=3").unwrap(); assert_eq!( diff --git a/crates/slingshot-cli/src/commands/health.rs b/crates/ally-cli/src/commands/health.rs similarity index 97% rename from crates/slingshot-cli/src/commands/health.rs rename to crates/ally-cli/src/commands/health.rs index a44f12e..d9667c3 100644 --- a/crates/slingshot-cli/src/commands/health.rs +++ b/crates/ally-cli/src/commands/health.rs @@ -2,11 +2,11 @@ use crate::client::{self, Control, unexpected}; use crate::live; -use slingshot_core::config::Config; -use slingshot_core::control::{Request, Response}; -use slingshot_core::presentation::{Style, Tone, capacity, row}; -use slingshot_core::protocol::{Health, Specs}; -use slingshot_core::storage; +use ally_core::config::Config; +use ally_core::control::{Request, Response}; +use ally_core::presentation::{Style, Tone, capacity, row}; +use ally_core::protocol::{Health, Specs}; +use ally_core::storage; pub async fn health(agent: Option, watch: bool) -> anyhow::Result { let config = Config::load()?; @@ -117,7 +117,7 @@ pub fn render(name: &str, specs: Option<&Specs>, health: &Health, style: Style) } fn workspace(free_mib: u64, style: Style) -> String { - match free_mib < slingshot_core::telemetry::DISK_WARNING_MIB { + match free_mib < ally_core::telemetry::DISK_WARNING_MIB { true => style.paint( format!("{} free Low space", capacity(free_mib)), Tone::Error, @@ -210,7 +210,7 @@ fn memory(used: u64, total: u64, style: Style) -> String { #[cfg(test)] mod tests { use super::*; - use slingshot_core::protocol::GpuHealth; + use ally_core::protocol::GpuHealth; fn sample() -> Health { Health { diff --git a/crates/slingshot-cli/src/commands/link.rs b/crates/ally-cli/src/commands/link.rs similarity index 86% rename from crates/slingshot-cli/src/commands/link.rs rename to crates/ally-cli/src/commands/link.rs index a21e520..cc7eda5 100644 --- a/crates/slingshot-cli/src/commands/link.rs +++ b/crates/ally-cli/src/commands/link.rs @@ -1,15 +1,15 @@ -//! `slingshot link `: pair with an Agent and remember it. +//! `ally link `: pair with an Agent and remember it. use crate::client; use crate::keys; use crate::project; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::keys as core_keys; -use slingshot_core::preflight::{self, Check, State}; -use slingshot_core::presentation::{self, home_path}; -use slingshot_core::protocol::Joining; -use slingshot_core::step; -use slingshot_core::tunnel; +use ally_core::config::{Agent, Config}; +use ally_core::keys as core_keys; +use ally_core::preflight::{self, Check, State}; +use ally_core::presentation::{self, home_path}; +use ally_core::protocol::Joining; +use ally_core::step; +use ally_core::tunnel; /// Take a pairing code, install this machine's key on the Agent, and save what it /// takes to reach it again. Every step says what it did. @@ -99,16 +99,16 @@ pub async fn link(code: String, name: Option) -> anyhow::Result { if let Err(error) = super::tools::offer(config.resolve(Some(&name))?).await { presentation::warning(format!( - "Could not set up tools on {name}: {error:#}. Run slingshot tools to try again" + "Could not set up tools on {name}: {error:#}. Run ally tools to try again" )); } if let Err(error) = super::menubar::offer(&name).await { presentation::warning(format!( - "Could not add {name} to the menu bar: {error:#}. Run slingshot menubar to try again" + "Could not add {name} to the menu bar: {error:#}. Run ally menubar to try again" )); } presentation::section("Ready"); - presentation::detail("Try it", "slingshot run uname -n"); + presentation::detail("Try it", "ally run uname -n"); eprintln!(); Ok(0) @@ -118,7 +118,7 @@ pub async fn link(code: String, name: Option) -> anyhow::Result { /// because pairing only works where the Agent can be reached directly. fn unreachable(host: &str, port: u16) -> String { format!( - "Could not reach {host}:{port}. Check that slingshot start is running there, and that this machine is on the same network or tailnet" + "Could not reach {host}:{port}. Check that ally start is running there, and that this machine is on the same network or tailnet" ) } @@ -165,9 +165,7 @@ mod tests { fn an_unreachable_agent_is_one_line_with_its_fix() { let message = unreachable("192.168.1.9", 7433); - assert!( - message.starts_with("Could not reach 192.168.1.9:7433. Check that slingshot start") - ); + assert!(message.starts_with("Could not reach 192.168.1.9:7433. Check that ally start")); assert!(!message.contains('\n')); } diff --git a/crates/slingshot-cli/src/commands/menubar.rs b/crates/ally-cli/src/commands/menubar.rs similarity index 84% rename from crates/slingshot-cli/src/commands/menubar.rs rename to crates/ally-cli/src/commands/menubar.rs index 66b4dd3..c2e9484 100644 --- a/crates/slingshot-cli/src/commands/menubar.rs +++ b/crates/ally-cli/src/commands/menubar.rs @@ -1,14 +1,14 @@ -//! `slingshot menubar`: build the Slingshot menu bar app if needed, open it, and tell it -//! where this program is. The app starts `slingshot internal-watch` for its numbers, and an +//! `ally menubar`: build the Ally menu bar app if needed, open it, and tell it +//! where this program is. The app starts `ally internal-watch` for its numbers, and an //! app opened from Finder or at login does not get the shell's PATH, so it cannot find the //! program alone. /// The app's bundle identifier, which is also where its settings live. #[cfg(target_os = "macos")] -const BUNDLE_ID: &str = "dev.slingshot.menubar"; +const BUNDLE_ID: &str = "dev.ally.menubar"; -/// The app's source, logo, and icon, built into this program so `slingshot menubar` can build the -/// app even after the Slingshot source folder is gone. +/// The app's source, logo, and icon, built into this program so `ally menubar` can build the +/// app even after the Ally source folder is gone. #[cfg(target_os = "macos")] const SOURCES: &[(&str, &[u8])] = &[ ( @@ -50,7 +50,7 @@ const SOURCES: &[(&str, &[u8])] = &[ /// Where an installed app records which source it was built from. #[cfg(target_os = "macos")] -const STAMP: &str = "Contents/Resources/slingshot-source"; +const STAMP: &str = "Contents/Resources/ally-source"; /// What `link` does about the menu bar app when it finishes. #[cfg(target_os = "macos")] @@ -69,9 +69,9 @@ fn offer_for(installed: bool, swift: bool, terminal: bool) -> Offer { match (installed, swift, terminal) { (true, _, _) => Offer::Restart, (false, false, _) => Offer::Later( - "Install the Xcode command line tools with xcode-select --install, then run slingshot menubar", + "Install the Xcode command line tools with xcode-select --install, then run ally menubar", ), - (false, true, false) => Offer::Later("Run slingshot menubar in a terminal to add it"), + (false, true, false) => Offer::Later("Run ally menubar in a terminal to add it"), (false, true, true) => Offer::Ask, } } @@ -84,11 +84,11 @@ pub async fn offer(_name: &str) -> anyhow::Result<()> { #[cfg(target_os = "macos")] pub async fn offer(name: &str) -> anyhow::Result<()> { - use slingshot_core::presentation::Style; - use slingshot_core::telemetry; + use ally_core::presentation::Style; + use ally_core::telemetry; let style = Style::stderr(); - slingshot_core::presentation::section("Menu bar"); + ally_core::presentation::section("Menu bar"); match offer_for( find_app().is_some(), telemetry::is_installed("swift"), @@ -110,17 +110,15 @@ pub async fn offer(name: &str) -> anyhow::Result<()> { eprintln!( " {}", style.dim( - "It is built with Swift into ~/Applications/Slingshot.app and starts at login." + "It is built with Swift into ~/Applications/Ally.app and starts at login." ) ); - if slingshot_core::presentation::confirm("Add it to your menu bar now?".to_string()) - .await? - { + if ally_core::presentation::confirm("Add it to your menu bar now?".to_string()).await? { show(name, None).await?; } else { eprintln!( " {}", - style.dim("Run slingshot menubar any time to do this later") + style.dim("Run ally menubar any time to do this later") ); } } @@ -131,14 +129,14 @@ pub async fn offer(name: &str) -> anyhow::Result<()> { #[cfg(not(target_os = "macos"))] pub async fn menubar(_agent: Option, _remove: bool) -> anyhow::Result { anyhow::bail!( - "The menu bar app is macOS only for now. slingshot health --watch shows the same numbers in a terminal" + "The menu bar app is macOS only for now. ally health --watch shows the same numbers in a terminal" ) } #[cfg(target_os = "macos")] pub async fn menubar(agent: Option, remove: bool) -> anyhow::Result { - use slingshot_core::config::Config; - use slingshot_core::presentation::{self, home_path}; + use ally_core::config::Config; + use ally_core::presentation::{self, home_path}; if remove { return uninstall().await; @@ -147,16 +145,16 @@ pub async fn menubar(agent: Option, remove: bool) -> anyhow::Result let target = config.resolve(agent.as_deref())?; let app = show(&target.name, agent.as_deref()).await?; presentation::detail("App", home_path(&app)); - presentation::detail("Starts", "at login, remove with slingshot menubar --remove"); + presentation::detail("Starts", "at login, remove with ally menubar --remove"); Ok(0) } /// Build the app if its source changed, tell it where this program is, and start it again, -/// so it always runs the helper from the Slingshot that is installed now. +/// so it always runs the helper from the Ally that is installed now. #[cfg(target_os = "macos")] async fn show(name: &str, agent: Option<&str>) -> anyhow::Result { + use ally_core::presentation::{self, home_path}; use anyhow::Context; - use slingshot_core::presentation::{self, home_path}; let app = match find_app().filter(|app| up_to_date(app)) { Some(app) => app, @@ -169,17 +167,12 @@ async fn show(name: &str, agent: Option<&str>) -> anyhow::Result defaults(&["write", BUNDLE_ID, "agent", agent])?, None => { @@ -214,7 +207,7 @@ fn cargo_build(program: &std::path::Path) -> bool { /// List what the menu bar app keeps, ask, then remove it with `remove_app`. #[cfg(target_os = "macos")] async fn uninstall() -> anyhow::Result { - use slingshot_core::presentation::{self, home_path}; + use ally_core::presentation::{self, home_path}; let Some(app) = find_app() else { presentation::success("The menu bar app is not installed"); @@ -229,9 +222,9 @@ async fn uninstall() -> anyhow::Result { presentation::detail("Settings", BUNDLE_ID); anyhow::ensure!( crate::ssh::wants_terminal(), - "Run slingshot menubar --remove in a terminal to confirm" + "Run ally menubar --remove in a terminal to confirm" ); - if !slingshot_core::presentation::confirm("Remove the menu bar app?".to_string()).await? { + if !ally_core::presentation::confirm("Remove the menu bar app?".to_string()).await? { return Ok(0); } remove_app(&app).await?; @@ -239,7 +232,7 @@ async fn uninstall() -> anyhow::Result { Ok(0) } -/// The installed app, if there is one, for `slingshot uninstall` to list. +/// The installed app, if there is one, for `ally uninstall` to list. #[cfg(target_os = "macos")] pub fn installed() -> Option { find_app() @@ -275,7 +268,7 @@ pub async fn remove_app(app: &std::path::Path) -> anyhow::Result<()> { .context("Could not start open")?; anyhow::ensure!( left.success(), - "Could not take the app out of login items. Remove Slingshot in System Settings, then General, then Login Items" + "Could not take the app out of login items. Remove Ally in System Settings, then General, then Login Items" ); fs::remove_dir_all(app).with_context(|| { format!( @@ -296,7 +289,7 @@ fn find_app() -> Option { let home = directories::BaseDirs::new().map(|dirs| dirs.home_dir().join("Applications")); home.into_iter() .chain(Some(std::path::PathBuf::from("/Applications"))) - .map(|folder| folder.join("Slingshot.app")) + .map(|folder| folder.join("Ally.app")) .find(|app| app.is_dir()) } @@ -315,15 +308,15 @@ fn stamp_of(sources: &[(&str, &[u8])]) -> String { all.extend_from_slice(contents); all.push(0); } - slingshot_core::source::digest(&all) + ally_core::source::digest(&all) } /// Build the app with Swift, sign it for this machine, and install it in ~/Applications in /// place of an older copy. A stranger needs only the Xcode command line tools for this. #[cfg(target_os = "macos")] fn install() -> anyhow::Result { + use ally_core::{step, telemetry}; use anyhow::Context; - use slingshot_core::{step, telemetry}; use std::fs; anyhow::ensure!( @@ -370,7 +363,7 @@ fn install() -> anyhow::Result { .join("Applications"); fs::create_dir_all(&applications) .with_context(|| format!("Could not create {}", applications.display()))?; - let app = applications.join("Slingshot.app"); + let app = applications.join("Ally.app"); if app.exists() { fs::remove_dir_all(&app) .with_context(|| format!("Could not replace the old {}", app.display()))?; @@ -416,18 +409,15 @@ fn assemble( use anyhow::Context; use std::fs; - let app = package.join(".build").join("Slingshot.app"); + let app = package.join(".build").join("Ally.app"); if app.exists() { fs::remove_dir_all(&app).with_context(|| format!("Could not clear {}", app.display()))?; } let contents = app.join("Contents"); fs::create_dir_all(contents.join("MacOS"))?; fs::create_dir_all(contents.join("Resources"))?; - fs::copy( - bin.join("SlingshotMenuBar"), - contents.join("MacOS").join("Slingshot"), - ) - .context("Swift finished but the app program was not where it said")?; + fs::copy(bin.join("AllyMenuBar"), contents.join("MacOS").join("Ally")) + .context("Swift finished but the app program was not where it said")?; fs::copy(package.join("Info.plist"), contents.join("Info.plist"))?; for resource in ["Logo.png", "AppIcon.icns"] { fs::copy( @@ -472,7 +462,7 @@ fn quit_running_app() { #[cfg(target_os = "macos")] fn app_running() -> bool { std::process::Command::new("pgrep") - .args(["-f", "Slingshot.app/Contents/MacOS/Slingshot"]) + .args(["-f", "Ally.app/Contents/MacOS/Ally"]) .stdout(std::process::Stdio::null()) .status() .is_ok_and(|status| status.success()) @@ -516,21 +506,17 @@ mod tests { matches!(offer_for(false, false, true), Offer::Later(how) if how.contains("xcode-select --install")) ); assert!( - matches!(offer_for(false, true, false), Offer::Later(how) if how.contains("slingshot menubar")) + matches!(offer_for(false, true, false), Offer::Later(how) if how.contains("ally menubar")) ); } #[test] fn a_cargo_build_is_told_apart_from_an_installed_program() { use std::path::Path; - assert!(cargo_build(Path::new( - "/src/slingshot/target/debug/slingshot" - ))); - assert!(cargo_build(Path::new( - "/src/slingshot/target/release/slingshot" - ))); - assert!(!cargo_build(Path::new("/home/me/.cargo/bin/slingshot"))); - assert!(!cargo_build(Path::new("/opt/target/slingshot"))); + assert!(cargo_build(Path::new("/src/ally/target/debug/ally"))); + assert!(cargo_build(Path::new("/src/ally/target/release/ally"))); + assert!(!cargo_build(Path::new("/home/me/.cargo/bin/ally"))); + assert!(!cargo_build(Path::new("/opt/target/ally"))); } #[test] diff --git a/crates/slingshot-cli/src/commands/mod.rs b/crates/ally-cli/src/commands/mod.rs similarity index 100% rename from crates/slingshot-cli/src/commands/mod.rs rename to crates/ally-cli/src/commands/mod.rs diff --git a/crates/slingshot-cli/src/commands/projects.rs b/crates/ally-cli/src/commands/projects.rs similarity index 93% rename from crates/slingshot-cli/src/commands/projects.rs rename to crates/ally-cli/src/commands/projects.rs index e258ca3..f5c2569 100644 --- a/crates/slingshot-cli/src/commands/projects.rs +++ b/crates/ally-cli/src/commands/projects.rs @@ -1,12 +1,12 @@ -//! `slingshot projects`: the project copies on the Agent, and freeing their space. +//! `ally projects`: the project copies on the Agent, and freeing their space. use super::ps::ago; use crate::client::{self, unexpected}; use crate::project; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::control::{ProjectSummary, Request, Response}; -use slingshot_core::presentation::{self, Style, Tone}; -use slingshot_core::{step, storage}; +use ally_core::config::{Agent, Config}; +use ally_core::control::{ProjectSummary, Request, Response}; +use ally_core::presentation::{self, Style, Tone}; +use ally_core::{step, storage}; pub async fn list(agent: Option) -> anyhow::Result { let config = Config::load()?; @@ -62,7 +62,7 @@ pub async fn remove(agent: Option, project: Option) -> anyhow::R } anyhow::ensure!( crate::ssh::wants_terminal(), - "Run slingshot projects remove in a terminal to confirm, since it deletes files" + "Run ally projects remove in a terminal to confirm, since it deletes files" ); if !presentation::confirm_deleting("Remove it?".to_string()).await? { presentation::success("Nothing was removed"); @@ -103,7 +103,7 @@ fn choose<'a>( .map(|p| &p.id); return projects.iter().find(|p| Some(&p.id) == id).ok_or_else(|| { anyhow::anyhow!( - "{} has no copy on {agent}. See every project with slingshot projects", + "{} has no copy on {agent}. See every project with ally projects", local.name ) }); @@ -119,7 +119,7 @@ fn choose<'a>( match matches.as_slice() { [found] => Ok(found), [] => anyhow::bail!( - "No project named {query} on {agent}. See every project with slingshot projects" + "No project named {query} on {agent}. See every project with ally projects" ), several => anyhow::bail!( "{} projects are named {query} on {agent}. Use the ID of the one you mean: {}", @@ -154,7 +154,7 @@ fn listing(project: &ProjectSummary, style: Style) -> Vec { if project.waiting > 0 { lines.push(style.dim(presentation::row( "Keep edits", - "run slingshot sync --pull in the project first", + "run ally sync --pull in the project first", ))); } lines @@ -169,7 +169,7 @@ pub fn render(agent: &str, projects: &[ProjectSummary], now: u64, style: Style) if projects.is_empty() { output.push_str(&format!( " None yet. {} in a project copies it\n", - style.paint("slingshot sync", Tone::Info) + style.paint("ally sync", Tone::Info) )); return output; } @@ -228,7 +228,7 @@ pub fn render(agent: &str, projects: &[ProjectSummary], now: u64, style: Style) " {}\n", style.dim(format!( "Free build output with {}", - style.paint("slingshot projects clean ", Tone::Info) + style.paint("ally projects clean ", Tone::Info) )) )); } @@ -288,7 +288,7 @@ mod tests { assert!(text.contains(" api MacBook-Pro-3f9c2a 2 MiB 2h ago 4\n")); assert!(text.contains(" notes archlaptop-a1b2c3 2 MiB 1.5 GiB never\n")); assert!(text.contains("\n 3 projects, 3.0 GiB\n")); - assert!(text.contains("Free build output with slingshot projects clean ")); + assert!(text.contains("Free build output with ally projects clean ")); assert!(!text.contains('\x1b')); } @@ -310,7 +310,7 @@ mod tests { #[test] fn an_empty_agent_says_how_to_add_a_project() { let text = render("archbox", &[], 0, Style::new(false)); - assert!(text.contains("None yet. slingshot sync in a project copies it")); + assert!(text.contains("None yet. ally sync in a project copies it")); } #[test] @@ -353,7 +353,7 @@ mod tests { project.waiting = 4; let lines = listing(&project, Style::new(false)).concat(); assert!(lines.contains("2 MiB, including 4 edits not brought back")); - assert!(lines.contains("run slingshot sync --pull in the project first")); + assert!(lines.contains("run ally sync --pull in the project first")); project.waiting = 0; assert!( !listing(&project, Style::new(false)) diff --git a/crates/slingshot-cli/src/commands/ps.rs b/crates/ally-cli/src/commands/ps.rs similarity index 95% rename from crates/slingshot-cli/src/commands/ps.rs rename to crates/ally-cli/src/commands/ps.rs index e1d130d..d718d2e 100644 --- a/crates/slingshot-cli/src/commands/ps.rs +++ b/crates/ally-cli/src/commands/ps.rs @@ -1,11 +1,11 @@ -//! `slingshot ps` and `slingshot stop`: Slingshot runs and sessions on the Agent. +//! `ally ps` and `ally stop`: Ally runs and sessions on the Agent. use crate::client::{self, unexpected}; -use slingshot_core::config::Config; -use slingshot_core::control::{Job, JobKind, JobState, Request, Response}; -use slingshot_core::presentation::{self, Style, Tone}; -use slingshot_core::step; -use slingshot_core::storage; +use ally_core::config::Config; +use ally_core::control::{Job, JobKind, JobState, Request, Response}; +use ally_core::presentation::{self, Style, Tone}; +use ally_core::step; +use ally_core::storage; pub async fn ps(agent: Option, all: bool) -> anyhow::Result { let config = Config::load()?; @@ -90,7 +90,7 @@ pub fn render( true => output.push_str(" No jobs recorded\n"), false => output.push_str(&format!( " Nothing running. See finished jobs with {}\n", - style.paint("slingshot ps --all", Tone::Info) + style.paint("ally ps --all", Tone::Info) )), } return output; @@ -232,7 +232,7 @@ mod tests { #[test] fn an_empty_list_explains_itself() { let text = render("archbox", &[], false, 0, None, Style::new(false)); - assert!(text.contains("Nothing running. See finished jobs with slingshot ps --all")); + assert!(text.contains("Nothing running. See finished jobs with ally ps --all")); } #[test] diff --git a/crates/slingshot-cli/src/commands/run.rs b/crates/ally-cli/src/commands/run.rs similarity index 90% rename from crates/slingshot-cli/src/commands/run.rs rename to crates/ally-cli/src/commands/run.rs index 783e281..6520a60 100644 --- a/crates/slingshot-cli/src/commands/run.rs +++ b/crates/ally-cli/src/commands/run.rs @@ -1,22 +1,22 @@ -//! `slingshot run `: sync the project, then run a command in the Agent copy. +//! `ally run `: sync the project, then run a command in the Agent copy. use crate::client::{self, Refused}; use crate::project::{self, Local}; use crate::route; use crate::ssh::{Disconnected, RemoteCommand}; use crate::transfer::{self, Direction}; -use slingshot_core::artifacts; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::control::{Request, Response}; -use slingshot_core::presentation::{self, Style, Tone}; -use slingshot_core::step; +use ally_core::artifacts; +use ally_core::config::{Agent, Config}; +use ally_core::control::{Request, Response}; +use ally_core::presentation::{self, Style, Tone}; +use ally_core::step; use std::time::{Duration, Instant}; /// Run `cmd` on the Agent and return its exit code. A non zero code is not an -/// error: slingshot did its job, and the command it ran happened to fail. +/// error: ally did its job, and the command it ran happened to fail. pub async fn run(agent: Option, cmd: Vec) -> anyhow::Result { if cmd.is_empty() { - anyhow::bail!("No command given. Try slingshot run echo hello"); + anyhow::bail!("No command given. Try ally run echo hello"); } let started = Instant::now(); @@ -68,7 +68,7 @@ pub async fn run(agent: Option, cmd: Vec) -> anyhow::Result } /// The last line of a run: how long it took and how the command exited. Exit 130 means -/// the command ended on Ctrl C or `slingshot stop`, which someone chose, so it is not a failure. +/// the command ended on Ctrl C or `ally stop`, which someone chose, so it is not a failure. fn finished(code: i32, took: Duration, style: Style) -> String { let took = step::elapsed(took); match code { @@ -78,7 +78,7 @@ fn finished(code: i32, took: Duration, style: Style) -> String { } } -/// 128 plus SIGINT, the exit code of a command ended by Ctrl C or `slingshot stop`. +/// 128 plus SIGINT, the exit code of a command ended by Ctrl C or `ally stop`. const STOPPED: i32 = 130; /// Run an interactive remote command, replacing SSH's messages about a dropped connection @@ -111,10 +111,10 @@ async fn settle(target: &Agent, result: anyhow::Result, lost: String) -> an fn lost_connection(name: &str) -> String { format!( "Lost connection to {name}\n The Agent stops the run once it notices, unless it finishes first.\n{}{}", - presentation::row("See how", "slingshot ps --all"), + presentation::row("See how", "ally ps --all"), presentation::row( "Next time", - "slingshot attach keeps work running through a disconnect" + "ally attach keeps work running through a disconnect" ) .trim_end() ) @@ -165,7 +165,7 @@ fn split_summary(local: &Local) -> Option { #[cfg(test)] mod tests { use super::*; - use slingshot_core::stack::Stack; + use ally_core::stack::Stack; use std::path::PathBuf; fn local(cwd: &str, stacks: Vec) -> Local { @@ -222,7 +222,7 @@ mod tests { message.starts_with("Lost connection to archbox\n"), "{message}" ); - assert!(message.contains("slingshot ps --all"), "{message}"); + assert!(message.contains("ally ps --all"), "{message}"); } #[test] diff --git a/crates/slingshot-cli/src/commands/sync.rs b/crates/ally-cli/src/commands/sync.rs similarity index 92% rename from crates/slingshot-cli/src/commands/sync.rs rename to crates/ally-cli/src/commands/sync.rs index 36c763d..7d75384 100644 --- a/crates/slingshot-cli/src/commands/sync.rs +++ b/crates/ally-cli/src/commands/sync.rs @@ -1,9 +1,9 @@ -//! `slingshot sync [--pull] [--check] [path]`: move eligible source between machines. +//! `ally sync [--pull] [--check] [path]`: move eligible source between machines. use crate::project::{self, Local}; use crate::transfer::{self, Direction, ProjectSession}; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::presentation; +use ally_core::config::{Agent, Config}; +use ally_core::presentation; pub async fn sync( agent: Option, @@ -24,7 +24,7 @@ pub async fn sync( let result = match check { true if !opened.project.initialized => { presentation::progress(format!( - "{} has no copy on {} yet. slingshot sync will copy it", + "{} has no copy on {} yet. ally sync will copy it", local.name, target.name )); Ok(()) @@ -123,8 +123,8 @@ pub fn report_kept(kept: usize, agent: &str, direction: Direction) { return; } let (place, hint) = match direction { - Direction::Push => (agent, "slingshot sync --pull --check"), - Direction::Pull => ("this machine", "slingshot sync --check"), + Direction::Push => (agent, "ally sync --pull --check"), + Direction::Pull => ("this machine", "ally sync --check"), }; presentation::warning(format!( "Kept {} changed only on {place}. Review with {hint}", diff --git a/crates/slingshot-cli/src/commands/tools.rs b/crates/ally-cli/src/commands/tools.rs similarity index 93% rename from crates/slingshot-cli/src/commands/tools.rs rename to crates/ally-cli/src/commands/tools.rs index e757975..092d8fa 100644 --- a/crates/slingshot-cli/src/commands/tools.rs +++ b/crates/ally-cli/src/commands/tools.rs @@ -1,15 +1,15 @@ -//! `slingshot tools`: install the developer tools this machine uses on the Agent, after +//! `ally tools`: install the developer tools this machine uses on the Agent, after //! showing every command and asking once. `link` runs the same step when it finishes. use crate::client::{self, unexpected}; use crate::project; use crate::route; use crate::ssh::{self, RemoteCommand}; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::control::{AgentTools, Request, Response}; -use slingshot_core::presentation::{self, Style, Tone, plural, row}; -use slingshot_core::step; -use slingshot_core::tools::{self, Tool}; +use ally_core::config::{Agent, Config}; +use ally_core::control::{AgentTools, Request, Response}; +use ally_core::presentation::{self, Style, Tone, plural, row}; +use ally_core::step; +use ally_core::tools::{self, Tool}; pub async fn tools(agent: Option) -> anyhow::Result { let config = Config::load()?; @@ -66,7 +66,7 @@ pub async fn offer(target: &Agent) -> anyhow::Result<()> { return Ok(()); } if !ssh::wants_terminal() { - presentation::warning("Run slingshot tools in a terminal to install them"); + presentation::warning("Run ally tools in a terminal to install them"); return Ok(()); } let them = match offered.len() { @@ -76,13 +76,13 @@ pub async fn offer(target: &Agent) -> anyhow::Result<()> { if !presentation::confirm(format!("Install {them} on {} now?", target.name)).await? { eprintln!( " {}", - Style::stderr().dim("Run slingshot tools any time to do this later") + Style::stderr().dim("Run ally tools any time to do this later") ); return Ok(()); } let lost = format!( - "Lost connection to {} while installing. Run slingshot tools again to see what finished", + "Lost connection to {} while installing. Run ally tools again to see what finished", target.name ); let (with_sudo, without_sudo) = by_sudo(&offered, manager, before.npm_writable, admin); @@ -114,7 +114,7 @@ pub async fn offer(target: &Agent) -> anyhow::Result<()> { match after.installed.contains(tool) { true => presentation::success(format!("{} installed", tool.name())), false => presentation::warning(format!( - "{} is still missing. Check the output above, then run slingshot tools again", + "{} is still missing. Check the output above, then run ally tools again", tool.name() )), } @@ -199,13 +199,13 @@ async fn offer_sign_in( target.name )); if !ssh::wants_terminal() { - presentation::warning("Run slingshot tools in a terminal to sign in"); + presentation::warning("Run ally tools in a terminal to sign in"); return Ok(()); } if !presentation::confirm(format!("Sign in on {} now?", target.name)).await? { eprintln!( " {}", - Style::stderr().dim("Run slingshot tools any time to do this later") + Style::stderr().dim("Run ally tools any time to do this later") ); return Ok(()); } @@ -243,14 +243,14 @@ async fn sign_in(target: &Agent, shell: &str, tool: Tool) -> anyhow::Result<()> ); remote.forward = sign_in.forward; let lost = format!( - "Lost connection to {} while signing in. Try again in slingshot attach with: {command}", + "Lost connection to {} while signing in. Try again in ally attach with: {command}", target.name ); let code = crate::commands::run::interact(target, &remote, lost).await?; match code { 0 => presentation::success(format!("Signed in to {}", tool.name())), _ => presentation::warning(format!( - "Sign in to {} did not finish. Try again in slingshot attach with: {command}", + "Sign in to {} did not finish. Try again in ally attach with: {command}", tool.name() )), } @@ -280,7 +280,7 @@ fn announce(action: &str, target: &Agent) { ); } -/// Each missing tool with the exact commands that will run, or a warning when Slingshot has +/// Each missing tool with the exact commands that will run, or a warning when Ally has /// none it trusts on that Agent. Without `admin`, commands that need `sudo` are listed apart, /// for someone who can run them. fn plan( diff --git a/crates/slingshot-cli/src/commands/uninstall.rs b/crates/ally-cli/src/commands/uninstall.rs similarity index 86% rename from crates/slingshot-cli/src/commands/uninstall.rs rename to crates/ally-cli/src/commands/uninstall.rs index 609f74f..1ea90a2 100644 --- a/crates/slingshot-cli/src/commands/uninstall.rs +++ b/crates/ally-cli/src/commands/uninstall.rs @@ -1,19 +1,19 @@ -//! `slingshot uninstall`: list everything Slingshot keeps on this machine, ask once, and +//! `ally uninstall`: list everything Ally keeps on this machine, ask once, and //! remove it. The program itself is removed afterwards with `cargo uninstall`, because a //! running program cannot safely delete itself. use crate::keys; -use slingshot_core::config::{self, Config}; -use slingshot_core::presentation::{self, home_path}; -use slingshot_core::storage; +use ally_core::config::{self, Config}; +use ally_core::presentation::{self, home_path}; +use ally_core::storage; use std::fs; use std::path::{Path, PathBuf}; -const REMOVE_PROGRAM: &str = "cargo uninstall slingshot-cli"; +const REMOVE_PROGRAM: &str = "cargo uninstall ally-cli"; /// What this machine keeps as a Client, and as an Agent when it is one. struct Found { - agent: slingshot_agent::uninstall::Found, + agent: ally_agent::uninstall::Found, agents: Vec, menubar: Option, /// The Client's own settings and data. Only these, never the whole folder, because on a @@ -26,7 +26,7 @@ impl Found { fn look() -> anyhow::Result { let data = storage::data_dir()?; Ok(Found { - agent: slingshot_agent::uninstall::Found::look()?, + agent: ally_agent::uninstall::Found::look()?, agents: Config::load_or_empty()?.names(), menubar: super::menubar::installed(), files: existing([ @@ -60,17 +60,17 @@ fn existing(paths: impl IntoIterator) -> Vec { pub async fn uninstall() -> anyhow::Result { let found = Found::look()?; if found.is_empty() { - presentation::success("Slingshot keeps nothing on this machine"); + presentation::success("Ally keeps nothing on this machine"); presentation::detail("Program", format!("remove it with {REMOVE_PROGRAM}")); return Ok(0); } - eprintln!("\n This removes Slingshot from this machine:\n"); + eprintln!("\n This removes Ally from this machine:\n"); for line in listing(&found) { eprintln!("{line}"); } anyhow::ensure!( crate::ssh::wants_terminal(), - "Run slingshot uninstall in a terminal to confirm, since it deletes files" + "Run ally uninstall in a terminal to confirm, since it deletes files" ); if !presentation::confirm_deleting("Remove all of this?".to_string()).await? { presentation::success("Nothing was removed"); @@ -96,7 +96,7 @@ pub async fn uninstall() -> anyhow::Result { let _ = fs::remove_dir(folder); } presentation::success(format!( - "Slingshot's files are gone. Remove the program with: {REMOVE_PROGRAM}" + "Ally's files are gone. Remove the program with: {REMOVE_PROGRAM}" )); if was_service && cfg!(target_os = "linux") { presentation::detail( @@ -161,7 +161,7 @@ mod tests { #[test] fn everything_found_is_listed_with_a_label_once_per_kind() { let found = Found { - agent: slingshot_agent::uninstall::Found { + agent: ally_agent::uninstall::Found { service: None, active: Vec::new(), data: None, @@ -170,7 +170,7 @@ mod tests { }, agents: vec!["archbox".into(), "devbox".into()], menubar: None, - files: vec![PathBuf::from("/x/slingshot/config.toml")], + files: vec![PathBuf::from("/x/ally/config.toml")], keys: vec![PathBuf::from("/x/k"), PathBuf::from("/x/k.pub")], }; let lines: Vec = listing(&found) @@ -183,7 +183,7 @@ mod tests { " Links archbox: this machine's key and environment files there", " devbox: this machine's key and environment files there", " Access 2 linked machines lose access (their lines in ~/.ssh/authorized_keys)", - " Settings /x/slingshot/config.toml", + " Settings /x/ally/config.toml", " Key /x/k", " /x/k.pub", ] @@ -193,7 +193,7 @@ mod tests { #[test] fn only_paths_that_exist_are_kept_once() { let here = std::env::temp_dir(); - let missing = here.join("slingshot-no-such-file"); + let missing = here.join("ally-no-such-file"); assert_eq!(existing([here.clone(), missing, here.clone()]), [here]); } } diff --git a/crates/slingshot-cli/src/commands/unlink.rs b/crates/ally-cli/src/commands/unlink.rs similarity index 91% rename from crates/slingshot-cli/src/commands/unlink.rs rename to crates/ally-cli/src/commands/unlink.rs index 53f8106..38c68dd 100644 --- a/crates/slingshot-cli/src/commands/unlink.rs +++ b/crates/ally-cli/src/commands/unlink.rs @@ -1,13 +1,13 @@ -//! `slingshot unlink`: clean up this Client's Slingshot data on an Agent, then forget it. +//! `ally unlink`: clean up this Client's Ally data on an Agent, then forget it. use crate::client::Refused; use crate::project; use crate::ssh::RemoteCommand; -use slingshot_core::config::Config; -use slingshot_core::control::{Request, Response}; -use slingshot_core::keys::marker; -use slingshot_core::presentation; -use slingshot_core::step; +use ally_core::config::Config; +use ally_core::control::{Request, Response}; +use ally_core::keys::marker; +use ally_core::presentation; +use ally_core::step; /// Remote cleanup comes first and stops the unlink when the Agent refuses, for example /// while this Client's projects still have a run, session, or sync in progress. @@ -43,7 +43,7 @@ pub async fn unlink(agent: Option) -> anyhow::Result { complete = false; cleaning.clear(); presentation::warning(format!( - "{error:#}. Remote cleanup is incomplete: environment files for this machine's projects remain in Slingshot storage on {}", + "{error:#}. Remote cleanup is incomplete: environment files for this machine's projects remain in Ally storage on {}", target.name )); } @@ -65,7 +65,7 @@ pub async fn unlink(agent: Option) -> anyhow::Result { } } - slingshot_core::keys::forget_host(&target.host, target.port)?; + ally_core::keys::forget_host(&target.host, target.port)?; config.remove(&target.name)?; let saved = config.save()?; diff --git a/crates/slingshot-cli/src/keys.rs b/crates/ally-cli/src/keys.rs similarity index 79% rename from crates/slingshot-cli/src/keys.rs rename to crates/ally-cli/src/keys.rs index 94713fa..2323f42 100644 --- a/crates/slingshot-cli/src/keys.rs +++ b/crates/ally-cli/src/keys.rs @@ -1,26 +1,26 @@ -//! Slingshot uses a dedicated SSH key so unlink can revoke its access. +//! Ally uses a dedicated SSH key so unlink can revoke its access. +use ally_core::keys::{marker, ssh_dir}; use anyhow::Context; -use slingshot_core::keys::{marker, ssh_dir}; use std::fs; use std::path::PathBuf; use std::process::Command; /// The private key file. The public one is the same path with .pub on the end. -const KEY_NAME: &str = "slingshot_ed25519"; +const KEY_NAME: &str = "ally_ed25519"; fn private_key_path() -> anyhow::Result { Ok(ssh_dir()?.join(KEY_NAME)) } -/// Slingshot's private and public key files, whether or not they exist. +/// Ally's private and public key files, whether or not they exist. pub fn files() -> anyhow::Result<[PathBuf; 2]> { let private = private_key_path()?; let public = private.with_extension("pub"); Ok([private, public]) } -/// Find slingshot's key, creating it the first time. Also says whether it was just made, so +/// Find ally's key, creating it the first time. Also says whether it was just made, so /// `link` can announce a new key with its path, because a tool that quietly makes keys cannot /// be audited. pub fn ensure(client_name: &str) -> anyhow::Result<(PathBuf, String, bool)> { @@ -45,11 +45,11 @@ pub fn ensure(client_name: &str) -> anyhow::Result<(PathBuf, String, bool)> { ]) .arg(&private) .status() - .context("Could not run ssh-keygen. Install OpenSSH, then run slingshot link again")?; + .context("Could not run ssh-keygen. Install OpenSSH, then run ally link again")?; if !status.success() { anyhow::bail!( - "Could not create the SSH key {}. Check that the folder is writable, then run slingshot link again", + "Could not create the SSH key {}. Check that the folder is writable, then run ally link again", private.display() ); } diff --git a/crates/slingshot-cli/src/lib.rs b/crates/ally-cli/src/lib.rs similarity index 77% rename from crates/slingshot-cli/src/lib.rs rename to crates/ally-cli/src/lib.rs index 17d5bb8..b555e28 100644 --- a/crates/slingshot-cli/src/lib.rs +++ b/crates/ally-cli/src/lib.rs @@ -1,4 +1,4 @@ -//! slingshot: run heavy work on another machine, from a light one. +//! ally: run heavy work on another machine, from a light one. mod client; mod commands; @@ -11,14 +11,14 @@ mod transfer; mod tunnel; mod watch; +use ally_core::presentation::{self, ColorMode, Style, Tone}; +use ally_core::protocol::DEFAULT_PORT; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; -use slingshot_core::presentation::{self, ColorMode, Style, Tone}; -use slingshot_core::protocol::DEFAULT_PORT; use std::path::PathBuf; /// Global options precede run so the remote command can receive its own flags. #[derive(Parser)] -#[command(name = "slingshot", version = env!("SLINGSHOT_VERSION"), about = "Use the CPU, RAM, and GPU of another machine from this one", long_about = None)] +#[command(name = "ally", version = env!("ALLY_VERSION"), about = "Use the CPU, RAM, and GPU of another machine from this one", long_about = None)] struct Cli { /// Control terminal colors. #[arg(long, global = true, value_parser = ["auto", "always", "never"], default_value = "auto")] @@ -57,7 +57,7 @@ enum Commands { #[command(hide = true)] InternalWatch, - /// The remote shell rsync uses, with Slingshot's saved SSH options. + /// The remote shell rsync uses, with Ally's saved SSH options. #[command(hide = true)] InternalRsh { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] @@ -71,7 +71,7 @@ enum Commands { name: Option, }, - #[command(about = "Agent: start Slingshot and print a pairing code")] + #[command(about = "Agent: start Ally and print a pairing code")] Start { /// The name this Agent will be known by. Defaults to its hostname. #[arg(long)] @@ -81,16 +81,16 @@ enum Commands { #[arg(long, default_value_t = DEFAULT_PORT)] port: u16, - /// Start Slingshot by itself at boot, even after answering no before. + /// Start Ally by itself at boot, even after answering no before. #[arg(long, conflicts_with = "remove")] boot: bool, - /// Stop Slingshot starting by itself at boot, and stop the running service. + /// Stop Ally starting by itself at boot, and stop the running service. #[arg(long)] remove: bool, }, - #[command(about = "Client: link to an Agent with the code slingshot start printed")] + #[command(about = "Client: link to an Agent with the code ally start printed")] Link { code: String, @@ -102,16 +102,14 @@ enum Commands { #[command(about = "Remove this machine's access and environment files from an Agent")] Unlink, - #[command( - about = "Remove everything Slingshot keeps on this machine, after listing it and asking" - )] + #[command(about = "Remove everything Ally keeps on this machine, after listing it and asking")] Uninstall, #[command(about = "Install the developer tools this machine uses on the Agent, after asking")] Tools, /// `trailing_var_arg` stops clap parsing after `run`, so flags like `--release` - /// reach the remote program untouched rather than being claimed by slingshot. + /// reach the remote program untouched rather than being claimed by ally. #[command(about = "Sync the project, then run a command on the Agent")] Run { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] @@ -155,16 +153,16 @@ enum Commands { action: Option, }, - #[command(about = "List Slingshot runs and sessions on the Agent")] + #[command(about = "List Ally runs and sessions on the Agent")] Ps { /// Include the most recent 100 finished jobs. #[arg(long)] all: bool, }, - #[command(about = "Stop a Slingshot run or session")] + #[command(about = "Stop an Ally run or session")] Stop { - /// The job ID from slingshot ps. A unique prefix is enough. + /// The job ID from ally ps. A unique prefix is enough. id: String, }, @@ -213,28 +211,22 @@ enum ProjectsAction { about = "Remove a project's build output on the Agent. The next build makes it again" )] Clean { - /// The project name or ID from slingshot projects. Defaults to the current project. + /// The project name or ID from ally projects. Defaults to the current project. project: Option, }, #[command(about = "Delete a project from the Agent, after asking")] Remove { - /// The project name or ID from slingshot projects. Defaults to the current project. + /// The project name or ID from ally projects. Defaults to the current project. project: Option, }, } -/// Help and errors always say `slingshot`, even when the program was started as `sling`, so -/// they match the docs. -fn command() -> clap::Command { - Cli::command().bin_name("slingshot") -} - -/// Slingshot's own warnings show by default. Libraries such as iroh only show real errors, +/// Ally's own warnings show by default. Libraries such as iroh only show real errors, /// because their internals mean nothing to the person running a command. -const DEFAULT_LOG: &str = "error,slingshot_cli=warn,slingshot_agent=warn,slingshot_core=warn"; +const DEFAULT_LOG: &str = "error,ally_cli=warn,ally_agent=warn,ally_core=warn"; -/// Pass through command exit codes. Slingshot failures exit with code 1. +/// Pass through command exit codes. Ally failures exit with code 1. #[tokio::main] pub async fn run() { let args: Vec<_> = std::env::args_os().collect(); @@ -242,7 +234,7 @@ pub async fn run() { presentation::configure(mode); tracing_subscriber::fmt() .with_env_filter( - tracing_subscriber::EnvFilter::try_from_env("SLINGSHOT_LOG") + tracing_subscriber::EnvFilter::try_from_env("ALLY_LOG") .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_LOG)), ) .with_writer(std::io::stderr) @@ -261,18 +253,18 @@ pub async fn run() { } ColorMode::Auto => clap::ColorChoice::Auto, }; - let matches = command().color(color).get_matches_from(args); + let matches = Cli::command().color(color).get_matches_from(args); let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit()); let result = match cli.command { - Commands::InternalControl => slingshot_agent::service::bridge().await, + Commands::InternalControl => ally_agent::service::bridge().await, Commands::InternalRun { project, cwd, cmd } => { - slingshot_agent::runner::run(project, cwd, cmd).await + ally_agent::runner::run(project, cwd, cmd).await } Commands::InternalRsh { args } => internal_rsh(args), Commands::InternalTunnel { key } => tunnel::run(key).await, Commands::InternalWatch => watch::run(cli.agent).await, - Commands::InternalDaemon { name } => slingshot_agent::daemon(name).await, + Commands::InternalDaemon { name } => ally_agent::daemon(name).await, Commands::Start { name, port, @@ -280,11 +272,11 @@ pub async fn run() { remove, } => { let at_boot = match (boot, remove) { - (true, _) => slingshot_agent::AtBoot::Install, - (_, true) => slingshot_agent::AtBoot::Remove, - _ => slingshot_agent::AtBoot::Ask, + (true, _) => ally_agent::AtBoot::Install, + (_, true) => ally_agent::AtBoot::Remove, + _ => ally_agent::AtBoot::Ask, }; - slingshot_agent::start(name, port, at_boot).await + ally_agent::start(name, port, at_boot).await } Commands::Link { code, name } => commands::link::link(code, name).await, Commands::Unlink => commands::unlink::unlink(cli.agent).await, @@ -335,8 +327,8 @@ pub async fn run() { /// nothing about it has to survive rsync's own argument splitting. fn internal_rsh(args: Vec) -> anyhow::Result { let name = - std::env::var("SLINGSHOT_RSH_AGENT").map_err(|_| anyhow::anyhow!("Missing Agent name"))?; - let config = slingshot_core::config::Config::load()?; + std::env::var("ALLY_RSH_AGENT").map_err(|_| anyhow::anyhow!("Missing Agent name"))?; + let config = ally_core::config::Config::load()?; let agent = config.resolve(Some(&name))?; if let Ok(token) = std::env::var(route::ROUTE_ENV) { route::assume(agent, &token); @@ -379,7 +371,7 @@ mod tests { #[test] fn remote_flags_stay_with_the_remote_command() { let cli = Cli::try_parse_from([ - "slingshot", + "ally", "--color", "never", "run", @@ -398,7 +390,7 @@ mod tests { #[test] fn start_sets_up_or_removes_the_boot_service_but_not_both() { - let cli = Cli::try_parse_from(["slingshot", "start", "--boot"]).unwrap(); + let cli = Cli::try_parse_from(["ally", "start", "--boot"]).unwrap(); assert!(matches!( cli.command, Commands::Start { @@ -407,22 +399,13 @@ mod tests { .. } )); - assert!(Cli::try_parse_from(["slingshot", "start", "--boot", "--remove"]).is_err()); - } - - #[test] - fn help_says_slingshot_when_started_as_sling() { - let error = command() - .try_get_matches_from(["sling", "bogus"]) - .unwrap_err() - .to_string(); - assert!(error.contains("Usage: slingshot"), "{error}"); + assert!(Cli::try_parse_from(["ally", "start", "--boot", "--remove"]).is_err()); } #[test] fn internal_helpers_keep_their_arguments() { let cli = Cli::try_parse_from([ - "slingshot", + "ally", "internal-run", "--project", "abc", @@ -440,9 +423,9 @@ mod tests { assert_eq!(cmd, ["cargo", "--color", "always"]); let cli = Cli::try_parse_from([ - "slingshot", + "ally", "internal-rsh", - "slingshot", + "ally", "rsync", "--server", "-a", @@ -453,10 +436,9 @@ mod tests { let Commands::InternalRsh { args } = cli.command else { panic!("Expected internal rsh") }; - assert_eq!(args, ["slingshot", "rsync", "--server", "-a", ".", "."]); + assert_eq!(args, ["ally", "rsync", "--server", "-a", ".", "."]); - let cli = - Cli::try_parse_from(["slingshot", "internal-daemon", "--name", "archbox"]).unwrap(); + let cli = Cli::try_parse_from(["ally", "internal-daemon", "--name", "archbox"]).unwrap(); let Commands::InternalDaemon { name } = cli.command else { panic!("Expected internal daemon") }; @@ -465,7 +447,7 @@ mod tests { #[test] fn sync_and_env_commands_parse() { - let cli = Cli::try_parse_from(["slingshot", "sync", "--pull", "--check", "app"]).unwrap(); + let cli = Cli::try_parse_from(["ally", "sync", "--pull", "--check", "app"]).unwrap(); assert!(matches!( cli.command, Commands::Sync { @@ -475,7 +457,7 @@ mod tests { path: Some(_) } )); - let cli = Cli::try_parse_from(["slingshot", "sync", "--pull", "--overwrite"]).unwrap(); + let cli = Cli::try_parse_from(["ally", "sync", "--pull", "--overwrite"]).unwrap(); assert!(matches!( cli.command, Commands::Sync { @@ -484,9 +466,9 @@ mod tests { .. } )); - assert!(Cli::try_parse_from(["slingshot", "sync", "--overwrite", "--check"]).is_err()); + assert!(Cli::try_parse_from(["ally", "sync", "--overwrite", "--check"]).is_err()); let cli = Cli::try_parse_from([ - "slingshot", + "ally", "env", "add", "local.env", @@ -501,31 +483,28 @@ mod tests { action: EnvAction::Add { replace: true, .. } } )); - let cli = Cli::try_parse_from(["slingshot", "env", "add", ".env"]).unwrap(); + let cli = Cli::try_parse_from(["ally", "env", "add", ".env"]).unwrap(); assert!(matches!( cli.command, Commands::Env { action: EnvAction::Add { target: None, .. } } )); - assert!(Cli::try_parse_from(["slingshot", "stop"]).is_err()); + assert!(Cli::try_parse_from(["ally", "stop"]).is_err()); } #[test] fn color_scan_respects_option_values_and_run_boundary() { for (args, expected) in [ ( - vec!["slingshot", "--agent", "run", "--color=always", "info"], + vec!["ally", "--agent", "run", "--color=always", "info"], ColorMode::Always, ), ( - vec!["slingshot", "run", "echo", "--color=always"], + vec!["ally", "run", "echo", "--color=always"], ColorMode::Auto, ), - ( - vec!["slingshot", "--color", "never", "--help"], - ColorMode::Never, - ), + (vec!["ally", "--color", "never", "--help"], ColorMode::Never), ] { let args: Vec<_> = args.into_iter().map(std::ffi::OsString::from).collect(); assert_eq!(color_mode(&args), expected); diff --git a/crates/slingshot-cli/src/live.rs b/crates/ally-cli/src/live.rs similarity index 97% rename from crates/slingshot-cli/src/live.rs rename to crates/ally-cli/src/live.rs index 740bc46..f435934 100644 --- a/crates/slingshot-cli/src/live.rs +++ b/crates/ally-cli/src/live.rs @@ -37,7 +37,7 @@ impl Drop for Screen { pub fn require_terminal() -> anyhow::Result<()> { anyhow::ensure!( std::io::stdin().is_terminal() && std::io::stdout().is_terminal(), - "Live views need an interactive terminal. Use slingshot health for a single snapshot" + "Live views need an interactive terminal. Use ally health for a single snapshot" ); Ok(()) } diff --git a/crates/ally-cli/src/main.rs b/crates/ally-cli/src/main.rs new file mode 100644 index 0000000..1dad626 --- /dev/null +++ b/crates/ally-cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + ally_cli::run(); +} diff --git a/crates/slingshot-cli/src/project.rs b/crates/ally-cli/src/project.rs similarity index 97% rename from crates/slingshot-cli/src/project.rs rename to crates/ally-cli/src/project.rs index 7eda669..e30664b 100644 --- a/crates/slingshot-cli/src/project.rs +++ b/crates/ally-cli/src/project.rs @@ -1,8 +1,8 @@ //! The local project a command applies to, and its persistent identity per Agent. +use ally_core::{stack, storage}; use anyhow::Context; use serde::{Deserialize, Serialize}; -use slingshot_core::{stack, storage}; use std::path::{Path, PathBuf}; /// A project on the Client and where the command was typed inside it. @@ -46,7 +46,7 @@ pub fn require(path: Option) -> anyhow::Result { }; locate(&start).with_context(|| { format!( - "No project found at {}. Use a folder inside a Git repository or one with Cargo.toml, package.json, pyproject.toml, requirements.txt, or slingshot.toml", + "No project found at {}. Use a folder inside a Git repository or one with Cargo.toml, package.json, pyproject.toml, requirements.txt, or ally.toml", start.display() ) }) @@ -121,7 +121,7 @@ mod tests { impl Temp { fn new() -> Temp { - let path = std::env::temp_dir().join(format!("slingshot-cli-{}", storage::new_id())); + let path = std::env::temp_dir().join(format!("ally-cli-{}", storage::new_id())); std::fs::create_dir_all(&path).unwrap(); Temp(path.canonicalize().unwrap()) } diff --git a/crates/slingshot-cli/src/route.rs b/crates/ally-cli/src/route.rs similarity index 98% rename from crates/slingshot-cli/src/route.rs rename to crates/ally-cli/src/route.rs index 782caa8..59a6557 100644 --- a/crates/slingshot-cli/src/route.rs +++ b/crates/ally-cli/src/route.rs @@ -2,10 +2,10 @@ //! that answers wins, so an unreachable home address costs one short timeout rather than //! one per candidate. When none answers, iroh reaches the Agent from any network. -use slingshot_core::config::Agent; -use slingshot_core::network::Network; -use slingshot_core::presentation::{Style, Tone}; -use slingshot_core::tunnel; +use ally_core::config::Agent; +use ally_core::network::Network; +use ally_core::presentation::{Style, Tone}; +use ally_core::tunnel; use std::net::{TcpStream, ToSocketAddrs}; use std::sync::Mutex; use std::time::Duration; @@ -14,7 +14,7 @@ const PROBE_TIMEOUT: Duration = Duration::from_millis(800); const SSH_PORT: u16 = 22; /// Set by the Client for the rsync helper it starts, so that process skips probing. -pub const ROUTE_ENV: &str = "SLINGSHOT_RSH_HOST"; +pub const ROUTE_ENV: &str = "ALLY_RSH_HOST"; const IROH: &str = "iroh"; diff --git a/crates/slingshot-cli/src/ssh.rs b/crates/ally-cli/src/ssh.rs similarity index 96% rename from crates/slingshot-cli/src/ssh.rs rename to crates/ally-cli/src/ssh.rs index b0ae12c..2f1de58 100644 --- a/crates/slingshot-cli/src/ssh.rs +++ b/crates/ally-cli/src/ssh.rs @@ -2,10 +2,10 @@ use crate::route::{self, Route}; use crate::tunnel; +use ally_core::config; +use ally_core::presentation::Style; use anyhow::Context; use shell_words::join; -use slingshot_core::config; -use slingshot_core::presentation::Style; use std::path::PathBuf; use std::process::Stdio; use std::sync::atomic::{AtomicBool, Ordering}; @@ -447,10 +447,8 @@ impl SshLog { fn create() -> anyhow::Result { use std::os::unix::fs::OpenOptionsExt; - let path = std::env::temp_dir().join(format!( - "slingshot-ssh-{}.log", - slingshot_core::storage::new_id() - )); + let path = + std::env::temp_dir().join(format!("ally-ssh-{}.log", ally_core::storage::new_id())); std::fs::OpenOptions::new() .write(true) .create_new(true) @@ -473,8 +471,8 @@ impl Drop for SshLog { } } -/// The `-e` helper rsync starts: SSH with Slingshot's saved options for one Agent. Running -/// through Slingshot avoids passing file paths inside rsync's own option parsing. +/// The `-e` helper rsync starts: SSH with Ally's saved options for one Agent. Running +/// through Ally avoids passing file paths inside rsync's own option parsing. pub fn exec_for_rsync(agent: &config::Agent, args: Vec) -> anyhow::Error { use std::os::unix::process::CommandExt; @@ -555,10 +553,10 @@ mod tests { #[test] fn a_program_path_with_a_space_stays_one_word() { let mut command = remote(&["internal-control"]); - command.program = "/Users/me/Application Support/slingshot".to_string(); + command.program = "/Users/me/Application Support/ally".to_string(); assert_eq!( command.command_line(), - "'/Users/me/Application Support/slingshot' internal-control" + "'/Users/me/Application Support/ally' internal-control" ); } @@ -575,7 +573,7 @@ mod tests { let mut command = remote(&["hi"]); command.user = Some("me".to_string()); command.port = Some(2222); - command.identity_file = Some(PathBuf::from("/keys/slingshot")); + command.identity_file = Some(PathBuf::from("/keys/ally")); command.known_hosts = Some(PathBuf::from("/keys/known_hosts")); let argv = command.to_ssh_args(); @@ -589,7 +587,7 @@ mod tests { "argv was: {argv:?}" ); assert!( - argv.windows(2).any(|w| w == ["-i", "/keys/slingshot"]), + argv.windows(2).any(|w| w == ["-i", "/keys/ally"]), "argv was: {argv:?}" ); assert!( @@ -685,7 +683,7 @@ mod tests { #[test] fn the_share_check_uses_only_the_existing_connection_and_never_a_terminal() { let mut command = remote(&["hi"]); - command.shared = Some(PathBuf::from("/tmp/slingshot-me/ab.sock")); + command.shared = Some(PathBuf::from("/tmp/ally-me/ab.sock")); command.tty = true; let check = command.share_check_args(); assert_eq!(&check[..4], ["-o", "ControlMaster=no", "-W", "127.0.0.1:9"]); @@ -723,12 +721,12 @@ mod tests { ); let mut command = remote(&["hi"]); - command.shared = Some(PathBuf::from("/tmp/slingshot-me/ab.sock")); + command.shared = Some(PathBuf::from("/tmp/ally-me/ab.sock")); let argv = command.to_ssh_args(); assert!(argv.contains(&"ControlMaster=auto".to_string()), "{argv:?}"); assert!( - argv.contains(&"ControlPath=\"/tmp/slingshot-me/ab.sock\"".to_string()), + argv.contains(&"ControlPath=\"/tmp/ally-me/ab.sock\"".to_string()), "{argv:?}" ); assert!(argv.contains(&"ControlPersist=30".to_string()), "{argv:?}"); diff --git a/crates/slingshot-cli/src/transfer.rs b/crates/ally-cli/src/transfer.rs similarity index 94% rename from crates/slingshot-cli/src/transfer.rs rename to crates/ally-cli/src/transfer.rs index cacc4d3..8e6d8a9 100644 --- a/crates/slingshot-cli/src/transfer.rs +++ b/crates/ally-cli/src/transfer.rs @@ -7,14 +7,14 @@ use crate::client::{self, Control, unexpected}; use crate::project::{self, Local}; use crate::route; +use ally_core::config::Agent; +use ally_core::control::{ProjectInfo, ProjectRef, Request, Response, Snapshot}; +use ally_core::presentation::{self, Style, Tone}; +use ally_core::source::{self, Manifest, Rules}; +use ally_core::step::Step; +use ally_core::storage; +use ally_core::sync::{self, Plan, StateDir}; use anyhow::{Context, bail, ensure}; -use slingshot_core::config::Agent; -use slingshot_core::control::{ProjectInfo, ProjectRef, Request, Response, Snapshot}; -use slingshot_core::presentation::{self, Style, Tone}; -use slingshot_core::source::{self, Manifest, Rules}; -use slingshot_core::step::Step; -use slingshot_core::storage; -use slingshot_core::sync::{self, Plan, StateDir}; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::time::Duration; @@ -234,7 +234,7 @@ pub async fn push( } pub fn syncing(local: &Local) -> Step { - slingshot_core::step::start(format!("Syncing {}", local.name)) + ally_core::step::start(format!("Syncing {}", local.name)) } /// End a sync step with what changed. A pull names what it brought back, since those are @@ -436,10 +436,10 @@ fn conflict_message(paths: &[String], agent: &str) -> String { for (label, command) in [ ( "Compare", - "slingshot sync --check, then slingshot sync --pull --check", + "ally sync --check, then ally sync --pull --check", ), - ("Keep mine", "slingshot sync --overwrite"), - ("Keep theirs", "slingshot sync --pull --overwrite"), + ("Keep mine", "ally sync --overwrite"), + ("Keep theirs", "ally sync --pull --overwrite"), ] { lines.push(presentation::row(label, command).trim_end().to_string()); } @@ -513,21 +513,21 @@ async fn rsync( direction: Direction, ) -> anyhow::Result<()> { ensure!( - slingshot_core::telemetry::is_installed("rsync"), + ally_core::telemetry::is_installed("rsync"), "rsync is not installed on this machine. Fix: {}", - slingshot_core::preflight::install_hint("rsync") + ally_core::preflight::install_hint("rsync") ); let exe = std::env::current_exe()?; - let exe = exe.to_str().context("Slingshot's own path is not UTF 8")?; + let exe = exe.to_str().context("Ally's own path is not UTF 8")?; ensure!( !exe.contains('\''), - "Slingshot's own path contains a quote, which rsync cannot use: {exe}" + "Ally's own path contains a quote, which rsync cannot use: {exe}" ); let list = FileList::write(&files.join("\n"))?; let local_path = format!("{}/", local.display()); let (from, to) = match direction { - Direction::Push => (local_path, "slingshot:.".to_string()), - Direction::Pull => ("slingshot:./".to_string(), local_path), + Direction::Push => (local_path, "ally:.".to_string()), + Direction::Pull => ("ally:./".to_string(), local_path), }; let rsync_path = format!("cd {} && rsync", escape_remote(remote)?); let mut child = tokio::process::Command::new("rsync") @@ -537,7 +537,7 @@ async fn rsync( .arg(format!("--files-from={}", list.0)) .arg(from) .arg(to) - .env("SLINGSHOT_RSH_AGENT", &agent.name) + .env("ALLY_RSH_AGENT", &agent.name) .env(route::ROUTE_ENV, route::resolve(agent).token()) .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -592,7 +592,7 @@ impl FileList { let path = file .to_str() .map(str::to_string) - .context("Slingshot storage path is not UTF 8")?; + .context("Ally storage path is not UTF 8")?; Ok(FileList(path)) } } @@ -680,7 +680,7 @@ fn preview_text( #[cfg(test)] mod tests { use super::*; - use slingshot_core::source::Entry; + use ally_core::source::Entry; fn file(hash: &str) -> Entry { Entry { @@ -698,16 +698,16 @@ mod tests { [ "NOTES.md changed on both this machine and archbox", " Nothing was changed on either machine.", - " Compare slingshot sync --check, then slingshot sync --pull --check", - " Keep mine slingshot sync --overwrite", - " Keep theirs slingshot sync --pull --overwrite", + " Compare ally sync --check, then ally sync --pull --check", + " Keep mine ally sync --overwrite", + " Keep theirs ally sync --pull --overwrite", ] ); let many = conflict_message(&["a.rs".to_string(), "b.rs".to_string()], "archbox"); assert!( many.starts_with("2 files changed on both this machine and archbox\n a.rs\n b.rs\n") ); - assert!(many.ends_with("Keep theirs slingshot sync --pull --overwrite")); + assert!(many.ends_with("Keep theirs ally sync --pull --overwrite")); } #[test] @@ -791,8 +791,8 @@ mod tests { #[test] fn remote_paths_survive_rsync_splitting() { assert_eq!( - escape_remote("/Users/me/Library/Application Support/slingshot").unwrap(), - "/Users/me/Library/Application\\ Support/slingshot" + escape_remote("/Users/me/Library/Application Support/ally").unwrap(), + "/Users/me/Library/Application\\ Support/ally" ); assert_eq!(escape_remote("/a/$(x)&b").unwrap(), "/a/\\$\\(x\\)\\&b"); assert!(escape_remote("/it's").is_err()); @@ -810,6 +810,6 @@ mod tests { let error = error.to_string(); assert!(error.starts_with("src/main.rs changed on both this machine and archbox")); assert!(error.contains("Nothing was changed")); - assert!(error.contains("slingshot sync --overwrite")); + assert!(error.contains("ally sync --overwrite")); } } diff --git a/crates/slingshot-cli/src/tunnel.rs b/crates/ally-cli/src/tunnel.rs similarity index 91% rename from crates/slingshot-cli/src/tunnel.rs rename to crates/ally-cli/src/tunnel.rs index 41c551f..614b9eb 100644 --- a/crates/slingshot-cli/src/tunnel.rs +++ b/crates/ally-cli/src/tunnel.rs @@ -1,12 +1,12 @@ -//! The Client end of reaching an Agent over iroh. ssh starts `slingshot internal-tunnel` as +//! The Client end of reaching an Agent over iroh. ssh starts `ally internal-tunnel` as //! its ProxyCommand and speaks to the Agent's sshd through this process's standard input //! and output, so ssh itself is unchanged whichever path carries it. use crate::project; +use ally_core::tunnel::{self, ALPN}; use anyhow::Context; use iroh::endpoint::{Connection, presets}; use iroh::{Endpoint, EndpointAddr}; -use slingshot_core::tunnel::{self, ALPN}; use std::path::PathBuf; use std::time::Duration; use tokio::io::{AsyncRead, AsyncWrite}; @@ -23,7 +23,7 @@ pub fn proxy_command(key: &str) -> String { let exe = std::env::current_exe() .ok() .and_then(|path| path.to_str().map(str::to_string)) - .unwrap_or_else(|| "slingshot".to_string()); + .unwrap_or_else(|| "ally".to_string()); proxy_line(&exe, key) } @@ -36,10 +36,10 @@ pub fn shared_socket(key: &str) -> Option { let name = format!("{}.sock", &key[..key.len().min(12)]); [std::env::temp_dir(), PathBuf::from("/tmp")] .into_iter() - .map(|base| base.join(format!("slingshot-{user}"))) + .map(|base| base.join(format!("ally-{user}"))) .find(|dir| { dir.join(&name).as_os_str().len() + SOCKET_SUFFIX < SOCKET_LIMIT - && slingshot_core::storage::private_dir(dir).is_ok() + && ally_core::storage::private_dir(dir).is_ok() }) .map(|dir| dir.join(name)) } @@ -99,10 +99,10 @@ impl Diagnosis { "Could not reach {name}, because this machine cannot reach iroh's relays. Check its internet connection" )), Diagnosis::Offline => Some(format!( - "{name} is not reachable. It may be off or asleep, or slingshot start is not running there" + "{name} is not reachable. It may be off or asleep, or ally start is not running there" )), Diagnosis::NotPaired => Some(format!( - "{name} no longer accepts this machine. Run slingshot link again from the same network as {name}" + "{name} no longer accepts this machine. Run ally link again from the same network as {name}" )), Diagnosis::Reachable => None, } @@ -174,8 +174,8 @@ mod tests { #[test] fn nothing_in_the_command_can_become_shell_syntax_or_an_ssh_token() { assert_eq!( - proxy_line("/App 100%/slingshot", "k"), - "'/App 100%%/slingshot' internal-tunnel k" + proxy_line("/App 100%/ally", "k"), + "'/App 100%%/ally' internal-tunnel k" ); } @@ -225,11 +225,9 @@ mod tests { } fn agent_root() -> PathBuf { - let root = std::env::temp_dir().join(format!( - "slingshot-diagnose-{}", - slingshot_core::storage::new_id() - )); - slingshot_core::storage::private_dir(&root).unwrap(); + let root = + std::env::temp_dir().join(format!("ally-diagnose-{}", ally_core::storage::new_id())); + ally_core::storage::private_dir(&root).unwrap(); root } @@ -237,7 +235,7 @@ mod tests { async fn real_agent(root: &std::path::Path) -> Endpoint { let agent = endpoint(vec![ALPN.to_vec()]).await; let sshd = std::net::SocketAddr::from(([127, 0, 0, 1], 9)); - tokio::spawn(slingshot_agent::tunnel::accept( + tokio::spawn(ally_agent::tunnel::accept( agent.clone(), root.to_path_buf(), sshd, @@ -250,7 +248,7 @@ mod tests { let root = agent_root(); let agent = real_agent(&root).await; let client = endpoint(Vec::new()).await; - slingshot_agent::clients::allow(&root, "laptop", &client.id().to_string()).unwrap(); + ally_agent::clients::allow(&root, "laptop", &client.id().to_string()).unwrap(); let diagnosis = check(&client, agent.addr(), Duration::from_secs(5)).await; std::fs::remove_dir_all(&root).unwrap(); diff --git a/crates/slingshot-cli/src/watch/event.rs b/crates/ally-cli/src/watch/event.rs similarity index 84% rename from crates/slingshot-cli/src/watch/event.rs rename to crates/ally-cli/src/watch/event.rs index c07b985..443fa46 100644 --- a/crates/slingshot-cli/src/watch/event.rs +++ b/crates/ally-cli/src/watch/event.rs @@ -1,12 +1,12 @@ -//! The lines `slingshot internal-watch` prints for the menu bar app. Each line is one JSON +//! The lines `ally internal-watch` prints for the menu bar app. Each line is one JSON //! object. The app only draws and posts what arrives here, so every threshold and every //! decision to notify stays in Rust. use crate::commands::health::{LOAD, Limits, MEMORY, TEMPERATURE}; +use ally_core::presentation::Tone; +use ally_core::protocol::Health; +use ally_core::telemetry::DISK_WARNING_MIB; use serde::Serialize; -use slingshot_core::presentation::Tone; -use slingshot_core::protocol::Health; -use slingshot_core::telemetry::DISK_WARNING_MIB; /// Raised whenever a field changes meaning or disappears, so the app can ask for an update /// rather than draw something wrong. @@ -168,7 +168,7 @@ impl Status { } /// Turn a connection failure into what it means and what to do. The messages matched here -/// are Slingshot's own, from the config, the control helper, and the iroh diagnosis. +/// are Ally's own, from the config, the control helper, and the iroh diagnosis. pub fn explain(name: &str, error: &str) -> Problem { let text = error.to_lowercase(); let has = |needles: &[&str]| needles.iter().any(|needle| text.contains(needle)); @@ -182,21 +182,21 @@ pub fn explain(name: &str, error: &str) -> Problem { if has(&["no agent configured"]) { problem( "No Agent linked yet".to_string(), - "Run slingshot start on the Agent, then link this machine with the code it prints." + "Run ally start on the Agent, then link this machine with the code it prints." .to_string(), - run(Machine::Client, "slingshot link "), + run(Machine::Client, "ally link "), ) } else if has(&["no longer accepts"]) { problem( format!("{name} no longer accepts this machine"), format!("Link again from the same network as {name}."), - run(Machine::Client, "slingshot link "), + run(Machine::Client, "ally link "), ) } else if has(&["not running on the agent", "connection refused"]) { problem( - format!("Slingshot isn't running on {name}"), - format!("{name} is on, but slingshot start has stopped."), - run(Machine::Agent, "slingshot start"), + format!("Ally isn't running on {name}"), + format!("{name} is on, but ally start has stopped."), + run(Machine::Agent, "ally start"), ) } else if has(&["cannot reach iroh's relays"]) { problem( @@ -204,10 +204,10 @@ pub fn explain(name: &str, error: &str) -> Problem { "Check the internet connection, then try again.".to_string(), None, ) - } else if has(&["versions differ", "update slingshot"]) { + } else if has(&["versions differ", "update ally"]) { problem( - "Slingshot versions differ".to_string(), - format!("Update Slingshot on this machine and on {name}."), + "Ally versions differ".to_string(), + format!("Update Ally on this machine and on {name}."), None, ) } else if has(&[ @@ -218,8 +218,8 @@ pub fn explain(name: &str, error: &str) -> Problem { ]) { problem( format!("{name} is off or asleep"), - "Nothing answered on the local network, tailnet, or iroh. Wake it and check that slingshot start is running.".to_string(), - run(Machine::Agent, "slingshot start"), + "Nothing answered on the local network, tailnet, or iroh. Wake it and check that ally start is running.".to_string(), + run(Machine::Agent, "ally start"), ) } else { problem(format!("Can't reach {name}"), error.to_string(), None) @@ -251,7 +251,7 @@ fn space(free_mib: u64) -> Space { } } -fn gpu(gpu: &slingshot_core::protocol::GpuHealth) -> Gpu { +fn gpu(gpu: &ally_core::protocol::GpuHealth) -> Gpu { Gpu { name: gpu.name.clone(), utilization: gpu @@ -271,7 +271,7 @@ fn gpu(gpu: &slingshot_core::protocol::GpuHealth) -> Gpu { #[cfg(test)] mod tests { use super::*; - use slingshot_core::protocol::GpuHealth; + use ally_core::protocol::GpuHealth; fn health() -> Health { Health { @@ -331,29 +331,29 @@ mod tests { fn failures_are_explained_with_a_fix_on_the_right_machine() { let cases = [ ( - "Could not reach archbox: Slingshot is not running on the Agent. Run slingshot start there: Connection refused (os error 111)", - "Slingshot isn't running on archbox", - Some((Machine::Agent, "slingshot start")), + "Could not reach archbox: Ally is not running on the Agent. Run ally start there: Connection refused (os error 111)", + "Ally isn't running on archbox", + Some((Machine::Agent, "ally start")), ), ( - "archbox is not reachable. It may be off or asleep, or slingshot start is not running there", + "archbox is not reachable. It may be off or asleep, or ally start is not running there", "archbox is off or asleep", - Some((Machine::Agent, "slingshot start")), + Some((Machine::Agent, "ally start")), ), ( "archbox did not answer in time", "archbox is off or asleep", - Some((Machine::Agent, "slingshot start")), + Some((Machine::Agent, "ally start")), ), ( - "archbox no longer accepts this machine. Run slingshot link again from the same network as archbox", + "archbox no longer accepts this machine. Run ally link again from the same network as archbox", "archbox no longer accepts this machine", - Some((Machine::Client, "slingshot link ")), + Some((Machine::Client, "ally link ")), ), ( - "No Agent configured yet\n\nOn the Agent: slingshot start", + "No Agent configured yet\n\nOn the Agent: ally start", "No Agent linked yet", - Some((Machine::Client, "slingshot link ")), + Some((Machine::Client, "ally link ")), ), ( "Could not reach archbox, because this machine cannot reach iroh's relays. Check its internet connection", @@ -361,8 +361,8 @@ mod tests { None, ), ( - "Slingshot versions differ between the machines (protocol 4 and 5). Update Slingshot on both machines", - "Slingshot versions differ", + "Ally versions differ between the machines (protocol 4 and 5). Update Ally on both machines", + "Ally versions differ", None, ), ("something new", "Can't reach archbox", None), @@ -393,7 +393,7 @@ mod tests { assert_eq!(line["online"], false); assert_eq!(line["problem"]["title"], "archbox is off or asleep"); assert_eq!(line["problem"]["fix"]["machine"], "agent"); - assert_eq!(line["problem"]["fix"]["command"], "slingshot start"); + assert_eq!(line["problem"]["fix"]["command"], "ally start"); assert_eq!(line["error"], "archbox did not answer in time"); } diff --git a/crates/slingshot-cli/src/watch/mod.rs b/crates/ally-cli/src/watch/mod.rs similarity index 93% rename from crates/slingshot-cli/src/watch/mod.rs rename to crates/ally-cli/src/watch/mod.rs index bf3a93b..29cabd0 100644 --- a/crates/slingshot-cli/src/watch/mod.rs +++ b/crates/ally-cli/src/watch/mod.rs @@ -1,7 +1,7 @@ -//! `slingshot internal-watch`: keeps one control connection to the Agent and prints what the +//! `ally internal-watch`: keeps one control connection to the Agent and prints what the //! menu bar app shows and notifies, one JSON line at a time. The app starts it and closes //! its input to stop it, so it never outlives the app. A `retry` line on its input skips the -//! wait before the next attempt, for the app's Try again button. When Slingshot is updated, +//! wait before the next attempt, for the app's Try again button. When Ally is updated, //! the helper exits cleanly and the app starts the new program in its place. pub mod event; @@ -9,9 +9,9 @@ pub mod state; use crate::client::{Control, unexpected}; use crate::route::{self, Route}; +use ally_core::config::{Agent, Config}; +use ally_core::control::{Request, Response}; use event::{Event, Line, Status, VERSION}; -use slingshot_core::config::{Agent, Config}; -use slingshot_core::control::{Request, Response}; use state::Watch; use std::convert::Infallible; use std::io::{BufRead, Write}; @@ -166,8 +166,8 @@ async fn poll( control: &mut Control, jobs: bool, ) -> anyhow::Result<( - slingshot_core::protocol::Health, - Option>, + ally_core::protocol::Health, + Option>, )> { let Response::Health(health) = control.call(Request::Health).await? else { return Err(unexpected()); @@ -198,7 +198,7 @@ fn emit(event: &Event) { } /// This program's file and when it was last written. An update such as `cargo install` -/// writes a new file, so a different time means a newer Slingshot is waiting. +/// writes a new file, so a different time means a newer Ally is waiting. struct Program { path: std::path::PathBuf, modified: std::time::SystemTime, @@ -226,10 +226,8 @@ mod tests { #[test] fn a_rewritten_program_file_counts_as_replaced() { - let path = std::env::temp_dir().join(format!( - "slingshot-watch-{}", - slingshot_core::storage::new_id() - )); + let path = + std::env::temp_dir().join(format!("ally-watch-{}", ally_core::storage::new_id())); std::fs::write(&path, "old").unwrap(); let program = Program::at(path.clone()).unwrap(); assert!(!program.replaced()); diff --git a/crates/slingshot-cli/src/watch/state.rs b/crates/ally-cli/src/watch/state.rs similarity index 96% rename from crates/slingshot-cli/src/watch/state.rs rename to crates/ally-cli/src/watch/state.rs index 1925eeb..596f901 100644 --- a/crates/slingshot-cli/src/watch/state.rs +++ b/crates/ally-cli/src/watch/state.rs @@ -4,10 +4,10 @@ use super::event::{self, Machine, Notice, NoticeKind}; use crate::commands::health::{MEMORY, TEMPERATURE}; -use slingshot_core::control::{Job, JobKind, JobState}; -use slingshot_core::presentation::{capacity, plural}; -use slingshot_core::protocol::Health; -use slingshot_core::telemetry::DISK_WARNING_MIB; +use ally_core::control::{Job, JobKind, JobState}; +use ally_core::presentation::{capacity, plural}; +use ally_core::protocol::Health; +use ally_core::telemetry::DISK_WARNING_MIB; use std::collections::HashSet; /// A run shorter than this finished while you were still looking at it. @@ -190,7 +190,7 @@ fn resource(title: String, body: String) -> Notice { } /// What a finished job is worth saying. The command is the subtitle, so the title can stay -/// short and plain. Stopping is something the person just did, whether with `slingshot stop` +/// short and plain. Stopping is something the person just did, whether with `ally stop` /// or with Ctrl C, which ends a run with exit code 130. A session usually ends because /// someone left it. So only surprises about those are told. fn notice(name: &str, job: &Job) -> Option { @@ -266,7 +266,7 @@ fn short(command: &str) -> String { #[cfg(test)] mod tests { use super::*; - use slingshot_core::protocol::GpuHealth; + use ally_core::protocol::GpuHealth; fn job(id: &str, kind: JobKind, state: JobState, took: u64) -> Job { Job { @@ -421,7 +421,7 @@ mod tests { #[test] fn unreachable_needs_two_failures_and_is_told_once_then_back() { - let refused = "Could not reach archbox: Slingshot is not running on the Agent. Run slingshot start there: Connection refused (os error 111)"; + let refused = "Could not reach archbox: Ally is not running on the Agent. Run ally start there: Connection refused (os error 111)"; let mut watch = Watch::new("archbox"); assert!(watch.reached("local network").is_empty()); assert!(watch.failed(refused).is_empty()); @@ -430,9 +430,9 @@ mod tests { assert_eq!(notices[0].title, "archbox went offline"); assert_eq!( notices[0].subtitle.as_deref(), - Some("Slingshot isn't running on archbox") + Some("Ally isn't running on archbox") ); - assert_eq!(notices[0].body, "Run slingshot start on archbox."); + assert_eq!(notices[0].body, "Run ally start on archbox."); assert!(watch.failed(refused).is_empty()); let notices = watch.reached("tailnet"); assert_eq!(notices[0].kind, NoticeKind::Back); diff --git a/crates/slingshot-cli/tests/output.rs b/crates/ally-cli/tests/output.rs similarity index 97% rename from crates/slingshot-cli/tests/output.rs rename to crates/ally-cli/tests/output.rs index 5e3f9ba..93c5a56 100644 --- a/crates/slingshot-cli/tests/output.rs +++ b/crates/ally-cli/tests/output.rs @@ -1,7 +1,7 @@ use std::process::{Command, Output}; fn invoke(args: &[&str], no_color: bool, term: &str) -> Output { - let mut command = Command::new(env!("CARGO_BIN_EXE_slingshot")); + let mut command = Command::new(env!("CARGO_BIN_EXE_ally")); command.args(args).env_remove("NO_COLOR").env("TERM", term); if no_color { command.env("NO_COLOR", "1"); diff --git a/crates/slingshot-core/Cargo.toml b/crates/ally-core/Cargo.toml similarity index 81% rename from crates/slingshot-core/Cargo.toml rename to crates/ally-core/Cargo.toml index a500aef..a2bb2ba 100644 --- a/crates/slingshot-core/Cargo.toml +++ b/crates/ally-core/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "slingshot-core" -description = "Shared rules and types for Slingshot, which runs heavy work on a powerful machine from a light one" +name = "ally-core" +description = "Shared rules and types for Ally, which runs heavy work on a powerful machine from a light one" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/slingshot-core/LICENSE-APACHE b/crates/ally-core/LICENSE-APACHE similarity index 100% rename from crates/slingshot-core/LICENSE-APACHE rename to crates/ally-core/LICENSE-APACHE diff --git a/crates/slingshot-core/LICENSE-MIT b/crates/ally-core/LICENSE-MIT similarity index 100% rename from crates/slingshot-core/LICENSE-MIT rename to crates/ally-core/LICENSE-MIT diff --git a/crates/slingshot-core/src/artifacts.rs b/crates/ally-core/src/artifacts.rs similarity index 100% rename from crates/slingshot-core/src/artifacts.rs rename to crates/ally-core/src/artifacts.rs diff --git a/crates/slingshot-core/src/config.rs b/crates/ally-core/src/config.rs similarity index 94% rename from crates/slingshot-core/src/config.rs rename to crates/ally-core/src/config.rs index cb8554b..541e05c 100644 --- a/crates/slingshot-core/src/config.rs +++ b/crates/ally-core/src/config.rs @@ -12,8 +12,8 @@ use std::path::PathBuf; /// Shown whenever there is no Agent to talk to. Names the two commands that fix it, /// because a stranger has no config file to look at yet. const NO_AGENTS: &str = "No Agent configured yet\n\n\ - On the Agent: slingshot start\n\ - On the Client: slingshot link "; + On the Agent: ally start\n\ + On the Client: ally link "; /// The whole config file. `agents` is a list so it renders as readable `[[agents]]` /// blocks for anyone who opens the file and edits it by hand. @@ -42,7 +42,7 @@ pub struct Agent { pub identity_file: Option, /// The file holding this Agent's ssh host keys, learned at pairing. pub known_hosts: Option, - /// Where the Slingshot program lives on the Agent, reported at pairing. + /// Where the Ally program lives on the Agent, reported at pairing. #[serde(default)] pub program: Option, /// Other addresses the Agent reported at pairing. Configs saved before this have none. @@ -71,10 +71,10 @@ impl Agent { self.client.clone().unwrap_or_else(keys::client_name) } - /// The Slingshot program to start over SSH. Pairing records the Agent's own path, so a - /// login shell without Slingshot on its PATH still works. + /// The Ally program to start over SSH. Pairing records the Agent's own path, so a + /// login shell without Ally on its PATH still works. pub fn program(&self) -> &str { - self.program.as_deref().unwrap_or("slingshot") + self.program.as_deref().unwrap_or("ally") } /// Every address to try, best first: this machine, the local network, anything else, @@ -91,10 +91,10 @@ impl Agent { } } -/// The platform's config directory for slingshot. The only place that knows this +/// The platform's config directory for ally. The only place that knows this /// path, so no OS specific path appears anywhere else. pub fn dir() -> anyhow::Result { - let Some(dirs) = ProjectDirs::from("", "", "slingshot") else { + let Some(dirs) = ProjectDirs::from("", "", "ally") else { anyhow::bail!("Could not determine home directory"); }; Ok(dirs.config_dir().to_path_buf()) @@ -104,8 +104,8 @@ fn path() -> anyhow::Result { Ok(dir()?.join("config.toml")) } -/// Where slingshot keeps the host keys of the Agents it has paired with. Kept apart -/// from your own `~/.ssh/known_hosts` so slingshot only ever edits its own files. +/// Where ally keeps the host keys of the Agents it has paired with. Kept apart +/// from your own `~/.ssh/known_hosts` so ally only ever edits its own files. pub fn known_hosts_path() -> anyhow::Result { Ok(dir()?.join("known_hosts")) } @@ -353,7 +353,7 @@ mod tests { fn no_agents_points_at_pairing() { let err = config("").resolve(None).unwrap_err().to_string(); - assert!(err.contains("slingshot link"), "message was: {err}"); + assert!(err.contains("ally link"), "message was: {err}"); } fn agent(name: &str) -> Agent { @@ -493,6 +493,6 @@ mod tests { #[test] fn an_agent_with_no_recorded_program_falls_back_to_the_path() { - assert_eq!(agent("archbox").program(), "slingshot"); + assert_eq!(agent("archbox").program(), "ally"); } } diff --git a/crates/slingshot-core/src/control.rs b/crates/ally-core/src/control.rs similarity index 95% rename from crates/slingshot-core/src/control.rs rename to crates/ally-core/src/control.rs index 2db21bb..208ca79 100644 --- a/crates/slingshot-core/src/control.rs +++ b/crates/ally-core/src/control.rs @@ -19,7 +19,7 @@ pub const VERSION: u32 = 14; /// Largest frame in either direction. Manifests for very large projects are the limit. pub const MAX_FRAME: u32 = 16 * 1024 * 1024; -/// Largest environment file accepted by `slingshot env add`. +/// Largest environment file accepted by `ally env add`. pub const MAX_ENVIRONMENT_FILE: usize = 1024 * 1024; /// This build's release, such as `0.1.0`, sent beside the protocol so a mismatch can name @@ -35,7 +35,7 @@ pub struct Envelope { } /// The other machine speaks a different control protocol. A higher protocol is a newer -/// Slingshot, so this says which machine needs updating. +/// Ally, so this says which machine needs updating. #[derive(Debug)] pub struct Mismatch { pub theirs: u32, @@ -52,7 +52,7 @@ impl std::fmt::Display for Mismatch { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Slingshot versions differ between the machines (protocol {} and {VERSION}). Update Slingshot on both machines", + "Ally versions differ between the machines (protocol {} and {VERSION}). Update Ally on both machines", self.theirs ) } @@ -124,7 +124,7 @@ pub enum Request { /// Which developer tools a session on the Agent would find. Tools, /// What the running daemon calls itself and whether iroh reaches it, for a second - /// `slingshot start` on the same account. + /// `ally start` on the same account. Status, /// Every project copy on the Agent, from any Client. Projects, @@ -160,7 +160,7 @@ pub enum Response { Cleaned { bytes: u64 }, } -/// One project copy on the Agent, as `slingshot projects` lists it. +/// One project copy on the Agent, as `ally projects` lists it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectSummary { pub id: String, @@ -176,7 +176,7 @@ pub struct ProjectSummary { pub waiting: usize, } -/// The running daemon, as a second `slingshot start` on the same account shows it. +/// The running daemon, as a second `ally start` on the same account shows it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentStatus { pub name: String, @@ -275,7 +275,7 @@ impl JobState { } } -/// One Slingshot run or session on the Agent. +/// One Ally run or session on the Agent. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Job { pub id: String, @@ -288,7 +288,7 @@ pub struct Job { pub ended: Option, pub exit_code: Option, /// The process group leader of a run, with its start time. Both must match before - /// Slingshot signals anything, so a reused process ID is never touched. + /// Ally signals anything, so a reused process ID is never touched. pub pid: Option, pub process_start: Option, /// Boot time when the job started, to tell a reboot apart from a normal exit. @@ -388,7 +388,7 @@ mod tests { .await .unwrap_err() .to_string(); - assert!(error.contains("Update Slingshot"), "{error}"); + assert!(error.contains("Update Ally"), "{error}"); a.write_u32(MAX_FRAME + 1).await.unwrap(); assert!(read_frame::<_, Request>(&mut b).await.is_err()); diff --git a/crates/slingshot-core/src/keys.rs b/crates/ally-core/src/keys.rs similarity index 88% rename from crates/slingshot-core/src/keys.rs rename to crates/ally-core/src/keys.rs index 2ff60d1..c75f206 100644 --- a/crates/slingshot-core/src/keys.rs +++ b/crates/ally-core/src/keys.rs @@ -1,15 +1,15 @@ -//! How slingshot labels the key it installs, and how a machine publishes its identity. +//! How ally labels the key it installs, and how a machine publishes its identity. use crate::{config, storage}; use anyhow::Context; use std::fs; use std::path::{Path, PathBuf}; -/// The comment written into slingshot's public key, and the marker `unlink` looks for +/// The comment written into ally's public key, and the marker `unlink` looks for /// when taking that key back off an Agent. Both sides have to agree on it, which is /// why it lives here rather than on either side. pub fn marker(client_name: &str) -> String { - format!("slingshot:{client_name}") + format!("ally:{client_name}") } /// This machine's hostname, which Clients linked before unique names still go by. @@ -120,14 +120,14 @@ fn write_lines(file: &Path, lines: &[String]) -> anyhow::Result<()> { storage::write_bytes(file, body.as_bytes()) } -/// How many Clients Slingshot gave access to this account, by its marker on their lines. +/// How many Clients Ally gave access to this account, by its marker on their lines. pub fn authorized_clients() -> anyhow::Result { let file = ssh_dir()?.join("authorized_keys"); let existing = fs::read_to_string(file).unwrap_or_default(); - Ok(existing.lines().filter(|line| from_slingshot(line)).count()) + Ok(existing.lines().filter(|line| from_ally(line)).count()) } -/// Remove every line Slingshot installed for a Client, keeping all other keys. +/// Remove every line Ally installed for a Client, keeping all other keys. pub fn revoke_clients() -> anyhow::Result { let file = ssh_dir()?.join("authorized_keys"); let Ok(existing) = fs::read_to_string(&file) else { @@ -135,7 +135,7 @@ pub fn revoke_clients() -> anyhow::Result { }; let kept: Vec = existing .lines() - .filter(|line| !from_slingshot(line)) + .filter(|line| !from_ally(line)) .map(str::to_string) .collect(); let removed = existing.lines().count() - kept.len(); @@ -145,8 +145,8 @@ pub fn revoke_clients() -> anyhow::Result { Ok(removed) } -/// A line whose comment is Slingshot's marker, as `authorized_line` writes it. -fn from_slingshot(line: &str) -> bool { +/// A line whose comment is Ally's marker, as `authorized_line` writes it. +fn from_ally(line: &str) -> bool { line.split_whitespace() .last() .is_some_and(|comment| comment.starts_with(&marker(""))) @@ -217,11 +217,11 @@ mod tests { use super::*; #[test] - fn only_lines_slingshot_installed_count_as_its_clients() { - assert!(from_slingshot("ssh-ed25519 AAAA slingshot:laptop-1a2b3c")); - assert!(!from_slingshot("ssh-ed25519 AAAA me@laptop")); - assert!(!from_slingshot("ssh-ed25519 AAAA slingshot-fan")); - assert!(!from_slingshot("")); + fn only_lines_ally_installed_count_as_its_clients() { + assert!(from_ally("ssh-ed25519 AAAA ally:laptop-1a2b3c")); + assert!(!from_ally("ssh-ed25519 AAAA me@laptop")); + assert!(!from_ally("ssh-ed25519 AAAA ally-fan")); + assert!(!from_ally("")); } #[test] @@ -237,7 +237,7 @@ mod tests { #[test] fn the_marker_names_the_client() { - assert_eq!(marker("laptop"), "slingshot:laptop"); + assert_eq!(marker("laptop"), "ally:laptop"); } #[test] @@ -254,7 +254,7 @@ mod tests { /// and then cannot ever be removed. #[test] fn an_authorized_line_ends_with_the_marker_used_to_revoke_it() { - let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 slingshot:mount"; + let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 ally:mount"; let line = authorized_line("archbox", key).unwrap(); @@ -269,7 +269,7 @@ mod tests { fn a_key_with_no_comment_still_gets_one() { let line = authorized_line("archbox", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5").unwrap(); - assert_eq!(line, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 slingshot:archbox"); + assert_eq!(line, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 ally:archbox"); } #[test] @@ -286,10 +286,10 @@ mod tests { let dir = crate::storage::testing::TempDir::new("keys"); let file = dir.path().join("authorized_keys"); - write_lines(&file, &["ssh-ed25519 AAAA slingshot:laptop".to_string()]).unwrap(); + write_lines(&file, &["ssh-ed25519 AAAA ally:laptop".to_string()]).unwrap(); assert_eq!( fs::read_to_string(&file).unwrap(), - "ssh-ed25519 AAAA slingshot:laptop\n" + "ssh-ed25519 AAAA ally:laptop\n" ); assert_eq!( fs::metadata(&file).unwrap().permissions().mode() & 0o777, diff --git a/crates/slingshot-core/src/lib.rs b/crates/ally-core/src/lib.rs similarity index 100% rename from crates/slingshot-core/src/lib.rs rename to crates/ally-core/src/lib.rs diff --git a/crates/slingshot-core/src/network.rs b/crates/ally-core/src/network.rs similarity index 100% rename from crates/slingshot-core/src/network.rs rename to crates/ally-core/src/network.rs diff --git a/crates/slingshot-core/src/preflight.rs b/crates/ally-core/src/preflight.rs similarity index 96% rename from crates/slingshot-core/src/preflight.rs rename to crates/ally-core/src/preflight.rs index f5ff8b0..d1b7da3 100644 --- a/crates/slingshot-core/src/preflight.rs +++ b/crates/ally-core/src/preflight.rs @@ -71,7 +71,7 @@ pub fn report(checks: &[Check]) -> bool { checks.iter().any(|c| c.state == State::Fail) } -/// Package managers Slingshot knows how to name in a fix, in the order they are looked for. +/// Package managers Ally knows how to name in a fix, in the order they are looked for. const PACKAGE_MANAGERS: &[&str] = &["pacman", "apt", "dnf", "zypper", "apk", "brew"]; /// The first known package manager on this machine. @@ -82,7 +82,7 @@ pub fn package_manager() -> Option<&'static str> { .find(|manager| is_installed(manager)) } -/// The command that installs `packages` with `manager`, or `None` for a manager Slingshot +/// The command that installs `packages` with `manager`, or `None` for a manager Ally /// does not know. pub fn install_command(manager: &str, packages: &str) -> Option { Some(match manager { @@ -173,13 +173,13 @@ fn docker_access(socket: &Path, user: &str) -> Option { } } -/// The checks `slingshot start` runs before it listens. SSH carries all work and control, +/// The checks `ally start` runs before it listens. SSH carries all work and control, /// rsync copies project source, and tmux is only needed once someone uses attach. pub fn start_checks() -> Vec { vec![ ssh_server_check(), tool_check("rsync", None), - tool_check("tmux", Some("slingshot attach")), + tool_check("tmux", Some("ally attach")), match is_installed("nvidia-smi") { true => Check::pass("GPU reporting available"), false => Check::warn( diff --git a/crates/slingshot-core/src/presentation.rs b/crates/ally-core/src/presentation.rs similarity index 96% rename from crates/slingshot-core/src/presentation.rs rename to crates/ally-core/src/presentation.rs index cc73ea1..d6c6aff 100644 --- a/crates/slingshot-core/src/presentation.rs +++ b/crates/ally-core/src/presentation.rs @@ -1,4 +1,4 @@ -//! Shared formatting for Slingshot output. Stream detection keeps redirected output plain. +//! Shared formatting for Ally output. Stream detection keeps redirected output plain. use anyhow::Context; use std::io::{IsTerminal, stderr, stdout}; @@ -223,14 +223,14 @@ mod tests { #[test] fn only_the_first_line_of_a_status_stands_out() { - let text = "Lost connection to archbox\n See how slingshot ps --all"; + let text = "Lost connection to archbox\n See how ally ps --all"; assert_eq!( Style::new(false).status(text, Tone::Error), - "✗ Lost connection to archbox\n See how slingshot ps --all" + "✗ Lost connection to archbox\n See how ally ps --all" ); assert_eq!( Style::new(true).status(text, Tone::Error), - "\x1b[31m✗\x1b[0m Lost connection to archbox\n\x1b[2m See how slingshot ps --all\x1b[0m" + "\x1b[31m✗\x1b[0m Lost connection to archbox\n\x1b[2m See how ally ps --all\x1b[0m" ); } diff --git a/crates/slingshot-core/src/protocol.rs b/crates/ally-core/src/protocol.rs similarity index 96% rename from crates/slingshot-core/src/protocol.rs rename to crates/ally-core/src/protocol.rs index 08be4da..029a3ef 100644 --- a/crates/slingshot-core/src/protocol.rs +++ b/crates/ally-core/src/protocol.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use spake2::{Ed25519Group, Identity, Password, Spake2}; -/// The pairing port `slingshot start` tries first. +/// The pairing port `ally start` tries first. pub const DEFAULT_PORT: u16 = 7433; /// What the Client sends. A pairing is `Start`, then `Join`, on one connection. @@ -73,8 +73,8 @@ pub enum Side { impl Side { fn label(self) -> &'static [u8] { match self { - Side::Client => b"slingshot client", - Side::Agent => b"slingshot agent", + Side::Client => b"ally client", + Side::Agent => b"ally agent", } } } @@ -147,7 +147,7 @@ pub struct Paired { /// The Agent's ssh host keys, so the Client can recognize it later without /// anybody being asked to eyeball a fingerprint. pub host_keys: Vec, - /// The Agent's Slingshot program path, so the Client can start helpers over SSH. + /// The Agent's Ally program path, so the Client can start helpers over SSH. pub program: Option, /// Every address the Agent answers on, so the Client can switch to another one when the /// address it paired on stops answering. @@ -188,7 +188,7 @@ pub struct Health { pub memory_total_mib: u64, pub swap_total_mib: u64, pub disk_free_mib: u64, - /// Free space where Slingshot keeps project copies and build output. + /// Free space where Ally keeps project copies and build output. #[serde(default)] pub workspace_free_mib: Option, pub gpus: Vec, diff --git a/crates/slingshot-core/src/source.rs b/crates/ally-core/src/source.rs similarity index 97% rename from crates/slingshot-core/src/source.rs rename to crates/ally-core/src/source.rs index b7d2cbb..fdc7895 100644 --- a/crates/slingshot-core/src/source.rs +++ b/crates/ally-core/src/source.rs @@ -2,7 +2,7 @@ //! //! Git ignore rules, version control internals, generated folders, and environment //! files never enter a manifest, so they are never copied, pulled, previewed, or -//! backed up. Environment files are handled separately by `slingshot env`. +//! backed up. Environment files are handled separately by `ally env`. use crate::storage; use anyhow::{Context, bail, ensure}; @@ -56,7 +56,7 @@ fn is_environment_name(name: &str) -> bool { name == ".env" || name.starts_with(".env.") || name.ends_with(".env") || name == ".envrc" } -/// Where `slingshot env add` may place a file inside the Agent's copy. Both machines check +/// Where `ally env add` may place a file inside the Agent's copy. Both machines check /// this, so the Client can refuse a bad target before reading a secret off disk. pub fn environment_target(target: &str) -> anyhow::Result<()> { let path = storage::relative(target)?; @@ -105,7 +105,7 @@ fn always_excluded(path: &str, has_file: impl Fn(&str) -> bool) -> bool { /// Extra exclusion patterns for a project, gathered on the Client and sent to the Agent /// so both machines apply exactly the same rules. Repository and global git excludes -/// come first and `slingshot.toml` patterns last. Every pattern is anchored at the root. +/// come first and `ally.toml` patterns last. Every pattern is anchored at the root. pub fn client_excludes(root: &Path) -> anyhow::Result> { let mut patterns = Vec::new(); patterns.extend(pattern_lines(&root.join(".git/info/exclude"))); @@ -116,7 +116,7 @@ pub fn client_excludes(root: &Path) -> anyhow::Result> { for pattern in &own { ensure!( !pattern.trim_start().starts_with('!'), - "sync.exclude in slingshot.toml cannot contain negated patterns: {pattern}" + "sync.exclude in ally.toml cannot contain negated patterns: {pattern}" ); } patterns.extend(own); @@ -171,9 +171,9 @@ struct SyncSettings { exclude: Vec, } -/// Read `sync.exclude` from `slingshot.toml`. +/// Read `sync.exclude` from `ally.toml`. fn project_excludes(root: &Path) -> anyhow::Result> { - let file = root.join("slingshot.toml"); + let file = root.join("ally.toml"); let text = match fs::read_to_string(&file) { Ok(text) => text, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), @@ -572,7 +572,7 @@ mod tests { assert!(!always_excluded("src/target/mod.rs", |f| f == "Cargo.toml")); assert!(always_excluded("web/node_modules/x/index.js", |_| false)); assert!(always_excluded(".git/config", |_| false)); - assert!(always_excluded("a/.slingshot-partial-123", |_| false)); + assert!(always_excluded("a/.ally-partial-123", |_| false)); } #[test] @@ -625,14 +625,14 @@ mod tests { dir.write("data/big.bin", ""); dir.write("data/small.txt", ""); dir.write("keep.txt", ""); - dir.write("slingshot.toml", "[sync]\nexclude = [\"data/*.bin\"]\n"); + dir.write("ally.toml", "[sync]\nexclude = [\"data/*.bin\"]\n"); let patterns = client_excludes(dir.path()).unwrap(); assert!(patterns.contains(&"private.txt".to_string())); let rules = Rules::new(dir.path(), &patterns).unwrap(); let manifest = scan(dir.path(), &rules, &Manifest::new(), None).unwrap(); assert_eq!( names(&manifest), - ["data/small.txt", "keep.txt", "slingshot.toml"] + ["ally.toml", "data/small.txt", "keep.txt"] ); assert!(rules.excluded("data/big.bin", |_| false)); assert!(!rules.excluded("data/small.txt", |_| false)); @@ -641,9 +641,9 @@ mod tests { #[test] fn negated_project_patterns_are_refused() { let dir = TempDir::new("negated"); - dir.write("slingshot.toml", "[sync]\nexclude = [\"!.env\"]\n"); + dir.write("ally.toml", "[sync]\nexclude = [\"!.env\"]\n"); assert!(client_excludes(dir.path()).is_err()); - dir.write("slingshot.toml", "[sync]\nexclude = [\"*.bin\"]\n"); + dir.write("ally.toml", "[sync]\nexclude = [\"*.bin\"]\n"); assert!( client_excludes(dir.path()) .unwrap() diff --git a/crates/slingshot-core/src/stack.rs b/crates/ally-core/src/stack.rs similarity index 90% rename from crates/slingshot-core/src/stack.rs rename to crates/ally-core/src/stack.rs index ac4ad0f..8c9e056 100644 --- a/crates/slingshot-core/src/stack.rs +++ b/crates/ally-core/src/stack.rs @@ -1,4 +1,4 @@ -//! Project discovery from Rust, Node, Python, and Slingshot marker files. +//! Project discovery from Rust, Node, Python, and Ally marker files. use std::path::{Path, PathBuf}; @@ -10,9 +10,9 @@ pub enum Stack { Python, } -/// Project markers. A slingshot.toml file also marks a project without a known stack. +/// Project markers. An ally.toml file also marks a project without a known stack. const MARKERS: &[(&str, Option)] = &[ - ("slingshot.toml", None), + ("ally.toml", None), ("Cargo.toml", Some(Stack::Rust)), ("package.json", Some(Stack::Node)), ("pyproject.toml", Some(Stack::Python)), @@ -83,7 +83,7 @@ mod tests { impl Sandbox { fn new(name: &str) -> Sandbox { - let root = std::env::temp_dir().join(format!("slingshot-stack-{name}")); + let root = std::env::temp_dir().join(format!("ally-stack-{name}")); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).expect("sandbox should be creatable"); @@ -200,14 +200,14 @@ mod tests { assert_eq!(project.root, sandbox.root.join("crates/inner")); } - /// A slingshot.toml marks a project even when nothing else does, so an override - /// can name a stack slingshot would not have detected. + /// An ally.toml marks a project even when nothing else does, so an override + /// can name a stack ally would not have detected. #[test] - fn a_slingshot_toml_alone_marks_a_project() { + fn an_ally_toml_alone_marks_a_project() { let sandbox = Sandbox::new("override"); - sandbox.file("slingshot.toml"); + sandbox.file("ally.toml"); - let project = find(&sandbox.root).expect("slingshot.toml should mark a root"); + let project = find(&sandbox.root).expect("ally.toml should mark a root"); assert_eq!(project.root, sandbox.root); assert!(project.stacks.is_empty()); diff --git a/crates/slingshot-core/src/step.rs b/crates/ally-core/src/step.rs similarity index 100% rename from crates/slingshot-core/src/step.rs rename to crates/ally-core/src/step.rs diff --git a/crates/slingshot-core/src/storage.rs b/crates/ally-core/src/storage.rs similarity index 94% rename from crates/slingshot-core/src/storage.rs rename to crates/ally-core/src/storage.rs index df51c7c..f558ec2 100644 --- a/crates/slingshot-core/src/storage.rs +++ b/crates/ally-core/src/storage.rs @@ -1,4 +1,4 @@ -//! Private Slingshot storage: directories, identifiers, atomic writes, and locks. +//! Private Ally storage: directories, identifiers, atomic writes, and locks. use anyhow::{Context, ensure}; use serde::{Serialize, de::DeserializeOwned}; @@ -8,11 +8,11 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::{Component, Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; -/// Slingshot's platform data directory. The Client and Agent each use their own subfolder, +/// Ally's platform data directory. The Client and Agent each use their own subfolder, /// so one machine can safely play both roles. pub fn data_dir() -> anyhow::Result { - let dirs = directories::ProjectDirs::from("", "", "slingshot") - .context("Could not determine the home directory for Slingshot storage")?; + let dirs = directories::ProjectDirs::from("", "", "ally") + .context("Could not determine the home directory for Ally storage")?; Ok(dirs.data_local_dir().to_path_buf()) } @@ -31,14 +31,14 @@ pub fn private_dir(path: &Path) -> anyhow::Result<()> { /// Names the temporary file an atomic write renames into place. Source scanning skips /// anything starting with it, so a write in flight is never picked up as project content. -pub const PARTIAL_PREFIX: &str = ".slingshot-partial-"; +pub const PARTIAL_PREFIX: &str = ".ally-partial-"; /// A random 128 bit identifier written as 32 lowercase hex characters. pub fn new_id() -> String { format!("{:032x}", rand::random::()) } -/// The leading part of an identifier, which is what job listings show and `slingshot stop` +/// The leading part of an identifier, which is what job listings show and `ally stop` /// accepts. Both sides shorten the same way so a copied prefix always matches. pub fn short_id(id: &str) -> &str { &id[..id.len().min(8)] @@ -50,7 +50,7 @@ pub fn check_id(value: &str) -> anyhow::Result<()> { && value .bytes() .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)), - "Invalid Slingshot identifier" + "Invalid Ally identifier" ); Ok(()) } @@ -198,7 +198,7 @@ pub(crate) mod testing { impl TempDir { pub fn new(label: &str) -> TempDir { - let path = std::env::temp_dir().join(format!("slingshot-{label}-{}", super::new_id())); + let path = std::env::temp_dir().join(format!("ally-{label}-{}", super::new_id())); std::fs::create_dir_all(&path).unwrap(); TempDir(path.canonicalize().unwrap()) } diff --git a/crates/slingshot-core/src/sync.rs b/crates/ally-core/src/sync.rs similarity index 100% rename from crates/slingshot-core/src/sync.rs rename to crates/ally-core/src/sync.rs diff --git a/crates/slingshot-core/src/telemetry.rs b/crates/ally-core/src/telemetry.rs similarity index 99% rename from crates/slingshot-core/src/telemetry.rs rename to crates/ally-core/src/telemetry.rs index c55ea9b..4caaab6 100644 --- a/crates/slingshot-core/src/telemetry.rs +++ b/crates/ally-core/src/telemetry.rs @@ -41,7 +41,7 @@ pub const DISK_WARNING_MIB: u64 = 2 * 1024; /// A live snapshot. CPU usage needs two samples with a gap between them, because a /// percentage is a change over time and a single reading has nothing to compare to. -/// `workspace` is where Slingshot keeps project copies, measured separately from `/`. +/// `workspace` is where Ally keeps project copies, measured separately from `/`. pub fn health(workspace: Option<&Path>) -> Health { let mut sys = System::new_all(); sys.refresh_cpu_usage(); diff --git a/crates/slingshot-core/src/tools.rs b/crates/ally-core/src/tools.rs similarity index 98% rename from crates/slingshot-core/src/tools.rs rename to crates/ally-core/src/tools.rs index f5e72a2..e952ddb 100644 --- a/crates/slingshot-core/src/tools.rs +++ b/crates/ally-core/src/tools.rs @@ -1,4 +1,4 @@ -//! The developer tools Slingshot can set up on the Agent. The Agent reports which it has, +//! The developer tools Ally can set up on the Agent. The Agent reports which it has, //! and the Client decides what to offer and shows every command before anything runs. use crate::preflight::install_command; @@ -109,7 +109,7 @@ impl Tool { } /// The commands that install this tool on an Agent using `manager`, where `npm_writable` - /// says whether npm's global folder needs `sudo`. `None` means Slingshot has no command it + /// says whether npm's global folder needs `sudo`. `None` means Ally has no command it /// trusts there, so the person installs it themselves. pub fn install(self, manager: Option<&str>, npm_writable: Option) -> Option> { match self { @@ -194,7 +194,7 @@ pub fn installed_here() -> Vec { /// Folders in the home folder where installers put programs for one user, such as Claude /// Code in `~/.local/bin`. Login shells that are not interactive often leave them off PATH, -/// so Slingshot adds them to everything it starts on the Agent instead of editing any file. +/// so Ally adds them to everything it starts on the Agent instead of editing any file. const USER_FOLDERS: [&str; 3] = [".local/bin", ".cargo/bin", ".opencode/bin"]; /// A shell line that puts `USER_FOLDERS` at the front of PATH. diff --git a/crates/slingshot-core/src/tunnel.rs b/crates/ally-core/src/tunnel.rs similarity index 95% rename from crates/slingshot-core/src/tunnel.rs rename to crates/ally-core/src/tunnel.rs index 0b878d5..4145fe6 100644 --- a/crates/slingshot-core/src/tunnel.rs +++ b/crates/ally-core/src/tunnel.rs @@ -7,8 +7,8 @@ use anyhow::Context; use iroh::{PublicKey, SecretKey}; use std::path::Path; -/// Names what flows over a Slingshot iroh connection: a raw stream to the Agent's sshd. -pub const ALPN: &[u8] = b"slingshot/ssh/1"; +/// Names what flows over an Ally iroh connection: a raw stream to the Agent's sshd. +pub const ALPN: &[u8] = b"ally/ssh/1"; /// Why the Agent closes a connection from a key that never paired. The Client looks for it /// to tell a refusal apart from an Agent that is simply off. diff --git a/crates/slingshot-cli/menubar/AppIcon.icns b/crates/slingshot-cli/menubar/AppIcon.icns deleted file mode 100644 index 2b260a8..0000000 Binary files a/crates/slingshot-cli/menubar/AppIcon.icns and /dev/null differ diff --git a/crates/slingshot-cli/menubar/Logo.png b/crates/slingshot-cli/menubar/Logo.png deleted file mode 100644 index 9a8d0c5..0000000 Binary files a/crates/slingshot-cli/menubar/Logo.png and /dev/null differ diff --git a/crates/slingshot-cli/src/bin/sling.rs b/crates/slingshot-cli/src/bin/sling.rs deleted file mode 100644 index 50169e8..0000000 --- a/crates/slingshot-cli/src/bin/sling.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! `sling`, the short name for `slingshot`. It runs the same program. - -fn main() { - slingshot_cli::run(); -} diff --git a/crates/slingshot-cli/src/main.rs b/crates/slingshot-cli/src/main.rs deleted file mode 100644 index ee6a903..0000000 --- a/crates/slingshot-cli/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - slingshot_cli::run(); -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 868180b..e2f2235 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Architecture -* How Slingshot works inside, and what every source file does. +* How Ally works inside, and what every source file does. * For contributors. Commands are in [USAGE.md](USAGE.md). ## Contents @@ -22,12 +22,12 @@ ## Overview -* Slingshot copies your project to a powerful machine, runs your commands there, and streams the results back. You keep your own editor and terminal. +* Ally copies your project to a powerful machine, runs your commands there, and streams the results back. You keep your own editor and terminal. * Three rules shape the design: 1. **The Client stays light.** It edits files and shows output, and never does heavy work. -2. **Slingshot wraps proven tools.** `ssh` connects, `rsync` copies, `tmux` keeps terminals running, and the `iroh` library connects across the internet. -3. **Slingshot says where work runs.** Every remote command starts with a line such as `▶ Running on archbox via tailnet`. +2. **Ally wraps proven tools.** `ssh` connects, `rsync` copies, `tmux` keeps terminals running, and the `iroh` library connects across the internet. +3. **Ally says where work runs.** Every remote command starts with a line such as `▶ Running on archbox via tailnet`. ## Key Terms @@ -35,7 +35,7 @@ | --- | --- | | **Client** | The machine you work on. | | **Agent** | The powerful machine that does the work. | -| **Daemon** | The background program started by `slingshot start` or the boot service. | +| **Daemon** | The background program started by `ally start` or the boot service. | | **Tailnet** | A private network between your machines, made by a VPN such as Tailscale. | | **Relay** | A public server that passes encrypted traffic when two machines cannot connect directly. | | **Source copy** | The Agent's copy of your project, holding only source files. | @@ -48,7 +48,7 @@ ```mermaid flowchart LR subgraph Client - CLI["slingshot"] + CLI["ally"] App["Menu bar app"] end @@ -72,61 +72,61 @@ flowchart LR ## The Crates -* One Rust workspace of three crates builds one program, `slingshot`. The command decides whether it acts as the Client or the Agent. +* One Rust workspace of three crates builds one program, `ally`. The command decides whether it acts as the Client or the Agent. ```mermaid flowchart LR - CLI["slingshot-cli"] --> AgentCrate["slingshot-agent"] - CLI --> Core["slingshot-core"] + CLI["ally-cli"] --> AgentCrate["ally-agent"] + CLI --> Core["ally-core"] AgentCrate --> Core ``` | Crate | Contains | | --- | --- | -| `slingshot-core` | Rules both machines share: message formats, settings, which files are source, sync, build output rules, and output style. | -| `slingshot-agent` | The daemon: pairing, the control service, project storage, jobs, and the iroh endpoint. | -| `slingshot-cli` | Every command, the `ssh` and `rsync` calls, choosing a path, and the menu bar helper. | +| `ally-core` | Rules both machines share: message formats, settings, which files are source, sync, build output rules, and output style. | +| `ally-agent` | The daemon: pairing, the control service, project storage, jobs, and the iroh endpoint. | +| `ally-cli` | Every command, the `ssh` and `rsync` calls, choosing a path, and the menu bar helper. | -* Dependencies flow one way, as drawn. A rule both machines need lives in `slingshot-core`. +* Dependencies flow one way, as drawn. A rule both machines need lives in `ally-core`. ## How The Machines Talk | Traffic | How It Travels | Purpose | | --- | --- | --- | | **Pairing** | JSON over TCP port 7433 | Trades a one time code for access. | -| **Control** | `ssh` starts `slingshot internal-control` | Short requests: health, jobs, sync plans, sessions, environment files, tools. | -| **Work** | `ssh` starts `slingshot internal-run`, or joins `tmux` | Runs your command in a real terminal. | -| **Files** | `rsync` through `slingshot internal-rsh` | Copies changed source files either way. | +| **Control** | `ssh` starts `ally internal-control` | Short requests: health, jobs, sync plans, sessions, environment files, tools. | +| **Work** | `ssh` starts `ally internal-run`, or joins `tmux` | Runs your command in a real terminal. | +| **Files** | `rsync` through `ally internal-rsh` | Copies changed source files either way. | * Only pairing has its own port. Everything else is `ssh`, so it is encrypted and every caller is checked. -* Commands starting with `internal-` are hidden helpers Slingshot starts itself. +* Commands starting with `internal-` are hidden helpers Ally starts itself. ### Pairing -1. `slingshot start` prints a code such as `192.168.1.9:7433:K7QW9ZR2`: address, port, and secret. -2. `slingshot link ` connects. Both sides turn the secret into a shared key with SPAKE2, which never sends the secret. +1. `ally start` prints a code such as `192.168.1.9:7433:K7QW9ZR2`: address, port, and secret. +2. `ally link ` connects. Both sides turn the secret into a shared key with SPAKE2, which never sends the secret. 3. The Client sends its ssh key and iroh identity, with a proof made from the shared key. -4. The Agent checks the proof, then installs the key under the Client's name, such as `slingshot:MacBook-Pro-3f9c2a`: the hostname plus the start of its iroh key. +4. The Agent checks the proof, then installs the key under the Client's name, such as `ally:MacBook-Pro-3f9c2a`: the hostname plus the start of its iroh key. 5. The Agent replies with its ssh identity, addresses, iroh identity, and hardware, with its own proof. 6. The Client checks that proof, so a fake Agent is caught, and saves the details. * A code works once, expires after 10 minutes, and is burned after 3 wrong tries. A guess only counts when sent to the Agent. * An Agent with linked Clients prints a code only when Enter is pressed. -* The boot service runs `slingshot internal-daemon`, which has no pairing port. A `slingshot start` beside it pairs in its own process, so the code never reaches the daemon. Pairing only writes `authorized_keys` and the iroh Client list, which the daemon reads fresh. -* `slingshot unlink` reverses pairing: removes the key, deletes the Client's environment files, and forgets the Agent. +* The boot service runs `ally internal-daemon`, which has no pairing port. An `ally start` beside it pairs in its own process, so the code never reaches the daemon. Pairing only writes `authorized_keys` and the iroh Client list, which the daemon reads fresh. +* `ally unlink` reverses pairing: removes the key, deletes the Client's environment files, and forgets the Agent. ### Control -1. The Client runs `ssh slingshot internal-control`. +1. The Client runs `ssh ally internal-control`. 2. The helper opens the daemon's private socket, usable only by the Agent's account. 3. Requests and replies are JSON, each with a version, a size limit, and a time limit. -* Message types live in `slingshot-core/src/control.rs`. Changing their shape raises `control::VERSION`, so mismatched versions refuse to talk. +* Message types live in `ally-core/src/control.rs`. Changing their shape raises `control::VERSION`, so mismatched versions refuse to talk. ### Work And Files * Commands, sessions, and copies all use the real `ssh` and `rsync`. -* Slingshot builds every `ssh` call itself, with its own key and known hosts list, so you never edit `~/.ssh/config`. +* Ally builds every `ssh` call itself, with its own key and known hosts list, so you never edit `~/.ssh/config`. ## How The Client Reaches The Agent @@ -139,7 +139,7 @@ flowchart LR | 3 | iroh | Both machines have internet, anywhere. | * iroh gets past home routers by having both machines reach out at once. When that fails, traffic goes through a relay. -* Over iroh, `ssh` connects through `slingshot internal-tunnel`, and the Agent passes it only to its own ssh server. A relay sees only encrypted `ssh`. +* Over iroh, `ssh` connects through `ally internal-tunnel`, and the Agent passes it only to its own ssh server. A relay sees only encrypted `ssh`. * Over iroh, calls share one ssh connection, so repeated calls are fast. ## A Run From Start To Finish @@ -158,11 +158,11 @@ sequenceDiagram CLI->>CLI: print Done in 42s · exit 0 ``` -* A failing command is not a Slingshot error. Its exit code is reported as is. +* A failing command is not an Ally error. Its exit code is reported as is. ## Keeping Files In Step -* `slingshot-core/src/source.rs` decides what counts as source. It follows `.gitignore`, and always leaves out `.git`, build output, and environment files. Left out files are never copied, pulled, previewed, or backed up. +* `ally-core/src/source.rs` decides what counts as source. It follows `.gitignore`, and always leaves out `.git`, build output, and environment files. Left out files are never copied, pulled, previewed, or backed up. 1. Each machine lists its source files with a fingerprint of each. 2. Each list is compared with the baseline. @@ -172,7 +172,7 @@ sequenceDiagram 6. The receiving side checks every staged file, then applies them all at once, or restores the previous state if anything fails. * `--pull` runs the same steps the other way. `--check` shows the plan only. -* `slingshot env add` stores environment files outside the source copy. Their contents never appear in arguments. +* `ally env add` stores environment files outside the source copy. Their contents never appear in arguments. ## Build Output @@ -185,46 +185,46 @@ sequenceDiagram | Python | `pyproject.toml` or `requirements.txt` | The pip cache | `PIP_CACHE_DIR` points to Agent storage. | * A Python `.venv` stays in the source copy, because `python3 -m venv` refuses a link. The scan skips it. -* Detection lives in `slingshot-core/src/stack.rs`, and the rules in `slingshot-core/src/artifacts.rs`. +* Detection lives in `ally-core/src/stack.rs`, and the rules in `ally-core/src/artifacts.rs`. ## Runs And Sessions | | Run | Session | | --- | --- | --- | -| Started by | `slingshot run` | `slingshot attach` | +| Started by | `ally run` | `ally attach` | | Lasts | As long as the connection. A drop stops it and records it as interrupted. | Until ended. It is a `tmux` session. | -| Works in | The project, through `~/Slingshot/` | The project through the same link, or the home folder outside one | +| Works in | The project, through `~/Ally/` | The project through the same link, or the home folder outside one | | Blocks syncs | Yes, it owns the project copy while it works | No | -* The `~/Slingshot/` link keeps prompts short, and lets tools such as Docker Compose name things after the project. +* The `~/Ally/` link keeps prompts short, and lets tools such as Docker Compose name things after the project. * Each project has one session, and the home folder has one. -* Sessions use Slingshot's own `tmux` server and settings, so a personal `tmux` setup is untouched. They start the account's login shell from the user database, with a full color terminal, a `UTF-8` locale, mouse scrolling, and a quiet bar. -* `slingshot stop ` asks a job to finish, then forces it after five seconds. The Agent checks a process's ID and start time first, so it never stops a program that reused an old ID. +* Sessions use Ally's own `tmux` server and settings, so a personal `tmux` setup is untouched. They start the account's login shell from the user database, with a full color terminal, a `UTF-8` locale, mouse scrolling, and a quiet bar. +* `ally stop ` asks a job to finish, then forces it after five seconds. The Agent checks a process's ID and start time first, so it never stops a program that reused an old ID. ## The Menu Bar App -* A SwiftUI app in `crates/slingshot-cli/menubar/`. It only displays and notifies. Every decision is made in Rust. +* A SwiftUI app in `crates/ally-cli/menubar/`. It only displays and notifies. Every decision is made in Rust. ```mermaid flowchart LR - App["Slingshot.app"] -->|starts| Watch["slingshot internal-watch"] + App["Ally.app"] -->|starts| Watch["ally internal-watch"] Watch -->|"health every 2s,
jobs every 4s"| Agent Watch -->|"status and notice lines"| App ``` * `internal-watch` prints one JSON line at a time: a **status** line with each value and its level, or a **notice** line with a notification already worded. -* Thresholds, rules, and wording live in `crates/slingshot-cli/src/watch/`. The line format has its own version, `watch::event::VERSION`, and the app refuses versions it does not know. +* Thresholds, rules, and wording live in `crates/ally-cli/src/watch/`. The line format has its own version, `watch::event::VERSION`, and the app refuses versions it does not know. * Try again writes `retry` to the helper, which reconnects at once. * The helper exits when its program file is replaced, such as by `cargo install`, and the app starts the new one. It checks every 5 seconds. -* The app's source, logo, and icon are built into `slingshot`. `slingshot menubar` writes them out, builds with Swift, signs for this machine, and installs to `~/Applications`. It rebuilds only when the source fingerprint changed. -* `slingshot menubar` also saves where `slingshot` is, since apps started at login cannot find it, and restarts a running app. +* The app's source, logo, and icon are built into `ally`. `ally menubar` writes them out, builds with Swift, signs for this machine, and installs to `~/Applications`. It rebuilds only when the source fingerprint changed. +* `ally menubar` also saves where `ally` is, since apps started at login cannot find it, and restarts a running app. * The app is always a login item. `--remove` starts it once with `--remove` to leave login items, since only the app can, then deletes it. ## Security * Pairing codes never cross the network. They work once, expire, and are burned after 3 wrong tries. Both sides prove they know the code. * The daemon listens only on the machine itself and the local network. -* Installed ssh keys are named so `slingshot unlink` can remove them. +* Installed ssh keys are named so `ally unlink` can remove them. * Arguments sent to the Agent are quoted one by one, never joined into one shell command. * Control messages carry a version, a size limit, and a time limit. * The Agent's user account is the security boundary. @@ -237,28 +237,28 @@ flowchart LR | Client | Where | | --- | --- | -| Saved Agents | `config.toml` in Slingshot's config folder | -| Agent ssh identities | `known_hosts` in Slingshot's config folder | -| Slingshot's ssh key | `~/.ssh`, under a Slingshot name | -| iroh identity | `client/` in Slingshot's data folder | +| Saved Agents | `config.toml` in Ally's config folder | +| Agent ssh identities | `known_hosts` in Ally's config folder | +| Ally's ssh key | `~/.ssh`, under an Ally name | +| iroh identity | `client/` in Ally's data folder | -* On the Agent, inside Slingshot's data folder: +* On the Agent, inside Ally's data folder: ``` jobs/ one record per run or session projects// ├── source/ the source copy ├── artifacts/ build output, such as target, node_modules, and the pip cache -├── environment/ files added with slingshot env +├── environment/ files added with ally env └── state/ the baseline, staging, and backups ``` ## File Reference -* Every source file, its purpose, which Slingshot files it uses, and which use it. Outside libraries are not listed. +* Every source file, its purpose, which Ally files it uses, and which use it. Outside libraries are not listed. ``` -slingshot/ +ally/ ├── .github/ │ ├── workflows/ci.yml the checks on every push to master and every pull request │ ├── dependabot.yml a weekly pull request for new crate and Action versions @@ -266,15 +266,15 @@ slingshot/ │ └── pull_request_template.md the checklist every pull request starts with ├── Cargo.toml the workspace and shared dependency versions ├── crates/ -│ ├── slingshot-core/ shared rules and types -│ ├── slingshot-agent/ the Agent daemon -│ └── slingshot-cli/ the slingshot program +│ ├── ally-core/ shared rules and types +│ ├── ally-agent/ the Agent daemon +│ └── ally-cli/ the ally program │ └── menubar/ the macOS menu bar app, shipped inside the crate ├── LICENSE-MIT, LICENSE-APACHE └── docs/ architecture, usage, troubleshooting, roadmap, and media ``` -### `slingshot-core` +### `ally-core` | File | Purpose | Uses | Used By | | --- | --- | --- | --- | @@ -292,79 +292,78 @@ slingshot/ | `artifacts.rs` | Where each kind of project keeps its build output on the Agent. | `stack` | Agent `projects`, Client `run` | | `telemetry.rs` | Reads hardware details and live use with `sysinfo` and `nvidia-smi`. | `presentation`, `protocol` | `preflight`, `tools`, Agent `lib`, Client `health`, `transfer`, `watch` | | `preflight.rs` | Setup checks that print the fix, and the package manager's install command. | `presentation`, `telemetry` | `tools`, Agent `lib`, `jobs`, `service`; Client `link`, `transfer` | -| `tools.rs` | The tools Slingshot can set up on the Agent, how to find them, and their install commands. | `preflight`, `stack`, `telemetry` | `control`, Agent `service`, Client `tools` | +| `tools.rs` | The tools Ally can set up on the Agent, how to find them, and their install commands. | `preflight`, `stack`, `telemetry` | `control`, Agent `service`, Client `tools` | | `presentation.rs` | The shared output style: symbols, colors, rows, and sizes. | none | `step`, `preflight`, `telemetry`, every command | | `step.rs` | A spinner for slow work, then a finished line with the time taken. | `presentation` | Agent `lib`; Client `client`, `transfer`, `run`, `link`, `ps`, `unlink` | -### `slingshot-agent` +### `ally-agent` | File | Purpose | Uses | Used By | | --- | --- | --- | --- | -| `lib.rs` | `slingshot start`: setup checks, the start screen, pairing, and starting the other parts. `slingshot internal-daemon`: the same without pairing, for the boot service. | `awake`, `clients`, `service`, `tunnel` | Client `lib` | +| `lib.rs` | `ally start`: setup checks, the start screen, pairing, and starting the other parts. `ally internal-daemon`: the same without pairing, for the boot service. | `awake`, `clients`, `service`, `tunnel` | Client `lib` | | `service.rs` | The private control socket, the `internal-control` helper that reaches it, and the tools probe run in the login shell. | `clients`, `jobs`, `projects` | `lib`, `runner`, Client `lib` | | `projects.rs` | Project storage: source copies, sync locks, build output, and environment files. Lists, cleans, and removes copies. | `jobs` | `service`, `jobs`, `runner`, `uninstall` | | `jobs.rs` | Job records, `tmux` sessions, and safe stopping. | `projects` | `service`, `projects`, `runner` | -| `runner.rs` | `slingshot internal-run`: runs one command in the project copy with a real terminal. | `jobs`, `projects`, `service` | Client `lib` | +| `runner.rs` | `ally internal-run`: runs one command in the project copy with a real terminal. | `jobs`, `projects`, `service` | Client `lib` | | `clients.rs` | The Clients allowed over iroh, and their names for the start screen. | none | `lib`, `service`, `tunnel` | | `tunnel.rs` | The iroh endpoint, which passes paired Clients to the local ssh server. | `clients` | `lib` | -| `uninstall.rs` | What `slingshot uninstall` removes on an Agent: the boot service, running work, Client access, the `~/Slingshot` links, and the data. | `boot`, `jobs`, `projects`, `service` | Client `uninstall` | +| `uninstall.rs` | What `ally uninstall` removes on an Agent: the boot service, running work, Client access, the `~/Ally` links, and the data. | `boot`, `jobs`, `projects`, `service` | Client `uninstall` | | `awake.rs` | Keeps the Agent awake while the daemon runs, and checks whether sleep is off. | none | `lib` | | `boot.rs` | Starting by itself: writes and removes the systemd user service or launchd agent. | none | `lib` | * `projects.rs` and `jobs.rs` use each other: a sync must know if a job runs, and a job must know where its project lives. -### `slingshot-cli` +### `ally-cli` | File | Purpose | Uses | Used By | | --- | --- | --- | --- | -| `lib.rs` | Reads the command line with `clap`, calls the command, and defines the hidden `internal-` helpers. | every command; Agent `lib`, `runner`, `service` | `main`, `bin/sling` | -| `main.rs` | The `slingshot` program. Calls `lib`. | `lib` | none, it is the entry point | -| `bin/sling.rs` | The `sling` program, the same under a shorter name. | `lib` | none, it is an entry point | +| `lib.rs` | Reads the command line with `clap`, calls the command, and defines the hidden `internal-` helpers. | every command; Agent `lib`, `runner`, `service` | `main` | +| `main.rs` | The `ally` program. Calls `lib`. | `lib` | none, it is the entry point | | `build.rs` | Puts the build's commit into `--version`, using `git` when present. | `git` | `lib` | | `client.rs` | The pairing connection, and the `Control` connection over `ssh`. | `route`, `ssh`, `tunnel` | `transfer`, `watch`, most commands | | `route.rs` | Chooses the path: local network, tailnet, or iroh. Prints the `▶` location line. | none | `client`, `ssh`, `transfer`, `watch`, `run`, `attach`, `tools` | | `ssh.rs` | Builds every `ssh` call, with safe quoting and a shared connection. | `route`, `tunnel` | `client`, `run`, `attach`, `unlink` | -| `tunnel.rs` | `slingshot internal-tunnel`: the Client's end of an iroh connection. | `project` | `client`, `ssh` | +| `tunnel.rs` | `ally internal-tunnel`: the Client's end of an iroh connection. | `project` | `client`, `ssh` | | `transfer.rs` | Performs a sync: lists files, plans, calls `rsync`, and reports. | `client`, `project`, `route` | `run`, `attach`, `sync` | | `project.rs` | Finds the current project and its ID on each Agent. | none | `transfer`, `tunnel`, most commands | -| `keys.rs` | Creates Slingshot's own ssh key. | none | `link` | +| `keys.rs` | Creates Ally's own ssh key. | none | `link` | | `live.rs` | Full screen views that refresh until Q or Ctrl C. | none | `health` | -| `watch/mod.rs` | `slingshot internal-watch`: polls the Agent and prints lines for the menu bar app. | `client`, `route`, `watch/event`, `watch/state` | `lib` | +| `watch/mod.rs` | `ally internal-watch`: polls the Agent and prints lines for the menu bar app. | `client`, `route`, `watch/event`, `watch/state` | `lib` | | `watch/event.rs` | The line format, value levels, and plain explanations of failures. | `commands/health` | `watch/mod`, `watch/state` | | `watch/state.rs` | Decides when a notification is due, and words it. | `commands/health`, `watch/event` | `watch/mod` | ### Commands -* One file per command in `crates/slingshot-cli/src/commands/`, each called by `lib.rs`. `mod.rs` lists them. +* One file per command in `crates/ally-cli/src/commands/`, each called by `lib.rs`. `mod.rs` lists them. | File | Command | Uses | | --- | --- | --- | -| `link.rs` | `slingshot link` | `client`, `keys`, `project`, `menubar`, `tools` | -| `tools.rs` | `slingshot tools`, and the tools step of `link` | `client`, `project`, `route`, `ssh`, `run` | -| `uninstall.rs` | `slingshot uninstall`: the Client's settings, key, app, and links, plus the Agent's part on an Agent | `keys`, `menubar`, `unlink`, Agent `uninstall` | -| `unlink.rs` | `slingshot unlink` | `client`, `project`, `ssh` | -| `run.rs` | `slingshot run` | `client`, `project`, `route`, `ssh`, `transfer` | -| `attach.rs` | `slingshot attach` | `client`, `project`, `route`, `ssh`, `transfer`, `run` | -| `sync.rs` | `slingshot sync` | `project`, `transfer` | -| `env.rs` | `slingshot env` | `client`, `project` | -| `projects.rs` | `slingshot projects`, `clean`, and `remove` | `client`, `project`, `ps` | -| `ps.rs` | `slingshot ps` and `slingshot stop` | `client` | -| `health.rs` | `slingshot health`, jobs in `--watch`, and the thresholds `watch` shares | `client`, `live`, `ps` | -| `menubar.rs` | `slingshot menubar`, and the offer at the end of `link` | `project`, `ssh`, `tools` | +| `link.rs` | `ally link` | `client`, `keys`, `project`, `menubar`, `tools` | +| `tools.rs` | `ally tools`, and the tools step of `link` | `client`, `project`, `route`, `ssh`, `run` | +| `uninstall.rs` | `ally uninstall`: the Client's settings, key, app, and links, plus the Agent's part on an Agent | `keys`, `menubar`, `unlink`, Agent `uninstall` | +| `unlink.rs` | `ally unlink` | `client`, `project`, `ssh` | +| `run.rs` | `ally run` | `client`, `project`, `route`, `ssh`, `transfer` | +| `attach.rs` | `ally attach` | `client`, `project`, `route`, `ssh`, `transfer`, `run` | +| `sync.rs` | `ally sync` | `project`, `transfer` | +| `env.rs` | `ally env` | `client`, `project` | +| `projects.rs` | `ally projects`, `clean`, and `remove` | `client`, `project`, `ps` | +| `ps.rs` | `ally ps` and `ally stop` | `client` | +| `health.rs` | `ally health`, jobs in `--watch`, and the thresholds `watch` shares | `client`, `live`, `ps` | +| `menubar.rs` | `ally menubar`, and the offer at the end of `link` | `project`, `ssh`, `tools` | ### Tests * Unit tests sit at the bottom of the file they test. -* `crates/slingshot-cli/tests/output.rs` runs the real program to check help text, colors, and errors. +* `crates/ally-cli/tests/output.rs` runs the real program to check help text, colors, and errors. -### `crates/slingshot-cli/menubar` +### `crates/ally-cli/menubar` | File | Purpose | | --- | --- | | `Package.swift` | The Swift package. No outside dependencies. | | `Info.plist` | The app's identity. Hides the Dock icon. | | `AppIcon.icns` | The icon on notifications and in Finder, made from the logo without smoothing. | -| `Logo.png` | The menu bar logo, drawn in the menu bar's color. A copy of `docs/media/slingshot.png` that ships with the crate. | +| `Logo.png` | The menu bar logo, drawn in the menu bar's color. `docs/media/ally.png` with the eyes cut out, so they still show when only the shape is drawn. | | `Sources/App.swift` | The starting point and menu bar icon. | | `Sources/Watcher.swift` | Starts `internal-watch`, reads its lines, restarts it if it stops, and sends `retry`. | | `Sources/Model.swift` | Reads the JSON lines. Must match `watch/event.rs`. | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c9d3dd0..2d6b3c2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -28,10 +28,10 @@ ### Phase 1: Pairing And Remote Commands -* `slingshot start` checks for an ssh server, `rsync`, and `tmux`, and prints a one time pairing code. -* `slingshot link` installs the Client's key on the Agent. The Client needs no ssh server. -* `slingshot run` runs a command with live output, typing, Ctrl C, and the real exit code. -* `slingshot health` shows the Agent's hardware and use. +* `ally start` checks for an ssh server, `rsync`, and `tmux`, and prints a one time pairing code. +* `ally link` installs the Client's key on the Agent. The Client needs no ssh server. +* `ally run` runs a command with live output, typing, Ctrl C, and the real exit code. +* `ally health` shows the Agent's hardware and use. ### Phase 2: Mounting The Project @@ -50,7 +50,7 @@ * The Client tries the local network, then the tailnet, then iroh, and every run names its path. * iroh connects directly when it can, and through a relay when not. A relay sees only encrypted data. -* The Agent accepts iroh only from paired Clients, stays awake while `slingshot start` runs, and closes iroh cleanly when stopped, so Clients reconnect at once. +* The Agent accepts iroh only from paired Clients, stays awake while `ally start` runs, and closes iroh cleanly when stopped, so Clients reconnect at once. * Errors tell an Agent that is off apart from a Client that is no longer paired. * **Tested:** a connection lost for 4 minutes during a run over iroh ends the run with a clear message, and the Agent stops it. @@ -58,42 +58,42 @@ | Area | What Works | | --- | --- | -| Menu bar app | CPU, RAM, GPU, VRAM, and workspace space; an offline screen with the cause, fix, and Try again; always a login item; follows a new `slingshot` by itself; moves to a better connection within a minute. `slingshot menubar` builds, signs, installs, and removes it. | -| Notifications | Finished, failed, and interrupted runs; the Agent going offline or returning; low memory, low disk, or a hot GPU. Shown with the Slingshot icon. | -| Sessions | `attach` from anywhere. Syncs first, and pulls edits back on leaving. Login shell, full color, `UTF-8`, mouse scrolling, a colored bar, `~/Slingshot/`, and the user folders on PATH. Works from terminals the Agent may not know, such as Kitty. | -| Tools | `link` ends by offering the Client's tools on the Agent, including OpenCode, and Ollama when the Agent has a GPU, then signs in to Claude Code and Codex. `slingshot tools` repeats it, offers sign in when signed out, and lists `sudo` steps for an admin on accounts without it. | -| Starting | A systemd user service with linger, or a launchd agent, offered on the first `slingshot start`. On Linux, a polkit rule keeps an Agent that can sleep awake with nobody logged in. `--boot` and `--remove` manage it. Moves to the next free port when the pairing port is taken. Warns when Docker is unusable, and says when sleep is off. | +| Menu bar app | CPU, RAM, GPU, VRAM, and workspace space; an offline screen with the cause, fix, and Try again; always a login item; follows a new `ally` by itself; moves to a better connection within a minute. `ally menubar` builds, signs, installs, and removes it. | +| Notifications | Finished, failed, and interrupted runs; the Agent going offline or returning; low memory, low disk, or a hot GPU. Shown with the Ally icon. | +| Sessions | `attach` from anywhere. Syncs first, and pulls edits back on leaving. Login shell, full color, `UTF-8`, mouse scrolling, a colored bar, `~/Ally/`, and the user folders on PATH. Works from terminals the Agent may not know, such as Kitty. | +| Tools | `link` ends by offering the Client's tools on the Agent, including OpenCode, and Ollama when the Agent has a GPU, then signs in to Claude Code and Codex. `ally tools` repeats it, offers sign in when signed out, and lists `sudo` steps for an admin on accounts without it. | +| Starting | A systemd user service with linger, or a launchd agent, offered on the first `ally start`. On Linux, a polkit rule keeps an Agent that can sleep awake with nobody logged in. `--boot` and `--remove` manage it. Moves to the next free port when the pairing port is taken. Warns when Docker is unusable, and says when sleep is off. | | Sync | `--overwrite` keeps one side of a conflict, with a backup. Pulls refuse names that differ only in case. A Python `.venv` works on the Agent. | | Output | Problems on one line with dimmed fixes. `link` and `tools` in sections. Stopped runs are not failed, and `ps --all` shows `Lost connection`. Version errors name the older machine. | -| Removal | `slingshot uninstall` lists everything on a Client or Agent, asks once with No as the default, and keeps the owner's own files and keys. | -| Projects | `slingshot projects` lists each copy on the Agent with its size, build output, last sync, and edits waiting. `clean` frees build output, and `remove` deletes a copy after asking. | +| Removal | `ally uninstall` lists everything on a Client or Agent, asks once with No as the default, and keeps the owner's own files and keys. | +| Projects | `ally projects` lists each copy on the Agent with its size, build output, last sync, and edits waiting. `clean` frees build output, and `remove` deletes a copy after asking. | ## Planned | Phase | Work | Issue | | --- | --- | --- | -| 5 | Port forwarding, plus ready and waiting notifications | [#9](https://github.com/ado11231/ss/issues/9) | -| 6 | Local AI models through Ollama | [#20](https://github.com/ado11231/ss/issues/20) | -| 6 | Pairing across networks, keeping the code safe through a relay | [#19](https://github.com/ado11231/ss/issues/19) | -| 6 | Watching several Agents from the menu bar | [#109](https://github.com/ado11231/ss/issues/109) | +| 5 | Port forwarding, plus ready and waiting notifications | [#9](https://github.com/ado11231/ally/issues/9) | +| 6 | Local AI models through Ollama | [#20](https://github.com/ado11231/ally/issues/20) | +| 6 | Pairing across networks, keeping the code safe through a relay | [#19](https://github.com/ado11231/ally/issues/19) | +| 6 | Watching several Agents from the menu bar | [#109](https://github.com/ado11231/ally/issues/109) | | 6 | A setting to use your own iroh relay, and stronger Linux Client support | | | 7 | Ready to run programs for macOS and Linux, on Intel and ARM | | -| 7 | Publishing to crates.io, AUR, and Homebrew | [#17](https://github.com/ado11231/ss/issues/17) | -| 7 | A one line installer, and `slingshot doctor` to check a setup | | +| 7 | Publishing to crates.io, AUR, and Homebrew | [#17](https://github.com/ado11231/ally/issues/17) | +| 7 | A one line installer, and `ally doctor` to check a setup | | -* Phase 7 is done when a newcomer goes from install to a working `slingshot run` in under five minutes, with no help. +* Phase 7 is done when a newcomer goes from install to a working `ally run` in under five minutes, with no help. ## Known Limitations * Starting by itself on a macOS Agent is untested. * After a power loss, the first iroh connection is untested. A crash is tested below. -* Edits in a session whose connection dropped stay on the Agent until `slingshot sync --pull`. +* Edits in a session whose connection dropped stay on the Agent until `ally sync --pull`. * The tools step installs Docker only with `pacman`, `apt`, `dnf`, and `zypper`, and Git, Node, and Python only with package managers it knows. * Untested: file watchers in sessions, two Clients on one Agent account at once, and large Node and Python projects. -* `slingshot.toml` supports only `sync.exclude`. +* `ally.toml` supports only `sync.exclude`. * Linking needs a shared network or tailnet. * The back online notification and resource warnings have not been seen on real machines. -* A dev server or container on the Agent cannot open in the Client's browser yet ([#9](https://github.com/ado11231/ss/issues/9)). +* A dev server or container on the Agent cannot open in the Client's browser yet ([#9](https://github.com/ado11231/ally/issues/9)). * The tools step on an account without `sudo` is checked only by unit tests. * Installing Ollama and OpenCode through the tools step is checked only by unit tests. On archbox, the step found both already installed. @@ -103,19 +103,19 @@ ### Phase 3: September 13, 2026, Same Network -1. The first `slingshot run cargo build` copied 47 files in 9 seconds. The second started in 1 second. +1. The first `ally run cargo build` copied 47 files in 9 seconds. The second started in 1 second. 2. Subfolders, exit codes, typing, and Ctrl C behaved as local. The copy held no `.git` or `target`. 3. Push, pull, preview, an Agent only edit, and a conflict worked. The conflict was refused and nothing changed. 4. Environment files were private to the Agent account, visible to `run`, and never synced. 5. A release build in `attach` kept running through four minutes offline, and `attach` returned to it. -6. Restarting `slingshot start` kept the session. Restarting the Agent marked it interrupted. +6. Restarting `ally start` kept the session. Restarting the Agent marked it interrupted. 7. `unlink` refused during a session, then removed the key and files after `stop`. ### Phase 4: September 22, 2026 1. `run` worked over the home network, the tailnet from a hotspot, and iroh with Tailscale off. 2. Over iroh, the first ssh connection took about 1.5 seconds, later ones about 0.3. -3. With `slingshot start` stopped, `run` failed after about 20 seconds, naming the cause. +3. With `ally start` stopped, `run` failed after about 20 seconds, naming the cause. 4. An unpaired Client was refused over iroh, and a paired one accepted. `sync` and `attach` worked over iroh. ### Phase 5: September 23 And 24, 2026 @@ -124,7 +124,7 @@ 2. Runs gave the right notifications, and none for a quick `echo hi`. 3. `attach` outside a project opened `~` with color, `UTF-8`, mouse scrolling, Claude Code, and `docker ps` working. In a project it synced first. 4. `cargo build` in a session ran on archbox while the Mac stayed quiet. `sync --pull` worked with the session open. -5. `slingshot menubar` built in 12 seconds, skipped the build when nothing changed, and rebuilt after a Swift change. +5. `ally menubar` built in 12 seconds, skipped the build when nothing changed, and rebuilt after a Swift change. ### Safe Pairing: September 25, 2026, Mac Only @@ -139,31 +139,31 @@ * A new account, `slingtest`, on archbox played a first time user. Both machines followed only the README. 1. `cargo install` needed `rustup default stable` and `~/.cargo/bin` on PATH, both from Arch Linux's `rustup` package. -2. `slingshot start` passed every check, found the GPU, and reached iroh. Pairing took 0.1 seconds. +2. `ally start` passed every check, found the GPU, and reached iroh. Pairing took 0.1 seconds. 3. Codex installed and signed in through the forwarded port. Claude Code installed into `~/.local/bin` but was reported missing; fixed the same day. 4. `cargo build --release` took 54 seconds on archbox against 3 minutes 20 on the Mac. The second run took 0.6 seconds. -5. Wi Fi off for 15 seconds during a run over iroh did not interrupt it. After restarting `slingshot start`, the Mac reconnected without linking again. +5. Wi Fi off for 15 seconds during a run over iroh did not interrupt it. After restarting `ally start`, the Mac reconnected without linking again. ### Menu Bar And Sign In: September 28, 2026 -1. The app stayed a login item, and switched to a changed `slingshot` within 3 to 9 seconds in four of five tries. Once it took 20 seconds, cause not found. -2. `slingshot menubar --remove` listed, asked, and removed the app. +1. The app stayed a login item, and switched to a changed `ally` within 3 to 9 seconds in four of five tries. Once it took 20 seconds, cause not found. +2. `ally menubar --remove` listed, asked, and removed the app. 3. `link` paired again and restarted the app. `health` showed the hardware heading above live numbers. -4. Closing the terminal running `slingshot start` had made the next iroh run take 22 seconds. After the fix, 1.4 to 1.6 seconds. -5. `slingshot tools` noticed Codex was signed out and offered to sign in. -6. A second `slingshot start` under another home moved to port 7434 and said so. +4. Closing the terminal running `ally start` had made the next iroh run take 22 seconds. After the fix, 1.4 to 1.6 seconds. +5. `ally tools` noticed Codex was signed out and offered to sign in. +6. A second `ally start` under another home moved to port 7434 and said so. ### Start Screen And Starting By Itself: October 6, 2026 -1. A new build beside an older running `slingshot start` refused with `A different version of Slingshot is already running for this account`. -2. With a linked Client, `slingshot start` showed the Client and no code. Enter printed a code. -3. The first `slingshot start` offered the service and asked once. After `sudo reboot` with nobody logged in, `run` worked 20 seconds after boot. Masked sleep kept archbox reachable. +1. A new build beside an older running `ally start` refused with `A different version of Ally is already running for this account`. +2. With a linked Client, `ally start` showed the Client and no code. Enter printed a code. +3. The first `ally start` offered the service and asked once. After `sudo reboot` with nobody logged in, `run` worked 20 seconds after boot. Masked sleep kept archbox reachable. 4. `--remove` and `--boot` removed and reinstalled the service. 5. Leaving `attach` pulled a file written on archbox. A conflict left each machine its own version. ### Fresh Install And Full Test: October 7, 2026 -* Both machines were cleaned and installed from the README. The Agent was a new account without `sudo`. Full results are on [#56](https://github.com/ado11231/ss/issues/56). +* Both machines were cleaned and installed from the README. The Agent was a new account without `sudo`. Full results are on [#56](https://github.com/ado11231/ally/issues/56). 1. The Mac install took 2m 43s. `link` paired, installed and signed in to the tools, and built the menu bar app in one flow. 2. A Rust release build took 2.6s on archbox and 7.3s on the Mac. `node_modules` and `dist` stayed on the Agent. `.env` never synced. @@ -182,14 +182,14 @@ ### Projects: October 15, 2026 -1. `slingshot projects` listed three projects on archbox with their sizes, last sync, and edits waiting. -2. `clean` on Slingshot itself freed 3.9 GiB, and the next `cargo build` made it again in 28 seconds. -3. A throwaway project showed 2 edits waiting. `remove` listed them, kept everything on Enter, and on `y` deleted the copy and its `~/Slingshot` link. +1. `ally projects` listed three projects on archbox with their sizes, last sync, and edits waiting. +2. `clean` on Ally itself freed 3.9 GiB, and the next `cargo build` made it again in 28 seconds. +3. A throwaway project showed 2 edits waiting. `remove` listed them, kept everything on Enter, and on `y` deleted the copy and its `~/Ally` link. ### Sleep Rule: October 16, 2026 -1. With sleep turned back on, `slingshot start --boot` showed the polkit rule, wrote it with `sudo`, and printed `This machine stays awake with nobody logged in`. -2. After `sudo reboot`, with only the login screen on archbox, Slingshot held the sleep lock from the first minute. +1. With sleep turned back on, `ally start --boot` showed the polkit rule, wrote it with `sudo`, and printed `This machine stays awake with nobody logged in`. +2. After `sudo reboot`, with only the login screen on archbox, Ally held the sleep lock from the first minute. 3. 15 minutes after boot, GNOME tried to suspend and was refused by the lock. archbox was still reachable after 30 minutes. ### Agent Crash Over iroh: October 16, 2026 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index d4060fa..71389a0 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -14,7 +14,7 @@ 8. [Jobs And GPU](#jobs-and-gpu) 9. [Projects](#projects) 10. [The Menu Bar App](#the-menu-bar-app) -11. [Removing Slingshot](#removing-slingshot) +11. [Removing Ally](#removing-ally) 12. [Getting Help](#getting-help) ## Starting The Agent @@ -22,44 +22,44 @@ | Message | Meaning | Fix | | --- | --- | --- | | `SSH server not running` | All work goes over ssh, so the Agent needs an ssh server. The Client does not. | macOS: turn on System Settings, General, Sharing, Remote Login. Linux: `sudo systemctl enable --now sshd`. | -| `Tool not installed: rsync` or `Tool not installed: tmux` | The Agent needs `rsync` to copy files and `tmux` to keep sessions running. | Run the install command printed, then `slingshot start` again. | +| `Tool not installed: rsync` or `Tool not installed: tmux` | The Agent needs `rsync` to copy files and `tmux` to keep sessions running. | Run the install command printed, then `ally start` again. | | `Docker is installed, but this account cannot use it` | A warning. The account is not in the `docker` group, so Docker commands fail with "permission denied". | Run the command printed, such as `sudo usermod -aG docker ado`, then restart the machine. Running sessions and the service keep their old groups until then. | -| `Another slingshot start is already running for this account` | Two `slingshot start` commands began at once. | Run `slingshot start` again. It shows the one running. | -| `A different version of Slingshot is already running for this account` | The running `slingshot start` is from another version, usually from before an update. | Ctrl C where it runs, then `slingshot start` again. | -| `Slingshot is running for this account but did not answer` | Another `slingshot start` runs here but did not reply. | Ctrl C the other one, then `slingshot start` again. | -| `Slingshot is already running here as archbox, not ` | `--name` differs from the running Agent's name, which linked Clients use. | Run `slingshot start` without `--name`. | -| `! Port 7433 is in use, perhaps by another slingshot start, so this one uses 7434` | Another program or account has the pairing port, so the next free one is used. | Nothing. The printed link line has the new port. | -| `Ports 7433 to 7442 are all in use` | All ten ports tried were taken. | Pick another with `slingshot start --port `. | -| `Could not stop this machine from sleeping` | A warning. A sleeping Agent cannot be reached. On Linux, a boot service with nobody logged in needs the polkit rule that `slingshot start --boot` writes. | Run `slingshot start --boot` in a terminal. Or turn sleep off: `sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target`. | -| `No iroh relay answered` | A warning. Other networks cannot reach the Agent yet. The same network still works, and Slingshot keeps trying. | Check the Agent's internet connection. | -| No message: unreachable after a restart | Slingshot does not start by itself on this Agent. | Run `slingshot start --boot` on the Agent. | +| `Another ally start is already running for this account` | Two `ally start` commands began at once. | Run `ally start` again. It shows the one running. | +| `A different version of Ally is already running for this account` | The running `ally start` is from another version, usually from before an update. | Ctrl C where it runs, then `ally start` again. | +| `Ally is running for this account but did not answer` | Another `ally start` runs here but did not reply. | Ctrl C the other one, then `ally start` again. | +| `Ally is already running here as archbox, not ` | `--name` differs from the running Agent's name, which linked Clients use. | Run `ally start` without `--name`. | +| `! Port 7433 is in use, perhaps by another ally start, so this one uses 7434` | Another program or account has the pairing port, so the next free one is used. | Nothing. The printed link line has the new port. | +| `Ports 7433 to 7442 are all in use` | All ten ports tried were taken. | Pick another with `ally start --port `. | +| `Could not stop this machine from sleeping` | A warning. A sleeping Agent cannot be reached. On Linux, a boot service with nobody logged in needs the polkit rule that `ally start --boot` writes. | Run `ally start --boot` in a terminal. Or turn sleep off: `sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target`. | +| `No iroh relay answered` | A warning. Other networks cannot reach the Agent yet. The same network still works, and Ally keeps trying. | Check the Agent's internet connection. | +| No message: unreachable after a restart | Ally does not start by itself on this Agent. | Run `ally start --boot` on the Agent. | ## Starting By Itself -* Service logs: `journalctl --user -u slingshot.service` on Linux, `~/Library/Application Support/slingshot/agent/daemon.log` on macOS. +* Service logs: `journalctl --user -u ally.service` on Linux, `~/Library/Application Support/ally/agent/daemon.log` on macOS. | Message | Meaning | Fix | | --- | --- | --- | -| `Slingshot is set to start by itself but is not running` | The service exists but stopped or failed. | Read the logs, then run `slingshot start --remove` and `slingshot start --boot`. | -| `Slingshot was set up to start at boot but did not start` | The service did not answer within 15 seconds. | Fix what the logs report, then `slingshot start --boot`. | -| `systemctl --user enable --now slingshot.service failed: `, or the same for `launchctl` | The service manager refused a step. | Fix the reason given, then `slingshot start --boot`. | -| `! Slingshot starts when you log in, not at boot` | Linux refused linger without an admin password. | Run the `sudo loginctl enable-linger ` command printed. | -| `Could not write /etc/polkit-1/rules.d/50-slingshot.rules. This machine may sleep with nobody logged in` | `sudo` was refused, or there was no terminal to type the password. | Run `slingshot start --boot` in a terminal. Without `sudo`, ask an admin, or turn sleep off. | -| `Could not delete /etc/polkit-1/rules.d/50-slingshot.rules` | `sudo` was refused while removing. | Run the `sudo rm` command printed. | -| `Slingshot is already running for this account. Stop the other slingshot start, then run slingshot start --boot again` | A `slingshot start` in another terminal holds the Agent. | Ctrl C there, then `slingshot start --boot`. | -| `Run slingshot start --remove in a terminal to confirm` | Removing asks first, and there was no terminal. | Run it in a terminal on the Agent. | -| `Starting at boot works on Linux and macOS`, or `A line break cannot go in a systemd unit` | No supported service manager, or the Agent name or program path has a line break. | Run `slingshot start` by hand, or rename the Agent with `--name`. | +| `Ally is set to start by itself but is not running` | The service exists but stopped or failed. | Read the logs, then run `ally start --remove` and `ally start --boot`. | +| `Ally was set up to start at boot but did not start` | The service did not answer within 15 seconds. | Fix what the logs report, then `ally start --boot`. | +| `systemctl --user enable --now ally.service failed: `, or the same for `launchctl` | The service manager refused a step. | Fix the reason given, then `ally start --boot`. | +| `! Ally starts when you log in, not at boot` | Linux refused linger without an admin password. | Run the `sudo loginctl enable-linger ` command printed. | +| `Could not write /etc/polkit-1/rules.d/50-ally.rules. This machine may sleep with nobody logged in` | `sudo` was refused, or there was no terminal to type the password. | Run `ally start --boot` in a terminal. Without `sudo`, ask an admin, or turn sleep off. | +| `Could not delete /etc/polkit-1/rules.d/50-ally.rules` | `sudo` was refused while removing. | Run the `sudo rm` command printed. | +| `Ally is already running for this account. Stop the other ally start, then run ally start --boot again` | An `ally start` in another terminal holds the Agent. | Ctrl C there, then `ally start --boot`. | +| `Run ally start --remove in a terminal to confirm` | Removing asks first, and there was no terminal. | Run it in a terminal on the Agent. | +| `Starting at boot works on Linux and macOS`, or `A line break cannot go in a systemd unit` | No supported service manager, or the Agent name or program path has a line break. | Run `ally start` by hand, or rename the Agent with `--name`. | ## Linking | Message | Meaning | Fix | | --- | --- | --- | -| `Could not reach 192.168.1.9:7433`, or `Could not reach the Agent at 192.168.1.9:7433` | The Client could not contact the Agent. | Check `slingshot start` runs on the Agent, and both machines share a network or tailnet. Linking across networks is not supported yet. | -| `That pairing code has expired or was already used` | A code works once, lasts 10 minutes, and is burned after 3 wrong tries. | Press Enter where `slingshot start` runs for a new code. | +| `Could not reach 192.168.1.9:7433`, or `Could not reach the Agent at 192.168.1.9:7433` | The Client could not contact the Agent. | Check `ally start` runs on the Agent, and both machines share a network or tailnet. Linking across networks is not supported yet. | +| `That pairing code has expired or was already used` | A code works once, lasts 10 minutes, and is burned after 3 wrong tries. | Press Enter where `ally start` runs for a new code. | | `That pairing code is not right` | The code does not match the Agent's. | Copy it again, exactly as printed. | | `Wrong pairing code from 192.168.1.40 (1 of 3)`, or `3 wrong pairing codes from 192.168.1.40. The code no longer works` | A machine at that address tried a wrong code. If not you, someone on your network tried to pair. | Press Enter for a new code. Pair only with machines you know. | | `Could not confirm that 192.168.1.9 is the Agent that printed this code` | The machine that answered could not prove it knows the code. Usually a typo; rarely another machine answering in the Agent's place. | Check the code and try again. If it keeps failing on a trusted network, open an issue. | -| `The Agent at 192.168.1.9 runs an older Slingshot`, or `This Agent needs a newer Slingshot` | Pairing changed, so both machines need the same version to link. Linked machines keep working. | Update the machine named, restart `slingshot start` if it is the Agent, then link again. | +| `The Agent at 192.168.1.9 runs an older Ally`, or `This Agent needs a newer Ally` | Pairing changed, so both machines need the same version to link. Linked machines keep working. | Update the machine named, restart `ally start` if it is the Agent, then link again. | | `Another machine is pairing right now` | The Agent pairs with one machine at a time. | Wait a few seconds and link again. | | `That does not look like a pairing code` | A code has three parts separated by colons, such as `192.168.1.9:7433:K7QW9ZR2`. | Copy the whole code. | @@ -67,98 +67,98 @@ | Message | Meaning | Fix | | --- | --- | --- | -| `Could not set up tools on archbox` | Linking worked. Only the tools step failed, for the reason after the message. | Fix it, then run `slingshot tools`. | -| `No install command for brew. Install it on the Agent yourself`, or `No package manager found` | Slingshot only runs install commands it trusts on that Agent. | Install the tool by its own instructions, such as inside `slingshot attach`. | -| `Docker is still missing. Check the output above, then run slingshot tools again` | An install failed, or the tool landed outside the login PATH, `~/.local/bin`, and `~/.cargo/bin`. | Fix the install error, or add the tool's folder to PATH in the Agent's `~/.profile` or `~/.bash_profile`. Then `slingshot tools` again. | -| `Sign in to Codex did not finish` | The sign in was cancelled or failed. The tool is still installed. | Run the command in the message inside `slingshot attach`. For Codex, keep port 1455 free on the Client. | -| `permission denied while trying to connect to the Docker daemon socket` | The account is not in the `docker` group, or was added after sessions and the service started. | `sudo usermod -aG docker ` if needed, then restart the machine. `slingshot run` sees it at once, sessions after the restart. | +| `Could not set up tools on archbox` | Linking worked. Only the tools step failed, for the reason after the message. | Fix it, then run `ally tools`. | +| `No install command for brew. Install it on the Agent yourself`, or `No package manager found` | Ally only runs install commands it trusts on that Agent. | Install the tool by its own instructions, such as inside `ally attach`. | +| `Docker is still missing. Check the output above, then run ally tools again` | An install failed, or the tool landed outside the login PATH, `~/.local/bin`, and `~/.cargo/bin`. | Fix the install error, or add the tool's folder to PATH in the Agent's `~/.profile` or `~/.bash_profile`. Then `ally tools` again. | +| `Sign in to Codex did not finish` | The sign in was cancelled or failed. The tool is still installed. | Run the command in the message inside `ally attach`. For Codex, keep port 1455 free on the Client. | +| `permission denied while trying to connect to the Docker daemon socket` | The account is not in the `docker` group, or was added after sessions and the service started. | `sudo usermod -aG docker ` if needed, then restart the machine. `ally run` sees it at once, sessions after the restart. | ## Reaching The Agent -* Slingshot tries the local network, then the tailnet, then iroh. The message says why the last try failed. +* Ally tries the local network, then the tailnet, then iroh. The message says why the last try failed. | Message | Meaning | Fix | | --- | --- | --- | -| `archbox is not reachable. It may be off or asleep, or slingshot start is not running there` | Nothing answered on any path. | Make sure the Agent is on and awake, and Slingshot runs there. | -| `Slingshot is not running on the Agent. Run slingshot start there` | The Agent is on, but Slingshot stopped. | Run `slingshot start` on the Agent. | +| `archbox is not reachable. It may be off or asleep, or ally start is not running there` | Nothing answered on any path. | Make sure the Agent is on and awake, and Ally runs there. | +| `Ally is not running on the Agent. Run ally start there` | The Agent is on, but Ally stopped. | Run `ally start` on the Agent. | | `archbox no longer accepts this machine` | The Agent no longer knows this Client, usually after an unlink or reset. | Link again from the Agent's network. | | `Could not reach archbox, because this machine cannot reach iroh's relays` | The Client has no working internet. | Check this machine's connection. | -| `archbox runs an older Slingshot 0.1.0 than this machine (0.2.0)`, or `This machine runs an older Slingshot (0.1.0) than archbox 0.2.0` | The versions cannot talk. The message names the older one. | Run `cargo install --locked --git https://github.com/ado11231/ss slingshot-cli` on it, and restart `slingshot start` if it is the Agent. | -| `! archbox runs Slingshot 0.1.0 and this machine runs 0.2.0` | They still work together, but the older one lacks fixes. | Update the older machine the same way. | -| `Slingshot was not found on the Agent` | The Agent's `slingshot` program was moved or removed. | Install it on the Agent again, then link again. | +| `archbox runs an older Ally 0.1.0 than this machine (0.2.0)`, or `This machine runs an older Ally (0.1.0) than archbox 0.2.0` | The versions cannot talk. The message names the older one. | Run `cargo install --locked --git https://github.com/ado11231/ally ally-cli` on it, and restart `ally start` if it is the Agent. | +| `! archbox runs Ally 0.1.0 and this machine runs 0.2.0` | They still work together, but the older one lacks fixes. | Update the older machine the same way. | +| `Ally was not found on the Agent` | The Agent's `ally` program was moved or removed. | Install it on the Agent again, then link again. | | `Host key verification failed` | The Agent's ssh identity changed, usually after a reinstall. | Link again. | -| `No Agent configured yet` | This machine is not linked. | Run `slingshot start` on the Agent, then `slingshot link ` here. | +| `No Agent configured yet` | This machine is not linked. | Run `ally start` on the Agent, then `ally link ` here. | | `Several Agents configured` | More than one Agent is linked, and none is the default. | Add `--agent `, or set `default = ""` in the config file named. | ## Running Commands | Message | Meaning | Fix | | --- | --- | --- | -| `No project found` | `sync` and `env` need a project. `attach` works anywhere. | Move into a Git repository, or a folder with `Cargo.toml`, `package.json`, `pyproject.toml`, `requirements.txt`, or `slingshot.toml`. | -| `Lost connection to archbox`, then `The Agent stops the run once it notices` | A run stops when its connection drops. | `slingshot ps --all` shows it as `Lost connection`. Use `slingshot attach` for long work. | -| `cargo was not found on the Agent. Check the name, or install it there`, exit 127 | The program is not on the Agent, or the name has a typo. | Check the name, or run `slingshot tools` to install it. | +| `No project found` | `sync` and `env` need a project. `attach` works anywhere. | Move into a Git repository, or a folder with `Cargo.toml`, `package.json`, `pyproject.toml`, `requirements.txt`, or `ally.toml`. | +| `Lost connection to archbox`, then `The Agent stops the run once it notices` | A run stops when its connection drops. | `ally ps --all` shows it as `Lost connection`. Use `ally attach` for long work. | +| `cargo was not found on the Agent. Check the name, or install it there`, exit 127 | The program is not on the Agent, or the name has a typo. | Check the name, or run `ally tools` to install it. | | `./build.sh is not allowed to run on the Agent. Check that it is executable`, exit 126 | The file exists but is not executable. | `chmod +x build.sh` on the Client, then run again. The sync copies the change. | -| `command not found` in a session, for a tool the Client has | Sessions use only tools on the Agent. | Run `slingshot tools`. | -| `Lost connection to archbox. The session keeps running there` | The session still runs on the Agent. | Run `slingshot attach` to return. | -| `! Did not bring back edits: NOTES.md changed on both this machine and archbox` | The same files changed in both places. Both versions are kept. | Follow the conflict row under [Syncing Files](#syncing-files), then `slingshot sync --pull`. | +| `command not found` in a session, for a tool the Client has | Sessions use only tools on the Agent. | Run `ally tools`. | +| `Lost connection to archbox. The session keeps running there` | The session still runs on the Agent. | Run `ally attach` to return. | +| `! Did not bring back edits: NOTES.md changed on both this machine and archbox` | The same files changed in both places. Both versions are kept. | Follow the conflict row under [Syncing Files](#syncing-files), then `ally sync --pull`. | | `RAM is 92% used` or `Only 1.2 GiB free on the Agent workspace disk` | A warning. The run still starts, but may be slow or fail. | Free memory or disk on the Agent. | | `No swap configured` | No swap, the disk space used when RAM runs out. A large job may be stopped when memory fills. | Add swap on the Agent. | -| `Live views need an interactive terminal` | `slingshot health --watch` redraws the screen. | Run it in a terminal, or use `slingshot health` to pipe the output. | +| `Live views need an interactive terminal` | `ally health --watch` redraws the screen. | Run it in a terminal, or use `ally health` to pipe the output. | ## Syncing Files | Message | Meaning | Fix | | --- | --- | --- | -| `NOTES.md changed on both this machine and archbox`, or `3 files changed on both this machine and archbox` | A conflict: the same files changed differently on both machines. Nothing was copied. | Compare with `slingshot sync --check` and `slingshot sync --pull --check`. Keep one side with `slingshot sync --overwrite` or `slingshot sync --pull --overwrite`, which back up the replaced files. Or make the files match by hand and sync again. | +| `NOTES.md changed on both this machine and archbox`, or `3 files changed on both this machine and archbox` | A conflict: the same files changed differently on both machines. Nothing was copied. | Compare with `ally sync --check` and `ally sync --pull --check`. Keep one side with `ally sync --overwrite` or `ally sync --pull --overwrite`, which back up the replaced files. Or make the files match by hand and sync again. | | `NOTES.md and notes.md differ only in case` | This machine's disk treats both names as one, as a Mac does, so pulling both would overwrite one. Nothing changed. | Rename or remove one on the Agent, then sync again. | -| `An interrupted sync of app needs recovery. Run slingshot sync to recover it` | A sync stopped partway, such as when the connection dropped. | Run `slingshot sync`. It finishes or undoes the earlier one. | +| `An interrupted sync of app needs recovery. Run ally sync to recover it` | A sync stopped partway, such as when the connection dropped. | Run `ally sync`. It finishes or undoes the earlier one. | | `Could not recover an interrupted sync. Backups are in ...` | The sync could not be finished or undone. | Make each listed file match its backup, or delete it, then sync again. | -| `This project has no copy on the Agent yet. Run slingshot sync first` | A pull needs a copy on the Agent. | Run `slingshot sync` once. | +| `This project has no copy on the Agent yet. Run ally sync first` | A pull needs a copy on the Agent. | Run `ally sync` once. | | `Unsupported source path` or `Unsafe source link` | A name or link cannot be copied safely, such as a link outside the project. | Remove it, or add it to `.gitignore`. | | `Message is too large. The project may have too many files` | The file list is over the limit. | Leave out large generated or data folders with `.gitignore` or `sync.exclude`. | -| `sync.exclude in slingshot.toml cannot contain negated patterns` | Patterns starting with `!` are not allowed. | Remove them. | +| `sync.exclude in ally.toml cannot contain negated patterns` | Patterns starting with `!` are not allowed. | Remove them. | ## Jobs And GPU | Message | Meaning | Fix | | --- | --- | --- | -| `No running Slingshot job matches abc123` | No running job has that ID. Finished jobs cannot be stopped. | Copy the ID from `slingshot ps`. | -| `GPU reporting unavailable, because nvidia-smi is not installed` | A warning. GPU use is read with `nvidia-smi`, which comes with the NVIDIA driver. | Nothing without an NVIDIA GPU. With one, install the driver and `slingshot start` again. | +| `No running Ally job matches abc123` | No running job has that ID. Finished jobs cannot be stopped. | Copy the ID from `ally ps`. | +| `GPU reporting unavailable, because nvidia-smi is not installed` | A warning. GPU use is read with `nvidia-smi`, which comes with the NVIDIA driver. | Nothing without an NVIDIA GPU. With one, install the driver and `ally start` again. | | `NVIDIA driver and library versions differ` | The driver was updated, but the old one is still loaded. | Restart the Agent. | -| `No GPU data available` | No NVIDIA card, or no `nvidia-smi`. | Nothing. Slingshot works without a GPU. | +| `No GPU data available` | No NVIDIA card, or no `nvidia-smi`. | Nothing. Ally works without a GPU. | ## Projects | Message | Meaning | Fix | | --- | --- | --- | -| `No project named web on archbox` | No copy on the Agent has that name. | Check the name with `slingshot projects`. | +| `No project named web on archbox` | No copy on the Agent has that name. | Check the name with `ally projects`. | | `2 projects are named app on archbox. Use the ID of the one you mean` | Two Clients each have a project with this name. | Run the command again with the ID from the message. | -| `app has no copy on archbox` | The current project was never synced to this Agent. | Nothing to clean or remove. `slingshot sync` copies it. | -| `app has a run in progress`, or `a session in progress` | The project is in use. | Stop it with the `slingshot stop` command in the message, or wait. | -| `Run slingshot projects remove in a terminal to confirm, since it deletes files` | It asks first, and a script cannot answer. | Run it in a terminal. | +| `app has no copy on archbox` | The current project was never synced to this Agent. | Nothing to clean or remove. `ally sync` copies it. | +| `app has a run in progress`, or `a session in progress` | The project is in use. | Stop it with the `ally stop` command in the message, or wait. | +| `Run ally projects remove in a terminal to confirm, since it deletes files` | It asks first, and a script cannot answer. | Run it in a terminal. | ## The Menu Bar App | Message | Meaning | Fix | | --- | --- | --- | -| `Swift is needed to build the menu bar app` | The app builds with Swift, from the Xcode command line tools. | Run `xcode-select --install`, then `slingshot menubar` again. | -| `Could not build the menu bar app. Swift said:`, then Swift's last lines | The build failed. The app needs macOS 14 or later. | Update the Xcode command line tools in System Settings, General, Software Update, and run `slingshot menubar` again. If it still fails, open an issue with the full message. | -| Panel: `Slingshot speaks format , and this app speaks format ` | The app is older than the `slingshot` program. | Run `slingshot menubar` to rebuild it. | -| Panel: `Can't find the slingshot program` | The saved path to `slingshot` was moved, deleted, or never saved. | Run `slingshot menubar`, and again after moving `slingshot`. | -| `! The app will run , which cargo clean deletes` | `slingshot menubar` ran from a build in a cargo `target` folder. | Run `cargo install --path crates/slingshot-cli`, then `slingshot menubar` again. | -| `Could not take the app out of login items` | `--remove` could not start the app to leave login items. | Turn off Slingshot in System Settings, General, Login Items, then `slingshot menubar --remove` again. | +| `Swift is needed to build the menu bar app` | The app builds with Swift, from the Xcode command line tools. | Run `xcode-select --install`, then `ally menubar` again. | +| `Could not build the menu bar app. Swift said:`, then Swift's last lines | The build failed. The app needs macOS 14 or later. | Update the Xcode command line tools in System Settings, General, Software Update, and run `ally menubar` again. If it still fails, open an issue with the full message. | +| Panel: `Ally speaks format , and this app speaks format ` | The app is older than the `ally` program. | Run `ally menubar` to rebuild it. | +| Panel: `Can't find the ally program` | The saved path to `ally` was moved, deleted, or never saved. | Run `ally menubar`, and again after moving `ally`. | +| `! The app will run , which cargo clean deletes` | `ally menubar` ran from a build in a cargo `target` folder. | Run `cargo install --path crates/ally-cli`, then `ally menubar` again. | +| `Could not take the app out of login items` | `--remove` could not start the app to leave login items. | Turn off Ally in System Settings, General, Login Items, then `ally menubar --remove` again. | | No message: shown as offline | The app cannot reach the Agent. | Open the panel for the cause and fix. It updates by itself, or press Try again. | -| No message: no notifications | Notifications are off, or nothing needed one. Runs under 10 seconds never notify, and each warning is sent once until it clears. | Turn them on in System Settings, Notifications, Slingshot. | +| No message: no notifications | Notifications are off, or nothing needed one. Runs under 10 seconds never notify, and each warning is sent once until it clears. | Turn them on in System Settings, Notifications, Ally. | -## Removing Slingshot +## Removing Ally | Message | Meaning | Fix | | --- | --- | --- | -| `Could not reach archbox, so the key is still there` | `slingshot unlink` could not reach the Agent. | On the Agent, delete the line in `~/.ssh/authorized_keys` ending with the name in the message. | -| `Nothing else was removed`, or unlink refuses | A job is still running. | Stop each with `slingshot stop `, then try again. | -| `Slingshot is running in a terminal on this machine. Stop it with Ctrl C there` | A `slingshot start` by hand would recreate files as they are removed. | Ctrl C where it runs, then `slingshot uninstall` again. | -| `Run slingshot uninstall in a terminal to confirm, since it deletes files` | It asks first, and a script cannot answer. | Run it in a terminal. | -| `Could not remove ... Delete it by hand` | A file could not be deleted, such as from its permissions. | Delete it yourself, then `slingshot uninstall` again. | +| `Could not reach archbox, so the key is still there` | `ally unlink` could not reach the Agent. | On the Agent, delete the line in `~/.ssh/authorized_keys` ending with the name in the message. | +| `Nothing else was removed`, or unlink refuses | A job is still running. | Stop each with `ally stop `, then try again. | +| `Ally is running in a terminal on this machine. Stop it with Ctrl C there` | An `ally start` by hand would recreate files as they are removed. | Ctrl C where it runs, then `ally uninstall` again. | +| `Run ally uninstall in a terminal to confirm, since it deletes files` | It asks first, and a script cannot answer. | Run it in a terminal. | +| `Could not remove ... Delete it by hand` | A file could not be deleted, such as from its permissions. | Delete it yourself, then `ally uninstall` again. | ## Getting Help diff --git a/docs/USAGE.md b/docs/USAGE.md index c8998c3..3911a5a 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -16,8 +16,8 @@ 10. [The Menu Bar App](#the-menu-bar-app) 11. [More Than One Agent](#more-than-one-agent) 12. [Project Settings](#project-settings) -13. [What Slingshot Copies](#what-slingshot-copies) -14. [Remove Slingshot](#remove-slingshot) +13. [What Ally Copies](#what-ally-copies) +14. [Remove Ally](#remove-ally) ## Words Used Here @@ -25,22 +25,21 @@ | --- | --- | | **Client** | The machine you work on. | | **Agent** | The powerful machine that does the work. | -| **Project** | A folder in a Git repository, or with a `Cargo.toml`, `package.json`, `pyproject.toml`, `requirements.txt`, or `slingshot.toml`. | +| **Project** | A folder in a Git repository, or with a `Cargo.toml`, `package.json`, `pyproject.toml`, `requirements.txt`, or `ally.toml`. | | **Job** | One run or session on the Agent. | ## Install * Steps for each system are in the [README](../README.md#setup). * To update, run the install command again on both machines. Both must run the same version. -* If the Agent starts by itself, restart it after updating: `systemctl --user restart slingshot.service` on Linux, or log out and in on macOS. -* `sling` is a short name for `slingshot`. +* If the Agent starts by itself, restart it after updating: `systemctl --user restart ally.service` on Linux, or log out and in on macOS. ## Set Up ### Start The Agent ```sh -slingshot start +ally start ``` * Checks the machine, then prints a pairing code and keeps the machine awake. @@ -60,18 +59,18 @@ slingshot start | Agent | File | Starts | Logs | | --- | --- | --- | --- | -| Linux | `~/.config/systemd/user/slingshot.service`, plus `loginctl enable-linger` | At boot | `journalctl --user -u slingshot.service` | -| macOS | `~/Library/LaunchAgents/dev.slingshot.agent.plist` | At login | `~/Library/Application Support/slingshot/agent/daemon.log` | +| Linux | `~/.config/systemd/user/ally.service`, plus `loginctl enable-linger` | At boot | `journalctl --user -u ally.service` | +| macOS | `~/Library/LaunchAgents/dev.ally.agent.plist` | At login | `~/Library/Application Support/ally/agent/daemon.log` | -* The service prints no code. Run `slingshot start` and press Enter for one. Ctrl C leaves the service running. +* The service prints no code. Run `ally start` and press Enter for one. Ctrl C leaves the service running. * The service keeps the PATH of the terminal it was set up from. -* If linger is refused, it starts at login, and Slingshot prints the `sudo` fix. -* On a Linux Agent that can sleep, it also writes `/etc/polkit-1/rules.d/50-slingshot.rules` with `sudo`. The rule lets your account keep the machine awake with nobody logged in. `--remove` deletes it. +* If linger is refused, it starts at login, and Ally prints the `sudo` fix. +* On a Linux Agent that can sleep, it also writes `/etc/polkit-1/rules.d/50-ally.rules` with `sudo`. The rule lets your account keep the machine awake with nobody logged in. `--remove` deletes it. ### Link The Client ```sh -slingshot link 192.168.1.9:7433:K7QW9ZR2 +ally link 192.168.1.9:7433:K7QW9ZR2 ``` * Both machines need the same network or tailnet for this step only. A tailnet is a private network made by a VPN such as Tailscale. @@ -81,7 +80,7 @@ slingshot link 192.168.1.9:7433:K7QW9ZR2 ### Set Up Tools On The Agent ```sh -slingshot tools +ally tools ``` * `link` runs this at the end. It offers each tool the Client has and the Agent lacks, plus what the project needs. @@ -98,9 +97,9 @@ slingshot tools * Lists every command, then asks once. `sudo` commands run in your terminal so you can type your password. * Then signs in to Claude Code and Codex. Accounts are never copied from the Client. -* For Codex, open the printed link on the Client. Slingshot forwards port 1455 for it. +* For Codex, open the printed link on the Client. Ally forwards port 1455 for it. * Without `sudo`, it installs what it can and lists the rest under "Ask an admin to run". -* `~/.local/bin`, `~/.cargo/bin`, and `~/.opencode/bin` are on the PATH of everything Slingshot starts. +* `~/.local/bin`, `~/.cargo/bin`, and `~/.opencode/bin` are on the PATH of everything Ally starts. * Versions are not matched. `.nvmrc` or `rust-toolchain.toml` still decide them. ## Run Work @@ -108,27 +107,27 @@ slingshot tools ### Run A Single Command ```sh -slingshot run cargo build +ally run cargo build ``` -* Copies changed files, then runs the command in the same folder of `~/Slingshot/` on the Agent. +* Copies changed files, then runs the command in the same folder of `~/Ally/` on the Agent. * The first line says where it runs: `▶ Running on archbox via local network · app · target → Agent disk`. * Typing, colors, and Ctrl C work as usual. The last line shows time and exit code. -* Slingshot's options go before `run`: `slingshot --agent archbox run cargo test`. +* Ally's options go before `run`: `ally --agent archbox run cargo test`. * Outside a project, nothing is copied and it runs in the Agent's home folder. * A run stops if the connection drops. Use a session for long work. ### Open A Session ```sh -slingshot attach [path] +ally attach [path] ``` -* In a project, opens a session in `~/Slingshot/`. Anywhere else, in the Agent's home folder. +* In a project, opens a session in `~/Ally/`. Anywhere else, in the Agent's home folder. * Every `attach` in a project copies your edits first. Leaving brings the Agent's edits back. * A conflict is a warning, and nothing is overwritten. -* A lost connection brings nothing back. Run `slingshot sync --pull` when done. -* The session keeps running when you leave. Run `slingshot attach` to return. +* A lost connection brings nothing back. Run `ally sync --pull` when done. +* The session keeps running when you leave. Run `ally attach` to return. | Inside A Session | How | | --- | --- | @@ -136,7 +135,7 @@ slingshot attach [path] | Copy | Drag to select. | | Clear all output | Ctrl B, then K | | Leave it running | Ctrl B, then D | -| End it | `exit`, or `slingshot stop ` | +| End it | `exit`, or `ally stop ` | ## Keep Files In Step @@ -144,8 +143,8 @@ slingshot attach [path] | Command | Effect | | --- | --- | -| `slingshot sync` | Copies your changes to the Agent. | -| `slingshot sync --pull` | Copies the Agent's changes back. | +| `ally sync` | Copies your changes to the Agent. | +| `ally sync --pull` | Copies the Agent's changes back. | | `--check` | Shows what would change. | | `--overwrite` | Keeps this side in a conflict, with a backup, after asking. | @@ -158,9 +157,9 @@ slingshot attach [path] * `.env` files are never synced. Add them on purpose. They are stored outside the project copy. ```sh -slingshot env add .env -slingshot env list -slingshot env remove .env +ally env add .env +ally env list +ally env remove .env ``` | Option For `env add` | Effect | @@ -174,16 +173,16 @@ slingshot env remove .env | Command | Shows | | --- | --- | -| `slingshot health` | System, CPU, RAM, GPU, VRAM, disk, and project space. | -| `slingshot health --watch` | The same every two seconds, with jobs. Q leaves. | +| `ally health` | System, CPU, RAM, GPU, VRAM, disk, and project space. | +| `ally health --watch` | The same every two seconds, with jobs. Q leaves. | ## Manage Jobs | Command | Effect | | --- | --- | -| `slingshot ps` | Lists running jobs. | -| `slingshot ps --all` | Adds the last 100 finished jobs. | -| `slingshot stop ` | Stops a job, forced after five seconds. A short ID works. | +| `ally ps` | Lists running jobs. | +| `ally ps --all` | Adds the last 100 finished jobs. | +| `ally stop ` | Stops a job, forced after five seconds. A short ID works. | * Finished runs show `Completed`, `Failed`, `Stopped`, `Interrupted`, or `Lost connection`. @@ -191,13 +190,13 @@ slingshot env remove .env | Command | Effect | | --- | --- | -| `slingshot projects` | Lists each project on the Agent: its Client, copy size, build output size, last sync, and edits waiting. | -| `slingshot projects clean [project]` | Removes the project's build output, including a Python `.venv`. The next build or install makes it again. | -| `slingshot projects remove [project]` | Deletes the project from the Agent, after listing it and asking. Default is No. | +| `ally projects` | Lists each project on the Agent: its Client, copy size, build output size, last sync, and edits waiting. | +| `ally projects clean [project]` | Removes the project's build output, including a Python `.venv`. The next build or install makes it again. | +| `ally projects remove [project]` | Deletes the project from the Agent, after listing it and asking. Default is No. | * Name a project as the list shows it, or leave it out for the current project. * If two Clients have a project with the same name, use its ID from the error message. -* Edits waiting are files changed on the Agent that `slingshot sync --pull` has not brought back. Pull them before `remove` to keep them. +* Edits waiting are files changed on the Agent that `ally sync --pull` has not brought back. Pull them before `remove` to keep them. * `clean` and `remove` wait until the project has no run or session. * The project on the Client is never touched. @@ -205,28 +204,28 @@ slingshot env remove .env | Command | Effect | | --- | --- | -| `slingshot menubar` | Builds the app into `~/Applications`. Needs the Xcode command line tools. | -| `slingshot menubar --remove` | Lists, asks, then deletes the app and its settings. | +| `ally menubar` | Builds the app into `~/Applications`. Needs the Xcode command line tools. | +| `ally menubar --remove` | Lists, asks, then deletes the app and its settings. | * Shows the Agent, connection, CPU, RAM, GPU, VRAM, and project space. * When the Agent is unreachable, shows why and the fix. * Notifies when a run of 10 seconds or more ends, a job is cut off by a lost connection or restart, the Agent goes offline or returns, or memory, disk, or GPU heat needs attention. -* No notifications? Turn them on in System Settings, Notifications, Slingshot. +* No notifications? Turn them on in System Settings, Notifications, Ally. ## More Than One Agent -* With one Agent, it is the default. With several, pass `--agent archbox`, or set `default = "archbox"` in Slingshot's `config.toml`. +* With one Agent, it is the default. With several, pass `--agent archbox`, or set `default = "archbox"` in Ally's `config.toml`. ## Project Settings -* Optional `slingshot.toml` in the project's top folder. Patterns work like `.gitignore`, and cannot start with `!`. +* Optional `ally.toml` in the project's top folder. Patterns work like `.gitignore`, and cannot start with `!`. ```toml [sync] exclude = ["data/", "*.mp4"] ``` -## What Slingshot Copies +## What Ally Copies * Source files only. Never `.gitignore` matches, `.git`, build output, environment files, or `sync.exclude` patterns. @@ -236,15 +235,15 @@ exclude = ["data/", "*.mp4"] | Node | `node_modules` links to Agent storage. | | Python | `.venv` stays in the Agent's copy. | -## Remove Slingshot +## Remove Ally | Command | Run On | Effect | | --- | --- | --- | -| `slingshot unlink` | Client | Removes this Client's key and environment files from the Agent. | -| `slingshot uninstall` | Either | Lists everything Slingshot added, asks once, then removes it. Default is No. | -| `cargo uninstall slingshot-cli` | Either | Removes the program, after `uninstall`. | +| `ally unlink` | Client | Removes this Client's key and environment files from the Agent. | +| `ally uninstall` | Either | Lists everything Ally added, asks once, then removes it. Default is No. | +| `cargo uninstall ally-cli` | Either | Removes the program, after `uninstall`. | * On the Agent, `uninstall` removes the service, jobs, linked Client keys, and project copies. Your own keys and files stay. -* Run `slingshot sync --pull` first to keep edits made on the Agent. +* Run `ally sync --pull` first to keep edits made on the Agent. * On Linux, linger stays on. Turn it off with `sudo loginctl disable-linger $USER`. -* To only stop the Agent, run `slingshot start --remove`. +* To only stop the Agent, run `ally start --remove`. diff --git a/docs/media/ally.png b/docs/media/ally.png new file mode 100644 index 0000000..127e806 Binary files /dev/null and b/docs/media/ally.png differ diff --git a/docs/media/demo.gif b/docs/media/demo.gif index 1e65a20..660159e 100644 Binary files a/docs/media/demo.gif and b/docs/media/demo.gif differ diff --git a/docs/media/demo.tape b/docs/media/demo.tape index 8918c29..d402043 100644 --- a/docs/media/demo.tape +++ b/docs/media/demo.tape @@ -1,4 +1,4 @@ -# The README recording. Needs a linked Agent reachable on the local network, with Slingshot +# The README recording. Needs a linked Agent reachable on the local network, with Ally # running there. Run from the repository root, then draw the window over it (edge, title # bar, and magenta corners) and make the magenta see through: # vhs docs/media/demo.tape @@ -16,14 +16,14 @@ Set Height 1100 Set Padding 30 Set WindowBar Colorful Set WindowBarSize 56 -Set Theme { "name": "Slingshot Gray", "background": "#262628", "foreground": "#e6e6e6", "cursor": "#e6e6e6", "selection": "#44444a", "black": "#1c1c1e", "brightBlack": "#7a7a80", "red": "#ff5c6c", "brightRed": "#ff7b88", "green": "#3ee07a", "brightGreen": "#6af59a", "yellow": "#ffcc33", "brightYellow": "#ffdb66", "blue": "#4da3ff", "brightBlue": "#7bbcff", "magenta": "#e070ff", "brightMagenta": "#ea94ff", "cyan": "#2fe0e0", "brightCyan": "#66f0f0", "white": "#d0d0d0", "brightWhite": "#ffffff" } +Set Theme { "name": "Ally Gray", "background": "#262628", "foreground": "#e6e6e6", "cursor": "#e6e6e6", "selection": "#44444a", "black": "#1c1c1e", "brightBlack": "#7a7a80", "red": "#ff5c6c", "brightRed": "#ff7b88", "green": "#3ee07a", "brightGreen": "#6af59a", "yellow": "#ffcc33", "brightYellow": "#ffdb66", "blue": "#4da3ff", "brightBlue": "#7bbcff", "magenta": "#e070ff", "brightMagenta": "#ea94ff", "cyan": "#2fe0e0", "brightCyan": "#66f0f0", "white": "#d0d0d0", "brightWhite": "#ffffff" } Set TypingSpeed 45ms Hide Type "tmux -f docs/media/demo.tmux.conf new-session -s demo" Enter Sleep 1s -Type `tmux select-pane -T 'archbox · Agent' && ssh -i ~/.ssh/slingshot_ed25519 -o IdentitiesOnly=yes -o "UserKnownHostsFile=\"$HOME/Library/Application Support/slingshot/known_hosts\"" ado@10.0.0.193` +Type `tmux select-pane -T 'archbox · Agent' && ssh -i ~/.ssh/ally_ed25519 -o IdentitiesOnly=yes -o "UserKnownHostsFile=\"$HOME/Library/Application Support/ally/known_hosts\"" ado@10.0.0.193` Enter Sleep 3s Type `unset PROMPT_COMMAND; PS1='\[\e[34m\]❯\[\e[0m\] '; clear` @@ -38,19 +38,19 @@ Up Sleep 500ms Show -Type "slingshot start" +Type "ally start" Sleep 400ms Enter Wait+Screen@30s /Press Enter for a code/ Sleep 2s Enter -Wait+Screen@30s /slingshot link/ +Wait+Screen@30s /ally link/ Sleep 3s Ctrl+A Down Hide -Type `code=$(tmux capture-pane -p -t :.0 | grep 'local network' | grep -oE '[0-9.]+:7433:[A-Z0-9]+'); clear; print -z "slingshot link $code"` +Type `code=$(tmux capture-pane -p -t :.0 | grep 'local network' | grep -oE '[0-9.]+:7433:[A-Z0-9]+'); clear; print -z "ally link $code"` Enter Sleep 500ms Show @@ -59,7 +59,7 @@ Enter Wait+Screen@30s /Try it/ Sleep 4s -Type "clear && slingshot attach" +Type "clear && ally attach" Sleep 400ms Enter Wait+Screen@30s /Ctrl B, D to detach/ diff --git a/docs/media/demo.tmux.conf b/docs/media/demo.tmux.conf index 98f33f5..0a6ec93 100644 --- a/docs/media/demo.tmux.conf +++ b/docs/media/demo.tmux.conf @@ -1,5 +1,5 @@ # The split screen for demo.tape: the Agent on top, the Client below. The prefix is Ctrl A, -# so Ctrl B reaches the Slingshot session inside the bottom pane. Each pane keeps its own +# so Ctrl B reaches the Ally session inside the bottom pane. Each pane keeps its own # color, blue for the Agent and purple for the Client, whichever one is active. set -g prefix C-a unbind C-b diff --git a/docs/media/slingshot.png b/docs/media/slingshot.png deleted file mode 100644 index 9a8d0c5..0000000 Binary files a/docs/media/slingshot.png and /dev/null differ diff --git a/docs/media/window.png b/docs/media/window.png index a46b587..6b4f1f9 100644 Binary files a/docs/media/window.png and b/docs/media/window.png differ