diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 415fad0..843a6fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: quality-gates: - runs-on: ubuntu-latest + runs-on: ci_runner steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 512b3aa..89974d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,13 @@ jobs: matrix: include: - target: linux-x86_64 - runner: ubuntu-latest + runner: ci_runner bazel_args: "--platforms=//bazel/config/platform/rust:linux_x86_64" - asset: crab-linux-x86_64 + asset: work-linux-x86_64 - target: macos-aarch64 - runner: macos-latest + runner: ci_runner bazel_args: "" - asset: crab-macos-aarch64 + asset: work-macos-aarch64 runs-on: ${{ matrix.runner }} steps: @@ -38,13 +38,13 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/bazel-disk-cache - key: bazel-disk-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'Cargo.lock', 'packages/crab_city_ui/pnpm-lock.yaml', 'packages/crab_city_ui/src/**') }} + key: bazel-disk-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'Cargo.lock', 'packages/workshop_ui/pnpm-lock.yaml', 'packages/workshop_ui/src/**') }} restore-keys: | bazel-disk-${{ matrix.target }}- - name: Build run: > - bazel build //packages/crab_city:crab_embedded + bazel build //packages/workshop:work_embedded --compilation_mode opt ${{ matrix.bazel_args }} --disk_cache=~/.cache/bazel-disk-cache @@ -52,14 +52,14 @@ jobs: - name: Verify embedded UI assets run: > - bazel test //packages/crab_city_ui/crate:embed_test + bazel test //packages/workshop_ui/crate:embed_test --compilation_mode opt ${{ matrix.bazel_args }} --disk_cache=~/.cache/bazel-disk-cache --repository_cache=~/.cache/bazel-repo-cache - name: Prepare artifact - run: cp bazel-bin/packages/crab_city/crab_embedded ${{ matrix.asset }} + run: cp bazel-bin/packages/workshop/work_embedded ${{ matrix.asset }} - name: Upload artifact uses: actions/upload-artifact@v4 @@ -69,7 +69,7 @@ jobs: release: needs: build - runs-on: ubuntu-latest + runs-on: ci_runner steps: - uses: actions/checkout@v4 @@ -81,13 +81,13 @@ jobs: - name: Prepare release assets run: | mkdir -p release - for dir in artifacts/crab-*/; do + for dir in artifacts/work-*/; do name=$(basename "$dir") cp "$dir/$name" "release/$name" done cp scripts/install.sh release/install.sh cd release - sha256sum crab-* install.sh > checksums-sha256.txt + sha256sum work-* install.sh > checksums-sha256.txt - name: Determine prerelease id: meta @@ -109,31 +109,31 @@ jobs: ## Install ```sh - curl -fsSL https://github.com/crabcity/crabcity/releases/latest/download/install.sh | bash + curl -fsSL https://github.com/empathic/workshop/releases/latest/download/install.sh | bash ``` Or install a specific version: ```sh - curl -fsSL https://github.com/crabcity/crabcity/releases/latest/download/install.sh | bash -s -- --version ${{ github.ref_name }} + curl -fsSL https://github.com/empathic/workshop/releases/latest/download/install.sh | bash -s -- --version ${{ github.ref_name }} ``` ## Downloads | Platform | Binary | |----------|--------| - | Linux x86_64 | [`crab-linux-x86_64`](https://github.com/crabcity/crabcity/releases/download/${{ github.ref_name }}/crab-linux-x86_64) | - | macOS (Apple Silicon) | [`crab-macos-aarch64`](https://github.com/crabcity/crabcity/releases/download/${{ github.ref_name }}/crab-macos-aarch64) | + | Linux x86_64 | [`work-linux-x86_64`](https://github.com/empathic/workshop/releases/download/${{ github.ref_name }}/work-linux-x86_64) | + | macOS (Apple Silicon) | [`work-macos-aarch64`](https://github.com/empathic/workshop/releases/download/${{ github.ref_name }}/work-macos-aarch64) | ## Manual install ```sh # Download the binary for your platform (adjust the asset name as needed) - # Linux x86_64: crab-linux-x86_64 - # macOS Apple Silicon: crab-macos-aarch64 - curl -fsSL https://github.com/crabcity/crabcity/releases/download/${{ github.ref_name }}/crab-PLATFORM -o crab - chmod +x crab - mv crab ~/.local/bin/crab + # Linux x86_64: work-linux-x86_64 + # macOS Apple Silicon: work-macos-aarch64 + curl -fsSL https://github.com/empathic/workshop/releases/download/${{ github.ref_name }}/work-PLATFORM -o work + chmod +x work + mv work ~/.local/bin/work ``` ## SHA256 Checksums diff --git a/CLAUDE.md b/CLAUDE.md index 6702560..bc0f91a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Documentation -When changing code, **update the associated docs in the same change**. Key files: `docs/architecture.md` (system design), `docs/web-terminal.md` (client terminal), `docs/configuration.md` (config), `packages/crab_city_ui/CLAUDE.md` (frontend conventions), this file (build/architecture notes). Stale docs are worse than no docs. +When changing code, **update the associated docs in the same change**. Key files: `docs/architecture.md` (system design), `docs/web-terminal.md` (client terminal), `docs/configuration.md` (config), `packages/workshop_ui/CLAUDE.md` (frontend conventions), this file (build/architecture notes). Stale docs are worse than no docs. ## Build System @@ -30,32 +30,32 @@ All Rust code uses edition 2024. Cargo defaults to edition 2021 for `cargo check ### Build Commands -- `cargo check -p crab_city` — quick compile check -- `cargo test -p crab_city` — run unit tests for the server +- `cargo check -p workshop` — quick compile check +- `cargo test -p workshop` — run unit tests for the server - `cargo test -p ` — run unit tests for any workspace crate - `bazel test //...` — full CI-equivalent (includes format check, edition 2024) -- `CRAB_CITY_UI_PATH=packages/crab_city_ui/build cargo build -p crab_city_ui` — build embedded UI crate +- `WORKSHOP_UI_PATH=packages/workshop_ui/build cargo build -p workshop_ui` — build embedded UI crate ### Desktop App (Tauri) -- `cargo check -p crab_city_desktop` — quick compile check -- `cargo test -p crab_city_desktop` — run unit tests -- `cd packages/crab_city_desktop && cargo tauri dev --config tauri.dev.conf.json` — launch desktop app with embedded server (auto-starts Vite dev server) -- `bazel build //packages/crab_city_desktop:macos_app` — build macOS `.app` bundle (debug) -- `bazel build --config=opt //packages/crab_city_desktop:macos_app` — build optimized `.app` bundle +- `cargo check -p workshop_desktop` — quick compile check +- `cargo test -p workshop_desktop` — run unit tests +- `cd packages/workshop_desktop && cargo tauri dev --config tauri.dev.conf.json` — launch desktop app with embedded server (auto-starts Vite dev server) +- `bazel build //packages/workshop_desktop:macos_app` — build macOS `.app` bundle (debug) +- `bazel build --config=opt //packages/workshop_desktop:macos_app` — build optimized `.app` bundle -**Dev workflow** (single terminal): `cd packages/crab_city_desktop && cargo tauri dev --config tauri.dev.conf.json` — the Tauri app starts an embedded server in-process, and Vite's dev proxy discovers it automatically via the `daemon.port` file. The `--config` flag merges `tauri.dev.conf.json` (devUrl + beforeDevCommand) into the base config. The base `tauri.conf.json` has no dev URL — production builds never reference external dev servers. +**Dev workflow** (single terminal): `cd packages/workshop_desktop && cargo tauri dev --config tauri.dev.conf.json` — the Tauri app starts an embedded server in-process, and Vite's dev proxy discovers it automatically via the `daemon.port` file. The `--config` flag merges `tauri.dev.conf.json` (devUrl + beforeDevCommand) into the base config. The base `tauri.conf.json` has no dev URL — production builds never reference external dev servers. -**Custom data directory**: `crab_city_desktop --data-dir /path/to/data` (defaults to `~/.crabcity`). +**Custom data directory**: `workshop_desktop --data-dir /path/to/data` (defaults to `~/.workshop`). -Note: `crab_city_desktop` is in workspace `members` but NOT in `default-members` (requires Tauri system deps). The desktop app depends on the `crab_city` library crate (with `embedded-ui` feature) — no separate daemon process. +Note: `workshop_desktop` is in workspace `members` but NOT in `default-members` (requires Tauri system deps). The desktop app depends on the `workshop` library crate (with `embedded-ui` feature) — no separate daemon process. ### Frontend (SvelteKit) -- `cd packages/crab_city_ui && pnpm install && pnpm build` — build the web UI -- `cd packages/crab_city_ui && pnpm dev` — dev server with hot reload -- `cd packages/crab_city_ui && pnpm test` — run Jest tests -- `cd packages/crab_city_ui && pnpm format` — format TS/Svelte with Prettier (also runs via `bazel run //tools/format`) +- `cd packages/workshop_ui && pnpm install && pnpm build` — build the web UI +- `cd packages/workshop_ui && pnpm dev` — dev server with hot reload +- `cd packages/workshop_ui && pnpm test` — run Jest tests +- `cd packages/workshop_ui && pnpm format` — format TS/Svelte with Prettier (also runs via `bazel run //tools/format`) ## TUI Styling @@ -69,15 +69,15 @@ The terminal theme is solarized. Hardcoded ANSI colors are invisible or clash: ### Package Dependency Graph ``` -crab_city (lib: server core, config, handlers, WS | bin: CLI + TUI) +workshop (lib: server core, config, handlers, WS | bin "work": CLI + TUI) ├── claude_convo (conversation log reader) ├── pty_manager (PTY lifecycle) └── virtual_terminal (screen buffer + viewport negotiation) -tty_wrapper (standalone HTTP-controlled PTY — not depended on by crab_city) -crab_city_ui (SvelteKit frontend — embedded via rust-embed feature flag) -crab_city_desktop (Tauri native desktop app — embeds crab_city server in-process) - └── crab_city (lib, with embedded-ui feature) +tty_wrapper (standalone HTTP-controlled PTY — not depended on by workshop) +workshop_ui (SvelteKit frontend — embedded via rust-embed feature flag) +workshop_desktop (Tauri native desktop app — embeds workshop server in-process) + └── workshop (lib, with embedded-ui feature) ``` ### Daemon Lifecycle @@ -88,7 +88,7 @@ One server per data directory, enforced by advisory file lock (`daemon.lock`). T - **`check_existing_server()`** — reads `daemon.pid`/`daemon.port`, verifies process alive via `kill(pid, 0)`, then health-checks `GET /health` - **`release_daemon_files()`** — PID-aware cleanup: only deletes state files if `daemon.pid` matches current process - **`DaemonLock`** — RAII guard; `Drop` calls `release_daemon_files()`, then releases the flock -- Both `crab server` and `EmbeddedServer::start()` acquire the lock before initializing +- Both `work server` and `EmbeddedServer::start()` acquire the lock before initializing - Desktop app calls `check_existing_server()` first — connects to existing daemon if healthy, otherwise starts embedded ### Server Internals diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d76b0a..7752d3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to Crab City +# Contributing to Workshop ## Prerequisites @@ -10,34 +10,34 @@ ## Clone and Build ```sh -git clone https://github.com/anthropics/crab-city && cd crab-city +git clone https://github.com/anthropics/workshop && cd workshop # Cargo for development (fast iteration) -cargo build -p crab_city +cargo build -p workshop # Bazel for CI-equivalent builds (includes format checks, edition 2024 enforcement) -bazel build //packages/crab_city:crab +bazel build //packages/workshop:work ``` ## Running the Server ```sh # Start server + TUI picker -cargo run -p crab_city +cargo run -p workshop # Start server only (for web UI development) -cargo run -p crab_city -- server --debug +cargo run -p workshop -- server --debug ``` The server starts on a random port on `127.0.0.1`. The port is printed on startup. ## Web UI Development -The frontend is a SvelteKit app in `packages/crab_city_ui/`. +The frontend is a SvelteKit app in `packages/workshop_ui/`. ```sh # Install dependencies -cd packages/crab_city_ui +cd packages/workshop_ui pnpm install # Start dev server with hot reload @@ -55,20 +55,20 @@ pnpm test In development, the server runs without the embedded UI. To build a binary with the UI baked in: ```sh -CRAB_CITY_UI_PATH=packages/crab_city_ui/build cargo build -p crab_city --features embedded-ui +WORKSHOP_UI_PATH=packages/workshop_ui/build cargo build -p workshop --features embedded-ui ``` Or use Bazel, which handles the frontend build automatically: ```sh -bazel build //packages/crab_city:crab +bazel build //packages/workshop:work ``` ## Testing ```sh # Unit tests for any package -cargo test -p crab_city +cargo test -p workshop cargo test -p virtual_terminal cargo test -p @@ -76,7 +76,7 @@ cargo test -p bazel test //... # Frontend tests -cd packages/crab_city_ui && pnpm test +cd packages/workshop_ui && pnpm test ``` ## Formatting @@ -119,7 +119,7 @@ These must stay in sync. See the [architecture doc](docs/architecture.md) for system design details, or the CLAUDE.md files in each package for module-level maps and patterns: - [`CLAUDE.md`](CLAUDE.md) — top-level build system and architecture notes -- [`packages/crab_city/CLAUDE.md`](packages/crab_city/CLAUDE.md) — server module map, key patterns, testing +- [`packages/workshop/CLAUDE.md`](packages/workshop/CLAUDE.md) — server module map, key patterns, testing ## Documentation diff --git a/Cargo.lock b/Cargo.lock index cd5be75..60aca16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,93 +808,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crab_city" -version = "0.45.1" -dependencies = [ - "anyhow", - "argon2", - "axum", - "chrono", - "clap", - "crab_city_ui", - "dirs 5.0.1", - "figment", - "futures", - "hotln", - "ignore", - "mime_guess", - "nix 0.29.0", - "password-hash", - "pty_manager", - "rand 0.8.5", - "ratatui", - "reqwest 0.12.28", - "rpassword", - "rust-embed", - "serde", - "serde_json", - "similar", - "sqlx", - "syndiff", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tokio-tungstenite 0.24.0", - "tokio-util", - "toml 0.8.2", - "toolpath-claude", - "toolpath-convo", - "tower", - "tower-http", - "tracing", - "tracing-subscriber", - "tree-sitter", - "tree-sitter-bash", - "tree-sitter-css", - "tree-sitter-go", - "tree-sitter-html", - "tree-sitter-javascript", - "tree-sitter-json", - "tree-sitter-language", - "tree-sitter-python", - "tree-sitter-rust", - "tree-sitter-toml-ng", - "tree-sitter-typescript", - "uuid", - "virtual_terminal", - "vt100", -] - -[[package]] -name = "crab_city_desktop" -version = "0.1.0" -dependencies = [ - "anyhow", - "blake3", - "brotli", - "clap", - "crab_city", - "plist", - "png", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-shell", - "tauri-plugin-window-state", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "crab_city_ui" -version = "0.2.0" -dependencies = [ - "rust-embed", -] - [[package]] name = "crc" version = "3.4.0" @@ -8107,6 +8020,93 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "workshop" +version = "0.45.1" +dependencies = [ + "anyhow", + "argon2", + "axum", + "chrono", + "clap", + "dirs 5.0.1", + "figment", + "futures", + "hotln", + "ignore", + "mime_guess", + "nix 0.29.0", + "password-hash", + "pty_manager", + "rand 0.8.5", + "ratatui", + "reqwest 0.12.28", + "rpassword", + "rust-embed", + "serde", + "serde_json", + "similar", + "sqlx", + "syndiff", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite 0.24.0", + "tokio-util", + "toml 0.8.2", + "toolpath-claude", + "toolpath-convo", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-css", + "tree-sitter-go", + "tree-sitter-html", + "tree-sitter-javascript", + "tree-sitter-json", + "tree-sitter-language", + "tree-sitter-python", + "tree-sitter-rust", + "tree-sitter-toml-ng", + "tree-sitter-typescript", + "uuid", + "virtual_terminal", + "vt100", + "workshop_ui", +] + +[[package]] +name = "workshop_desktop" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake3", + "brotli", + "clap", + "plist", + "png", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-shell", + "tauri-plugin-window-state", + "tokio", + "tracing", + "tracing-subscriber", + "workshop", +] + +[[package]] +name = "workshop_ui" +version = "0.2.0" +dependencies = [ + "rust-embed", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 4fc2347..7aaeb14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ [workspace] members = [ - "packages/crab_city", - "packages/crab_city_desktop", - "packages/crab_city_ui/crate", + "packages/workshop", + "packages/workshop_desktop", + "packages/workshop_ui/crate", "packages/pty_manager", "packages/tty_wrapper", "packages/virtual_terminal", ] -# crab_city_ui/crate requires CRAB_CITY_UI_PATH env var pointing to SvelteKit build output. -# crab_city_desktop requires Tauri system dependencies (WebKit, etc.). +# workshop_ui/crate requires WORKSHOP_UI_PATH env var pointing to SvelteKit build output. +# workshop_desktop requires Tauri system dependencies (WebKit, etc.). # Exclude both from default `cargo build` to avoid build failures. Build with: -# CRAB_CITY_UI_PATH=packages/crab_city_ui/build cargo build -p crab_city_ui -# cargo check -p crab_city_desktop +# WORKSHOP_UI_PATH=packages/workshop_ui/build cargo build -p workshop_ui +# cargo check -p workshop_desktop default-members = [ - "packages/crab_city", + "packages/workshop", "packages/pty_manager", "packages/tty_wrapper", "packages/virtual_terminal", diff --git a/MODULE.bazel b/MODULE.bazel index d7103a5..4f50050 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ module( - name = "crabcity", - repo_name = "crabcity", + name = "workshop", + repo_name = "workshop", ) bazel_dep(name = "platforms", version = "1.0.0") @@ -34,14 +34,14 @@ bazel_dep(name = "rules_nodejs", version = "6.7.3") npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm") npm.npm_translate_lock( name = "npm", - npmrc = "//packages/crab_city_ui:.npmrc", - pnpm_lock = "//packages/crab_city_ui:pnpm-lock.yaml", + npmrc = "//packages/workshop_ui:.npmrc", + pnpm_lock = "//packages/workshop_ui:pnpm-lock.yaml", ) use_repo(npm, "npm") rules_ts_ext = use_extension("@aspect_rules_ts//ts:extensions.bzl", "ext") rules_ts_ext.deps( - ts_version_from = "//packages/crab_city_ui:package.json", + ts_version_from = "//packages/workshop_ui:package.json", ) use_repo(rules_ts_ext, "npm_typescript") @@ -62,13 +62,13 @@ use_repo(rust, "rust_toolchains") register_toolchains("@rust_toolchains//:all") -# Single-source the crab_city version from Cargo.toml +# Single-source the workshop version from Cargo.toml cargo_version = use_extension("//bazel:cargo_version.bzl", "cargo_version") cargo_version.parse( - name = "crab_version", - cargo_toml = "//packages/crab_city:Cargo.toml", + name = "workshop_version", + cargo_toml = "//packages/workshop:Cargo.toml", ) -use_repo(cargo_version, "crab_version") +use_repo(cargo_version, "workshop_version") crate_index = use_extension("@rules_rust//crate_universe:extension.bzl", "crate") diff --git a/README.md b/README.md index 53f7fb6..aeef94e 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,131 @@ -# Crab City +# Workshop -Run multiple Claude Code instances. Share them with your team. Search everything. +Collaborative Claude Code for teams. - +Run multiple Claude instances, share them with teammates in real time, and +search across every conversation you've ever had — from the terminal or the +browser. -## Why Crab City? - -**Before Crab City:** - -- You want to run several Claude instances at once (one per task, one per repo) - but managing them is a mess -- You want a teammate to see what Claude is doing on your machine, or pick up - where you left off -- You want to search across all your past Claude conversations, not dig through - JSONL files - -Crab City fixes all of that. It's a local server that manages Claude Code -instances, multiplexes their terminals, and gives you a TUI and web dashboard to -drive everything. + ## Install ```sh -curl -fsSL https://github.com/crabcity/crabcity/releases/latest/download/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/empathic-ai/workshop/main/scripts/install.sh | bash ``` -This installs the `crab` binary to `~/.local/bin`. You need [Claude -Code](https://docs.anthropic.com/en/docs/claude-code) installed separately. - -To build from source instead, see [CONTRIBUTING.md](CONTRIBUTING.md). +Installs the `work` binary to `~/.local/bin`. Requires +[Claude Code](https://docs.anthropic.com/en/docs/claude-code). -## Quick Start - -**Launch the TUI picker** (starts a background daemon automatically): +## Get Started ```sh -crab +work ``` -This opens a terminal UI where you create and attach to Claude Code instances. -Each instance gets its own PTY — create as many as you want. - -To switch between Claude and the picker/overview TUI, just type `CTRL+]`. +A TUI opens where you create and switch between Claude Code sessions. Press +`Ctrl+]` to move between sessions and the overview. A background daemon keeps +everything running — close the TUI and your sessions stay alive. -**Or start the server directly** for the web UI: +For the web UI: ```sh -crab server +work server ``` -Then open `http://127.0.0.1:` for the full dashboard: live terminal, -conversation viewer, task board, and instance management. - -With a local server, just run `crab` to attach. +Then open the URL it prints. You get live terminals, a conversation viewer, +task board, and fleet overview — all in the browser. The CLI and web UI talk +to the same daemon, so use whichever you prefer. -To switch between Claude and the picker/overview TUI, just type `CTRL+]`. +## Invite Your Team -## Common Workflows - -### Managing instances from the CLI +Workshop is built for collaboration. To share over a tunnel: ```sh -crab list # show running instances -crab attach swift-amber-falcon # attach to an instance by name -crab kill # stop an instance -crab kill-server # stop the daemon and all instances +work server --profile tunnel +work auth enable ``` -### Searching past conversations - -Crab City imports your Claude conversation history into a searchable SQLite database: - -```sh -crab server --import-all # import everything from ~/.claude/projects/ -crab server --import-from ~/project # import a specific project -``` - -The web UI gives you full-text search across all conversations with syntax-highlighted code blocks and diffs. - -### Sharing with your team - -Set a profile and enable auth: +Or on your LAN: ```sh -crab server --profile tunnel # or --profile server -crab auth enable +work server --profile server +work auth enable ``` -Share the URL. The first person to register becomes admin. - -| Profile | Binds to | Auth | Use case | -|----------|-------------|------|---------------------------------| -| `local` | `127.0.0.1` | off | Solo development (default) | -| `tunnel` | `127.0.0.1` | on | Tunneling (ngrok, cloudflared) | -| `server` | `0.0.0.0` | on | LAN or public deployment | +Share the URL. The first person to register becomes admin. From there: -When sharing, multiple users can: -- **Watch the same instance** — with a lock system to coordinate typing -- **See who's online** — live presence shows who's connected and where -- **Chat** — real-time messaging overlaid on instances -- **Share tasks** — a task board tied to instances, synced across clients +- **Shared terminals** — watch what Claude is doing on a teammate's instance, + with a lock to coordinate who's typing +- **Live presence** — see who's connected and which instance they're focused on +- **Chat** — real-time messaging, per-instance or global +- **Task board** — create and assign tasks tied to instances, synced to every + client -Your local CLI (`127.0.0.1`) always bypasses auth, so `crab list` and `crab attach` keep working. - -## Configuration +Your local CLI always bypasses auth, so `work attach` keeps working without +credentials. -All config lives in `~/.crabcity/config.toml`: +## Search Everything -```toml -profile = "local" +Workshop automatically syncs conversation history from Claude Code into a +local SQLite database — every session, every project, fully searchable. -[auth] -enabled = false -session_ttl_secs = 604800 # 7 days -allow_registration = true - -[server] -host = "127.0.0.1" -port = 0 # 0 = auto-select -max_buffer_mb = 25 # output buffer per instance -hang_timeout_secs = 300 # hang detection (0 = disabled) +```sh +work server --import-all # sync all projects +work server --import-from ~/myapp # sync one project ``` -Layering: CLI flags > env vars > config.toml > profile defaults. +The web UI gives you full-text search with syntax-highlighted code and diffs. +Find that conversation from three weeks ago where Claude solved the exact +problem you're looking at now. -Environment variables use the `CRAB_` prefix with `__` as a section separator -(e.g. `CRAB_AUTH__ENABLED=true`). +## Configuration -Full reference: [docs/configuration.md](docs/configuration.md). +All config lives in `~/.workshop/config.toml`. See +[docs/configuration.md](docs/configuration.md) for the full reference — +profiles, env vars, auth settings, server tuning. ## FAQ -**What exactly runs when I type `crab`?** -A daemon process starts in the background (if not already running) that manages -all your Claude instances. The TUI connects to it. So does the web UI. Closing -the TUI doesn't kill your instances — they keep running until you `crab kill` them -or `crab kill-server`. +
+What happens when I type work? -**How is this different from tmux + Claude Code?** -Crab City gives you things tmux can't: real-time state detection (is Claude -thinking? running a tool? idle?), a searchable conversation database, multi-user -auth, a web dashboard, and a task board. The terminal multiplexing is aware of -Claude's protocol, not just raw bytes. +A daemon starts in the background (if one isn't already running) and manages +all your Claude instances. The TUI connects to it. So does the web UI. +Closing the TUI doesn't kill anything — instances keep running until you +`work kill` them or `work kill-server`. +
-**Does my data leave my machine?** -No. Everything is local — SQLite database, conversation logs, config. If you -enable the `server` profile, you're explicitly choosing to bind to a network -interface, but the data stays on that machine. +
+How is this different from tmux? -**Can I use this with other AI coding tools?** -Currently built for Claude Code only. The conversation importer, state detection, -and instance management are all Claude Code-specific. +Workshop understands Claude's protocol. It detects state (thinking, tool use, +idle), imports conversations into a searchable database, provides multi-user +auth with live presence, and gives you a web dashboard. tmux sees raw bytes. +
-**What platforms are supported?** -Linux (x86_64, aarch64) and macOS (Apple Silicon, Intel via Rosetta 2). +
+Does my data leave my machine? -## Architecture +No. Everything is local: SQLite database, conversation history, config. The +`tunnel` and `server` profiles bind to a network interface so teammates can +connect, but the data stays on your machine. +
-``` -┌─────────────────────────────────────────────────┐ -│ Web UI (SvelteKit) or TUI (ratatui) │ -├─────────────────────────────────────────────────┤ -│ Server (axum) │ -│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ Instance │ │ WebSocket│ │ Conversation │ │ -│ │ Manager │ │ Mux │ │ Import + Search │ │ -│ └──────────┘ └──────────┘ └──────────────────┘ │ -├─────────────────────────────────────────────────┤ -│ PTY Layer (pty_manager + virtual_terminal) │ -│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ claude │ │ claude │ │ claude │ │ -│ │ instance │ │ instance │ │ instance │ │ -│ └──────────┘ └──────────┘ └──────────────────┘ │ -└─────────────────────────────────────────────────┘ - SQLite (conversations, tasks, auth) -``` - -Each instance is an isolated Tokio task with its own PTY handle and virtual -terminal. The server detects Claude's state from conversation logs with a -terminal-heuristic fallback, and broadcasts changes to all connected clients -over WebSocket. - -## Development - -```sh -cargo check -p crab_city # quick compile check -cargo test -p crab_city # run unit tests -bazel test //... # full CI build + tests -bazel run //tools/format # format code (never use rustfmt directly) -``` +
+What platforms are supported? -See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development guide. +Linux (x86_64, aarch64) and macOS (Apple Silicon, Intel). +
-## Documentation +## Learn More -- [Configuration Reference](docs/configuration.md) — CLI options, profiles, config.toml, env vars -- [Architecture](docs/architecture.md) — system design, WebSocket protocol, state detection -- [Operations](docs/operations.md) — endpoints, metrics, troubleshooting, database management -- [Contributing](CONTRIBUTING.md) — dev setup, building, testing, code style +- [Configuration](docs/configuration.md) — profiles, env vars, CLI options +- [Architecture](docs/architecture.md) — system design, protocols, state + detection +- [Operations](docs/operations.md) — endpoints, metrics, troubleshooting +- [Contributing](CONTRIBUTING.md) — building from source, dev setup, testing ## License -Apache 2.0 — Copyright 2025 Empathic, Inc. +Apache 2.0 — Copyright 2025-2026 Empathic, Inc. diff --git a/docs/architecture.md b/docs/architecture.md index 0840bf4..e39da16 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -This document covers the internal design of Crab City for contributors and curious users. +This document covers the internal design of Workshop for contributors and curious users. ## Three-Layer Architecture @@ -32,14 +32,14 @@ This document covers the internal design of Crab City for contributors and curio ## Package Dependency Graph ``` -crab_city (server + CLI + TUI) +workshop (server + CLI + TUI) ├── claude_convo (conversation log reader) ├── pty_manager (PTY lifecycle) └── virtual_terminal (screen buffer + viewport negotiation) -tty_wrapper (standalone HTTP-controlled PTY — not depended on by crab_city) -crab_city_ui (SvelteKit frontend — embedded via rust-embed feature flag) -crab_city_desktop (Tauri native desktop app — discovers/starts daemon, loads web UI) +tty_wrapper (standalone HTTP-controlled PTY — not depended on by workshop) +workshop_ui (SvelteKit frontend — embedded via rust-embed feature flag) +workshop_desktop (Tauri native desktop app — discovers/starts daemon, loads web UI) ``` ## Instance Lifecycle @@ -47,7 +47,7 @@ crab_city_desktop (Tauri native desktop app — discovers/starts daemon, lo Instances flow through: **Created → Running → Stopped**. ``` -crab create / web UI "New Instance" +workshop create / web UI "New Instance" │ ▼ instance_manager.rs @@ -150,13 +150,13 @@ SQLite via sqlx with embedded migrations (`db.rs`). ### Config -- **Location**: `~/.crabcity/crabcity.db` (configurable via `--data-dir`) +- **Location**: `~/.workshop/workshop.db` (configurable via `--data-dir`) - **Migrations**: Embedded in the binary, run automatically on startup - **Compile-time checked queries**: Uses `sqlx::query!` / `sqlx::query_as!` ## Auth -Auth middleware has a **loopback bypass** — CLI/TUI requests to `127.0.0.1` work without credentials. This means your local `crab` commands never need a token, even when auth is enabled for remote users. +Auth middleware has a **loopback bypass** — CLI/TUI requests to `127.0.0.1` work without credentials. This means your local `workshop` commands never need a token, even when auth is enabled for remote users. For remote connections, auth uses JWT sessions with a configurable TTL. The first user to register becomes the admin. @@ -164,7 +164,7 @@ For remote connections, auth uses JWT sessions with a configurable TTL. The firs Multiple clients share a single PTY per instance: -- `virtual_terminal` maintains the screen buffer and negotiates dimensions as `min(all active viewports)`. On resize, the visible screen is saved, a fresh `vt100::Parser` is created at the new dimensions (clearing scrollback), and the visible content is restored. The PTY program's SIGWINCH redraw then rebuilds scrollback at the correct width — no duplicates, no virtual trim tracking. Both the server-side `VirtualTerminal::resize()` and the TUI client use this approach. The `recorder` submodule captures PTY output/input/resize events with microsecond timestamps for golden-test replay (enabled via `CRAB_CITY_VT_RECORD` env var) +- `virtual_terminal` maintains the screen buffer and negotiates dimensions as `min(all active viewports)`. On resize, the visible screen is saved, a fresh `vt100::Parser` is created at the new dimensions (clearing scrollback), and the visible content is restored. The PTY program's SIGWINCH redraw then rebuilds scrollback at the correct width — no duplicates, no virtual trim tracking. Both the server-side `VirtualTerminal::resize()` and the TUI client use this approach. The `recorder` submodule captures PTY output/input/resize events with microsecond timestamps for golden-test replay (enabled via `WORKSHOP_VT_RECORD` env var) - `websocket_proxy.rs` manages the fan-out from one PTY to N WebSocket clients ## Web Terminal (Client-Side) diff --git a/docs/configuration.md b/docs/configuration.md index b10c27d..1280b79 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration Reference -Crab City uses layered configuration. Each layer overrides the one below it: +Workshop uses layered configuration. Each layer overrides the one below it: ``` CLI flags > env vars > config.toml > profile defaults > struct defaults @@ -8,7 +8,7 @@ CLI flags > env vars > config.toml > profile defaults > struct defaults ## CLI Options -### `crab server` +### `work server` | Flag | Description | Default | |------|-------------|---------| @@ -18,7 +18,7 @@ CLI flags > env vars > config.toml > profile defaults > struct defaults | `--instance-base-port ` | Base port for instances | `9000` | | `--default-command ` | Default command for new instances | — | | `-d, --debug` | Enable debug logging | `false` | -| `--data-dir ` | Custom data directory | `~/.crabcity` | +| `--data-dir ` | Custom data directory | `~/.workshop` | | `--reset-db` | Reset database (with confirmation prompt) | — | | `--import-all` | Import all existing Claude conversations on startup | — | | `--import-from ` | Import conversations from a specific project directory | — | @@ -27,14 +27,14 @@ CLI flags > env vars > config.toml > profile defaults > struct defaults | Command | Description | |---------|-------------| -| `crab` | Start daemon + open TUI picker (default) | -| `crab attach ` | Attach to an instance by name or ID prefix | -| `crab list [--json]` | List running instances | -| `crab kill ` | Stop a specific instance | -| `crab kill-server` | Stop the daemon and all instances | -| `crab auth enable` | Enable authentication | -| `crab auth disable` | Disable authentication | -| `crab auth status` | Show current auth status | +| `work` | Start daemon + open TUI picker (default) | +| `work attach ` | Attach to an instance by name or ID prefix | +| `work list [--json]` | List running instances | +| `work kill ` | Stop a specific instance | +| `work kill-server` | Stop the daemon and all instances | +| `work auth enable` | Enable authentication | +| `work auth disable` | Disable authentication | +| `work auth status` | Show current auth status | ## Profiles @@ -56,7 +56,7 @@ Profiles set sensible defaults for common deployment scenarios. Specify with `-- ## Config File -Location: `~/.crabcity/config.toml` +Location: `~/.workshop/config.toml` Full annotated reference: @@ -93,42 +93,42 @@ scrollback_lines = 10000 ## Environment Variables -Every config field can be set via environment variable using the `CRAB_` prefix with `__` (double underscore) as the section separator. +Every config field can be set via environment variable using the `WORKSHOP_` prefix with `__` (double underscore) as the section separator. | Variable | Config equivalent | Example | |----------|-------------------|---------| -| `CRAB_PROFILE` | `profile` | `tunnel` | -| `CRAB_AUTH__ENABLED` | `auth.enabled` | `true` | -| `CRAB_AUTH__SESSION_TTL_SECS` | `auth.session_ttl_secs` | `604800` | -| `CRAB_AUTH__ALLOW_REGISTRATION` | `auth.allow_registration` | `true` | -| `CRAB_SERVER__HOST` | `server.host` | `0.0.0.0` | -| `CRAB_SERVER__PORT` | `server.port` | `8080` | -| `CRAB_SERVER__MAX_BUFFER_MB` | `server.max_buffer_mb` | `50` | -| `CRAB_SERVER__MAX_HISTORY_KB` | `server.max_history_kb` | `128` | -| `CRAB_SERVER__HANG_TIMEOUT_SECS` | `server.hang_timeout_secs` | `600` | -| `CRAB_SERVER__SCROLLBACK_LINES` | `server.scrollback_lines` | `10000` | -| `CRAB_SERVER__VT_RECORD_DIR` | `server.vt_record_dir` | — | +| `WORKSHOP_PROFILE` | `profile` | `tunnel` | +| `WORKSHOP_AUTH__ENABLED` | `auth.enabled` | `true` | +| `WORKSHOP_AUTH__SESSION_TTL_SECS` | `auth.session_ttl_secs` | `604800` | +| `WORKSHOP_AUTH__ALLOW_REGISTRATION` | `auth.allow_registration` | `true` | +| `WORKSHOP_SERVER__HOST` | `server.host` | `0.0.0.0` | +| `WORKSHOP_SERVER__PORT` | `server.port` | `8080` | +| `WORKSHOP_SERVER__MAX_BUFFER_MB` | `server.max_buffer_mb` | `50` | +| `WORKSHOP_SERVER__MAX_HISTORY_KB` | `server.max_history_kb` | `128` | +| `WORKSHOP_SERVER__HANG_TIMEOUT_SECS` | `server.hang_timeout_secs` | `600` | +| `WORKSHOP_SERVER__SCROLLBACK_LINES` | `server.scrollback_lines` | `10000` | +| `WORKSHOP_SERVER__VT_RECORD_DIR` | `server.vt_record_dir` | — | Legacy environment variables (still supported): | Variable | Description | Default | |----------|-------------|---------| -| `CRAB_CITY_MAX_BUFFER_MB` | Maximum output buffer per instance (MB) | `1` | -| `CRAB_CITY_MAX_HISTORY_KB` | Maximum history bytes sent on focus switch (KB) | `64` | -| `CRAB_CITY_HANG_TIMEOUT_SECS` | Hang detection timeout (0 = disabled) | `300` | +| `WORKSHOP_MAX_BUFFER_MB` | Maximum output buffer per instance (MB) | `1` | +| `WORKSHOP_MAX_HISTORY_KB` | Maximum history bytes sent on focus switch (KB) | `64` | +| `WORKSHOP_HANG_TIMEOUT_SECS` | Hang detection timeout (0 = disabled) | `300` | ## Conversation Import -Crab City can import Claude conversation logs from `~/.claude/projects/` into its local SQLite database for full-text search and browsing. +Workshop can import Claude conversation logs from `~/.claude/projects/` into its local SQLite database for full-text search and browsing. ### On startup ```sh # Import all conversations from all projects -crab server --import-all +work server --import-all # Import from a specific project directory -crab server --import-from /path/to/project +work server --import-from /path/to/project ``` ### At runtime (via API) @@ -144,12 +144,12 @@ The web UI provides a searchable notebook-style conversation viewer with syntax- ## Data Directory -All runtime data lives in `~/.crabcity/` (override with `--data-dir`): +All runtime data lives in `~/.workshop/` (override with `--data-dir`): ``` -~/.crabcity/ +~/.workshop/ ├── config.toml Configuration file -├── crabcity.db SQLite database +├── workshop.db SQLite database ├── exports/ Exported conversations └── logs/ Server logs ``` @@ -162,17 +162,17 @@ The web UI is a SvelteKit application that can be embedded into the Rust binary ```sh # Build the SvelteKit app -cd packages/crab_city_ui +cd packages/workshop_ui pnpm install pnpm build cd ../.. # Build the Rust binary with embedded UI -CRAB_CITY_UI_PATH=packages/crab_city_ui/build cargo build -p crab_city --features embedded-ui +WORKSHOP_UI_PATH=packages/workshop_ui/build cargo build -p workshop --features embedded-ui ``` Or use Bazel, which handles everything automatically: ```sh -bazel build //packages/crab_city:crab +bazel build //packages/workshop:work ``` diff --git a/docs/design/startup-experience.md b/docs/design/startup-experience.md index 344160f..9e3867c 100644 --- a/docs/design/startup-experience.md +++ b/docs/design/startup-experience.md @@ -2,7 +2,7 @@ ## Problem -When Claude starts up slowly, Crab City looks broken. The TUI shows a blank terminal. The web UI shows an empty conversation view. There's no indication that anything is happening, no hint about what to do, and no way to productively use the wait time. Messages typed during startup are silently lost or ignored. +When Claude starts up slowly, Workshop looks broken. The TUI shows a blank terminal. The web UI shows an empty conversation view. There's no indication that anything is happening, no hint about what to do, and no way to productively use the wait time. Messages typed during startup are silently lost or ignored. The root cause is a state gap: the `ClaudeState` enum jumps straight to `Idle`, but "Idle" means "ready and waiting for input." During startup, the instance is *not* idle — it's booting. This state is real but unrepresentable in the model, so it's invisible in every view. @@ -19,7 +19,7 @@ Every piece of this design says the same thing: **the system is in control, and Add a `Starting` variant to `ClaudeState`: ```rust -// packages/crab_city/src/inference/state.rs +// packages/workshop/src/inference/state.rs pub enum ClaudeState { Starting, // NEW: PTY spawned, Claude not yet at prompt Idle, @@ -40,11 +40,11 @@ pub enum ClaudeState { **Transition trigger:** The inference manager already watches terminal output via `StateSignal::TerminalOutput`. Add a heuristic: if the instance has `Starting` state and we see Claude prompt patterns or an assistant conversation entry, transition to `Idle`. The existing signal path handles this naturally. **Files:** -- `packages/crab_city/src/inference/state.rs` — add variant, update `is_active()`, `Default` (keep as `Idle`, set `Starting` explicitly at creation) -- `packages/crab_city/src/instance_actor.rs` — set initial `claude_state: Some(ClaudeState::Starting)` -- `packages/crab_city/src/inference/manager.rs` — add `Starting` → `Idle` transition on first prompt/output -- `packages/crab_city/src/ws/protocol.rs` — serde tests for new variant -- `packages/crab_city_ui/src/lib/types.ts` — add `{ type: 'Starting' }` to `ClaudeState` union +- `packages/workshop/src/inference/state.rs` — add variant, update `is_active()`, `Default` (keep as `Idle`, set `Starting` explicitly at creation) +- `packages/workshop/src/instance_actor.rs` — set initial `claude_state: Some(ClaudeState::Starting)` +- `packages/workshop/src/inference/manager.rs` — add `Starting` → `Idle` transition on first prompt/output +- `packages/workshop/src/ws/protocol.rs` — serde tests for new variant +- `packages/workshop_ui/src/lib/types.ts` — add `{ type: 'Starting' }` to `ClaudeState` union ### Layer 2: Phased Progress (Web) @@ -59,11 +59,11 @@ The frontend infers boot phases from existing signals — no enum bloat needed: Each line appears as its milestone is reached. The visual shifts from "is it broken?" to "I can see where it is." -**ConversationView** (`packages/crab_city_ui/src/lib/components/ConversationView.svelte`): Replace the empty-state "Start a conversation" message with a boot progress panel when `claude_state?.type === 'Starting'`. Styled per the CRT brand book — amber phosphor, uppercase monospace, adapting the aesthetic that `BootSequence.svelte` already established. But this one is *real*, not cosmetic. Each checkpoint lights up as it's reached. +**ConversationView** (`packages/workshop_ui/src/lib/components/ConversationView.svelte`): Replace the empty-state "Start a conversation" message with a boot progress panel when `claude_state?.type === 'Starting'`. Styled per the CRT brand book — amber phosphor, uppercase monospace, adapting the aesthetic that `BootSequence.svelte` already established. But this one is *real*, not cosmetic. Each checkpoint lights up as it's reached. -**Sidebar** (`packages/crab_city_ui/src/lib/components/sidebar/InstanceItem.svelte`): The `stateInfo` label shows `STARTING` with a slow rhythmic pulse (distinct from the fast flicker of active work). Other team members see "this instance is starting up" — ambient awareness, no action needed. +**Sidebar** (`packages/workshop_ui/src/lib/components/sidebar/InstanceItem.svelte`): The `stateInfo` label shows `STARTING` with a slow rhythmic pulse (distinct from the fast flicker of active work). Other team members see "this instance is starting up" — ambient awareness, no action needed. -**MainHeader** (`packages/crab_city_ui/src/lib/components/main-view/MainHeader.svelte`): The baud panel shows `BOOT` verb during `Starting` state with the baud meter in a slow pulse pattern. +**MainHeader** (`packages/workshop_ui/src/lib/components/main-view/MainHeader.svelte`): The baud panel shows `BOOT` verb during `Starting` state with the baud meter in a slow pulse pattern. ### Layer 3: The Input Bar Stays Hot @@ -77,7 +77,7 @@ Each line appears as its milestone is reached. The visual shifts from "is it bro This turns dead wait time into **planning time**. You're not waiting for Claude — you're *briefing* Claude. -**File:** `packages/crab_city_ui/src/lib/components/MessageInput.svelte` +**File:** `packages/workshop_ui/src/lib/components/MessageInput.svelte` ### Layer 4: Messages Queue as Tasks @@ -95,8 +95,8 @@ This means: - When Claude reaches `Idle`, tasks dispatch in `sort_order` using the existing `TaskDispatch` machinery. **Files:** -- `packages/crab_city_ui/src/lib/stores/ws-handlers.ts` — on `StateChange` to `Idle`, flush pending tasks -- `packages/crab_city_ui/src/lib/stores/instances.ts` — wire task creation on submit during `Starting` +- `packages/workshop_ui/src/lib/stores/ws-handlers.ts` — on `StateChange` to `Idle`, flush pending tasks +- `packages/workshop_ui/src/lib/stores/instances.ts` — wire task creation on submit during `Starting` - Existing task API (`handlers/tasks.rs`, `repository/tasks.rs`) — no changes needed ### Layer 5: Time-Aware Messaging @@ -137,8 +137,8 @@ When state transitions to `Idle`, the bar updates with a brief reverse-video fla The status bar subscribes to `StateChange` messages via the instance WebSocket — the attach loop already has a `select!` handling WebSocket frames. **Files:** -- `packages/crab_city/src/cli/attach.rs` — add status bar overlay, subscribe to state changes -- `packages/crab_city/src/cli/terminal.rs` — extend `TerminalGuard` with status bar support +- `packages/workshop/src/cli/attach.rs` — add status bar overlay, subscribe to state changes +- `packages/workshop/src/cli/terminal.rs` — extend `TerminalGuard` with status bar support - Uses existing compositor `Anchor::BottomLeft` + `Attrs` with `REVERSED`/`BOLD` modifiers (solarized-safe) ### Layer 7: The Seamless Transition diff --git a/docs/operations.md b/docs/operations.md index 62b3224..c63b2d0 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -1,6 +1,6 @@ # Operations Guide -Running and maintaining a Crab City server. +Running and maintaining a Workshop server. ## Endpoints @@ -55,19 +55,19 @@ curl http://localhost:PORT/metrics ```sh # Default -RUST_LOG=crab_city=info cargo run -p crab_city -- server +RUST_LOG=workshop=info cargo run -p workshop -- server # Debug -RUST_LOG=crab_city=debug cargo run -p crab_city -- server +RUST_LOG=workshop=debug cargo run -p workshop -- server # Specific modules -RUST_LOG=crab_city::ws=debug,crab_city::inference=trace cargo run -p crab_city -- server +RUST_LOG=workshop::ws=debug,workshop::inference=trace cargo run -p workshop -- server ``` Or use the `--debug` flag for debug-level logging: ```sh -crab server --debug +work server --debug ``` ### Key Log Messages @@ -82,12 +82,12 @@ crab server --debug ## Data Directory -All runtime data lives in `~/.crabcity/` (override with `--data-dir`): +All runtime data lives in `~/.workshop/` (override with `--data-dir`): ``` -~/.crabcity/ +~/.workshop/ ├── config.toml Configuration file -├── crabcity.db SQLite database +├── workshop.db SQLite database ├── exports/ Exported conversations └── logs/ Server logs ``` @@ -96,13 +96,13 @@ All runtime data lives in `~/.crabcity/` (override with `--data-dir`): ### Location -Default: `~/.crabcity/crabcity.db` (SQLite) +Default: `~/.workshop/workshop.db` (SQLite) ### Backup ```sh # Manual backup -cp ~/.crabcity/crabcity.db ~/.crabcity/backup-$(date +%Y%m%d).db +cp ~/.workshop/workshop.db ~/.workshop/backup-$(date +%Y%m%d).db # Using API curl -X POST http://localhost:PORT/api/admin/backup @@ -112,17 +112,17 @@ curl -X POST http://localhost:PORT/api/admin/backup ```sh # With confirmation prompt -crab server --reset-db +work server --reset-db ``` ### Import Conversations ```sh # Import all Claude Code conversations -crab server --import-all +work server --import-all # Import from specific project -crab server --import-from /path/to/project +work server --import-from /path/to/project # Via API (while running) curl -X POST http://localhost:PORT/api/admin/import \ @@ -144,11 +144,11 @@ Recommended shutdown timeout: 30 seconds. ```sh # Graceful stop -kill -TERM $(pgrep crab_city) +kill -TERM $(pgrep work) # Wait for clean shutdown, then force if needed sleep 30 -kill -9 $(pgrep crab_city) +kill -9 $(pgrep work) ``` ## Error Scenarios and Recovery diff --git a/docs/postmortem/jest_coverage_bazel.md b/docs/postmortem/jest_coverage_bazel.md index 78ae5e6..59fe7a7 100644 --- a/docs/postmortem/jest_coverage_bazel.md +++ b/docs/postmortem/jest_coverage_bazel.md @@ -2,7 +2,7 @@ ## Problem Statement -Running `bazel coverage //packages/crab_city_ui:unit_tests` produced **0% coverage +Running `bazel coverage //packages/workshop_ui:unit_tests` produced **0% coverage across all metrics** despite all 156 tests passing. Running the same test binary manually from the execroot produced 100% coverage. The coverage data was being silently discarded. @@ -20,17 +20,17 @@ filesystem for each test invocation: ``` execroot/ _main/ - packages/crab_city_ui/ # symlinks to source tree (source files) + packages/workshop_ui/ # symlinks to source tree (source files) bazel-out/ darwin_arm64-fastbuild/ bin/ - packages/crab_city_ui/ + packages/workshop_ui/ dist-test/ # ts_project compiled output (.js, .js.map) unit_tests_/ unit_tests # the test binary (shell wrapper) unit_tests.runfiles/ _main/ - packages/crab_city_ui/ + packages/workshop_ui/ dist-test/ fileLinkMatch.js -> ../../../../dist-test/fileLinkMatch.js (symlink to bin dir) fileLinkMatch.js.map -> ../../../../dist-test/fileLinkMatch.js.map @@ -42,9 +42,9 @@ There are three important directory trees: | Tree | Path | Contains | |------|------|----------| -| **Source tree** | `execroot/_main/packages/crab_city_ui/` | Original `.ts` files (symlinks to workspace) | -| **Bin dir** | `execroot/_main/bazel-out/.../bin/packages/crab_city_ui/` | `ts_project` outputs (`.js`, `.js.map`) | -| **Runfiles** | `.../unit_tests.runfiles/_main/packages/crab_city_ui/` | Symlinks to bin dir outputs | +| **Source tree** | `execroot/_main/packages/workshop_ui/` | Original `.ts` files (symlinks to workspace) | +| **Bin dir** | `execroot/_main/bazel-out/.../bin/packages/workshop_ui/` | `ts_project` outputs (`.js`, `.js.map`) | +| **Runfiles** | `.../unit_tests.runfiles/_main/packages/workshop_ui/` | Symlinks to bin dir outputs | When the **sandbox** is enabled (`darwin-sandbox` strategy), Bazel adds a fourth layer: the entire execroot is copied/symlinked into an ephemeral sandbox @@ -127,7 +127,7 @@ Jest's `rootDir` defaults to the directory containing the config file. The confi file lives in the **runfiles** tree: ``` -rootDir = .../unit_tests.runfiles/_main/packages/crab_city_ui/ +rootDir = .../unit_tests.runfiles/_main/packages/workshop_ui/ ``` But Node's `require()` / `vm.SourceTextModule` resolve symlinks when loading @@ -140,16 +140,16 @@ runfiles/.../dist-test/fileLinkMatch.js → bin/.../dist-test/fileLinkMatch.js V8 records coverage against the **resolved (real) path**: ``` -V8 URL = file:///...bazel-out/darwin_arm64-fastbuild/bin/packages/crab_city_ui/dist-test/fileLinkMatch.js +V8 URL = file:///...bazel-out/darwin_arm64-fastbuild/bin/packages/workshop_ui/dist-test/fileLinkMatch.js ``` The `startsWith` check fails because the V8 URL starts with the **bin dir** path, not the **runfiles** path: ``` -bin/.../packages/crab_city_ui/dist-test/fileLinkMatch.js +bin/.../packages/workshop_ui/dist-test/fileLinkMatch.js does NOT startWith -runfiles/_main/packages/crab_city_ui/ +runfiles/_main/packages/workshop_ui/ ``` **Every single coverage entry is filtered out. Result: 0%.** @@ -260,7 +260,7 @@ so `url.startsWith(rootDir)` passes — even inside Bazel's sandbox. The environment variables are provided by `aspect_rules_js`: - `JS_BINARY__EXECROOT`: absolute path to the Bazel execroot (sandbox path when sandboxed) - `JS_BINARY__BINDIR`: relative path like `bazel-out/darwin_arm64-fastbuild/bin` -- `JS_BINARY__PACKAGE`: the Bazel package, e.g. `packages/crab_city_ui` +- `JS_BINARY__PACKAGE`: the Bazel package, e.g. `packages/workshop_ui` - `RUNFILES`: absolute path to the runfiles root - `JS_BINARY__WORKSPACE`: workspace name, e.g. `_main` @@ -322,9 +322,9 @@ returns the directory unchanged — it doesn't chase the symlinks of its content ``` sandbox/darwin-sandbox/7292/execroot/_main/ - bazel-out/.../bin/packages/crab_city_ui/ ← real directory (realpathSync = self) + bazel-out/.../bin/packages/workshop_ui/ ← real directory (realpathSync = self) unit_tests_/unit_tests.runfiles/_main/ - packages/crab_city_ui/ + packages/workshop_ui/ dist-test/ fileLinkMatch.js ← symlink → real execroot ``` @@ -335,11 +335,11 @@ the real execroot prefix, then extract the bin dir path from it: ```javascript const probeFile = path.join(runfilesBase, workspace, pkg, 'jest.config.cjs'); const realProbe = fs.realpathSync(probeFile); -// realProbe: /real/execroot/_main/bazel-out/.../bin/packages/crab_city_ui/ -// unit_tests_/unit_tests.runfiles/_main/packages/crab_city_ui/jest.config.cjs -const idx = realProbe.indexOf(binSuffix); // binSuffix = "bazel-out/.../bin/packages/crab_city_ui" +// realProbe: /real/execroot/_main/bazel-out/.../bin/packages/workshop_ui/ +// unit_tests_/unit_tests.runfiles/_main/packages/workshop_ui/jest.config.cjs +const idx = realProbe.indexOf(binSuffix); // binSuffix = "bazel-out/.../bin/packages/workshop_ui" realBinDir = realProbe.substring(0, idx + binSuffix.length); -// realBinDir: /real/execroot/_main/bazel-out/.../bin/packages/crab_city_ui +// realBinDir: /real/execroot/_main/bazel-out/.../bin/packages/workshop_ui ``` This gives us the canonical path that V8 will use for file URLs, regardless of @@ -398,10 +398,10 @@ bin dir path (100%) and once from the runfiles path (0%). This happened because the runfiles directory is a subdirectory of the bin dir: ``` -/packages/crab_city_ui/unit_tests_/unit_tests.runfiles/_main/packages/crab_city_ui/dist-test/ +/packages/workshop_ui/unit_tests_/unit_tests.runfiles/_main/packages/workshop_ui/dist-test/ ``` -Since `rootDir` is `/packages/crab_city_ui/`, both paths pass the +Since `rootDir` is `/packages/workshop_ui/`, both paths pass the `startsWith(rootDir)` check. The runfiles copies were loaded by the haste map module but never executed (the actual execution used the resolved bin dir paths), so they reported 0%. @@ -543,7 +543,7 @@ inspect or follow its contents' symlinks. Only `realpathSync` on a *file* (which is an actual symlink) resolves to the real execroot. ``` -realpathSync("/sandbox/.../bin/packages/crab_city_ui") → same path (real dir) +realpathSync("/sandbox/.../bin/packages/workshop_ui") → same path (real dir) realpathSync("/sandbox/.../bin/.../runfiles/.../jest.config.cjs") → /real/execroot/... (symlink) ``` diff --git a/docs/state-inference.md b/docs/state-inference.md index 58f85d7..b4051a9 100644 --- a/docs/state-inference.md +++ b/docs/state-inference.md @@ -1,6 +1,6 @@ # Claude State Inference System -How crab_city detects whether Claude is idle, thinking, responding, executing a +How workshop detects whether Claude is idle, thinking, responding, executing a tool, or waiting for user input — and presents that to connected clients. ## Table of Contents diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index ee378a6..7435e34 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -1 +1 @@ -# Crab City packages +# Workshop packages diff --git a/packages/tty_wrapper/README.md b/packages/tty_wrapper/README.md index 6a7d462..68b34b7 100644 --- a/packages/tty_wrapper/README.md +++ b/packages/tty_wrapper/README.md @@ -2,7 +2,7 @@ Standalone HTTP-controlled TTY wrapper. Wraps an interactive program (shell, Claude, etc.) in a PTY and exposes it over HTTP and WebSocket. -This package is **independent** from `crab_city` — it can be built and run on its own. +This package is **independent** from `workshop` — it can be built and run on its own. ## Usage diff --git a/packages/crab_city/BUILD.bazel b/packages/workshop/BUILD.bazel similarity index 86% rename from packages/crab_city/BUILD.bazel rename to packages/workshop/BUILD.bazel index 3f44db0..234d4b8 100644 --- a/packages/crab_city/BUILD.bazel +++ b/packages/workshop/BUILD.bazel @@ -1,6 +1,6 @@ load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") -load("@crab_version//:defs.bzl", "VERSION") load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@workshop_version//:defs.bzl", "VERSION") package(default_visibility = ["//visibility:public"]) @@ -64,7 +64,7 @@ _BIN_DEPS = [ # Library crate — shared between binary, desktop app, and tests rust_library( - name = "crab_city_lib", + name = "workshop_lib", srcs = glob( ["src/**/*.rs"], exclude = [ @@ -73,7 +73,7 @@ rust_library( "src/embedded_ui.rs", ], ), - crate_name = "crab_city", + crate_name = "workshop", edition = "2024", rustc_env = {"CARGO_PKG_VERSION": VERSION}, deps = _LIB_DEPS, @@ -81,7 +81,7 @@ rust_library( # Library crate with embedded UI feature rust_library( - name = "crab_city_lib_embedded", + name = "workshop_lib_embedded", srcs = glob( ["src/**/*.rs"], exclude = [ @@ -90,40 +90,40 @@ rust_library( ], ), crate_features = ["embedded-ui"], - crate_name = "crab_city", + crate_name = "workshop", edition = "2024", rustc_env = {"CARGO_PKG_VERSION": VERSION}, deps = _LIB_DEPS + [ - "//packages/crab_city_ui/crate:crab_city_ui_crate", + "//packages/workshop_ui/crate:workshop_ui_crate", "@crate_index//:mime_guess", "@crate_index//:rust-embed", ], ) -# Base crab binary without embedded UI +# Base workshop binary without embedded UI rust_binary( - name = "crab", + name = "work", srcs = ["src/main.rs"] + glob(["src/cli/**/*.rs"]), edition = "2024", rustc_env = {"CARGO_PKG_VERSION": VERSION}, - deps = [":crab_city_lib"] + _LIB_DEPS + _BIN_DEPS, + deps = [":workshop_lib"] + _LIB_DEPS + _BIN_DEPS, ) -# crab with embedded SvelteKit UI - single binary deployment +# workshop with embedded SvelteKit UI - single binary deployment rust_binary( - name = "crab_embedded", + name = "work_embedded", srcs = ["src/main.rs"] + glob(["src/cli/**/*.rs"]), crate_features = ["embedded-ui"], edition = "2024", rustc_env = {"CARGO_PKG_VERSION": VERSION}, - deps = [":crab_city_lib_embedded"] + _LIB_DEPS + _BIN_DEPS, + deps = [":workshop_lib_embedded"] + _LIB_DEPS + _BIN_DEPS, ) # Test targets split by module for parallel execution and caching. # Each target filters to a module prefix via TESTBRIDGE_TEST_ONLY. _TEST_DEPS = ["@crate_index//:tempfile"] -# Library tests — modules whose #[cfg(test)] blocks live in :crab_city_lib +# Library tests — modules whose #[cfg(test)] blocks live in :workshop_lib _LIB_TEST_FILTERS = { "models_test": "models::tests", "ws_test": "ws::", @@ -136,7 +136,7 @@ _LIB_TEST_FILTERS = { [rust_test( name = name, - crate = ":crab_city_lib", + crate = ":workshop_lib", env = {"TESTBRIDGE_TEST_ONLY": test_filter}, deps = _TEST_DEPS, ) for name, test_filter in _LIB_TEST_FILTERS.items()] @@ -148,18 +148,18 @@ rust_test( "--skip", test_filter, ]], - crate = ":crab_city_lib", + crate = ":workshop_lib", deps = _TEST_DEPS, ) -# Binary tests — CLI modules whose #[cfg(test)] blocks live in :crab +# Binary tests — CLI modules whose #[cfg(test)] blocks live in :work _BIN_TEST_FILTERS = { "cli_test": "cli::", } [rust_test( name = name, - crate = ":crab", + crate = ":work", env = {"TESTBRIDGE_TEST_ONLY": test_filter}, deps = _TEST_DEPS, ) for name, test_filter in _BIN_TEST_FILTERS.items()] @@ -173,12 +173,12 @@ rust_test( "--skip", test_filter, ]], - crate = ":crab", + crate = ":work", deps = _TEST_DEPS, ) test_suite( - name = "crab_city_tests", + name = "workshop_tests", tests = [":" + name for name in _LIB_TEST_FILTERS.keys()] + [":" + name for name in _BIN_TEST_FILTERS.keys()] + [ ":bin_misc_test", diff --git a/packages/crab_city/CLAUDE.md b/packages/workshop/CLAUDE.md similarity index 94% rename from packages/crab_city/CLAUDE.md rename to packages/workshop/CLAUDE.md index 7f93995..882c9ab 100644 --- a/packages/crab_city/CLAUDE.md +++ b/packages/workshop/CLAUDE.md @@ -1,6 +1,6 @@ -# crab_city — CLAUDE.md +# workshop — CLAUDE.md -The main server, CLI client, and TUI picker. This is a lib+bin crate: the library (`src/lib.rs`) contains the server core, config, handlers, and WebSocket subsystem; the binary (`src/main.rs`) adds the CLI and TUI. The `crab_city_desktop` Tauri app depends on the library crate. +The main server, CLI client, and TUI picker. This is a lib+bin crate: the library (`src/lib.rs`) contains the server core, config, handlers, and WebSocket subsystem; the binary (`src/main.rs`) adds the CLI and TUI. The `workshop_desktop` Tauri app depends on the library crate. ## Module Map @@ -9,7 +9,7 @@ src/ ├── lib.rs Library root — public module declarations, re-exports AppState ├── main.rs CLI entry point (clap), server loop (uses library server functions) ├── server.rs Shared server init, router builder, EmbeddedServer, daemon file helpers -├── config.rs Figment-based layered config (CrabCityConfig, ServerConfig, AuthConfig) +├── config.rs Figment-based layered config (WorkshopConfig, ServerConfig, AuthConfig) ├── db.rs SQLite init + embedded migrations ├── models.rs Shared data types (Instance, Message, Task, User, etc.) ├── auth.rs JWT/session auth, registration, middleware (loopback bypass) @@ -80,7 +80,7 @@ Each instance carries an `InstanceKind` enum (`Structured { provider }` or `Unst ## Testing ```sh -cargo test -p crab_city +cargo test -p workshop ``` Test helpers are in `test_helpers.rs`. Tests that need a database use `tempfile` for isolated SQLite instances. diff --git a/packages/crab_city/Cargo.toml b/packages/workshop/Cargo.toml similarity index 92% rename from packages/crab_city/Cargo.toml rename to packages/workshop/Cargo.toml index 1901a6a..322cd7c 100644 --- a/packages/crab_city/Cargo.toml +++ b/packages/workshop/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "crab_city" +name = "workshop" version = "0.45.1" edition = "2024" [lib] -name = "crab_city" +name = "workshop" path = "src/lib.rs" [[bin]] -name = "crab" +name = "work" path = "src/main.rs" [dependencies] @@ -79,7 +79,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } tokio-tungstenite = { version = "0.24", features = ["connect"] } # Embedded UI assets -crab_city_ui = { path = "../crab_city_ui/crate", optional = true } +workshop_ui = { path = "../workshop_ui/crate", optional = true } rust-embed = { version = "8", optional = true } mime_guess = { version = "2", optional = true } @@ -98,4 +98,4 @@ tempfile = { workspace = true } [features] default = [] -embedded-ui = ["crab_city_ui", "rust-embed", "mime_guess"] +embedded-ui = ["workshop_ui", "rust-embed", "mime_guess"] diff --git a/packages/crab_city/migrations/001_initial_schema.sql b/packages/workshop/migrations/001_initial_schema.sql similarity index 98% rename from packages/crab_city/migrations/001_initial_schema.sql rename to packages/workshop/migrations/001_initial_schema.sql index a5cd141..b62b239 100644 --- a/packages/crab_city/migrations/001_initial_schema.sql +++ b/packages/workshop/migrations/001_initial_schema.sql @@ -1,4 +1,4 @@ --- Initial schema for Crab City conversation persistence +-- Initial schema for Workshop conversation persistence CREATE TABLE IF NOT EXISTS conversations ( id TEXT PRIMARY KEY, diff --git a/packages/crab_city/src/auth.rs b/packages/workshop/src/auth.rs similarity index 97% rename from packages/crab_city/src/auth.rs rename to packages/workshop/src/auth.rs index fa90590..85419ca 100644 --- a/packages/crab_city/src/auth.rs +++ b/packages/workshop/src/auth.rs @@ -107,7 +107,7 @@ fn extract_session_token(headers: &HeaderMap) -> Option { let cookie_header = headers.get(header::COOKIE)?.to_str().ok()?; for cookie in cookie_header.split(';') { let cookie = cookie.trim(); - if let Some(value) = cookie.strip_prefix("crab_session=") { + if let Some(value) = cookie.strip_prefix("workshop_session=") { return Some(value.to_string()); } } @@ -116,7 +116,7 @@ fn extract_session_token(headers: &HeaderMap) -> Option { fn make_session_cookie(token: &str, auth_config: &AuthConfig) -> String { let mut cookie = format!( - "crab_session={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}", + "workshop_session={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}", token, auth_config.session_ttl_secs ); if auth_config.https { @@ -126,7 +126,7 @@ fn make_session_cookie(token: &str, auth_config: &AuthConfig) -> String { } fn make_clear_cookie(auth_config: &AuthConfig) -> String { - let mut cookie = "crab_session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0".to_string(); + let mut cookie = "workshop_session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0".to_string(); if auth_config.https { cookie.push_str("; Secure"); } @@ -930,7 +930,7 @@ mod tests { let cookie = make_session_cookie("test_token", &config); - assert!(cookie.contains("crab_session=test_token")); + assert!(cookie.contains("workshop_session=test_token")); assert!(cookie.contains("HttpOnly")); assert!(cookie.contains("SameSite=Lax")); assert!(cookie.contains("Path=/")); @@ -949,7 +949,7 @@ mod tests { let cookie = make_session_cookie("test_token", &config); - assert!(cookie.contains("crab_session=test_token")); + assert!(cookie.contains("workshop_session=test_token")); assert!(cookie.contains("Secure")); assert!(cookie.contains("Max-Age=7200")); } @@ -965,7 +965,7 @@ mod tests { let cookie = make_clear_cookie(&config); - assert!(cookie.contains("crab_session=")); + assert!(cookie.contains("workshop_session=")); assert!(cookie.contains("Max-Age=0")); } @@ -977,7 +977,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert( header::COOKIE, - HeaderValue::from_static("crab_session=abc123"), + HeaderValue::from_static("workshop_session=abc123"), ); assert_eq!(extract_session_token(&headers), Some("abc123".to_string())); @@ -985,7 +985,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert( header::COOKIE, - HeaderValue::from_static("other=foo; crab_session=xyz789; another=bar"), + HeaderValue::from_static("other=foo; workshop_session=xyz789; another=bar"), ); assert_eq!(extract_session_token(&headers), Some("xyz789".to_string())); @@ -1266,7 +1266,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let set_cookie = resp.headers().get("set-cookie").unwrap().to_str().unwrap(); - assert!(set_cookie.contains("crab_session=")); + assert!(set_cookie.contains("workshop_session=")); let body: serde_json::Value = serde_json::from_slice( &axum::body::to_bytes(resp.into_body(), 10_000) @@ -1462,7 +1462,7 @@ mod tests { .split(';') .next() .unwrap() - .strip_prefix("crab_session=") + .strip_prefix("workshop_session=") .unwrap(); // Call /me with the session cookie @@ -1471,7 +1471,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/auth/me") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .body(Body::empty()) .unwrap(), ) @@ -1565,7 +1565,7 @@ mod tests { .split(';') .next() .unwrap() - .strip_prefix("crab_session=") + .strip_prefix("workshop_session=") .unwrap() .to_string(); @@ -1577,7 +1577,7 @@ mod tests { .method("POST") .uri("/api/auth/change-password") .header("content-type", "application/json") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .body(json_body(serde_json::json!({ "current_password": "password123", "new_password": "newpassword456" @@ -1634,7 +1634,7 @@ mod tests { .split(';') .next() .unwrap() - .strip_prefix("crab_session=") + .strip_prefix("workshop_session=") .unwrap() .to_string(); @@ -1646,7 +1646,7 @@ mod tests { .method("POST") .uri("/api/auth/change-password") .header("content-type", "application/json") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .body(json_body(serde_json::json!({ "current_password": "wrongpassword", "new_password": "newpassword456" @@ -1685,7 +1685,7 @@ mod tests { .split(';') .next() .unwrap() - .strip_prefix("crab_session=") + .strip_prefix("workshop_session=") .unwrap() .to_string(); @@ -1697,7 +1697,7 @@ mod tests { .method("POST") .uri("/api/auth/change-password") .header("content-type", "application/json") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .body(json_body(serde_json::json!({ "current_password": "password123", "new_password": "short" @@ -1993,7 +1993,7 @@ mod tests { .split(';') .next() .unwrap() - .strip_prefix("crab_session=") + .strip_prefix("workshop_session=") .unwrap() .to_string(); @@ -2036,7 +2036,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/protected") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .body(Body::empty()) .unwrap(), ) @@ -2057,7 +2057,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/protected") - .header("cookie", "crab_session=invalid_token") + .header("cookie", "workshop_session=invalid_token") .body(Body::empty()) .unwrap(), ) @@ -2078,7 +2078,7 @@ mod tests { Request::builder() .method("POST") .uri("/api/protected") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .header("content-type", "application/json") .body(Body::empty()) .unwrap(), @@ -2100,7 +2100,7 @@ mod tests { Request::builder() .method("POST") .uri("/api/protected") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .header("X-CSRF-Token", csrf_token) .body(Body::empty()) .unwrap(), @@ -2122,7 +2122,7 @@ mod tests { Request::builder() .method("POST") .uri("/api/protected") - .header("cookie", format!("crab_session={}", session_token)) + .header("cookie", format!("workshop_session={}", session_token)) .header("X-CSRF-Token", "wrong_csrf_token") .body(Body::empty()) .unwrap(), diff --git a/packages/crab_city/src/claude_driver.rs b/packages/workshop/src/claude_driver.rs similarity index 100% rename from packages/crab_city/src/claude_driver.rs rename to packages/workshop/src/claude_driver.rs diff --git a/packages/crab_city/src/cli/attach.rs b/packages/workshop/src/cli/attach.rs similarity index 99% rename from packages/crab_city/src/cli/attach.rs rename to packages/workshop/src/cli/attach.rs index 83b74e3..ea6ac30 100644 --- a/packages/crab_city/src/cli/attach.rs +++ b/packages/workshop/src/cli/attach.rs @@ -13,10 +13,10 @@ use tokio_tungstenite::tungstenite; use crate::cli::daemon::{DaemonError, DaemonInfo}; use crate::cli::terminal::get_terminal_size; -use crab_city::config::{MAX_SCROLLBACK_LINES, MIN_SCROLLBACK_LINES}; -use crab_city::inference::ClaudeState; -use crab_city::ws::{ClientMessage, ServerMessage}; use virtual_terminal::walk_row; +use workshop::config::{MAX_SCROLLBACK_LINES, MIN_SCROLLBACK_LINES}; +use workshop::inference::ClaudeState; +use workshop::ws::{ClientMessage, ServerMessage}; /// Default scrollback lines if config fetch fails. const DEFAULT_SCROLLBACK_LINES: usize = 10_000; @@ -510,7 +510,7 @@ fn apply_action( ) -> Option { match action { InputAction::Detach => { - eprintln!("\r\n[crab: detached]"); + eprintln!("\r\n[work: detached]"); return Some(AttachOutcome::Detached); } InputAction::ScrollUp(n) => { @@ -617,7 +617,7 @@ fn run_event_loop( loop { // 1. Drain all pending WS messages if drain_ws(ws_read_rx, vt_parser, &mut state) { - eprintln!("\r\n[crab: exited]"); + eprintln!("\r\n[work: exited]"); return Ok(AttachOutcome::Exited); } diff --git a/packages/crab_city/src/cli/auth.rs b/packages/workshop/src/cli/auth.rs similarity index 93% rename from packages/crab_city/src/cli/auth.rs rename to packages/workshop/src/cli/auth.rs index dcf9981..98080c6 100644 --- a/packages/crab_city/src/cli/auth.rs +++ b/packages/workshop/src/cli/auth.rs @@ -1,13 +1,13 @@ -//! `crab auth enable|disable|status` — manage authentication on a running daemon. +//! `work auth enable|disable|status` — manage authentication on a running daemon. use anyhow::{Context, Result}; use super::daemon::{self, DaemonInfo}; -use crab_city::config::{CrabCityConfig, FileConfig, load_config}; +use workshop::config::{FileConfig, WorkshopConfig, load_config}; -/// `crab auth enable` — write auth.enabled=true to config.toml, restart server, +/// `work auth enable` — write auth.enabled=true to config.toml, restart server, /// and if no users exist prompt for admin credentials. -pub async fn enable_command(config: &CrabCityConfig) -> Result<()> { +pub async fn enable_command(config: &WorkshopConfig) -> Result<()> { set_auth_enabled(config, true)?; eprintln!("Auth enabled in {}", config.config_toml_path().display()); @@ -30,8 +30,8 @@ pub async fn enable_command(config: &CrabCityConfig) -> Result<()> { Ok(()) } -/// `crab auth disable` — write auth.enabled=false to config.toml, restart server. -pub async fn disable_command(config: &CrabCityConfig) -> Result<()> { +/// `work auth disable` — write auth.enabled=false to config.toml, restart server. +pub async fn disable_command(config: &WorkshopConfig) -> Result<()> { set_auth_enabled(config, false)?; eprintln!("Auth disabled in {}", config.config_toml_path().display()); @@ -47,9 +47,9 @@ pub async fn disable_command(config: &CrabCityConfig) -> Result<()> { Ok(()) } -/// `crab auth status` — read config and print current auth state. +/// `work auth status` — read config and print current auth state. /// If the daemon is running, fetches effective config from `/api/admin/config`. -pub async fn status_command(config: &CrabCityConfig) -> Result<()> { +pub async fn status_command(config: &WorkshopConfig) -> Result<()> { // Try to get live config from running daemon if let Some(daemon) = daemon::check_daemon(config) && daemon::health_check_pub(&daemon).await @@ -114,7 +114,7 @@ pub async fn status_command(config: &CrabCityConfig) -> Result<()> { // --------------------------------------------------------------------------- /// Read-modify-write `config.toml` to set `[auth] enabled`. -fn set_auth_enabled(config: &CrabCityConfig, enabled: bool) -> Result<()> { +fn set_auth_enabled(config: &WorkshopConfig, enabled: bool) -> Result<()> { let path = config.config_toml_path(); // Read existing TOML or start fresh @@ -248,9 +248,9 @@ async fn maybe_create_admin(daemon: &DaemonInfo) -> Result<()> { mod tests { use super::*; - fn temp_config() -> (tempfile::TempDir, CrabCityConfig) { + fn temp_config() -> (tempfile::TempDir, WorkshopConfig) { let dir = tempfile::tempdir().unwrap(); - let config = CrabCityConfig::new(Some(dir.path().to_path_buf())).unwrap(); + let config = WorkshopConfig::new(Some(dir.path().to_path_buf())).unwrap(); (dir, config) } diff --git a/packages/crab_city/src/cli/daemon.rs b/packages/workshop/src/cli/daemon.rs similarity index 94% rename from packages/crab_city/src/cli/daemon.rs rename to packages/workshop/src/cli/daemon.rs index 888ed09..92c8ec6 100644 --- a/packages/crab_city/src/cli/daemon.rs +++ b/packages/workshop/src/cli/daemon.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use tokio_tungstenite::tungstenite; -use crab_city::config::CrabCityConfig; +use workshop::config::WorkshopConfig; #[derive(Debug, thiserror::Error)] pub enum DaemonError { @@ -58,7 +58,7 @@ impl DaemonInfo { } /// Check if a daemon is already running by reading PID/port files and verifying the process. -pub fn check_daemon(config: &CrabCityConfig) -> Option { +pub fn check_daemon(config: &WorkshopConfig) -> Option { let pid_path = config.daemon_pid_path(); let port_path = config.daemon_port_path(); @@ -91,7 +91,7 @@ pub fn check_daemon(config: &CrabCityConfig) -> Option { } /// Start a new daemon process in the background. -pub fn start_daemon(config: &CrabCityConfig) -> Result<()> { +pub fn start_daemon(config: &WorkshopConfig) -> Result<()> { let exe = std::env::current_exe().context("Failed to determine current executable")?; // Ensure log directory exists @@ -113,7 +113,7 @@ pub fn start_daemon(config: &CrabCityConfig) -> Result<()> { // Pass data-dir if non-default let default_data_dir = dirs::home_dir() .expect("Could not find home directory") - .join(".crabcity"); + .join(".workshop"); if config.data_dir != default_data_dir { cmd.arg("--data-dir").arg(&config.data_dir); } @@ -136,17 +136,17 @@ pub fn start_daemon(config: &CrabCityConfig) -> Result<()> { } /// Require a daemon to already be running. Returns an error if none is found. -pub async fn require_running_daemon(config: &CrabCityConfig) -> Result { +pub async fn require_running_daemon(config: &WorkshopConfig) -> Result { if let Some(info) = check_daemon(config) && health_check(&info).await { return Ok(info); } - anyhow::bail!("No crab daemon is running. Start one with `crab` or `crab server`.") + anyhow::bail!("No work daemon is running. Start one with `work` or `work server`.") } /// Ensure a daemon is running. Start one if needed, then wait for it to be healthy. -pub async fn ensure_daemon(config: &CrabCityConfig) -> Result { +pub async fn ensure_daemon(config: &WorkshopConfig) -> Result { // First check if already running if let Some(info) = check_daemon(config) { // Verify it's actually healthy @@ -158,7 +158,7 @@ pub async fn ensure_daemon(config: &CrabCityConfig) -> Result { let _ = std::fs::remove_file(config.daemon_port_path()); } - eprintln!("Starting crab daemon..."); + eprintln!("Starting work daemon..."); start_daemon(config)?; // Poll for the port file to appear (daemon writes it after binding) @@ -197,7 +197,7 @@ pub async fn ensure_daemon(config: &CrabCityConfig) -> Result { /// Try to rediscover a running daemon from PID/port files on disk. /// Used when the current DaemonInfo is stale (e.g. server restarted on a new port). -pub async fn rediscover_daemon(config: &CrabCityConfig) -> Option { +pub async fn rediscover_daemon(config: &WorkshopConfig) -> Option { let info = check_daemon(config)?; if health_check(&info).await { Some(info) @@ -230,24 +230,24 @@ pub fn stop_daemon(info: &DaemonInfo) { } } -// write_daemon_files / cleanup_daemon_files are now in crab_city::server +// write_daemon_files / cleanup_daemon_files are now in workshop::server #[cfg(test)] mod tests { use super::*; - use crab_city::server::write_daemon_files; + use workshop::server::write_daemon_files; - /// Build a throwaway `CrabCityConfig` rooted in a temp directory. + /// Build a throwaway `WorkshopConfig` rooted in a temp directory. /// Returns (config, _tempdir_guard) — keep the guard alive or the dir disappears. - fn temp_config() -> (CrabCityConfig, tempfile::TempDir) { + fn temp_config() -> (WorkshopConfig, tempfile::TempDir) { let tmp = tempfile::tempdir().unwrap(); let data_dir = tmp.path().to_path_buf(); let logs_dir = data_dir.join("logs"); std::fs::create_dir_all(&logs_dir).unwrap(); std::fs::create_dir_all(data_dir.join("state")).unwrap(); - let config = CrabCityConfig { + let config = WorkshopConfig { data_dir: data_dir.clone(), - db_path: data_dir.join("crabcity.db"), + db_path: data_dir.join("workshop.db"), exports_dir: data_dir.join("exports"), logs_dir, }; diff --git a/packages/crab_city/src/cli/mod.rs b/packages/workshop/src/cli/mod.rs similarity index 95% rename from packages/crab_city/src/cli/mod.rs rename to packages/workshop/src/cli/mod.rs index c354e91..2d46cfb 100644 --- a/packages/crab_city/src/cli/mod.rs +++ b/packages/workshop/src/cli/mod.rs @@ -12,20 +12,20 @@ use tokio_tungstenite::tungstenite; use tracing::{debug, error, info, warn}; use attach::AttachOutcome; -use crab_city::config::CrabCityConfig; use daemon::{DaemonError, DaemonInfo}; use picker::{PickerEvent, PickerResult}; +use workshop::config::WorkshopConfig; /// Default command: ensure daemon, show picker if instances exist, else create new. /// After detaching from a session, returns to the picker. -pub async fn default_command(config: &CrabCityConfig) -> Result<()> { +pub async fn default_command(config: &WorkshopConfig) -> Result<()> { let daemon = daemon::ensure_daemon(config).await?; // First run: if no instances at all, create one directly let instances = match fetch_instances(&daemon).await { Ok(inst) => inst, Err(DaemonError::Unavailable) => { - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => return Err(e.into()), @@ -38,7 +38,7 @@ pub async fn default_command(config: &CrabCityConfig) -> Result<()> { let instance = match create_instance(&daemon, None, Some(&cwd)).await { Ok(inst) => inst, Err(DaemonError::Unavailable) => { - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => return Err(e.into()), @@ -49,7 +49,7 @@ pub async fn default_command(config: &CrabCityConfig) -> Result<()> { delete_instance(&daemon, &instance.id).await; } Err(DaemonError::Unavailable) => { - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => return Err(e.into()), @@ -61,7 +61,7 @@ pub async fn default_command(config: &CrabCityConfig) -> Result<()> { /// Attach to an existing instance (by name, ID, or prefix). No target: show picker. /// After detaching from a session, returns to the picker. -pub async fn attach_command(config: &CrabCityConfig, target: Option) -> Result<()> { +pub async fn attach_command(config: &WorkshopConfig, target: Option) -> Result<()> { let daemon = daemon::require_running_daemon(config).await?; if let Some(t) = target { @@ -76,7 +76,7 @@ pub async fn attach_command(config: &CrabCityConfig, target: Option) -> return Ok(()); } Err(DaemonError::Unavailable) => { - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => return Err(e.into()), @@ -88,7 +88,7 @@ pub async fn attach_command(config: &CrabCityConfig, target: Option) -> /// Picker → attach → detach → picker loop. Exits on Quit or when no instances remain. /// Owns the ratatui terminal so picker and settings can share it. -async fn session_loop(config: &CrabCityConfig, daemon: DaemonInfo) -> Result<()> { +async fn session_loop(config: &WorkshopConfig, daemon: DaemonInfo) -> Result<()> { use std::io::IsTerminal; let has_tty = std::io::stdin().is_terminal(); @@ -108,12 +108,12 @@ async fn session_loop(config: &CrabCityConfig, daemon: DaemonInfo) -> Result<()> async fn session_loop_inner( terminal: &mut Option, - config: &CrabCityConfig, + config: &WorkshopConfig, mut daemon: DaemonInfo, ) -> Result<()> { /// Try to rediscover the daemon after an Unavailable error. /// Returns `true` if a new daemon was found and `daemon` was updated. - async fn try_rediscover(config: &CrabCityConfig, daemon: &mut DaemonInfo) -> bool { + async fn try_rediscover(config: &WorkshopConfig, daemon: &mut DaemonInfo) -> bool { if let Some(new) = daemon::rediscover_daemon(config).await { *daemon = new; true @@ -136,7 +136,7 @@ async fn session_loop_inner( if try_rediscover(config, &mut daemon).await { continue; } - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => return Err(e.into()), @@ -159,7 +159,7 @@ async fn session_loop_inner( if try_rediscover(config, &mut daemon).await { continue; } - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => { @@ -192,7 +192,7 @@ async fn session_loop_inner( if try_rediscover(config, &mut daemon).await { continue; } - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => { @@ -211,7 +211,7 @@ async fn session_loop_inner( if try_rediscover(config, &mut daemon).await { continue; } - eprintln!("[crab: server stopped]"); + eprintln!("[work: server stopped]"); return Ok(()); } Err(e) => { @@ -323,7 +323,7 @@ enum WsLifecycleEvent { } /// Kill a specific session by name, ID, or prefix. -pub async fn kill_command(config: &CrabCityConfig, target: &str) -> Result<()> { +pub async fn kill_command(config: &WorkshopConfig, target: &str) -> Result<()> { let daemon = daemon::require_running_daemon(config).await?; let instance_id = resolve_instance(&daemon, target).await?; delete_instance(&daemon, &instance_id).await; @@ -339,7 +339,7 @@ pub async fn kill_command(config: &CrabCityConfig, target: &str) -> Result<()> { } /// Stop the daemon and all sessions. -pub async fn kill_server_command(config: &CrabCityConfig, force: bool) -> Result<()> { +pub async fn kill_server_command(config: &WorkshopConfig, force: bool) -> Result<()> { let daemon = daemon::require_running_daemon(config).await?; if !force { @@ -368,7 +368,7 @@ pub async fn kill_server_command(config: &CrabCityConfig, force: bool) -> Result } /// List running instances. -pub async fn list_command(config: &CrabCityConfig, json: bool) -> Result<()> { +pub async fn list_command(config: &WorkshopConfig, json: bool) -> Result<()> { let daemon = daemon::ensure_daemon(config).await?; let instances = fetch_instances(&daemon).await?; @@ -505,7 +505,7 @@ async fn resolve_instance(daemon: &DaemonInfo, target: &str) -> Result { /// Tries exact ID match, then name match, then ID prefix match. fn match_instance(instances: &[InstanceInfo], target: &str) -> Result { if instances.is_empty() { - anyhow::bail!("No running instances. Use `crab` to create one."); + anyhow::bail!("No running instances. Use `work` to create one."); } // Try exact ID match diff --git a/packages/crab_city/src/cli/picker.rs b/packages/workshop/src/cli/picker.rs similarity index 99% rename from packages/crab_city/src/cli/picker.rs rename to packages/workshop/src/cli/picker.rs index 37c63e2..fc61e67 100644 --- a/packages/crab_city/src/cli/picker.rs +++ b/packages/workshop/src/cli/picker.rs @@ -151,7 +151,7 @@ fn picker_loop( let list = List::new(items) .block( Block::default() - .title(" crab: select session ") + .title(" work: select session ") .title( Line::styled( format!(" {} ", base_url), diff --git a/packages/crab_city/src/cli/settings.rs b/packages/workshop/src/cli/settings.rs similarity index 99% rename from packages/crab_city/src/cli/settings.rs rename to packages/workshop/src/cli/settings.rs index 59529b8..f6ceca7 100644 --- a/packages/crab_city/src/cli/settings.rs +++ b/packages/workshop/src/cli/settings.rs @@ -13,7 +13,7 @@ use serde::Deserialize; use std::time::Duration; use super::daemon::DaemonInfo; -use crab_city::config::{MAX_SCROLLBACK_LINES, MIN_SCROLLBACK_LINES}; +use workshop::config::{MAX_SCROLLBACK_LINES, MIN_SCROLLBACK_LINES}; /// Mirrors the server's GET /api/admin/config response. #[derive(Deserialize, Clone, Debug)] diff --git a/packages/crab_city/src/cli/terminal.rs b/packages/workshop/src/cli/terminal.rs similarity index 100% rename from packages/crab_city/src/cli/terminal.rs rename to packages/workshop/src/cli/terminal.rs diff --git a/packages/crab_city/src/config.rs b/packages/workshop/src/config.rs similarity index 94% rename from packages/crab_city/src/config.rs rename to packages/workshop/src/config.rs index 190fb52..89a8ef3 100644 --- a/packages/crab_city/src/config.rs +++ b/packages/workshop/src/config.rs @@ -13,9 +13,9 @@ use tracing::info; // config.toml: [auth] // enabled = true // -// env var: CRAB_AUTH__ENABLED=true (double underscore = nesting) +// env var: WORKSHOP_AUTH__ENABLED=true (double underscore = nesting) // -// (single underscore stays within field names: CRAB_AUTH__SESSION_TTL_SECS) +// (single underscore stays within field names: WORKSHOP_AUTH__SESSION_TTL_SECS) /// Named configuration presets. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, clap::ValueEnum)] @@ -123,14 +123,14 @@ pub const MIN_SCROLLBACK_LINES: usize = 100; /// Maximum scrollback lines (~400MB worst-case at 80 cols). pub const MAX_SCROLLBACK_LINES: usize = 100_000; -/// Build a figment that layers: defaults → profile defaults → config.toml → CRAB_* env vars. +/// Build a figment that layers: defaults → profile defaults → config.toml → WORKSHOP_* env vars. /// /// Profile defaults sit above struct defaults but below config.toml/env. /// The CLI profile takes priority over the config file profile. /// /// Env vars use double-underscore for nesting into sections: -/// `CRAB_AUTH__ENABLED=true` → `auth.enabled = true` -/// `CRAB_SERVER__MAX_BUFFER_MB=50` → `server.max_buffer_mb = 50` +/// `WORKSHOP_AUTH__ENABLED=true` → `auth.enabled = true` +/// `WORKSHOP_SERVER__MAX_BUFFER_MB=50` → `server.max_buffer_mb = 50` pub fn load_config(data_dir: &Path, cli_profile: Option<&Profile>) -> figment::Figment { use figment::{ Figment, @@ -140,7 +140,7 @@ pub fn load_config(data_dir: &Path, cli_profile: Option<&Profile>) -> figment::F // Pass 1: peek at profile from config.toml/env (CLI overrides file) let base = Figment::from(Serialized::defaults(FileConfig::default())) .merge(Toml::file(data_dir.join("config.toml"))) - .merge(Env::prefixed("CRAB_").split("__")); + .merge(Env::prefixed("WORKSHOP_").split("__")); let profile: Option = cli_profile .cloned() @@ -152,7 +152,7 @@ pub fn load_config(data_dir: &Path, cli_profile: Option<&Profile>) -> figment::F Figment::from(Serialized::defaults(FileConfig::default())) .merge(Serialized::defaults(profile_layer)) .merge(Toml::file(data_dir.join("config.toml"))) - .merge(Env::prefixed("CRAB_").split("__")) + .merge(Env::prefixed("WORKSHOP_").split("__")) } /// Convert a profile into a `FileConfig` with the profile's default values filled in. @@ -322,7 +322,7 @@ impl ServerConfig { // ============================================================================= #[derive(Clone, Debug)] -pub struct CrabCityConfig { +pub struct WorkshopConfig { pub data_dir: PathBuf, pub db_path: PathBuf, #[allow(dead_code)] @@ -330,12 +330,12 @@ pub struct CrabCityConfig { pub logs_dir: PathBuf, } -impl CrabCityConfig { +impl WorkshopConfig { pub fn new(custom_dir: Option) -> Result { let data_dir = custom_dir.unwrap_or_else(|| { dirs::home_dir() .expect("Could not find home directory") - .join(".crabcity") + .join(".workshop") }); std::fs::create_dir_all(&data_dir) @@ -353,7 +353,7 @@ impl CrabCityConfig { std::fs::create_dir_all(&state_dir) .with_context(|| format!("Failed to create state directory: {:?}", state_dir))?; - let db_path = data_dir.join("crabcity.db"); + let db_path = data_dir.join("workshop.db"); info!("Data directory: {}", data_dir.display()); @@ -532,15 +532,15 @@ mod tests { assert_eq!(sc.instance.hang_timeout.unwrap().as_secs(), 600); } - // ── CrabCityConfig ────────────────────────────────────────────────── + // ── WorkshopConfig ────────────────────────────────────────────────── #[test] - fn test_crab_city_config_with_custom_dir() { + fn test_workshop_config_with_custom_dir() { let tmp = tempfile::tempdir().unwrap(); - let config = CrabCityConfig::new(Some(tmp.path().to_path_buf())).unwrap(); + let config = WorkshopConfig::new(Some(tmp.path().to_path_buf())).unwrap(); assert_eq!(config.data_dir, tmp.path()); - assert_eq!(config.db_path, tmp.path().join("crabcity.db")); + assert_eq!(config.db_path, tmp.path().join("workshop.db")); assert_eq!(config.exports_dir, tmp.path().join("exports")); assert_eq!(config.logs_dir, tmp.path().join("logs")); assert!(tmp.path().join("exports").exists()); @@ -551,17 +551,17 @@ mod tests { #[test] fn test_db_url() { let tmp = tempfile::tempdir().unwrap(); - let config = CrabCityConfig::new(Some(tmp.path().to_path_buf())).unwrap(); + let config = WorkshopConfig::new(Some(tmp.path().to_path_buf())).unwrap(); let url = config.db_url(); assert!(url.starts_with("sqlite://")); - assert!(url.contains("crabcity.db")); + assert!(url.contains("workshop.db")); assert!(url.ends_with("?mode=rwc")); } #[test] fn test_path_helpers() { let tmp = tempfile::tempdir().unwrap(); - let config = CrabCityConfig::new(Some(tmp.path().to_path_buf())).unwrap(); + let config = WorkshopConfig::new(Some(tmp.path().to_path_buf())).unwrap(); assert_eq!(config.state_dir(), tmp.path().join("state")); assert_eq!( @@ -580,7 +580,7 @@ mod tests { #[test] fn test_reset_database() { let tmp = tempfile::tempdir().unwrap(); - let config = CrabCityConfig::new(Some(tmp.path().to_path_buf())).unwrap(); + let config = WorkshopConfig::new(Some(tmp.path().to_path_buf())).unwrap(); // Create fake db files std::fs::write(&config.db_path, "fake db").unwrap(); @@ -599,7 +599,7 @@ mod tests { #[test] fn test_reset_database_no_file() { let tmp = tempfile::tempdir().unwrap(); - let config = CrabCityConfig::new(Some(tmp.path().to_path_buf())).unwrap(); + let config = WorkshopConfig::new(Some(tmp.path().to_path_buf())).unwrap(); // Should not error when file doesn't exist config.reset_database().unwrap(); } diff --git a/packages/crab_city/src/db.rs b/packages/workshop/src/db.rs similarity index 99% rename from packages/crab_city/src/db.rs rename to packages/workshop/src/db.rs index b734ad9..4672424 100644 --- a/packages/crab_city/src/db.rs +++ b/packages/workshop/src/db.rs @@ -3,7 +3,7 @@ use sqlx::Row; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; use tracing::info; -use crate::config::CrabCityConfig; +use crate::config::WorkshopConfig; #[derive(Clone)] pub struct Database { @@ -11,7 +11,7 @@ pub struct Database { } impl Database { - pub async fn new(config: &CrabCityConfig) -> Result { + pub async fn new(config: &WorkshopConfig) -> Result { info!("🗄️ Connecting to database: {}", config.db_path.display()); let pool = SqlitePoolOptions::new() diff --git a/packages/crab_city/src/embedded_ui.rs b/packages/workshop/src/embedded_ui.rs similarity index 97% rename from packages/crab_city/src/embedded_ui.rs rename to packages/workshop/src/embedded_ui.rs index b68cc41..84c56eb 100644 --- a/packages/crab_city/src/embedded_ui.rs +++ b/packages/workshop/src/embedded_ui.rs @@ -1,7 +1,7 @@ //! Embedded UI serving when compiled with the `embedded-ui` feature. //! //! This module serves the SvelteKit SPA via Axum using assets from the -//! crab_city_ui crate. +//! workshop_ui crate. use axum::{ Router, @@ -10,7 +10,7 @@ use axum::{ response::IntoResponse, routing::get, }; -use crab_city_ui::Assets as UiAssets; +use workshop_ui::Assets as UiAssets; /// Create a router that serves the embedded SPA under /spa/ pub fn spa_router() -> Router { diff --git a/packages/crab_city/src/files/browser.rs b/packages/workshop/src/files/browser.rs similarity index 100% rename from packages/crab_city/src/files/browser.rs rename to packages/workshop/src/files/browser.rs diff --git a/packages/crab_city/src/files/mod.rs b/packages/workshop/src/files/mod.rs similarity index 100% rename from packages/crab_city/src/files/mod.rs rename to packages/workshop/src/files/mod.rs diff --git a/packages/crab_city/src/files/reader.rs b/packages/workshop/src/files/reader.rs similarity index 100% rename from packages/crab_city/src/files/reader.rs rename to packages/workshop/src/files/reader.rs diff --git a/packages/crab_city/src/files/search.rs b/packages/workshop/src/files/search.rs similarity index 100% rename from packages/crab_city/src/files/search.rs rename to packages/workshop/src/files/search.rs diff --git a/packages/crab_city/src/files/types.rs b/packages/workshop/src/files/types.rs similarity index 100% rename from packages/crab_city/src/files/types.rs rename to packages/workshop/src/files/types.rs diff --git a/packages/crab_city/src/git/branches.rs b/packages/workshop/src/git/branches.rs similarity index 100% rename from packages/crab_city/src/git/branches.rs rename to packages/workshop/src/git/branches.rs diff --git a/packages/crab_city/src/git/diff.rs b/packages/workshop/src/git/diff.rs similarity index 100% rename from packages/crab_city/src/git/diff.rs rename to packages/workshop/src/git/diff.rs diff --git a/packages/crab_city/src/git/executor.rs b/packages/workshop/src/git/executor.rs similarity index 100% rename from packages/crab_city/src/git/executor.rs rename to packages/workshop/src/git/executor.rs diff --git a/packages/crab_city/src/git/log.rs b/packages/workshop/src/git/log.rs similarity index 100% rename from packages/crab_city/src/git/log.rs rename to packages/workshop/src/git/log.rs diff --git a/packages/crab_city/src/git/mod.rs b/packages/workshop/src/git/mod.rs similarity index 100% rename from packages/crab_city/src/git/mod.rs rename to packages/workshop/src/git/mod.rs diff --git a/packages/crab_city/src/git/status.rs b/packages/workshop/src/git/status.rs similarity index 100% rename from packages/crab_city/src/git/status.rs rename to packages/workshop/src/git/status.rs diff --git a/packages/crab_city/src/git/types.rs b/packages/workshop/src/git/types.rs similarity index 100% rename from packages/crab_city/src/git/types.rs rename to packages/workshop/src/git/types.rs diff --git a/packages/crab_city/src/handlers/admin.rs b/packages/workshop/src/handlers/admin.rs similarity index 99% rename from packages/crab_city/src/handlers/admin.rs rename to packages/workshop/src/handlers/admin.rs index d72d1b3..a19c720 100644 --- a/packages/crab_city/src/handlers/admin.rs +++ b/packages/workshop/src/handlers/admin.rs @@ -588,7 +588,7 @@ pub async fn patch_config_handler( /// Read-modify-write config.toml to persist the given overrides. fn save_overrides_to_config( - config: &crate::config::CrabCityConfig, + config: &crate::config::WorkshopConfig, req: &ConfigPatchRequest, ) -> anyhow::Result<()> { use anyhow::Context; @@ -662,10 +662,10 @@ fn save_overrides_to_config( #[cfg(test)] mod tests { use super::*; - use crate::config::CrabCityConfig; + use crate::config::WorkshopConfig; - fn make_config(tmp: &std::path::Path) -> CrabCityConfig { - CrabCityConfig::new(Some(tmp.to_path_buf())).unwrap() + fn make_config(tmp: &std::path::Path) -> WorkshopConfig { + WorkshopConfig::new(Some(tmp.to_path_buf())).unwrap() } #[test] diff --git a/packages/crab_city/src/handlers/browse.rs b/packages/workshop/src/handlers/browse.rs similarity index 100% rename from packages/crab_city/src/handlers/browse.rs rename to packages/workshop/src/handlers/browse.rs diff --git a/packages/crab_city/src/handlers/bug_report.rs b/packages/workshop/src/handlers/bug_report.rs similarity index 100% rename from packages/crab_city/src/handlers/bug_report.rs rename to packages/workshop/src/handlers/bug_report.rs diff --git a/packages/crab_city/src/handlers/conversations/database.rs b/packages/workshop/src/handlers/conversations/database.rs similarity index 100% rename from packages/crab_city/src/handlers/conversations/database.rs rename to packages/workshop/src/handlers/conversations/database.rs diff --git a/packages/crab_city/src/handlers/conversations/format.rs b/packages/workshop/src/handlers/conversations/format.rs similarity index 100% rename from packages/crab_city/src/handlers/conversations/format.rs rename to packages/workshop/src/handlers/conversations/format.rs diff --git a/packages/crab_city/src/handlers/conversations/live.rs b/packages/workshop/src/handlers/conversations/live.rs similarity index 100% rename from packages/crab_city/src/handlers/conversations/live.rs rename to packages/workshop/src/handlers/conversations/live.rs diff --git a/packages/crab_city/src/handlers/conversations/mod.rs b/packages/workshop/src/handlers/conversations/mod.rs similarity index 100% rename from packages/crab_city/src/handlers/conversations/mod.rs rename to packages/workshop/src/handlers/conversations/mod.rs diff --git a/packages/crab_city/src/handlers/health.rs b/packages/workshop/src/handlers/health.rs similarity index 100% rename from packages/crab_city/src/handlers/health.rs rename to packages/workshop/src/handlers/health.rs diff --git a/packages/crab_city/src/handlers/inbox.rs b/packages/workshop/src/handlers/inbox.rs similarity index 100% rename from packages/crab_city/src/handlers/inbox.rs rename to packages/workshop/src/handlers/inbox.rs diff --git a/packages/crab_city/src/handlers/instances.rs b/packages/workshop/src/handlers/instances.rs similarity index 99% rename from packages/crab_city/src/handlers/instances.rs rename to packages/workshop/src/handlers/instances.rs index d40a8ae..0786a98 100644 --- a/packages/crab_city/src/handlers/instances.rs +++ b/packages/workshop/src/handlers/instances.rs @@ -507,9 +507,9 @@ mod tests { #[tokio::test] async fn test_set_custom_name_request_deserialization() { - let json = r#"{"custom_name": "My Crab"}"#; + let json = r#"{"custom_name": "My Instance"}"#; let req: SetCustomNameRequest = serde_json::from_str(json).unwrap(); - assert_eq!(req.custom_name.as_deref(), Some("My Crab")); + assert_eq!(req.custom_name.as_deref(), Some("My Instance")); let json_null = r#"{"custom_name": null}"#; let req2: SetCustomNameRequest = serde_json::from_str(json_null).unwrap(); @@ -679,7 +679,7 @@ mod tests { .method("POST") .uri("/instances") .header("content-type", "application/json") - .body(Body::from(r#"{"name":"my-crab","working_dir":"/tmp"}"#)) + .body(Body::from(r#"{"name":"my-instance","working_dir":"/tmp"}"#)) .unwrap(), ) .await @@ -690,7 +690,7 @@ mod tests { .await .unwrap(); let created: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(created["name"], "my-crab"); + assert_eq!(created["name"], "my-instance"); } #[tokio::test] diff --git a/packages/crab_city/src/handlers/mod.rs b/packages/workshop/src/handlers/mod.rs similarity index 100% rename from packages/crab_city/src/handlers/mod.rs rename to packages/workshop/src/handlers/mod.rs diff --git a/packages/crab_city/src/handlers/notes.rs b/packages/workshop/src/handlers/notes.rs similarity index 100% rename from packages/crab_city/src/handlers/notes.rs rename to packages/workshop/src/handlers/notes.rs diff --git a/packages/crab_city/src/handlers/settings.rs b/packages/workshop/src/handlers/settings.rs similarity index 100% rename from packages/crab_city/src/handlers/settings.rs rename to packages/workshop/src/handlers/settings.rs diff --git a/packages/crab_city/src/handlers/tasks.rs b/packages/workshop/src/handlers/tasks.rs similarity index 100% rename from packages/crab_city/src/handlers/tasks.rs rename to packages/workshop/src/handlers/tasks.rs diff --git a/packages/crab_city/src/handlers/websocket.rs b/packages/workshop/src/handlers/websocket.rs similarity index 100% rename from packages/crab_city/src/handlers/websocket.rs rename to packages/workshop/src/handlers/websocket.rs diff --git a/packages/crab_city/src/import.rs b/packages/workshop/src/import.rs similarity index 100% rename from packages/crab_city/src/import.rs rename to packages/workshop/src/import.rs diff --git a/packages/crab_city/src/inference/manager.rs b/packages/workshop/src/inference/manager.rs similarity index 100% rename from packages/crab_city/src/inference/manager.rs rename to packages/workshop/src/inference/manager.rs diff --git a/packages/crab_city/src/inference/mod.rs b/packages/workshop/src/inference/mod.rs similarity index 100% rename from packages/crab_city/src/inference/mod.rs rename to packages/workshop/src/inference/mod.rs diff --git a/packages/crab_city/src/inference/state.rs b/packages/workshop/src/inference/state.rs similarity index 100% rename from packages/crab_city/src/inference/state.rs rename to packages/workshop/src/inference/state.rs diff --git a/packages/crab_city/src/instance_actor.rs b/packages/workshop/src/instance_actor.rs similarity index 100% rename from packages/crab_city/src/instance_actor.rs rename to packages/workshop/src/instance_actor.rs diff --git a/packages/crab_city/src/instance_manager.rs b/packages/workshop/src/instance_manager.rs similarity index 99% rename from packages/crab_city/src/instance_manager.rs rename to packages/workshop/src/instance_manager.rs index da54e78..106e004 100644 --- a/packages/crab_city/src/instance_manager.rs +++ b/packages/workshop/src/instance_manager.rs @@ -526,7 +526,7 @@ mod tests { let inst = ClaudeInstance { id: "inst-1".to_string(), name: "swift-azure-falcon".to_string(), - custom_name: Some("My Crab".to_string()), + custom_name: Some("My Instance".to_string()), wrapper_port: 0, working_dir: "/tmp".to_string(), command: "claude".to_string(), @@ -542,12 +542,12 @@ mod tests { let json = serde_json::to_value(&inst).unwrap(); assert_eq!(json["id"], "inst-1"); assert_eq!(json["name"], "swift-azure-falcon"); - assert_eq!(json["custom_name"], "My Crab"); + assert_eq!(json["custom_name"], "My Instance"); assert_eq!(json["running"], true); assert_eq!(json["session_id"], "sess-abc"); let rt: ClaudeInstance = serde_json::from_value(json).unwrap(); assert_eq!(rt.id, "inst-1"); - assert_eq!(rt.custom_name, Some("My Crab".to_string())); + assert_eq!(rt.custom_name, Some("My Instance".to_string())); } #[test] diff --git a/packages/crab_city/src/lib.rs b/packages/workshop/src/lib.rs similarity index 100% rename from packages/crab_city/src/lib.rs rename to packages/workshop/src/lib.rs diff --git a/packages/crab_city/src/main.rs b/packages/workshop/src/main.rs similarity index 93% rename from packages/crab_city/src/main.rs rename to packages/workshop/src/main.rs index 2fe92b0..a0b8c68 100644 --- a/packages/crab_city/src/main.rs +++ b/packages/workshop/src/main.rs @@ -10,20 +10,20 @@ use tracing_subscriber::prelude::*; mod cli; -use crab_city::config::{ - AuthConfig, CrabCityConfig, FileConfig, Profile, ServerConfig, load_config, +use workshop::config::{ + AuthConfig, FileConfig, Profile, ServerConfig, WorkshopConfig, load_config, }; -use crab_city::server; +use workshop::server; #[derive(Parser)] -#[command(name = "crab")] +#[command(name = "work")] #[command(version = VERSION)] #[command(about = "Terminal multiplexer for Claude Code instances")] struct Cli { #[command(subcommand)] command: Option, - /// Custom data directory (defaults to ~/.crabcity) + /// Custom data directory (defaults to ~/.workshop) #[arg(long, global = true)] data_dir: Option, } @@ -138,7 +138,7 @@ enum AuthCommands { async fn main() -> Result<()> { let cli = Cli::parse(); - let config = CrabCityConfig::new(cli.data_dir.clone())?; + let config = WorkshopConfig::new(cli.data_dir.clone())?; // Install crash diagnostics (terminal restore + crash report file) install_panic_hook(config.logs_dir.clone()); @@ -150,7 +150,7 @@ async fn main() -> Result<()> { match cli.command { None => { - // Bare `crab`: create new instance in cwd and attach + // Bare `work`: create new instance in cwd and attach cli::default_command(&config).await } Some(Commands::Attach(args)) => cli::attach_command(&config, args.target).await, @@ -186,7 +186,7 @@ fn install_panic_hook(logs_dir: std::path::PathBuf) { let crash_path = logs_dir.join(format!("crash-{timestamp}.log")); if let Ok(mut f) = std::fs::File::create(&crash_path) { use std::io::Write; - let _ = writeln!(f, "=== Crab City Crash Report ==="); + let _ = writeln!(f, "=== Workshop Crash Report ==="); let _ = writeln!(f, "Time: {}", chrono::Local::now()); let _ = writeln!(f, "Version: {VERSION}"); let _ = writeln!(f); @@ -203,7 +203,7 @@ fn install_panic_hook(logs_dir: std::path::PathBuf) { let _ = writeln!(f); let _ = writeln!(f, "Backtrace:\n{backtrace}"); - eprintln!("\n[crab] crash report saved to {}", crash_path.display()); + eprintln!("\n[work] crash report saved to {}", crash_path.display()); } // 4. Chain to default hook for the standard panic output @@ -224,7 +224,7 @@ fn init_cli_tracing(logs_dir: &std::path::Path) { let writer = std::sync::Mutex::new(file); let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("crab=debug,warn")); + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("workshop=debug,warn")); tracing_subscriber::registry() .with( @@ -236,12 +236,12 @@ fn init_cli_tracing(logs_dir: &std::path::Path) { .init(); } -async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { +async fn run_server(args: ServerArgs, config: WorkshopConfig) -> Result<()> { // Setup logging let default_directive = if args.debug { - "crab=debug,tower_http=debug,info" + "workshop=debug,tower_http=debug,info" } else { - "crab=info,tower_http=info,warn" + "workshop=info,tower_http=info,warn" }; let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_directive)); @@ -250,7 +250,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { .with(env_filter) .init(); - info!("Starting Crab City - Claude Code Instance Manager"); + info!("Starting Workshop - Claude Code Instance Manager"); let config = Arc::new(config); @@ -277,7 +277,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { let pid_info = std::fs::read_to_string(config.daemon_pid_path()) .unwrap_or_else(|_| "unknown".to_string()); anyhow::bail!( - "Another server is already running (PID {}). Use `crab kill-server` to stop it.", + "Another server is already running (PID {}). Use `work kill-server` to stop it.", pid_info.trim() ); } @@ -302,7 +302,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { .unwrap_or_default(); if args.reset_admin { if fc_initial.auth.enabled { - crab_city::onboarding::reset_admin(&core.repository).await?; + workshop::onboarding::reset_admin(&core.repository).await?; } else { warn!("--reset-admin ignored: auth is not enabled"); } @@ -364,7 +364,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { auth_config_raw.session_ttl_secs ); } else { - info!("Authentication disabled (use `crab auth enable` to enable)"); + info!("Authentication disabled (use `work auth enable` to enable)"); } info!( @@ -375,7 +375,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { // Onboarding only on first iteration if first_iteration { - crab_city::onboarding::maybe_run_onboarding(&core.repository, &auth_config_raw).await?; + workshop::onboarding::maybe_run_onboarding(&core.repository, &auth_config_raw).await?; } let auth_config = Arc::new(auth_config_raw); @@ -430,7 +430,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { server::write_daemon_files(&config, pid, actual_addr.port())?; if first_iteration { - info!("Crab City listening on http://{}", actual_addr); + info!("Workshop listening on http://{}", actual_addr); info!(""); info!("Web UI: http://{}/", actual_addr); info!("API endpoints:"); @@ -462,7 +462,7 @@ async fn run_server(args: ServerArgs, config: CrabCityConfig) -> Result<()> { ctrl_c.await.expect("Failed to install Ctrl+C handler"); } info!("Received shutdown signal, notifying clients..."); - shutdown_gsm.broadcast_lifecycle(crab_city::ws::ServerMessage::Shutdown { + shutdown_gsm.broadcast_lifecycle(workshop::ws::ServerMessage::Shutdown { reason: "Server shutting down".to_string(), }); }; diff --git a/packages/crab_city/src/metrics.rs b/packages/workshop/src/metrics.rs similarity index 100% rename from packages/crab_city/src/metrics.rs rename to packages/workshop/src/metrics.rs diff --git a/packages/crab_city/src/models.rs b/packages/workshop/src/models.rs similarity index 100% rename from packages/crab_city/src/models.rs rename to packages/workshop/src/models.rs diff --git a/packages/crab_city/src/notes.rs b/packages/workshop/src/notes.rs similarity index 100% rename from packages/crab_city/src/notes.rs rename to packages/workshop/src/notes.rs diff --git a/packages/crab_city/src/onboarding.rs b/packages/workshop/src/onboarding.rs similarity index 96% rename from packages/crab_city/src/onboarding.rs rename to packages/workshop/src/onboarding.rs index 96cf138..2cbc415 100644 --- a/packages/crab_city/src/onboarding.rs +++ b/packages/workshop/src/onboarding.rs @@ -2,7 +2,7 @@ //! //! Supports two modes: //! - **Interactive**: prompts the operator in the terminal -//! - **Headless**: reads from `CRAB_CITY_ADMIN_USERNAME` / `CRAB_CITY_ADMIN_PASSWORD` env vars +//! - **Headless**: reads from `WORKSHOP_ADMIN_USERNAME` / `WORKSHOP_ADMIN_PASSWORD` env vars use anyhow::{Result, bail}; use tracing::info; @@ -32,12 +32,12 @@ pub async fn maybe_run_onboarding( info!("No users found -- running first-time admin setup"); // Check for headless env vars first - let env_username = std::env::var("CRAB_CITY_ADMIN_USERNAME").ok(); - let env_password = std::env::var("CRAB_CITY_ADMIN_PASSWORD").ok(); + let env_username = std::env::var("WORKSHOP_ADMIN_USERNAME").ok(); + let env_password = std::env::var("WORKSHOP_ADMIN_PASSWORD").ok(); let (username, password, display_name) = match (env_username, env_password) { (Some(u), Some(p)) => { - let dn = std::env::var("CRAB_CITY_ADMIN_DISPLAY_NAME") + let dn = std::env::var("WORKSHOP_ADMIN_DISPLAY_NAME") .ok() .unwrap_or_else(|| u.clone()); info!("Creating admin from environment variables (headless mode)"); @@ -107,7 +107,7 @@ pub async fn reset_admin(repository: &ConversationRepository) -> Result<()> { let stdout = io::stdout(); println!(); - println!("=== Crab City: Reset Admin Password ==="); + println!("=== Workshop: Reset Admin Password ==="); println!(); // Username @@ -176,7 +176,7 @@ async fn interactive_prompt() -> Result<(String, String, String)> { let stdout = io::stdout(); println!(); - println!("=== Crab City: First-Time Setup ==="); + println!("=== Workshop: First-Time Setup ==="); println!(); println!("Auth is enabled but no accounts exist yet."); println!("Create the initial admin account:"); diff --git a/packages/crab_city/src/persistence.rs b/packages/workshop/src/persistence.rs similarity index 100% rename from packages/crab_city/src/persistence.rs rename to packages/workshop/src/persistence.rs diff --git a/packages/crab_city/src/process_driver.rs b/packages/workshop/src/process_driver.rs similarity index 100% rename from packages/crab_city/src/process_driver.rs rename to packages/workshop/src/process_driver.rs diff --git a/packages/crab_city/src/repository/attributions.rs b/packages/workshop/src/repository/attributions.rs similarity index 100% rename from packages/crab_city/src/repository/attributions.rs rename to packages/workshop/src/repository/attributions.rs diff --git a/packages/crab_city/src/repository/auth.rs b/packages/workshop/src/repository/auth.rs similarity index 100% rename from packages/crab_city/src/repository/auth.rs rename to packages/workshop/src/repository/auth.rs diff --git a/packages/crab_city/src/repository/chat.rs b/packages/workshop/src/repository/chat.rs similarity index 100% rename from packages/crab_city/src/repository/chat.rs rename to packages/workshop/src/repository/chat.rs diff --git a/packages/crab_city/src/repository/conversations.rs b/packages/workshop/src/repository/conversations.rs similarity index 100% rename from packages/crab_city/src/repository/conversations.rs rename to packages/workshop/src/repository/conversations.rs diff --git a/packages/crab_city/src/repository/entries.rs b/packages/workshop/src/repository/entries.rs similarity index 100% rename from packages/crab_city/src/repository/entries.rs rename to packages/workshop/src/repository/entries.rs diff --git a/packages/crab_city/src/repository/inbox.rs b/packages/workshop/src/repository/inbox.rs similarity index 100% rename from packages/crab_city/src/repository/inbox.rs rename to packages/workshop/src/repository/inbox.rs diff --git a/packages/crab_city/src/repository/mod.rs b/packages/workshop/src/repository/mod.rs similarity index 100% rename from packages/crab_city/src/repository/mod.rs rename to packages/workshop/src/repository/mod.rs diff --git a/packages/crab_city/src/repository/search.rs b/packages/workshop/src/repository/search.rs similarity index 100% rename from packages/crab_city/src/repository/search.rs rename to packages/workshop/src/repository/search.rs diff --git a/packages/crab_city/src/repository/settings.rs b/packages/workshop/src/repository/settings.rs similarity index 100% rename from packages/crab_city/src/repository/settings.rs rename to packages/workshop/src/repository/settings.rs diff --git a/packages/crab_city/src/repository/tasks.rs b/packages/workshop/src/repository/tasks.rs similarity index 100% rename from packages/crab_city/src/repository/tasks.rs rename to packages/workshop/src/repository/tasks.rs diff --git a/packages/crab_city/src/repository/test_helpers.rs b/packages/workshop/src/repository/test_helpers.rs similarity index 100% rename from packages/crab_city/src/repository/test_helpers.rs rename to packages/workshop/src/repository/test_helpers.rs diff --git a/packages/crab_city/src/server.rs b/packages/workshop/src/server.rs similarity index 98% rename from packages/crab_city/src/server.rs rename to packages/workshop/src/server.rs index fe7d1b2..e295a9b 100644 --- a/packages/crab_city/src/server.rs +++ b/packages/workshop/src/server.rs @@ -17,7 +17,7 @@ use uuid::Uuid; use crate::auth::AuthState; use crate::config::{ - AuthConfig, CrabCityConfig, FileConfig, Profile, RuntimeOverrides, ServerConfig, load_config, + AuthConfig, FileConfig, Profile, RuntimeOverrides, ServerConfig, WorkshopConfig, load_config, }; /// Callback for reporting startup progress to a host (e.g. the desktop loading page). @@ -76,7 +76,7 @@ impl MakeSpan for RequestIdMakeSpan { /// Long-lived server state that survives router rebuilds during config reloads. pub struct ServerCore { - pub config: Arc, + pub config: Arc, pub db: Arc, pub repository: Arc, pub persistence_service: Arc, @@ -98,7 +98,7 @@ pub struct AppState { pub instance_manager: Arc, pub conversation_watchers: Arc>>>, - pub config: Arc, + pub config: Arc, pub server_config: Arc, pub auth_config: Arc, pub metrics: Arc, @@ -114,7 +114,7 @@ pub struct AppState { /// Initialize the long-lived server core (DB, instance manager, etc.). pub async fn init_server_core( - config: Arc, + config: Arc, options: &ServerOptions, progress: Option<&StartupProgress>, ) -> Result { @@ -455,7 +455,7 @@ pub fn build_router( } /// Write daemon PID and port files after the server binds. -pub fn write_daemon_files(config: &CrabCityConfig, pid: u32, port: u16) -> Result<()> { +pub fn write_daemon_files(config: &WorkshopConfig, pid: u32, port: u16) -> Result<()> { use anyhow::Context; std::fs::write(config.daemon_pid_path(), pid.to_string()) .context("Failed to write daemon PID file")?; @@ -469,7 +469,7 @@ pub fn write_daemon_files(config: &CrabCityConfig, pid: u32, port: u16) -> Resul /// Reads `daemon.pid` and only deletes state files if the PID matches /// `std::process::id()`. This prevents server A from clobbering server B's /// files when both target the same data directory. -pub fn release_daemon_files(config: &CrabCityConfig) { +pub fn release_daemon_files(config: &WorkshopConfig) { let dominated = match std::fs::read_to_string(config.daemon_pid_path()) { Ok(contents) => contents.trim().parse::().ok() == Some(std::process::id()), Err(_) => true, // file gone — nothing to protect @@ -486,7 +486,7 @@ pub fn release_daemon_files(config: &CrabCityConfig) { /// The kernel releases the flock automatically if the process crashes. /// On `Drop`, PID-aware file cleanup runs so stale files don't linger. pub struct DaemonLock { - config: Arc, + config: Arc, /// Held for its `Drop` impl which calls `flock(LOCK_UN)`. _flock: nix::fcntl::Flock, } @@ -502,7 +502,7 @@ impl Drop for DaemonLock { /// /// Returns `Some(DaemonLock)` on success. Returns `None` if another process /// already holds the lock (i.e. another server is running on this data dir). -pub fn try_acquire_daemon_lock(config: &Arc) -> Result> { +pub fn try_acquire_daemon_lock(config: &Arc) -> Result> { use nix::fcntl::{Flock, FlockArg}; let lock_path = config.daemon_lock_path(); @@ -538,7 +538,7 @@ pub fn try_acquire_daemon_lock(config: &Arc) -> Result Option { +pub fn check_existing_server(config: &WorkshopConfig) -> Option { // Read PID file and verify process is alive let pid_str = std::fs::read_to_string(config.daemon_pid_path()).ok()?; let pid: u32 = pid_str.trim().parse().ok()?; @@ -595,7 +595,7 @@ impl EmbeddedServer { /// If `progress` is provided, it will be called with human-readable status /// messages during startup (e.g. "Initializing database..."). pub async fn start( - config: CrabCityConfig, + config: WorkshopConfig, options: ServerOptions, progress: Option, ) -> Result { diff --git a/packages/crab_city/src/test_helpers.rs b/packages/workshop/src/test_helpers.rs similarity index 97% rename from packages/crab_city/src/test_helpers.rs rename to packages/workshop/src/test_helpers.rs index ccc979f..92bcab8 100644 --- a/packages/crab_city/src/test_helpers.rs +++ b/packages/workshop/src/test_helpers.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; use crate::AppState; -use crate::config::{AuthConfig, CrabCityConfig, RuntimeOverrides, ServerConfig, ServerFileConfig}; +use crate::config::{AuthConfig, RuntimeOverrides, ServerConfig, ServerFileConfig, WorkshopConfig}; use crate::db::Database; use crate::instance_manager::{ClaudeInstance, InstanceManager}; use crate::metrics::ServerMetrics; @@ -19,7 +19,7 @@ use crate::repository::ConversationRepository; /// continue to have a valid directory. pub async fn test_app_state() -> (AppState, tempfile::TempDir) { let tmp = tempfile::tempdir().expect("tempdir"); - let config = CrabCityConfig::new(Some(tmp.path().to_path_buf())).expect("config"); + let config = WorkshopConfig::new(Some(tmp.path().to_path_buf())).expect("config"); let pool = sqlx::sqlite::SqlitePoolOptions::new() .max_connections(1) diff --git a/packages/crab_city/src/virtual_terminal.rs b/packages/workshop/src/virtual_terminal.rs similarity index 100% rename from packages/crab_city/src/virtual_terminal.rs rename to packages/workshop/src/virtual_terminal.rs diff --git a/packages/crab_city/src/ws/conversation_watcher.rs b/packages/workshop/src/ws/conversation_watcher.rs similarity index 100% rename from packages/crab_city/src/ws/conversation_watcher.rs rename to packages/workshop/src/ws/conversation_watcher.rs diff --git a/packages/crab_city/src/ws/focus.rs b/packages/workshop/src/ws/focus.rs similarity index 99% rename from packages/crab_city/src/ws/focus.rs rename to packages/workshop/src/ws/focus.rs index 611d211..209843e 100644 --- a/packages/crab_city/src/ws/focus.rs +++ b/packages/workshop/src/ws/focus.rs @@ -694,8 +694,8 @@ mod tests { fn decode_incomplete_4byte_then_noncontination() { // 3 bytes of a 4-byte char buffered, then ASCII let mut dec = Utf8StreamDecoder::new(); - let crab = "🦀".as_bytes(); // [0xF0, 0x9F, 0xA6, 0x80] - let r1 = dec.decode(&crab[..3]); + let emoji = "🦀".as_bytes(); // [0xF0, 0x9F, 0xA6, 0x80] + let r1 = dec.decode(&emoji[..3]); assert_eq!(r1, ""); let r2 = dec.decode(b"ok"); assert_eq!(r2, "\u{FFFD}ok"); diff --git a/packages/crab_city/src/ws/handler.rs b/packages/workshop/src/ws/handler.rs similarity index 100% rename from packages/crab_city/src/ws/handler.rs rename to packages/workshop/src/ws/handler.rs diff --git a/packages/crab_city/src/ws/merging_watcher.rs b/packages/workshop/src/ws/merging_watcher.rs similarity index 100% rename from packages/crab_city/src/ws/merging_watcher.rs rename to packages/workshop/src/ws/merging_watcher.rs diff --git a/packages/crab_city/src/ws/mod.rs b/packages/workshop/src/ws/mod.rs similarity index 100% rename from packages/crab_city/src/ws/mod.rs rename to packages/workshop/src/ws/mod.rs diff --git a/packages/crab_city/src/ws/protocol.rs b/packages/workshop/src/ws/protocol.rs similarity index 99% rename from packages/crab_city/src/ws/protocol.rs rename to packages/workshop/src/ws/protocol.rs index e49cfaf..dc9aabd 100644 --- a/packages/crab_city/src/ws/protocol.rs +++ b/packages/workshop/src/ws/protocol.rs @@ -1380,7 +1380,7 @@ mod tests { fn test_server_message_instance_renamed() { let msg = ServerMessage::InstanceRenamed { instance_id: "inst-1".to_string(), - custom_name: Some("My Crab".to_string()), + custom_name: Some("My Instance".to_string()), }; let json = serde_json::to_string(&msg).unwrap(); let decoded: ServerMessage = serde_json::from_str(&json).unwrap(); @@ -1390,7 +1390,7 @@ mod tests { custom_name, } => { assert_eq!(instance_id, "inst-1"); - assert_eq!(custom_name, Some("My Crab".to_string())); + assert_eq!(custom_name, Some("My Instance".to_string())); } _ => panic!("Expected InstanceRenamed"), } diff --git a/packages/crab_city/src/ws/session_discovery.rs b/packages/workshop/src/ws/session_discovery.rs similarity index 100% rename from packages/crab_city/src/ws/session_discovery.rs rename to packages/workshop/src/ws/session_discovery.rs diff --git a/packages/crab_city/src/ws/state_manager.rs b/packages/workshop/src/ws/state_manager.rs similarity index 100% rename from packages/crab_city/src/ws/state_manager.rs rename to packages/workshop/src/ws/state_manager.rs diff --git a/packages/crab_city_desktop/BUILD.bazel b/packages/workshop_desktop/BUILD.bazel similarity index 83% rename from packages/crab_city_desktop/BUILD.bazel rename to packages/workshop_desktop/BUILD.bazel index df9a367..c9d0155 100644 --- a/packages/crab_city_desktop/BUILD.bazel +++ b/packages/workshop_desktop/BUILD.bazel @@ -18,7 +18,7 @@ cargo_build_script( name = "build_script", srcs = ["build.rs"], build_script_env = { - "CARGO_MANIFEST_DIR": "packages/crab_city_desktop", + "CARGO_MANIFEST_DIR": "packages/workshop_desktop", # Cargo propagates this from tauri's build.rs (links = "Tauri", cargo:dev=true). # In Bazel we set it directly. false = production mode (webview starts blank, # our setup() navigates to the embedded server URL). @@ -46,13 +46,13 @@ cargo_build_script( ) rust_binary( - name = "_crab_city_desktop_raw", + name = "_workshop_desktop_raw", srcs = glob(["src/**/*.rs"]), compile_data = _TAURI_DATA, edition = "2024", rustc_env = { # Proc macro generate_context!() reads tauri.conf.json relative to this - "CARGO_MANIFEST_DIR": "packages/crab_city_desktop", + "CARGO_MANIFEST_DIR": "packages/workshop_desktop", }, # no-sandbox: tauri proc macros read config and icon files from disk # manual: must be built via tauri_binary (which applies cfg transition) @@ -64,7 +64,7 @@ rust_binary( visibility = ["//visibility:private"], deps = [ ":build_script", - "//packages/crab_city:crab_city_lib_embedded", + "//packages/workshop:workshop_lib_embedded", "@crate_index//:clap", "@crate_index//:serde", "@crate_index//:serde_json", @@ -78,18 +78,18 @@ rust_binary( ) tauri_binary( - name = "crab_city_desktop", - binary = ":_crab_city_desktop_raw", + name = "workshop_desktop", + binary = ":_workshop_desktop_raw", target_compatible_with = ["@platforms//os:macos"], ) rust_test( - name = "_crab_city_desktop_test_raw", + name = "_workshop_desktop_test_raw", compile_data = _TAURI_DATA, - crate = ":_crab_city_desktop_raw", + crate = ":_workshop_desktop_raw", edition = "2024", rustc_env = { - "CARGO_MANIFEST_DIR": "packages/crab_city_desktop", + "CARGO_MANIFEST_DIR": "packages/workshop_desktop", }, # manual: must be built via tauri_test (which applies cfg transition) tags = [ @@ -101,16 +101,16 @@ rust_test( ) tauri_test( - name = "crab_city_desktop_test", + name = "workshop_desktop_test", target_compatible_with = ["@platforms//os:macos"], - test = ":_crab_city_desktop_test_raw", + test = ":_workshop_desktop_test_raw", ) macos_app( name = "macos_app", - binary = ":crab_city_desktop", - binary_name = "crab_city_desktop", - bundle_name = "CrabCity", + binary = ":workshop_desktop", + binary_name = "workshop_desktop", + bundle_name = "Workshop", info_plist = "Info.plist", target_compatible_with = ["@platforms//os:macos"], ) diff --git a/packages/crab_city_desktop/CLAUDE.md b/packages/workshop_desktop/CLAUDE.md similarity index 86% rename from packages/crab_city_desktop/CLAUDE.md rename to packages/workshop_desktop/CLAUDE.md index ff9aae1..10e5147 100644 --- a/packages/crab_city_desktop/CLAUDE.md +++ b/packages/workshop_desktop/CLAUDE.md @@ -1,6 +1,6 @@ -# crab_city_desktop — CLAUDE.md +# workshop_desktop — CLAUDE.md -Native desktop app for Crab City using Tauri 2. Embeds the `crab_city` server in-process (no separate daemon) and wraps the SvelteKit web UI in a native window with native OS integration. +Native desktop app for Workshop using Tauri 2. Embeds the `workshop` server in-process (no separate daemon) and wraps the SvelteKit web UI in a native window with native OS integration. ## Architecture @@ -14,14 +14,14 @@ src/ **Key types**: - `ServerMode` — enum: `Embedded(EmbeddedServer)` (we own it) or `External { port }` (existing daemon) - `AppState` — holds `Mutex>` and `AtomicU16` for the server port -- `EmbeddedServer` (from `crab_city::server`) — starts/stops the axum server, writes daemon files for CLI discovery +- `EmbeddedServer` (from `workshop::server`) — starts/stops the axum server, writes daemon files for CLI discovery -**Custom data directory**: `--data-dir /path/to/data` via clap (defaults to `~/.crabcity`). +**Custom data directory**: `--data-dir /path/to/data` via clap (defaults to `~/.workshop`). ## Native OS Integration - **Menu bar**: App menu (About, Settings `Cmd+,`, Quit `Cmd+Q`), Edit (undo/redo/cut/copy/paste/select_all), View (Reload `Cmd+R`, Toggle DevTools `Cmd+Alt+I`, Fullscreen), Window (minimize/zoom/close) -- **System tray**: Left-click shows window, right-click context menu (Show Window, Quit). Tooltip shows "Crab City" +- **System tray**: Left-click shows window, right-click context menu (Show Window, Quit). Tooltip shows "Workshop" - **Window state persistence**: `tauri-plugin-window-state` saves/restores position, size, maximized state across launches. Window starts hidden (visible: false) to prevent flash before state restore - **macOS close behavior**: Closing the window (Cmd+W / red X) hides it — the app stays running in the tray. Cmd+Q or tray Quit actually exits - **Smooth loading transition**: Loading screen fades out before navigating to the server URL (no white flash) @@ -29,10 +29,10 @@ src/ ## Build & Test ```sh -cargo check -p crab_city_desktop -cargo test -p crab_city_desktop -bazel build //packages/crab_city_desktop -bazel test //packages/crab_city_desktop:crab_city_desktop_test +cargo check -p workshop_desktop +cargo test -p workshop_desktop +bazel build //packages/workshop_desktop +bazel test //packages/workshop_desktop:workshop_desktop_test ``` ### Bazel + Tauri Compatibility @@ -47,7 +47,7 @@ DevTools APIs (`is_devtools_open()`, `open_devtools()`, `close_devtools()`) are ## Dev Workflow -Single terminal: `cd packages/crab_city_desktop && cargo tauri dev --config tauri.dev.conf.json` +Single terminal: `cd packages/workshop_desktop && cargo tauri dev --config tauri.dev.conf.json` The `--config` flag merges `tauri.dev.conf.json` (which adds `devUrl` and `beforeDevCommand`) into the base config. This launches Vite's dev server, then opens the Tauri window. The embedded server starts in-process and writes `daemon.port`, which Vite's `dynamicBackendProxy` reads to proxy `/api/*` and WebSocket requests. @@ -82,10 +82,10 @@ When the external server dies, the health monitor navigates back to `tauri://loc ## Release Build (macOS .app bundle) ```sh -bazel build //packages/crab_city_desktop:macos_app # debug -bazel build --config=opt //packages/crab_city_desktop:macos_app # optimized +bazel build //packages/workshop_desktop:macos_app # debug +bazel build --config=opt //packages/workshop_desktop:macos_app # optimized ``` The `tauri_binary` wrapper rule applies a Starlark transition that keeps `host_compilation_mode` in sync with `compilation_mode`, so both `-c opt` and `--config=opt` work correctly. -Produces `CrabCity.app` with the Tauri binary (which includes the embedded server) in `Contents/MacOS/`. No sidecar binary needed. The `macos_app` rule in `macos_app.bzl` uses a tree artifact (directory output) to assemble the bundle structure. Code signing and notarization are deferred to when public distribution is needed. +Produces `Workshop.app` with the Tauri binary (which includes the embedded server) in `Contents/MacOS/`. No sidecar binary needed. The `macos_app` rule in `macos_app.bzl` uses a tree artifact (directory output) to assemble the bundle structure. Code signing and notarization are deferred to when public distribution is needed. diff --git a/packages/crab_city_desktop/Cargo.toml b/packages/workshop_desktop/Cargo.toml similarity index 86% rename from packages/crab_city_desktop/Cargo.toml rename to packages/workshop_desktop/Cargo.toml index ea5b656..da6a574 100644 --- a/packages/crab_city_desktop/Cargo.toml +++ b/packages/workshop_desktop/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "crab_city_desktop" +name = "workshop_desktop" version = "0.1.0" edition = "2024" -description = "Native desktop app for Crab City" +description = "Native desktop app for Workshop" publish = false [build-dependencies] @@ -20,7 +20,7 @@ plist = "1" serde_json = "1" [dependencies] -crab_city = { path = "../crab_city" } +workshop = { path = "../workshop" } tauri = { version = "2", features = ["tray-icon"] } tauri-plugin-shell = "2" tauri-plugin-window-state = "2" @@ -34,4 +34,4 @@ serde_json = { workspace = true } [features] default = [] -embedded-ui = ["crab_city/embedded-ui"] +embedded-ui = ["workshop/embedded-ui"] diff --git a/packages/crab_city_desktop/Info.plist b/packages/workshop_desktop/Info.plist similarity index 82% rename from packages/crab_city_desktop/Info.plist rename to packages/workshop_desktop/Info.plist index 285c8a7..48056ed 100644 --- a/packages/crab_city_desktop/Info.plist +++ b/packages/workshop_desktop/Info.plist @@ -4,15 +4,15 @@ CFBundleName - Crab City + Workshop CFBundleIdentifier - com.crabcity.desktop + com.workshop.desktop CFBundleVersion 0.1.0 CFBundleShortVersionString 0.1.0 CFBundleExecutable - crab_city_desktop + workshop_desktop CFBundlePackageType APPL NSHighResolutionCapable diff --git a/packages/crab_city_desktop/build.rs b/packages/workshop_desktop/build.rs similarity index 100% rename from packages/crab_city_desktop/build.rs rename to packages/workshop_desktop/build.rs diff --git a/packages/crab_city_desktop/capabilities/default.json b/packages/workshop_desktop/capabilities/default.json similarity index 87% rename from packages/crab_city_desktop/capabilities/default.json rename to packages/workshop_desktop/capabilities/default.json index d295b04..633904a 100644 --- a/packages/crab_city_desktop/capabilities/default.json +++ b/packages/workshop_desktop/capabilities/default.json @@ -1,6 +1,6 @@ { "identifier": "default", - "description": "Default capability set for Crab City desktop", + "description": "Default capability set for Workshop desktop", "windows": ["main"], "permissions": [ "core:default", diff --git a/packages/crab_city_desktop/icons/128x128.png b/packages/workshop_desktop/icons/128x128.png similarity index 100% rename from packages/crab_city_desktop/icons/128x128.png rename to packages/workshop_desktop/icons/128x128.png diff --git a/packages/crab_city_desktop/icons/128x128@2x.png b/packages/workshop_desktop/icons/128x128@2x.png similarity index 100% rename from packages/crab_city_desktop/icons/128x128@2x.png rename to packages/workshop_desktop/icons/128x128@2x.png diff --git a/packages/crab_city_desktop/icons/32x32.png b/packages/workshop_desktop/icons/32x32.png similarity index 100% rename from packages/crab_city_desktop/icons/32x32.png rename to packages/workshop_desktop/icons/32x32.png diff --git a/packages/crab_city_desktop/icons/icon.png b/packages/workshop_desktop/icons/icon.png similarity index 100% rename from packages/crab_city_desktop/icons/icon.png rename to packages/workshop_desktop/icons/icon.png diff --git a/packages/crab_city_desktop/loading-dist/index.html b/packages/workshop_desktop/loading-dist/index.html similarity index 99% rename from packages/crab_city_desktop/loading-dist/index.html rename to packages/workshop_desktop/loading-dist/index.html index 2716b8b..0bd5a1d 100644 --- a/packages/crab_city_desktop/loading-dist/index.html +++ b/packages/workshop_desktop/loading-dist/index.html @@ -3,7 +3,7 @@ - Crab City + Workshop