Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ permissions:
contents: read

jobs:
case-only-renames:
name: Case-only link renames (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: Test case-only link renames on a case-insensitive filesystem
run: cargo test --test cli case_only --locked

rust:
name: Rust
runs-on: ubuntu-22.04
Expand Down
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ Zed Workspace Dock is a Rust CLI for creating and opening Zed `.code-workspace`

Use this file as the local operating guide for AI agents working in this repository. Keep changes aligned with the current CLI contract in `README.md`, the project language in `CONTEXT.md`, and the release decisions in `docs/adr/`.

> [!IMPORTANT]
> The supported way to add folders to an existing registered workspace today is to recreate it with `create ... --name <name> --force` and pass the full folder list again. Do not document or implement an incremental `add` workflow unless the CLI actually gains that command.

## Current Product Shape

- Canonical product name: Zed Workspace Dock.
- Canonical short name, repository slug, Cargo package, and installed binary: `zwd`.
- Public command and only installed binary: `zwd`, backed by `src/bin/zwd.rs`.
- Commands: `create`, `open`, `install`, and `list`.
- Commands: `create`, `open`, `sync`, `add`, `remove`, `delete`, `status`, `install`, and `list`.
- Default create mode: `symlink`.
- Alternate open/create mode: `folders`.
- Workspace format: strict JSON `.code-workspace` files with optional `folders` and optional `zwd`.
Expand All @@ -22,7 +19,7 @@ Use this file as the local operating guide for AI agents working in this reposit
- Zed task templates come from `resources/zed-tasks.json`.
- JSON Schemas live under `resources/schemas/` and are documentation/editor/test resources, not runtime validators.

Windows support is partial in the MVP: folder mode is supported, while symlink dock mode is currently macOS/Linux-oriented.
Windows dock mode uses directory symbolic links and may require Developer Mode or symlink privileges. CI runs the full tests on Linux, case-only link rename regressions on macOS, and checks Windows compilation.

## Repository Map

Expand Down Expand Up @@ -96,7 +93,10 @@ If a change affects the Zed UX directly, keep the manual Zed smoke-test gap visi

- Never modify a dock directory unless it has a valid `.zwd-lock.json` lock owned by this tool.
- Abort on existing dock directories without a valid lock.
- Abort when unmanaged files are present inside a lock-owned dock.
- Preserve unmanaged files, directories, and manual symlinks during open, sync, add, and remove.
- `delete` previews only; `delete --force` removes all contents of a validated owned dock, including extras, without following symlinks.
- Reject symlink dock roots and symlink lock files. Block managed link conflicts before reconciliation.
- Treat the workspace file as configuration and the lock as derived ownership state.
- Symlink project folders; do not copy source folders into dock roots.
- Do not delete or mutate symlink targets.
- Keep workspace names and dock link names to single filesystem entry names.
Expand Down Expand Up @@ -135,7 +135,7 @@ Use conventional commit messages with lowercase imperative subjects, for example

```text
feat: add registered workspace listing
fix: reject unmanaged dock contents
fix: preserve unmanaged dock contents
docs: document release automation
```

Expand Down
12 changes: 10 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,21 @@ A standalone workspace file written inside a user-provided output directory via
_Avoid_: Local registry, exported registry entry

**Symlink dock**:
A lock-protected directory whose entries are symbolic links to the projects from a workspace file. One symlink dock belongs to one workspace file path.
A lock-protected directory containing managed project symlinks and optionally user-created files, directories, or symlinks. One symlink dock belongs to one workspace file path.
_Avoid_: Temporary copy, workspace copy, virtual workspace

**Dock lock**:
The JSON ownership record stored as `.zwd-lock.json` inside a symlink dock. A dock lock identifies the owning workspace file and the symlink entries managed by Zed Workspace Dock.
_Avoid_: Marker, ownership marker, cache manifest

**Managed dock link**:
A project symlink whose name and target are recorded in the owning dock lock.
_Avoid_: Any symlink in the dock

**Unmanaged dock content**:
A dock entry not recorded as a managed link in the dock lock. It belongs to the dock's local contents but not to workspace membership.
_Avoid_: Invalid content, inconsistent workspace

**Dock reconciliation**:
The safe in-place synchronization of an existing owned symlink dock with its workspace membership.
_Avoid_: Dock refresh, cache rebuild
Expand Down Expand Up @@ -104,7 +112,7 @@ Older planning text may use pluralized product names, describe `zwd` as an alias
- Created workspaces store folder paths as canonical absolute paths resolved from the current working directory at creation time.
- `add <paths>... --workspace <workspace>` and `remove <paths>... --workspace <workspace>` change workspace membership. The workspace flag is optional only from the root of a valid owned dock.
- `add` and `remove` are idempotent and synchronize an existing dock incrementally. They do not create a dock that does not already exist.
- `delete <workspace>` prints a deletion preview; `delete <workspace> --force` removes the workspace file and its valid owned dock, never its project folders.
- `delete <workspace>` prints a deletion preview; `delete <workspace> --force` removes the workspace file and its entire valid owned dock, including unmanaged content, never symlink targets.
- `status [workspace]` reports workspace and dock health. An absent dock is normal, while unresolvable folders or unsafe docks are unhealthy states.
- `open <simple-name>` prefers a registered workspace over a same-name file or directory in the current working directory. Use an explicit path such as `./work.code-workspace` when the local file is intended.
- `list` prints registered workspaces as `name<TAB>path`.
Expand Down
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Zed can open multiple folders directly, but its terminal still benefits from a s
- Choose between direct `folders` mode and managed `symlink` dock mode.
- Add or remove one or more project folders while synchronizing an existing dock safely.
- Install global Zed tasks backed by the packaged task templates.
- Rebuild dock roots safely using `.zwd-lock.json` ownership locks.
- Reconcile dock links safely using `.zwd-lock.json` ownership locks, preserving extra files and manual symlinks.
- Use the short `zwd` command for repeated terminal workflows.

## Install
Expand Down Expand Up @@ -158,13 +158,26 @@ zwd remove ../legacy

Both commands are idempotent. Adding an existing folder or removing an absent one reports that state without changing the workspace or dock. A dock that already exists is reconciled immediately and incrementally; a dock that does not exist is still materialized by the next `zwd open` in symlink mode.

Synchronize a dock without opening Zed:

```bash
zwd sync work
zwd sync ./work.code-workspace
# From the root of a managed dock:
zwd sync
```

`open` and `sync` recover missing managed links and reconcile changes from the `.code-workspace` configuration. Correct links remain in place. Files, directories such as `.dirac`, and manually added symlinks are preserved without being adopted into the lock. A changed managed symlink target, a managed link replaced by a real file/directory, or an unmanaged entry occupying a required link name causes an error before link changes. Restore or move that conflicting entry manually before retrying.

`sync` creates an absent dock for a symlink-mode workspace. For a folder-mode workspace it synchronizes an existing dock, or succeeds with a message when there is no dock. Workspace inference uses only the lock in the current dock root; it does not search parent directories. The workspace file remains the source of truth, so synchronization does not restore projects removed from that file.

Inspect a workspace and its dock health:

```bash
zwd status work
```

`status` reports the workspace path, registration, mode, resolved folders, and dock state. It exits unsuccessfully after reporting an unresolvable folder or an unsafe dock. A missing dock is normal until symlink mode has opened the workspace.
`status` reports the workspace path, registration, mode, resolved folders, and dock state without changing files. Extra dock content is healthy; missing managed links, changed destinations, stale lock membership, unresolvable folders, or invalid ownership are unhealthy. Use `sync` to recover missing links or reconcile configuration changes. A missing dock is normal before the first `open` or `sync` in symlink mode.

Preview workspace deletion, then confirm it:

Expand All @@ -173,7 +186,7 @@ zwd delete work
zwd delete work --force
```

`delete` accepts a registered name or an explicit `.code-workspace` path. Without `--force`, it only lists the workspace file and owned dock that would be removed. With `--force`, it removes the workspace file and only its validated, lock-owned dock. It never deletes project folders, does not close applications using the dock, and refuses to delete while its own current directory is that dock root.
`delete` accepts a registered name or an explicit `.code-workspace` path. Without `--force`, it previews the workspace file and owned dock and states that all local dock contents would be removed. With `--force`, it removes the entire validated, lock-owned dock, including extra files, nested directories, and manual symlinks, then removes the workspace file. Symlinks are unlinked without following their targets, so linked project folders are preserved. Files stored only inside the dock are deleted. The command does not close applications using the dock and refuses to delete from the dock root or a real subdirectory beneath it. Missing project targets do not prevent deletion. Filesystem errors can interrupt deletion; the workspace file is kept if dock removal fails.

## Zed Tasks

Expand Down Expand Up @@ -247,14 +260,16 @@ The schemas are documentation, editor, and test resources. Runtime parsing still
- Rebuilds modify only lock-owned docks.
- Incremental edits reconcile only changed symlinks in an existing owned dock.
- Existing dock directories without a valid lock abort.
- Unmanaged files inside a lock-owned dock abort.
- Extra files and manual symlinks are preserved during opening and reconciliation; `delete --force` removes all local dock contents.
- Existing dock roots must be real directories and locks must be regular files, not symlinks.
- Managed link conflicts abort synchronization before any link changes.
- Project folders are symlinked, not copied.
- Symlink targets are not deleted or mutated.
- `folders[].name` must be one filesystem entry name, not a path.
- `folders[].name` cannot use reserved dock metadata names such as `.zwd-lock.json`.
- Registered workspace names must be one filesystem entry name without `.code-workspace`; `open` accepts the name with or without that extension.
- Registered workspace creation does not overwrite an existing workspace unless `--force` is passed.
- `delete --force` never bypasses dock ownership or unmanaged-content checks.
- `delete --force` never bypasses dock ownership checks; it confirms deletion of the entire owned dock.

## Development

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ date: "2026-08-08"
authors: "Project maintainer"
tags: ["architecture", "cli", "workspace", "dock", "safety"]
supersedes: ""
superseded_by: ""
superseded_by: "ADR-0010 (unmanaged content and deletion policy only)"
---

# ADR-0009: Mutate workspaces with owned dock reconciliation

## Status

Accepted
Accepted; unmanaged-content restrictions and deletion policy are superseded by ADR-0010. Other decisions remain in effect.

## Context

Expand Down
52 changes: 52 additions & 0 deletions docs/adr/adr-0010-allow-unmanaged-dock-content-and-add-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: "ADR-0010: Allow unmanaged dock content and add sync"
status: "Accepted"
date: "2026-10-31"
authors: "Project maintainer"
tags: ["cli", "dock", "safety"]
supersedes: "ADR-0009 (unmanaged content and deletion policy only)"
superseded_by: ""
---

# ADR-0010: Allow unmanaged dock content and add sync

## Status

Accepted.

## Context

Issue #35 demonstrates that tools commonly create configuration directories in the dock root. Rejecting every extra entry prevents reopening otherwise valid workspaces. The old reconciliation algorithm relied on that rejection before removing entries outside desired workspace membership.

## Decision

Preserve unmanaged files, directories, and symlinks during `open`, `sync`, `add`, and `remove`. Reconcile only previously managed links, using the workspace file as configuration and the lock as derived ownership state. Recover missing links automatically; reject changed managed destinations, real entries replacing managed links, and collisions with unmanaged entries before modifying links.

Add `sync [workspace]` to perform reconciliation without opening Zed. Accept registered names and explicit paths, or infer ownership only from the current dock root. Create absent symlink-mode docks. For folder-mode workspaces, reconcile an existing dock or report that none requires synchronization. Do not adopt manual symlinks or edit workspace membership during sync.

Keep `delete` as a non-mutating preview. Explicit `delete --force` authorizes removal of the entire validated owned dock, including unmanaged contents and extra symlinks, followed by the workspace file. Never follow symlinks or delete their targets. Ownership validation remains mandatory and independent of link health or project availability. Reject execution inside the dock or its real descendants.

## Consequences

- Configuration generated by editors and agents no longer blocks opening workspaces.
- Correct symlinks stay in place, reducing disruption to active editor sessions.
- Files kept exclusively inside the dock are deleted by `delete --force`; the preview and usage documentation state this explicitly.
- Ownership checks reject symlink dock roots, non-regular locks, invalid link names, duplicate names, relative ownership/target paths, and unsupported versions.
- Lock version 1 and the persistence layout remain unchanged. Filesystem failures may interrupt removal; deletion is not transactional.

## Alternatives Considered

- Reject all extras: conflicts with routine tool behavior and issue #35.
- Prompt during open or adopt manual symlinks: disrupts automated tasks or implicitly changes ownership.
- Preserve extras after deletion: leaves unregistered content in the cache and conflicts with the selected whole-dock deletion contract.
- Restore exclusively from the lock: can resurrect projects removed from the authoritative workspace file.

## Implementation Notes

Separate ownership validation from health inspection and reconciliation. Validate conflicts before applying changes and preserve unmanaged entries during rollback. Use standard-library recursive removal without following symlinks. Keep schema resources descriptive rather than runtime validators.

## References

- [Issue #35](https://github.com/runrly/zwd/issues/35)
- [ADR-0009](adr-0009-mutate-workspaces-with-owned-dock-reconciliation.md)
- [Rust remove_dir_all](https://doc.rust-lang.org/std/fs/fn.remove_dir_all.html)
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ pub enum Commands {
#[arg(long)]
force: bool,
},
/// Synchronize a dock without opening Zed. Infer the workspace from the dock root.
Sync {
workspace: Option<PathBuf>,
},
Status {
workspace: Option<PathBuf>,
},
Expand Down
Loading
Loading