# Tasks: yconn
- [x] **Add `yconn install` command to install project connections into a target layer** [cli] M
- Acceptance: (1) `yconn install` reads the project `.yconn/connections.yaml` discovered by the upward walk from the current directory and copies all connections into the target layer file (`~/.config/yconn/connections.yaml` by default, `/etc/yconn/connections.yaml` with `--layer system`); (2) for each connection not present in the target file, the entry is appended and `Writing: <path>` is printed to stdout; (3) for each connection whose name already exists in the target file, the user is prompted interactively `Connection '<name>' already exists — update? [y/N]`; if the user answers `y` or `Y` the entry is replaced and `Updating: <path>` is printed; if the user answers anything else the connection is skipped and `Skipping: <name> (already exists)` is printed; (4) when no project config is found the command exits with a clear error; (5) the `--layer` flag accepts `user` (default) or `system` only — `project` is rejected with a clear error since installing into the project layer is circular; (6) unit tests in `src/commands/install.rs` cover: new connections appended and `Writing:` line emitted, existing connection with `y` answer replaced and `Updating:` line emitted, existing connection with `N` answer skipped and `Skipping:` line emitted, missing project config returns error, `--layer project` returns error; (7) a functional test in `tests/functional.rs` writes a project config with two connections (`alpha`, `beta`), runs `yconn install`, and asserts both appear in the user layer file; a second functional test pre-populates the user layer with `alpha`, runs `yconn install` with `y` stdin for the update prompt, and asserts `alpha` is updated and `beta` is appended; `make test` passes
- Depends on: Restructure `yconn ssh-config` into subcommands: `install`, `print`, `uninstall`, `disable`, `enable`
- Modify: src/cli/mod.rs, src/main.rs, src/commands/mod.rs, tests/functional.rs
- Create: src/commands/install.rs
- Reuse: src/commands/add.rs:entry_exists (detect existing connection name in target file content), src/commands/add.rs:insert_connection (append new connection block under `connections:`), src/commands/add.rs:build_entry (construct YAML block from Connection fields), src/commands/add.rs:set_private_permissions (apply 0o600 after writing), src/commands/add.rs:layer_arg_to_layer (convert LayerArg to Layer), src/commands/add.rs:layer_path (resolve target directory path from Layer), src/config/mod.rs:LoadedConfig::project_dir (already-discovered `.yconn/` path from upward walk — use as the source directory), src/config/mod.rs:Connection (iterate `cfg.connections` to get all source entries with their field values), src/display/mod.rs:Renderer (route all output through renderer; use `renderer.print_line` or equivalent for `Writing:`/`Updating:`/`Skipping:` messages), tests/functional.rs:TestEnv (write_project_config, write_user_config, run)
- Risks: `build_entry` in `src/commands/add.rs` takes individual field arguments (`host`, `user`, `port`, etc.) rather than a `&Connection` — the install handler must destructure each `Connection` to call it, or a thin wrapper must be added; the `replace_connection` operation (for update path) does not exist yet — it must be implemented in `install.rs` by reading the target file, locating the existing entry block by name, replacing it, and writing back; the upward-walk result is available as `cfg.project_dir` (a `PathBuf` to the `.yconn/` directory) — derive the source file path as `cfg.project_dir.unwrap().join("connections.yaml")`; `--layer project` must be explicitly rejected before dispatching — check `layer_arg == Some(LayerArg::Project)` at the top of the handler and bail with a clear message; the interactive prompt for existing connections must read from stdin line by line — use the same `prompt` / `BufRead` pattern as `add.rs` and `user.rs` so the handler is testable with injected input; connections loaded via `cfg.connections` are the fully merged active set — the install command must re-read only the project-layer connections from `cfg.project_dir` rather than iterating all merged connections, to avoid accidentally installing user- or system-layer entries into the target
- [x] **Improve yconn install progress output to show connection name per write/skip** [cli] S
- Acceptance: (1) for each new connection written, `run_impl` prints `Writing: connection <name> -> <filepath>` instead of `Writing: <filepath>`; (2) for each connection skipped (user answered N or defaulted), `run_impl` prints `Skipping: connection <name> -> <filepath> (already up to date)` instead of `Skipping: <name> (already exists)`; (3) for each connection updated (user answered y), `run_impl` prints `Updating: connection <name> -> <filepath>` instead of `Updating: <filepath>`; (4) unit tests in `src/commands/install.rs` are updated so `test_new_connections_appended_and_writing_emitted` asserts the output contains `Writing: connection alpha ->` and the target path, `test_existing_connection_y_replaces_and_updating_emitted` asserts `Updating: connection alpha ->` and the target path, and `test_existing_connection_n_skipped_and_skipping_emitted` asserts `Skipping: connection alpha ->` and the target path; (5) a functional test in `tests/functional.rs` runs `yconn install` with two connections and asserts stdout contains `Writing: connection alpha ->` and `Writing: connection beta ->`; `make test` passes
- Depends on: Add `yconn install` command to install project connections into a target layer
- Modify: src/commands/install.rs, tests/functional.rs
- Create: none
- Reuse: src/commands/install.rs:run_impl (the three `writeln!` call sites at lines 134, 137, and 146 are the only change sites — update the format strings to include the connection name and `->` separator before the path), src/commands/install.rs:extract_connection_names (unchanged — `name` is already in scope at each writeln! site inside the `for name in &connection_names` loop)
- Risks: the three existing unit tests each assert on specific output strings (`Writing: {target_str}`, `Updating: {target_str}`, `Skipping: alpha (already exists)`) — all three assertions must be updated to match the new format or the tests will fail; any functional test in `tests/functional.rs` that currently asserts exact `Writing:` or `Skipping:` output from `yconn install` must be audited and updated; the `->` separator must be a literal ` -> ` (space-arrow-space) consistent with the format used by `yconn ssh-config install` output elsewhere in the codebase — verify no other install output format uses a different separator before committing to this choice
- [x] **Show user variable resolution table above connections in yconn list output** [cli] M
- Acceptance: `yconn list` prints a `Users:` table before the connections table (separated by a blank line) whenever one or more `${...}` placeholders appear in any active connection's fields; each placeholder row shows its resolved value and source (layer + path) or `[unresolved]` with a `→ yconn users add --user KEY:VALUE` hint; the Users table is omitted entirely when no placeholders exist; connections table USER column preserves the raw `${variable}` syntax unchanged; unit tests in `src/commands/list.rs` cover: placeholder present and resolved shows correct value and source, placeholder present and unresolved shows `[unresolved]` and fix hint, no placeholders produces no Users table output, mixed resolved and unresolved placeholders both appear; `make test` passes
- Depends on: Improve yconn install progress output to show connection name per write/skip
- Modify: src/commands/list.rs, src/display/mod.rs
- Create: none
- Reuse: src/config/mod.rs:LoadedConfig::users (resolve placeholder keys against this map), src/config/mod.rs:expand_user_field (call with empty inline_overrides to detect unresolved tokens without mutating connection data), src/config/mod.rs:UserEntry (source layer and path for the resolved-value SOURCE column), src/display/mod.rs:UserRow (reuse directly — key=variable name without `${}`, value=resolved value or `[unresolved]`, source=layer+path or fix hint, shadowed=false), src/display/mod.rs:Renderer::user_list (renders the Users table; called before `list` when rows is non-empty), src/display/mod.rs:render_user_list (already formats VARIABLE/VALUE/SOURCE columns with `${key}` display — reuse without modification)
- Risks: scanning all active connection fields for `${...}` tokens must cover all fields that support template expansion (primarily `user:`, but verify whether `host:` or `key:` can also carry placeholders — if yes, those must be scanned too); `render_user_list` displays each key as `${key}` already, so the VARIABLE column output is correct without changes; the Users table must use `Renderer::user_list` rather than a new renderer method to avoid duplicating the table-formatting logic — verify `user_list` accepts `&[UserRow]` (it does, per the existing signature); the blank line between the Users table and the connections table must be emitted by `list::run` (via `println!()` or a `Renderer` helper) after calling `renderer.user_list` and before calling `renderer.list`, not inside either renderer method, to keep the renderers stateless; deduplication of placeholder keys is required — if `${deploy_user}` appears in three connections it should produce only one row in the Users table; placeholder extraction must handle fields that contain multiple `${...}` tokens (e.g. a hypothetical `user: "${prefix}_${suffix}"`) by emitting one row per unique key; the `--all` flag passes shadowed connections to the renderer — decide whether placeholders from shadowed connections should appear in the Users table (safest: scan only active connections, not `all_connections`, to avoid surfacing variables from overridden entries)
- [x] **Prompt for missing user during yconn install / ssh-config install** [cli] M
- Acceptance: (1) `yconn install` and `yconn ssh-config install` scan all project connections for `${key}` user-field tokens that are not present in `cfg.users`; when one or more unresolved keys are found the command halts before writing any files and prints a grouped prompt listing each missing key and the connection names that reference it (e.g. `Missing user variable '${t1user}' used by: conn-a, conn-b`); the user is then prompted to provide a value for each missing key following the same flow as `yconn users add` (value is written to the target layer's config via `write_user_entry`); after all missing keys are resolved the command proceeds normally; (2) when all user variables are already resolved the commands behave identically to today with no extra prompts; (3) unit tests in `src/commands/install.rs` cover: connections with unresolved `${key}` trigger prompt and supplied value is written, connections with all keys resolved skip prompting, multiple connections referencing the same missing key produce a single prompt listing all connection names; (4) unit tests in `src/commands/ssh_config.rs` cover: `run_install` with unresolved key halts and prompts, supplied value resolves the token in generated Host blocks; (5) functional tests in `tests/functional.rs` cover: `yconn install` with missing user variable prompts and writes the value then completes the install, `yconn ssh-config install` with missing user variable prompts and writes the value then generates correct Host blocks; `make test` passes
- Depends on: Show user variable resolution table above connections in yconn list output
- Modify: src/commands/install.rs, src/commands/ssh_config.rs, tests/functional.rs
- Create: none
- Reuse: src/config/mod.rs:expand_user_field (detect unresolved keys before proceeding), src/config/mod.rs:LoadedConfig::users (check which keys are already defined), src/commands/ssh_config.rs:extract_unresolved_key (extract key name from unresolved token), src/commands/user.rs:write_user_entry (write prompted value to target layer config), src/commands/user.rs:layer_path (resolve target directory for writing user entries)
- Risks: `run_impl` in `install.rs` currently receives `project_file` and `target_file` paths but has no access to a `LoadedConfig` — it cannot call `expand_user_field` without either threading `cfg` through or re-implementing token scanning; the simplest approach is to scan for `${...}` tokens in raw YAML user fields via regex or string matching in `run_impl` and cross-reference against the target file's existing `users:` section; `run_install` in `ssh_config.rs` already calls `expand_user_field` per connection — the prompt-and-write logic must run as a pre-pass before the per-connection loop so that all missing keys are collected first and each key is prompted only once regardless of how many connections reference it; the prompted value must be persisted via `write_user_entry` to the user-layer config so subsequent runs do not re-prompt — this means the install commands gain a write side-effect on `~/.config/yconn/connections.yaml` even though their primary target may be a different file; `extract_unresolved_key` is currently `fn` (private) in `ssh_config.rs` — it must be made `pub(crate)` or moved to a shared location so `install.rs` can reuse it; the interactive prompt must read from the same `input: &mut dyn BufRead` already threaded through `run_impl` (install) and must be added to `run_install` (ssh-config) which currently uses `stdin` implicitly via `println!` — threading a `BufRead` into `run_install` requires a signature change and updating all call sites including tests
- [x] **Restructure `auth` from a string field into a structured node with `type`, `key`, and `cmd` fields** [core] L
- Acceptance: (1) `auth` is a YAML mapping node with `type` (required: `"key"` or `"password"`), `key` (required when type=key, path to private key), and `cmd` (optional, shell command string); (2) the old flat format (`auth: key` + sibling `key:` field) is removed — this is a breaking change; (3) `Auth` is a serde-tagged Rust enum with `Key { key, cmd }` and `Password` variants; (4) `RawConn.auth` is `Option<Auth>`, `Connection.auth` is `Auth`; (5) `build_args` in `connect` uses `Auth::Key { ref key, .. }` to emit `-i` flag; (6) `yconn show` displays `Auth:`, `Key:`, and `Cmd:` (when present) lines; (7) `yconn list` shows auth type label in the AUTH column; (8) `yconn ssh-config` outputs `# auth:` comment with type label and `IdentityFile` from `auth.key()`; (9) `yconn connections add` wizard emits nested `auth:` YAML block; (10) `yconn show --dump` serializes `auth` as a nested YAML mapping; (11) `cmd` field is parsed and stored only — no execution logic; (12) all unit tests, functional tests, and example configs updated to new format; (13) `make test` passes; (14) `make lint` passes
- Depends on: Add `yconn install` command to install project connections into a target layer
- Modify: src/config/mod.rs, src/connect/mod.rs, src/display/mod.rs, src/commands/show.rs, src/commands/list.rs, src/commands/connect.rs, src/commands/ssh_config.rs, src/commands/add.rs, src/security/mod.rs, tests/functional.rs, config/connections.yaml, CLAUDE.md
- Create: none
- Reuse: src/config/mod.rs:RawConn (replace `auth: Option<String>` + `key: Option<String>` with `auth: Option<Auth>`), src/config/mod.rs:Connection (replace `auth: String` + `key: Option<String>` with `auth: Auth`), src/config/mod.rs:build_connection (map `raw.auth` directly to `conn.auth`), src/config/mod.rs:validate_connections (check `auth` is `Some`), src/connect/mod.rs:build_args (match on `Auth` enum for `-i` flag), src/display/mod.rs:ConnectionDetail (add `cmd: Option<String>` field and render it), src/commands/show.rs:DumpConn (serialize `Auth` enum as nested YAML), src/commands/add.rs:build_entry (emit nested `auth:` YAML block)
- Risks: serde `#[serde(tag = "type")]` internally-tagged enum requires the YAML mapping to contain a `type` key — verify `serde_yaml` handles this correctly for both serialization and deserialization; all inline YAML test fixtures (~30+ in `tests/functional.rs` alone) must be updated with correct nested indentation or deserialization will fail silently; `DumpConn` in `show.rs` uses `Serialize` — the `Auth` enum must derive both `Serialize` and `Deserialize` with matching tag configuration; `build_entry` in `add.rs` constructs YAML by hand — the nested `auth:` block requires careful indentation (6 spaces for inner fields if connection fields use 4); the `security` module's credential-field scanning checks YAML mapping keys not values, so `type: password` (where "password" is a value) should not trigger a false positive — verify this
- [x] **Add identity-only connection type for ssh-config-only entries (e.g. git hosts)** [core] M
- Acceptance: (1) a new `auth: identity` type is accepted in connection YAML with a required `key` field (path to private key); (2) `yconn ssh-config install` generates Host blocks for identity connections containing `IdentityFile <key>` and `IdentitiesOnly yes` directives; (3) `yconn connect` allows connecting to identity connections but prints a warning to stderr that the host may not support interactive SSH; (4) `yconn list` and `yconn show` display the auth type as `identity` in the AUTH column and detail view respectively; (5) `yconn connections add` wizard includes `identity` as a third option alongside `key` and `password` and prompts for `key` when selected; (6) `yconn show --dump` serializes `auth: identity` connections correctly; (7) config validation rejects `auth: identity` without a `key` field (same as `auth: key`); (8) unit tests in `src/config/mod.rs` cover: identity connection parsed correctly, identity without key rejected; unit tests in `src/connect/mod.rs` cover: `build_args` for identity auth produces `ssh -F /dev/null -i <key> user@host` (same as key auth) and warning is emitted; unit tests in `src/commands/ssh_config.rs` cover: `render_ssh_config` for identity auth emits `IdentityFile` and `IdentitiesOnly yes`; unit tests in `src/commands/add.rs` cover: wizard with identity choice emits correct YAML; functional tests in `tests/functional.rs` cover: identity connection round-trip (add, list, show, ssh-config install); `make test` passes
- Depends on: Restructure `auth` from a string field into a structured node with `type`, `key`, and `cmd` fields
- Modify: src/config/mod.rs, src/connect/mod.rs, src/display/mod.rs, src/commands/ssh_config.rs, src/commands/show.rs, src/commands/list.rs, src/commands/connect.rs, src/commands/add.rs, tests/functional.rs, config/connections.yaml
- Create: none
- Reuse: src/config/mod.rs:Auth (add `Identity { key, cmd }` variant to the serde-tagged enum), src/config/mod.rs:validate_connections (extend key-required check to cover identity type), src/connect/mod.rs:build_args (extend Auth match to handle Identity variant — emit `-i <key>` same as Key), src/commands/ssh_config.rs:render_ssh_config (extend key-emission branch to also emit `IdentitiesOnly yes` for identity auth), src/commands/add.rs:prompt_choice (add "identity" to the choices list), src/commands/add.rs:build_entry (handle identity auth type in YAML generation), src/display/mod.rs:ConnectionRow (auth field already renders as string — "identity" will display automatically), src/display/mod.rs:ConnectionDetail (auth field renders as string — "identity" will display automatically)
- Risks: the `IdentitiesOnly yes` directive must appear after `IdentityFile` in the generated Host block — verify `render_ssh_config` emits them in the correct order; if the auth restructuring task uses `#[serde(tag = "type")]` the new `Identity` variant must use the same tag scheme — the YAML representation will be `auth: { type: identity, key: ... }` and `serde_yaml` must round-trip this correctly; `build_args` for identity auth is functionally identical to key auth (both emit `-i <key>`) but the connect handler must additionally emit a warning via `renderer.warn` — this requires threading a renderer or warning callback into the connect path, which currently has no such parameter; alternatively the warning can be emitted by the CLI command handler in `src/commands/connect.rs` before calling `connect::run`, by inspecting `conn.auth`; the `IdentitiesOnly yes` directive tells SSH to use only the specified identity file and ignore ssh-agent — this is the correct behaviour for git hosts but may surprise users who expect agent-forwarded keys to work; the warning message for `yconn connect` should clearly state "this connection is configured as identity-only (e.g. for git hosts) and may not support interactive SSH sessions"
- [x] **Extend example config/connections.yaml to showcase all documented features** [docs] S
- Acceptance: `config/connections.yaml` includes working examples of: (1) all three `auth` types (`key`, `password`, `identity`) using the nested `auth:` mapping format with `type`, `key`, and `cmd` fields; (2) at least one connection with `auth.cmd` populated; (3) at least one connection with a `group` field (inline group tag); (4) at least one connection with a `${variable}` template in the `user` field; (5) a `users:` block with at least two entries including one that resolves a template used in a connection; (6) an uncommented `docker:` block with `image`, `pull`, and `args` fields all present; (7) at least five connections demonstrating different field combinations (with/without port, with/without link, different auth types, with/without group); (8) the file remains valid YAML that parses without error via `serde_yaml`; (9) comments are present explaining each feature for quick-start reference; no code changes, no new tests required
- Depends on: Add identity-only connection type for ssh-config-only entries (e.g. git hosts)
- Modify: config/connections.yaml
- Create: none
- Reuse: src/config/mod.rs:RawConn (field names define valid YAML keys), src/config/mod.rs:Auth (enum variants define valid auth type/key/cmd combinations), src/config/mod.rs:DockerConfig (fields define valid docker block keys)
- Risks: the example file is loaded by some functional tests or used as a documentation reference -- verify no test imports or parses `config/connections.yaml` directly before changing it; the `cmd` field is parsed and stored only (no execution logic) so any example value is safe; ensure the `${variable}` template in the example matches a key in the `users:` block so the example is self-consistent
- [x] **Rewrite make install to build and install a distro-native package (deb or Arch) instead of raw file copy** [packaging] S
- Acceptance: `make install` detects the distro via `/etc/os-release` (Debian/Ubuntu vs Arch), depends on `make package` to produce the `.deb` or `.pkg.tar.zst`, then runs `sudo dpkg -i` or `sudo pacman -U` as appropriate; unrecognized distros get a clear error message suggesting manual installation; `sudo` is scoped to only the package-manager invocation (the user never runs `sudo make install`); the existing `make package`, `build-deb`, and `build-pkg` targets are unchanged; manually running `make install` on a Debian-based system installs the `.deb` and on Arch installs the `.pkg.tar.zst`
- Depends on: Extend example config/connections.yaml to showcase all documented features
- Modify: Makefile
- Create: none
- Reuse: scripts/install-deps.sh:is_debian_like (distro-detection pattern using /etc/os-release, ID, ID_LIKE), scripts/install-deps.sh:is_arch_like (same pattern for Arch detection), scripts/build-deb.sh (output path convention: dist/${BINARY}_${VERSION}_amd64.deb), scripts/build-pkg.sh (output path convention: dist/${BINARY}-${VERSION}-1-x86_64.pkg.tar.zst)
- Risks: `make install` currently accepts a `PREFIX` variable for the install path — removing raw-copy semantics is a breaking change for anyone using `PREFIX=/opt make install`; the `sudo` prompt inside a Makefile recipe may confuse users who expect `make install` to be non-interactive — the error message for unrecognized distros should also mention that `make package` can be used standalone to build packages without installing; the `.deb` and `.pkg.tar.zst` filenames are constructed from `BINARY` and `VERSION` Makefile variables which must stay in sync with the scripts; `dpkg -i` does not resolve dependencies (unlike `apt install`) — if the `.deb` declares dependencies the user may need to run `sudo apt-get install -f` afterwards, or the recipe should use `sudo apt-get install ./dist/<pkg>.deb` instead of `dpkg -i`