diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 6aa7960..cbd291f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,102 +1,104 @@ -{"_type": "issue", "id": "hm-68a", "title": "introduce an element that is to be used anywhere where i am typing a reference to a secret. it should autocomplete as i type, showing me the closest match based on levenshtein distance. the same code should be used to also add a 'did you mean X' when himitsu search returns no results", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-07T06:22:32Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-09T02:11:54Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "Implemented Levenshtein-based 'did you mean' for himitsu search and a reusable TUI autocomplete popup wired into the search view (Ctrl+Space).", "started_at": "2026-05-07T06:22:32Z"} -{"_type": "issue", "id": "hm-7ob", "title": "Update command palette layout", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-07T06:06:18Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-07T06:10:27Z", "started_at": "2026-05-07T06:06:18Z", "closed_at": "2026-05-07T06:10:27Z", "close_reason": "Updated command palette layout and copy", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-isi", "title": "add support for TUI hints - a small bit of text that can render in subtle text on the bottom left corner, sort of like a floating element", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-06T23:21:58Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-09T02:11:54Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "Added bottom-left ambient hint surface; new-secret form publishes step-aware tips for path/tags/expires_at.", "started_at": "2026-05-06T23:21:58Z"} -{"_type": "issue", "id": "hm-3w5", "title": "all form fields should have placeholders containing an example", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-06T23:20:58Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-09T02:11:54Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "All new-secret form fields (and the generic FormView) now render muted placeholder examples when empty + unfocused.", "started_at": "2026-05-06T23:20:58Z"} -{"_type": "issue", "id": "hm-3rr", "title": "add a submit button to the new secret tui, and add a confirm dialog when clicking esc that lets you save or discard", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-06T23:20:25Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-09T02:11:54Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "Added a final [ submit ] tab stop and an unsaved-changes confirm dialog that fires on Esc with any populated field.", "started_at": "2026-05-06T23:20:25Z"} -{"_type": "issue", "id": "hm-6if", "title": "Add tags support for secrets (search + env composition)", "description": "## Motivation\n\nToday a secret's only addressable axis is its path. That works for hierarchies (`prod/api/STRIPE_KEY`) but not for crosscutting groupings \u2014 \"everything PCI\", \"everything the mobile team owns\", \"rotate-this-quarter\". Users currently fake it with path conventions, which collide with the env-prefix model.\n\nAdd free-form tags to secrets so that:\n- An item can carry many tags (`[\"pci\", \"stripe\", \"mobile\"]`).\n- `himitsu search` and `himitsu ls` can filter by tag.\n- The env DSL can select by tag, so an env like `pci-prod` can be authored as `tag: pci` instead of enumerating paths.\n\n## Data model\n\nTags live on `SecretValue` (the encrypted payload), as `repeated string tags = 9` in `proto/secrets.proto`. They travel with the value, are written/read through the existing age envelope, and stay atomic with the secret they describe.\n\n`SecretEntry.tags` (already declared at `proto/secrets.proto:89`) is repurposed as the manifest projection of the same field \u2014 populated by listing code that decrypts to surface metadata, identical to how descriptions are currently surfaced in `cli::search::search_core`.\n\nBackwards compatibility: empty tags is the default; legacy envelopes round-trip through `crypto::secret_value::decode` unchanged.\n\nTag grammar: `[A-Za-z0-9_.-]+`, 1\u201364 chars, no whitespace, case-sensitive. Validated at write time.\n\n## Authoring surfaces\n\n### CLI\n- `himitsu set --tag --tag ...` \u2014 repeatable flag on `cli/set.rs`.\n- `himitsu write --tag ` \u2014 same flag wired through `cli/write.rs`.\n- `himitsu tag add ...` / `rm ...` / `list` \u2014 new subcommand at `rust/src/cli/tag.rs`, registered in `cli/mod.rs`. Mutates by decrypt \u2192 edit `tags` \u2192 re-encrypt \u2192 write.\n\n### TUI\n- `tui/views/new_secret.rs`: add a `Tags` step (comma-separated input, validated per char as the user types) between `Description` and `EnvKey`.\n- `tui/views/secret_viewer.rs`: render tags as chips in the metadata pane.\n- `tui/views/search.rs`: render tag chips next to each result row; clicking/pressing a chip refines the search.\n\n## Search & filter\n\n- `himitsu search [--tag t]...`: AND-semantics across tag flags; empty query + tags = \"all secrets with these tags\". Tags read off the same `Decoded` struct produced by `search_core` decrypt loop.\n- `himitsu ls --tag t [--tag t]...`: same AND filter, applied after path-prefix walk.\n- Query-string sugar (out of scope for this epic, file as follow-up): `tag:foo bar` syntax inside the freeform query.\n\n## Env DSL \u2014 new `Tag` entry\n\nExtend `EnvEntry` (`rust/src/config/mod.rs:191`) with a `Tag(String)` variant. YAML shapes the deserializer accepts (extending `EnvEntry::deserialize` at `mod.rs:215`):\n\n- Inline string with `tag:` prefix:\n ```yaml\n envs:\n pci-prod:\n - tag:pci\n - tag:prod\n ```\n AND-semantics across multiple `tag:` entries inside one env (consistent with CLI `--tag` repetition).\n\n- Map form for explicit alias-rename, parallel to existing alias:\n ```yaml\n envs:\n pci-prod:\n - { tag: pci } # all pci-tagged secrets, key = last path segment\n - { STRIPE: tag:stripe } # alias: env-key STRIPE := the (singular) stripe-tagged secret\n ```\n Map-form must error when more than one secret matches the tag (alias requires a single value).\n\nResolver (`rust/src/config/env_resolver.rs:63`) gains a parallel branch to `resolve_concrete` / `resolve_wildcard`: walk `available_secrets`, decrypt-or-cache to read `tags`, include those that match. Wildcard capture refs (`$1`) are not extended to tag entries in this epic \u2014 out of scope.\n\n## Out of scope\n- Query-string `tag:` sugar inside `search ` (file as follow-up after this lands).\n- Sharing/inheriting tags across stores via policy.\n- Tag autocomplete in the TUI tag input (nice-to-have follow-up).\n- Capture-ref interpolation inside `Tag` env entries.\n- Renaming/merging tags as a first-class operation (do it manually via `tag rm` + `tag add`).\n\n## Success criteria\n- `himitsu set foo --tag a --tag b` round-trips: `himitsu get foo` shows tags in the metadata block, `himitsu tag foo list` prints `a\\nb`.\n- `himitsu search --tag a` returns only secrets carrying tag `a`; same for `ls --tag a`.\n- `himitsu codegen ` against an env with `- tag:a` produces a SOPS bundle whose keys are exactly the secrets tagged `a`.\n- TUI new-secret form accepts comma-separated tags; secret-viewer renders them; search rows show tag chips.\n- All existing tests pass; new tests cover: tag validation, `EnvEntry` (de)serialization round-trip for both string and map forms, resolver behavior with tag entries, `search`/`ls` filter logic.\n- Legacy secrets (no tags field) decode unchanged.\n", "status": "closed", "priority": 2, "issue_type": "epic", "owner": "noreply@anthropic.com", "created_at": "2026-05-05T12:14:38Z", "created_by": "Claude", "updated_at": "2026-05-09T02:11:54Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "Tags support already shipped end-to-end across CLI (set/write/search/ls/tag), config (EnvEntry::Tag, env resolver), and TUI (new-secret Tags step, viewer chips, search row chips). Closing the meta-epic.", "started_at": "2026-05-05T12:14:38Z"} -{"_type": "issue", "id": "hm-scm", "title": "instead of adding a row and indenting items that have a common prefix, add a key to fold/unfold by 1 level, get rid of the extra row, and use a sutble accent to show when folded/unfolded", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-04T12:25:10Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-05T03:57:37Z", "started_at": "2026-05-04T12:25:35Z", "closed_at": "2026-05-05T03:57:37Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-bs1", "title": "footer items are not spaced evenly - right-most item should be right-aligned", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-04T12:12:52Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-04T12:14:30Z", "started_at": "2026-05-04T12:13:19Z", "closed_at": "2026-05-04T12:14:30Z", "close_reason": "render_distributed_footer now anchors first item Left and last item Right so the footer row reads as evenly distributed across the full width", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-dsa", "title": "ctrl+P command menu should contain all valid commands from the stateless CLI", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-04T12:12:25Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-05T03:57:37Z", "started_at": "2026-05-04T12:25:22Z", "closed_at": "2026-05-05T03:57:37Z", "close_reason": "Ctrl+P palette now lists every visible top-level CLI command. Wired commands (new/sync/rekey/join/add-remote/switch-store/envs/help/quit/toggle-store-column) keep their behavior. New entries (recipient ls/add/rm/show, remote list/remove/default, context show/set/clear, generate, export, check, docs, import, git) emit an info toast naming the equivalent CLI invocation; full TUI forms can be wired per-command as follow-up.", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-8jm", "title": "fix himitsu sync failing in the current state", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-04T12:09:41Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-04T12:17:24Z", "started_at": "2026-05-04T12:11:24Z", "closed_at": "2026-05-04T12:17:24Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-mw9", "title": "add any compatible commands from the stateless CLI to the command picker in the TUI", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-04T01:15:29Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-04T01:18:27Z", "started_at": "2026-05-04T01:15:44Z", "closed_at": "2026-05-04T01:18:27Z", "close_reason": "Added sync, rekey, and join commands to the TUI command palette", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-1wl", "title": "himitsu import should convert SOME_KEY to some-key. it should also allow me to do a simple glob filter like foo/* and only import matches, and then it should also let me (unless it does already) set a prefix like --to some/target which would create some/target/{foo,bar,...} or --to github:org/repo#foo would put them in the foo directory. also, --to foo/bar should be equivalent to --to foo/bar/ (the trailing slash is ignored)", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-04T01:01:12Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-04T01:07:04Z", "started_at": "2026-05-04T01:01:36Z", "closed_at": "2026-05-04T01:07:04Z", "close_reason": "Implemented key normalization (SOME_KEY\u2192some-key), --filter glob, --to prefix, and --keep-names opt-out", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-u0v", "title": "auto-detect if recipient list doesn't contain own age key", "description": "## Context\n in that case, the sync status should glow orange and tell you to run 'himitsu join' which would simply add own public key to the stores recipient list, commit, and push. github actions should then rekey in a self-serve way if it is configured with a key in secrets. adding this key to secrets is also something that himitsu can do for you automatically. this task can be seen as idempotently ensuring those 3 conditions are met", "notes": "Scope per description: idempotently ensure (1) own pubkey is in the store's recipient list, (2) GitHub Actions has a configured age key in repo secrets, (3) auto-rekey via Actions is wired up. Surface state via TUI store-health glow + 'himitsu join' command that performs (1)-(3). Significant product/UX design \u2014 defer until owner aligns on join command shape and Actions self-serve flow.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-03T21:56:09Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:12:52Z", "started_at": "2026-05-03T22:06:52Z", "closed_at": "2026-05-03T22:12:52Z", "close_reason": "Implemented himitsu join command and TUI NotRecipient health indicator", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-bqk", "title": "himitsu isn't automatically running init when user has no store", "notes": "Investigation 2026-05-03: tui::run (rust/src/tui/mod.rs:33) auto-launches the wizard only on missing key (~/.local/share/himitsu/key). Gap: a user with a key but no resolved store gets an empty dashboard. Fix would be to also trigger run_init_flow when ctx.store.as_os_str().is_empty(). Risk: run_init_flow currently calls run() recursively after the wizard completes (mod.rs:126); init can complete-and-Success without creating a store if --name/--project are not used in the wizard, which would create an infinite loop. Either (a) require --name in the wizard's success path, (b) only re-enter run() when wizard reports a store was created, or (c) return Ok(()) from run() when store is still empty post-wizard. Needs interactive testing.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-03T21:52:55Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:07:19Z", "started_at": "2026-05-03T22:05:14Z", "closed_at": "2026-05-03T22:07:19Z", "close_reason": "TUI now launches init flow when key exists but no store resolves, with post-wizard no-store guard.", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-4y5", "title": "Cross-repo ref: prompt to clone missing referenced store", "description": "When a qualified cross-repo reference (github:org/repo#path) hits a store that isn't checked out locally, prompt for confirmation before cloning. Interactive: y/N prompt (default no). Non-interactive (no TTY): error with hint to run remote add. Add --yes to bypass confirmation for scripted use. TUI: small modal.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T10:55:51Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-01T10:57:55Z", "started_at": "2026-05-01T10:56:38Z", "closed_at": "2026-05-01T10:57:55Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-w6c", "title": "Cross-repo ref: prefer github:org/repo#path syntax (accept legacy / form)", "description": "Switch the canonical cross-repo reference syntax from github:org/repo/path/to/secret to github:org/repo#path/to/secret to disambiguate the slug from the path. Parser must accept both forms; the # form is the canonical one (used in docs, error messages, codegen). The slash form continues to parse as a fallback.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T10:55:51Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-01T10:57:55Z", "started_at": "2026-05-01T10:55:55Z", "closed_at": "2026-05-01T10:57:55Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-k4c", "title": "init should have placeholder for config set to project if in a git repo, unless the user has not configured a global store yet", "description": "In the init wizard's remote-store step, when inside a git repo (or parent is), default the placeholder to the current repo's origin slug and write to /himitsu.yaml as project-scoped config. Always run the global step first (skipped if already configured), labeled 'configure global store', then run a separate 'configure project' step (skipped if not in a git repo).", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T10:29:47Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-01T10:42:26Z", "closed_at": "2026-05-01T10:42:26Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-anm", "title": "Expand README sync and store health docs", "description": "Document sync behavior, auto-commit/push, no-remote and not-pushed warnings, --no-push, auto_pull, and TUI store health states.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:23Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:14Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-1lt", "title": "Make README quick start TUI-first", "description": "Revise README Quick Start so the primary path launches the TUI first, then presents CLI commands as scripting/automation alternatives.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:22Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:15Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-qku", "title": "Document TUI theme and key configuration", "description": "Add README documentation for global config TUI settings, including tui.theme, tui.keys, accepted built-in themes, and HIMITSU_TUI_THEME override.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:22Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:15Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-o89", "title": "Fix README search documentation", "description": "Update README search feature and command docs to reflect current behavior: search reads store files directly and --refresh is retained as a no-op compatibility flag, not a SQLite index rebuild.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:21Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:16Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-1b7", "title": "Review README and stabilize demo sync state", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:37:30Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-01T00:40:56Z", "started_at": "2026-05-01T00:37:37Z", "closed_at": "2026-05-01T00:40:56Z", "close_reason": "Reviewed README and added demo sync-state override", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-2f9", "title": "Refresh VHS demo tapes", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T23:45:18Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-01T00:03:22Z", "started_at": "2026-04-30T23:45:23Z", "closed_at": "2026-05-01T00:03:22Z", "close_reason": "Refreshed VHS demo as sourced pure TUI tape and validated render", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-wqb", "title": "allow editing name of items", "description": "In the himitsu TUI, pressing Enter on an item opens an editor that lets you edit every field EXCEPT the item's name. Make the name field editable too.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T23:19:04Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-09T02:11:54Z", "started_at": "2026-04-30T23:23:03Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "Editing the path field in the secret-viewer edit doc now renames the secret while preserving created_at + history (covered by finish_edit_renames_secret_and_preserves_history)."} -{"_type": "issue", "id": "hm-6qj", "title": "revamp envs feature.", "description": "## Context\n envs should ultimately be stored like this:\n# config.yaml\n...\nenvs:\n my-env:\n - SOME_KEY: some-exact-match # optionally override env key\n - other-exact-match # uses env key set on item or fallsback to upper(replace(name, -,_)) (forward slash replaced with __)\n - arbitrary-glob/*\nmy-env-{dev,prod,stg}:\n - SOME_KEY: ref-requires-{} # requires specifying where to replace\n - some-group/item-name-{} # using the {} is not required if you have a better format\n\n\nTherefore, the env editor should have an initial browser where i can select an existing env or add a new one, then take me into a 2-panel layout where I am editing the left side as if it was hx and the right pane shows me a preview of what env would be generated based on matches and/or any errors. there should be fuzzy find support and some auto-complete like feature that lets me discover the right labels as im editing", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T23:16:44Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-09T02:11:54Z", "started_at": "2026-04-30T23:23:04Z", "dependency_count": 0, "dependent_count": 0, "comment_count": 0, "closed_at": "2026-05-09T02:11:54Z", "close_reason": "envs feature revamp shipped: 2-pane DSL editor with live preview pane, fuzzy-find autocomplete corpus over secret names/groups, scope-aware browser, wildcard envs."} -{"_type": "issue", "id": "hm-5l4", "title": "Highlight self in recipient ls", "description": "Update himitsu recipient ls output so recipient rows whose full age public key matches the current user's own public key are highlighted in TTY output without changing non-TTY/plain output.", "status": "closed", "priority": 2, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T12:26:21Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T12:28:59Z", "started_at": "2026-04-30T12:26:43Z", "closed_at": "2026-04-30T12:28:59Z", "close_reason": "Highlighted recipient ls rows whose full public key matches the local self public key in TTY output, preserving plain non-TTY output", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-cak", "title": "Fix search footer alignment and centering", "description": "Apply split-layout footer alignment to the search view and correct small-screen search canvas centering.", "status": "closed", "priority": 2, "issue_type": "bug", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T11:01:04Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T11:02:44Z", "started_at": "2026-04-30T11:01:33Z", "closed_at": "2026-04-30T11:02:44Z", "close_reason": "Aligned search footer right actions with split layout and replaced Layout-based canvas centering with deterministic centered Rect math", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-774", "title": "all screens should be the same size as the search view - apply whatever constraints the search view have globally", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T04:01:29Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T10:35:08Z", "started_at": "2026-04-30T10:29:32Z", "closed_at": "2026-04-30T10:35:08Z", "close_reason": "Added shared standard_canvas margin/max-size helper and applied search view constraints across TUI screens and overlays", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-w6y", "title": "Review and prune stale tests", "description": "Review the current test suite and remove tests that cover deprecated behavior, stale expectations, or backwards-compatibility-only behavior no longer relevant to the Rust rewrite.", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T03:52:16Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T04:03:23Z", "started_at": "2026-04-30T03:52:27Z", "closed_at": "2026-04-30T04:03:23Z", "close_reason": "Pruned stale/deprecated/backwards-compat test coverage and verified test suite passes", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-aqi", "title": "Submodule-aware commit/push in dispatcher", "description": "Enable single-store + git-submodule-for-includes workflow. Previously, himitsu set to a path inside a submodule would strand the file: submodule dirty, parent clean, nothing pushed. Dispatcher now commits submodules first, pushes them first, holds back parent push if any submodule push fails. auto_pull gains --recurse-submodules. Safe detached-HEAD auto-checkout only when HEAD matches origin default-branch tip.", "status": "closed", "priority": 2, "issue_type": "feature", "owner": "demo@himitsu.dev", "created_at": "2026-04-24T09:18:44Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-24T09:19:01Z", "closed_at": "2026-04-24T09:19:01Z", "close_reason": "Landed: list_submodules/has_unpushed_commits/ensure_on_branch helpers in git.rs; Context::commit walks dirty submodules then parent, Context::push submodules-first with hold-back on failure, pull_if_remote recurses. 4 new tests covering round-trip with real local bare remotes. 460 tests green.", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-fy3", "title": "himitsu git status should never have unstaged / working changes. every action should commit, and make the git history be essentially an append-only log", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T22:13:42Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T22:40:35Z", "closed_at": "2026-04-21T22:40:35Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-ic2", "title": "himitsu codegen emits .sops.yaml", "description": "Extend `himitsu codegen` (rust/src/cli/codegen.rs) to accept an env label and emit a valid SOPS file at `.sops.yaml`. Today codegen's `--env` flag is already threaded through (see rust/src/cli/codegen.rs:55-56) for language bindings; this task adds a new output mode where the env itself is the target.\n\n## CLI surface\n\n- `himitsu codegen ` \u2014 shorthand for `--lang sops --env --output .sops.yaml`.\n- `himitsu codegen --output path.yaml` \u2014 override output path.\n- Accepts concrete labels (`foo/dev`) and wildcards (`foo/*`); wildcard form emits one file per matched child (`foo-dev.sops.yaml`, `foo-prod.sops.yaml`), or a single merged file if `--merge` is given (decide in review).\n\n## Output format\n\n- Top-level keys come from the resolved env entries (hm-qjz), each mapped to its decrypted secret value.\n- File is encrypted via `sops` rules: either the project's existing `.sops.yaml` rules file or an inline `creation_rules` block derived from himitsu recipients (TBD \u2014 call out in design doc).\n- Generated file contains the same `AUTO-GENERATED` header banner codegen already emits (rust/src/cli/codegen.rs:537).\n\n## Acceptance\n\n- `himitsu codegen foo/dev` produces `foo-dev.sops.yaml` that `sops -d` can decrypt with the configured recipients.\n- Wildcard case produces the expected file set.\n- Errors clearly when the env label is unknown or when captures fail to resolve.", "notes": "## Integration decision (2026-04-21)\n\n**Shell out to the `sops` CLI.** Generate plaintext YAML from the resolved env tree, write it to the output path, then invoke `sops --encrypt --in-place `. Key/recipient resolution is delegated to the user's existing `.sops.yaml` rules file.\n\n- Requires `sops` on PATH \u2014 detect and emit a helpful error if missing.\n- No new crypto crate dependency.\n- Existing `AUTO-GENERATED` header banner applies to the plaintext emission before encryption.", "status": "closed", "priority": 2, "issue_type": "feature", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:39:24Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T21:32:38Z", "closed_at": "2026-04-21T21:32:38Z", "close_reason": "Landed: codegen positional triggers sops path. Resolver->decrypt->YAML->sops encrypt-in-place. Missing-sops error has install hint. 6 new tests green, 1 ignored smoke.", "dependencies": [{"issue_id": "hm-ic2", "depends_on_id": "hm-20e", "type": "blocks", "created_at": "2026-04-21T13:39:42Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-ic2", "depends_on_id": "hm-r2e", "type": "parent", "created_at": "2026-04-21T13:39:45Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-20e", "title": "Env resolver: expand wildcards and capture references", "description": "Central pure-function resolver that both the TUI preview and `himitsu codegen ` depend on. Given the full env map + a target label, return the concrete resolved entries (key \u2192 secret path, no wildcards, no captures remaining).\n\n## Behavior\n\n- Input: `&Config.envs`, target label (e.g. `foo/*` or `foo/dev`).\n- Resolution:\n 1. If label is concrete, return its entries verbatim (Aliases already resolved to path refs).\n 2. If label ends in `/*`, enumerate sibling envs whose labels match `foo/` (or deeper \u2014 TBD in schema design), and for each child, substitute captures into the glob's entries (e.g. `\\$1` \u2192 the matched segment).\n 3. Merge overlapping keys: concrete child entries win over wildcard-inherited ones.\n- Output: `Vec` plus diagnostics for unresolved captures or missing targets.\n\n## Why a dedicated resolver\n\nCodegen (hm-nm9) and the TUI preview (hm-kli) both need identical semantics. Putting it in one place means users never see a mismatch between TUI preview and generated SOPS output.\n\n## Acceptance\n\n- Table-driven tests for: concrete label, wildcard with 0/1/N children, nested wildcards (if allowed by schema), captures in Alias paths, capture index out of range.\n- Resolver returns deterministic order (sort by key) so generated SOPS files are diff-friendly.", "notes": "## Output shape correction (2026-04-21)\n\nThe resolver returns a **nested tree**, not a flat `Vec`. Shape (proposed):\n\n```rust\nenum EnvNode {\n Leaf { secret_path: String },\n Branch(BTreeMap),\n}\n```\n\nReasoning: `foo/*` expansion over children `foo/bar` and `foo/bar/baz` must produce a single nested structure, not a flat list and not multiple resolver invocations. Captures (`\\$1`) still substitute path segments, but the output is always a tree rooted at the queried label.\n\nGlob entries reference secret variables only \u2014 never other env groups. The resolver does not follow cross-env references.\n\nDeterminism: `BTreeMap` gives sorted keys \u2192 diff-friendly downstream codegen.", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:39:15Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T21:12:58Z", "closed_at": "2026-04-21T21:12:58Z", "close_reason": "Wave 2 landed in commit. Resolver returns deterministic EnvNode trees for concrete + wildcard labels. Cache mirrors envs into SQLite with scope isolation, SHA-256 staleness, atomic refresh. 18 new tests green.", "dependencies": [{"issue_id": "hm-20e", "depends_on_id": "hm-5v4", "type": "blocks", "created_at": "2026-04-21T13:39:39Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-20e", "depends_on_id": "hm-r2e", "type": "parent", "created_at": "2026-04-21T13:39:44Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 2, "comment_count": 0} -{"_type": "issue", "id": "hm-zzj", "title": "TUI Envs tab: list, create, edit, delete presets", "description": "Add a dedicated Envs tab alongside the existing TUI views (see rust/src/tui/views/). It becomes the primary UX for authoring preset environments.\n\n## UX sketch\n\n- Left pane: tree of env labels grouped by scope (project first, then global). `foo`, `foo/bar`, `foo/*` render as a hierarchy.\n- Right pane: the selected env's resolved entries \u2014 Singles, Aliases, and the expanded form of any Glob (so users see what `foo/*` actually produces).\n- Keymap reuses the project's existing conventions (rust/src/tui/keymap.rs): `n` new env, `e` edit, `d` delete, `/` search, `Enter` drill in.\n- Inline editor for entry kinds (Single | Glob | Alias) with a live preview column showing capture-group expansions.\n\n## Wiring\n\n- Reads come from the SQLite cache (hm-8ht).\n- Writes go through the mutation layer (hm-d1m); optimistic UI with toast rollback on failure.\n- Scope indicator visible at all times; creation defaults to project scope inside a git repo, global otherwise.\n\n## Acceptance\n\n- Can create `foo`, `foo/bar`, and `foo/*` entries end-to-end from the TUI.\n- Editing an Alias with a capture ref renders the live expansion for each matched child env.\n- Deleting an env removes it from YAML + cache on disk.", "notes": "## Scope reduction (2026-04-21)\n\n**First slice: read-only + delete.** Creation and inline editing are deferred to follow-up issues (to be filed when this one lands).\n\nIncluded in v1:\n- Tree/list view of envs grouped by scope (project/global).\n- Right pane shows resolved entries (Singles, Aliases, expanded Globs via hm-20e).\n- `d` deletes the selected env (with confirmation), routed through the mutation API (hm-d1m).\n- `/` search, scope indicator, navigation.\n\nDeferred to follow-ups:\n- `n` new env flow with inline editor.\n- `e` edit existing entries.\n- Live capture-reference preview editor.\n\nRationale: smallest slice that validates the cache + resolver + mutation stack end-to-end through real UI. Creation UX deserves its own design pass once read-path is proven.", "status": "closed", "priority": 2, "issue_type": "feature", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:39:06Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T21:57:31Z", "closed_at": "2026-04-21T21:57:31Z", "close_reason": "Read+delete TUI tab landed. Epic complete: schema + cache + resolver + mutation + codegen sops + TUI all merged. 5 new view tests; 337 lib tests pass.", "dependencies": [{"issue_id": "hm-zzj", "depends_on_id": "hm-20e", "type": "blocks", "created_at": "2026-04-21T13:39:41Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-zzj", "depends_on_id": "hm-8ht", "type": "blocks", "created_at": "2026-04-21T13:39:40Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-zzj", "depends_on_id": "hm-d1m", "type": "blocks", "created_at": "2026-04-21T13:39:40Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-zzj", "depends_on_id": "hm-r2e", "type": "parent", "created_at": "2026-04-21T13:39:45Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 3, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-d1m", "title": "Config mutation layer: write envs to project or global scope", "description": "Add a mutation API that the TUI and any future CLI subcommands call to create/update/delete env presets. Scope is inferred the same way `codegen` already does it (rust/src/cli/codegen.rs:118-127): walk up for `.himitsu.yaml`, else fall back to global config.\n\n## Requirements\n\n- Single entry point: `envs::upsert(label, entries, scope_hint) -> Result<()>`; sibling `envs::delete(label, scope)`.\n- Preserve YAML formatting/comments where feasible (serde_yaml round-trip is lossy \u2014 evaluate `yaml-rust2` or string-level edits if comments matter here).\n- Atomic writes: temp file + rename so a crash mid-write does not corrupt config.\n- After every mutation, refresh the SQLite cache row for that scope.\n- Validation: reject labels that violate the grammar from the schema-design task.\n\n## Acceptance\n\n- Unit tests for project vs global routing, label validation, atomic write behavior.\n- Integration test: mutate via API \u2192 reload `Config` from disk \u2192 observe change.", "notes": "## YAML fidelity decision (2026-04-21)\n\n**Accept lossy round-trip via `serde_yaml`.** Comments and custom formatting in `.himitsu.yaml` / global config will not survive a mutation. This matches the current codebase pattern and keeps the implementation small.\n\n- Document the lossy behavior in the TUI help panel + user-facing docs.\n- Atomic write (temp file + rename) still required to avoid partial writes.", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:38:54Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T21:23:55Z", "closed_at": "2026-04-21T21:23:55Z", "close_reason": "Landed: upsert/delete/read API with ScopeHint inference, atomic writes, validation, cache refresh. Config gained first-class envs field. 11 new tests green.", "dependencies": [{"issue_id": "hm-d1m", "depends_on_id": "hm-5v4", "type": "blocks", "created_at": "2026-04-21T13:39:37Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-d1m", "depends_on_id": "hm-8ht", "type": "blocks", "created_at": "2026-04-21T13:39:38Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-d1m", "depends_on_id": "hm-r2e", "type": "parent", "created_at": "2026-04-21T13:39:44Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 2, "dependent_count": 1, "comment_count": 0} -{"_type": "issue", "id": "hm-8ht", "title": "SQLite cache for env presets (project + global scope)", "description": "Mirror `envs:` from YAML config into SQLite so the TUI can list/search without re-parsing YAML on every keystroke, and so other subsystems (codegen, check) have a uniform read API.\n\n## Requirements\n\n- Tables (proposed):\n - `envs(id INTEGER PK, label TEXT, scope TEXT CHECK(scope IN ('project','global')), config_path TEXT, mtime INTEGER)`\n - `env_entries(id INTEGER PK, env_id INTEGER REFERENCES envs(id) ON DELETE CASCADE, kind TEXT, value TEXT, alias_key TEXT)`\n- Rebuild strategy: on load, compare config file hash/mtime against cached row; rebuild if stale.\n- Scope handling: project rows keyed by git-root path so switching projects does not pollute.\n- YAML remains source of truth \u2014 every write path must rewrite YAML then refresh the cache; never the reverse.\n\n## Non-goals\n\n- No schema migrations framework yet \u2014 initial table ships with the feature.\n- No cross-device sync; the cache is purely local.\n\n## Acceptance\n\n- Round-trip test: load YAML \u2192 cache \u2192 query \u2192 matches parsed `Config.envs`.\n- Stale detection: mutating YAML on disk triggers cache rebuild on next read.", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:38:47Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T21:12:58Z", "closed_at": "2026-04-21T21:12:58Z", "close_reason": "Wave 2 landed in commit. Resolver returns deterministic EnvNode trees for concrete + wildcard labels. Cache mirrors envs into SQLite with scope isolation, SHA-256 staleness, atomic refresh. 18 new tests green.", "dependencies": [{"issue_id": "hm-8ht", "depends_on_id": "hm-5v4", "type": "blocks", "created_at": "2026-04-21T13:39:36Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-8ht", "depends_on_id": "hm-r2e", "type": "parent", "created_at": "2026-04-21T13:39:43Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 2, "comment_count": 0} -{"_type": "issue", "id": "hm-5v4", "title": "Design env schema: wildcard semantics + capture references", "description": "Formalize the env label/entry model before any TUI or codegen work lands. The current `EnvEntry` enum (rust/src/config/mod.rs:134) has `Single`, `Glob`, `Alias` \u2014 decide how capture references like `/\\$1/postgres-url` are expressed on-disk and in-memory.\n\n## Questions to resolve\n\n- Label grammar: confirm `foo`, `foo/bar`, `foo/*` are the only legal forms; reject `foo/*/bar`? Or allow mid-path wildcards?\n- Does a `foo/*` env enumerate its children by scanning other env labels in config, or by scanning the secret namespace? (Probably: scan sibling env labels that match `foo/`.)\n- Capture-ref syntax: `\\$1`, `\\${1}`, `{{1}}`? Which path segments are captured \u2014 everything matched by `*`?\n- Do captures only make sense inside `Glob` entries, or also inside `Alias.path`? Likely the latter: `Alias { key: DB_URL, path: \\\"/\\$1/postgres-url\\\" }` under a `foo/*` env.\n- Round-trip serde: new `EnvEntry` variant or reuse existing `Glob`/`Alias` with pattern detection?\n\n## Deliverable\n\nA short design doc (in this issue's `--design`) + updated serde tests in rust/src/config/mod.rs covering:\n- Capture-ref in Alias paths\n- Invalid label rejection\n- Expansion semantics (input env set \u2192 resolved concrete entries)", "notes": "## Locked constraints (2026-04-21)\n\n- **Reject mid-path wildcards.** Only `foo`, `foo/bar`, `foo/*` are valid. `foo/*/bar`, `*/foo`, and similar are hard errors at config-parse time.\n- **Glob entries reference secret variables only, never other env groups.** Inside a `foo/*` env, entries describe secret paths (optionally with `\\$1` captures) \u2014 they do not pull in other env labels.\n- **Wildcards produce nested output, never multiple files.** `foo/*` expanded over concrete children `foo/bar`, `foo/bar/baz` yields a single tree:\n ```\n foo:\n bar:\n baz: \n ```\n This mirrors how sops would naturally represent a hierarchical namespace.\n\nThese constraints must be enforced by the label grammar validator and reflected in the resolver's output shape (which is a tree, not a flat list \u2014 see hm-20e).", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:38:39Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T20:51:37Z", "closed_at": "2026-04-21T20:51:37Z", "close_reason": "Schema + validator landed: validate_env_label, parse_captures, validate_envs, ProjectConfig::validate. 15 new tests green, full workspace builds clean. Mid-path wildcards, bare-wildcard, bad chars, capture-in-concrete-env, and high-capture-index all rejected. Resolver (hm-20e) and codegen (hm-ic2) can now consume these helpers.", "dependencies": [{"issue_id": "hm-5v4", "depends_on_id": "hm-r2e", "type": "parent", "created_at": "2026-04-21T13:39:43Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 3, "comment_count": 0} -{"_type": "issue", "id": "hm-r2e", "title": "Preset environment management (TUI tab + codegen integration)", "description": "## Motivation\n\nUsers already declare environment presets in config under `envs:` as a `BTreeMap>` with `Single`, `Glob`, and `Alias` variants (see rust/src/config/mod.rs:116, :134). Today this config is only consumable by read paths \u2014 there is no UX for authoring, visualizing, or sharing envs, and `himitsu codegen` currently emits language bindings rather than deployable secret bundles.\n\nThis epic adds a first-class preset environment manager that:\n- Surfaces envs as a dedicated tab in the TUI\n- Treats env labels as a path namespace: `foo`, `foo/bar`, `foo/*` are all valid, where `foo/*` is a wildcard shortcut that expands across children\n- Supports capture-group references inside wildcard entries, e.g. `/$1/postgres-url` lets `foo/*` expand into `foo/dev/postgres-url`, `foo/prod/postgres-url`, etc.\n- Mutates the correct config file based on context (project `.himitsu.yaml` vs global config)\n- Mirrors the config into the SQLite cache so reads are fast and the TUI stays responsive\n- Extends `himitsu codegen ` to emit `.sops.yaml` \u2014 a valid SOPS-encrypted secret bundle \u2014 so envs become deployable artifacts, not just labels\n\n## Data model\n\nConceptually this is a many-to-many relationship: envs \u2194 secrets. In SQLite this is cached as (example, subject to design child):\n\n- `envs(id, label, scope)` \u2014 `scope` \u2208 {project, global}; `label` stores the full namespaced path (`foo`, `foo/bar`)\n- `env_entries(env_id, kind, value, alias_key, capture_pattern)` \u2014 one row per EnvEntry, preserving Single/Glob/Alias semantics\n\nAuthoritative storage stays YAML; SQLite is a rebuilt cache keyed off config file mtime/hash.\n\n## Scope boundary\n\n- Scope (project vs global) is inferred the same way `codegen` already does it \u2014 walk up for `.himitsu.yaml`, else global.\n- All mutations are writes to the YAML config plus a cache-refresh; no new storage of truth in SQLite.\n\n## Out of scope\n\n- Changing how secrets themselves are stored/encrypted\n- Re-keying or rotation flows (separate surface)\n- Any non-SOPS codegen output format (typescript/go/etc. stay as-is)\n\n## Success criteria\n\n- TUI has an Envs tab that lists, creates, edits, and deletes env presets with wildcard + alias support\n- Wildcard capture references (`$1`, `$2`, ...) resolve correctly in both TUI preview and codegen\n- `himitsu codegen ` produces a `.sops.yaml` that round-trips through `sops` without manual editing\n- SQLite cache stays consistent with YAML after every mutation; restart reflects on-disk state", "status": "closed", "priority": 2, "issue_type": "epic", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:38:26Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T21:57:31Z", "closed_at": "2026-04-21T21:57:31Z", "close_reason": "Read+delete TUI tab landed. Epic complete: schema + cache + resolver + mutation + codegen sops + TUI all merged. 5 new view tests; 337 lib tests pass.", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-vy9", "title": "remove reference to legacy 'groups' command - use recipients in a path-based manner instead, e.g. team/person or team/*", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T20:26:12Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-21T20:34:51Z", "closed_at": "2026-04-21T20:34:51Z", "close_reason": "removed group references from README and CLI help, updated to path-based recipients", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-czj", "title": "TUI details/edit should support arbitrary fields (key/value pairs)", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T19:31:36Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T19:41:28Z", "closed_at": "2026-04-18T19:41:28Z", "close_reason": "annotations wired through decode/encode, TUI viewer displays them, edit doc accepts custom key:value as annotations", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-tz3", "title": "TUI should display status of remote store (if its out of sync/unhealthy)", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:53:59Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T16:26:20Z", "closed_at": "2026-04-18T16:26:20Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6mo", "title": "get rid of groups. recipients should allow for path-based names which would allow the equivalent of groups by just using foo/*", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:53:12Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T16:26:21Z", "closed_at": "2026-04-18T16:26:21Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6nb", "title": "add export command that lets you create SOPS-compatble files, mapping multiple keys to secrets, with support for foo/bar/* which would include all matching secrets with their default key", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:51:13Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T16:26:21Z", "closed_at": "2026-04-18T16:26:21Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-jk1", "title": "add support for SOPS to import", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:49:37Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T16:26:21Z", "closed_at": "2026-04-18T16:26:21Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-far", "title": "import command should allow importing the whole vault, including a dry run", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:49:05Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T16:26:21Z", "closed_at": "2026-04-18T16:26:21Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-yxb", "title": "search should show time ago format for created", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:38:47Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T15:54:48Z", "closed_at": "2026-04-18T15:54:48Z", "close_reason": "Implemented in TUI search view: humanized dates via humanize_age() and description column added", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-zzu", "title": "search page should show excerpt of description.", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:38:26Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T15:54:48Z", "closed_at": "2026-04-18T15:54:48Z", "close_reason": "Implemented in TUI search view: humanized dates via humanize_age() and description column added", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-pmy", "title": "every mutation should trigger a git sync to remote if configured", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-18T15:23:14Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-18T16:26:21Z", "closed_at": "2026-04-18T16:26:21Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-bln", "title": "himitsu 'get/read' completions should show available secrets, not files in the current directory", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T14:11:07Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-17T07:14:55Z", "closed_at": "2026-04-17T07:14:55Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-p8g", "title": "add import command, start with import from 1password via op cli", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T14:09:11Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-17T07:14:57Z", "closed_at": "2026-04-17T07:14:57Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-t81", "title": "use 'n days ago' format for date in search. include description, and put store as last column.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T14:08:28Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-17T07:14:58Z", "closed_at": "2026-04-17T07:14:58Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-f6z", "title": "TUI: integration test harness driving App via simulated key events", "description": "Build a test harness that constructs App, feeds a sequence of crossterm::event::KeyEvent through the main loop tick, and asserts on rendered buffer state. Enables end-to-end coverage of search\u2192viewer\u2192new-secret flows without spawning a real terminal.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T06:53:32Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T07:01:04Z", "closed_at": "2026-04-15T07:01:04Z", "close_reason": "Merged via parallel worktree agents; cargo test --bin himitsu tui::harness:: \u2192 3 passed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-3cm", "title": "TUI: configurable keybindings via config file", "description": "Allow users to remap TUI key bindings through the himitsu config file. Define a KeyMap struct, default to current bindings, let config override by action name (e.g. 'quit', 'new_secret', 'switch_store'). Threaded through App to each view's handle_key.", "status": "closed", "priority": 2, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T06:53:30Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T07:19:59Z", "closed_at": "2026-04-15T07:19:59Z", "close_reason": "Merged to main; 231 tests pass (toast + configurable keymap). Conflicts in app.rs/search.rs/secret_viewer.rs resolved by keeping toast-based action returns on top of keymap dispatch", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-o15", "title": "TUI: status-line toast for transient feedback", "description": "Add a toast/status-line component that renders transient messages (saved, copied, deleted, error) for N seconds then clears. Currently feedback is inline-only in individual views. Should be globally mounted in App so every view can emit a toast.", "status": "closed", "priority": 2, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T06:53:21Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T07:19:59Z", "closed_at": "2026-04-15T07:19:59Z", "close_reason": "Merged to main; 231 tests pass (toast + configurable keymap). Conflicts in app.rs/search.rs/secret_viewer.rs resolved by keeping toast-based action returns on top of keymap dispatch", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-64c", "title": "record new demo after major tui changes", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T02:28:44Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T02:54:24Z", "closed_at": "2026-04-15T02:54:24Z", "close_reason": "Rewrote and re-recorded all 6 TUI tapes (us-008..us-013) against search-as-root + new-secret metadata + store picker flow", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-r4i", "title": "new secret flow should alow setting metadata, not just edit", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-14T14:23:45Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T02:46:54Z", "closed_at": "2026-04-15T02:46:54Z", "close_reason": "metadata fields (description/url/totp/env_key/expires_at) added to new-secret form with tab nav, validation, 13 new tests", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-y6n", "title": "provide alternative hotkey for ctrl+s to save, as it is commmonly set in tmux and conflicts", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-14T14:23:19Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-14T14:28:32Z", "closed_at": "2026-04-14T14:28:32Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-72k", "title": "get rid of 'env', its being used as a group which is incorrect. it shouldnt even show in the search tui. env is for codegen which I will spec out later", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-14T14:20:35Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-14T14:28:32Z", "closed_at": "2026-04-14T14:28:32Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-8z8", "title": "search view should have a hotkey for copying to clipboard", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T18:07:09Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-14T14:23:23Z", "closed_at": "2026-04-14T14:23:23Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-57k", "title": "tui should render all stores together, rendering the store as a column", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T18:06:03Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T18:11:43Z", "closed_at": "2026-04-13T18:11:43Z", "close_reason": "multi-store grouping with per-store header rows", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-rnx", "title": "TUI secret viewer should render all decoded metadata", "description": "The secret viewer in the TUI only shows path/env/created_at/lastmodified/recipients. It should also render description, url, totp, expires_at, and env_key from the decoded SecretValue so users can see everything they just edited without round-tripping through the editor.", "status": "closed", "priority": 2, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T18:05:44Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T18:12:51Z", "closed_at": "2026-04-13T18:12:51Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-ldq", "title": "Add env_key metadata field to SecretValue", "description": "Prepare for 'himitsu exec path/* -- cmd' by letting each secret carry a default environment variable name. When unset, callers derive one from the path (typically last segment uppercased). Description field already exists from hm-dvs/hm-de4.", "acceptance_criteria": "proto field added; --env-key flag on set; validates as POSIX env var name; surfaced by himitsu get; round-trips through encode/decode", "status": "closed", "priority": 2, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T17:20:50Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T17:21:05Z", "closed_at": "2026-04-13T17:21:05Z", "close_reason": "env_key field added end-to-end with validation + tests", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-tcp", "title": "columns should have header labels in tui", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T17:17:56Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T18:12:51Z", "closed_at": "2026-04-13T18:12:51Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-3ir", "title": "the default tui should use a search bar with fuzzy find instead of the two-column navigator", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T04:04:48Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T04:11:38Z", "closed_at": "2026-04-13T04:11:38Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-jjc", "title": "add fuzzy find to search", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:14:50Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-yu4", "title": "recipients should allow for adding metadata. adding a recipient should additionally prompt for a description (optional)", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:11:02Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-dvs", "title": "add a field called totp that can be used to associate a TOTP key for any secret. also add a URL field that can be used to store the website it is associated with", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:10:15Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-de4", "title": "add a expires_at field to metadata, can be optionally set to remind when the secret is about to expire", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:08:26Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6xd", "title": ".himitsu/recipients should contain a flat list of recipients. the group should not be determined by folder nesting, but rather by a mapping in .himitsu.yaml - this solves the issue where a recipient cannot belong to multiple groups without duplicating files", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:06:39Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-ftf", "title": "implement himitsu read/write which are like get/set except they return just the plaintext secret, render no metadata, and are meant to be used in other tools", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:04:41Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-381", "title": "implement completions", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T03:02:56Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-28r", "title": "himitsu search should look similar to gh repo list", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:56:40Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:39:11Z", "closed_at": "2026-04-13T03:39:11Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-iq6", "title": "himitsu tui navigation should look more like a table that shows the path, description, last modified date, and store it belongs to. folders should be sorted first just like in the file explorer in most IDEs", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:40:26Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:51:25Z", "closed_at": "2026-04-13T03:51:25Z", "close_reason": "Closed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb.6", "title": "US-013: Store/env switch from dashboard", "description": "Let the user switch the active store (recipients/data dir) without restarting the TUI. Add a store picker (e.g. 's' key) that lists configured stores from config and rebuilds the dashboard against the chosen store. Scope of 'env switch' is unclear \u2014 envs are derived from path prefixes, so selection already works; confirm with user whether this bead should also cover a true multi-context switch.", "notes": "Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape \u2192 regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:51Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:16Z", "closed_at": "2026-04-13T03:12:16Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-6vb.6", "depends_on_id": "hm-6vb", "type": "parent-child", "created_at": "2026-04-12T19:14:50Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb.3", "title": "US-011: Drill into secret from dashboard", "description": "Pressing Enter on a secret in the dashboard's right-hand list currently does nothing. Wire it to open the SecretViewerView directly, skipping the search detour. Also add j/k/arrow navigation within the secret list (currently only env list navigates).", "notes": "Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape \u2192 regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:40Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:15Z", "closed_at": "2026-04-13T03:12:15Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-6vb.3", "depends_on_id": "hm-6vb", "type": "parent-child", "created_at": "2026-04-12T19:14:39Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb.4", "title": "US-012: Help overlay ('?')", "description": "Add a modal help overlay bound to '?' (and maybe F1) that lists all keybindings for the current view. Should be view-aware: dashboard, search, and viewer each show their own bindings. Dismiss with Esc or '?'.", "notes": "Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape \u2192 regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:40Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:15Z", "closed_at": "2026-04-13T03:12:15Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-6vb.4", "depends_on_id": "hm-6vb", "type": "parent-child", "created_at": "2026-04-12T19:14:39Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb.5", "title": "US-008: Create secret from TUI", "description": "Add a 'new secret' flow reachable from the dashboard (e.g. 'n' keybinding). Should prompt for env/path and value (value input hidden), then call the same internal path as 'himitsu set' \u2014 no subprocess. On success, refresh the dashboard list and select the new entry.", "notes": "Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape \u2192 regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:40Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:15Z", "closed_at": "2026-04-13T03:12:15Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-6vb.5", "depends_on_id": "hm-6vb", "type": "parent-child", "created_at": "2026-04-12T19:14:39Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb.1", "title": "US-009: Delete secret from TUI", "description": "Add a delete action in the secret viewer and/or search result list (e.g. 'd' with a confirmation prompt). Wire to the existing store delete path. Refresh the parent view on success and surface errors as a status line.", "notes": "Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape \u2192 regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:39Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:15Z", "closed_at": "2026-04-13T03:12:15Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-6vb.1", "depends_on_id": "hm-6vb", "type": "parent-child", "created_at": "2026-04-12T19:14:39Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb.2", "title": "US-010: Edit secret value from TUI", "description": "Currently 'e' on the secret viewer rekeys \u2014 rebind rekey and add a real edit flow that decrypts the current value, opens it in an in-TUI editor (or $EDITOR fallback with terminal suspend, matching the init wizard pattern), and re-encrypts on save. Decide with the user whether to preserve 'e' for edit and move rekey to a less prominent key.", "notes": "Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape \u2192 regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:39Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:15Z", "closed_at": "2026-04-13T03:12:15Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-6vb.2", "depends_on_id": "hm-6vb", "type": "parent-child", "created_at": "2026-04-12T19:14:39Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6vb", "title": "TUI Phase 2: CRUD + UX polish", "description": "Follow-on epic to hm-nck. The Rust/ratatui TUI shipped as a read-only explorer (dashboard \u2192 search \u2192 viewer). This epic adds the missing CRUD actions (create/edit/delete), first-class navigation from the dashboard, a help overlay, and store/env switching so the TUI stands on its own without shelling out to the CLI for common operations.", "status": "closed", "priority": 2, "issue_type": "epic", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T02:14:19Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T03:12:16Z", "closed_at": "2026-04-13T03:12:16Z", "close_reason": "all steps complete", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-c84", "title": "US-007: Global navigation and keybindings", "description": "Consistent global keybindings: q quits from dashboard (ignored in inputs), Ctrl-C always quits with terminal cleanup, Esc goes back one view, / enters search from dashboard. Footer hint bar on every view. Resize event re-lays out without panic. Event loop uses crossterm::event::poll with sane timeout (no busy spin).", "acceptance_criteria": "q/Ctrl-C/Esc/'/' bindings wired; footer hint on each view; resize handled; poll-based event loop", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:48Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T02:10:40Z", "closed_at": "2026-04-13T02:10:40Z", "close_reason": "Global nav + keybindings wired via DashboardAction router in 02af8ab. q/Ctrl-C/Esc/'/' bindings live; footer on every view; poll-based loop with 100ms timeout; resize auto-re-lays out via ratatui frame draw each tick.", "dependencies": [{"issue_id": "hm-c84", "depends_on_id": "hm-djm", "type": "blocks", "created_at": "2026-04-12T18:15:19Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-c84", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:13Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-xe5", "title": "US-006: Secret viewer", "description": "Show path, env, recipients, created_at, lastmodified. Value hidden by default; 'r' reveals via existing get code path. 'y' copies to clipboard via arboard (graceful no-op if unavailable). 'e' triggers rekey for the current secret. Esc returns to previous view.", "acceptance_criteria": "metadata shown; r reveals value; y copies via arboard; e triggers rekey; esc returns", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:39Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T02:09:54Z", "closed_at": "2026-04-13T02:09:54Z", "close_reason": "Implemented SecretViewerView with reveal/copy/rekey/esc, wired into router via SearchAction::OpenViewer; 6 tests passing", "dependencies": [{"issue_id": "hm-xe5", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:13Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-xe5", "depends_on_id": "hm-rd2", "type": "blocks", "created_at": "2026-04-12T18:15:20Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-pp9", "title": "US-005: Search view", "description": "/ from dashboard opens search with focused text input. Results update as query changes. Uses extracted core of crate::cli::search (refactor search::run to split IO from logic). Results show store, path, created_at. Enter on a result navigates to secret viewer. Esc returns to dashboard.", "acceptance_criteria": "search view opens on /; live results; uses extracted search core; Enter opens viewer; Esc returns to dashboard", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:32Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T02:09:46Z", "closed_at": "2026-04-13T02:09:46Z", "close_reason": "Landed in 02af8ab", "dependencies": [{"issue_id": "hm-pp9", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:12Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-pp9", "depends_on_id": "hm-rd2", "type": "blocks", "created_at": "2026-04-12T18:15:19Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-rd2", "title": "US-004: Dashboard view (envs + secrets)", "description": "Two-pane layout: left = environments list, right = secrets for selected env. Header shows active store slug and env count. Arrow keys / j/k navigate envs. Right pane updates live on selection change. Data via internal Rust APIs (ls equivalent), not subprocess. Empty state message when no envs. Footer lists keybindings.", "acceptance_criteria": "two-pane layout; live right-pane updates; uses internal APIs not subprocess; empty state handled; footer keybindings shown", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:25Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:52:00Z", "closed_at": "2026-04-13T01:52:00Z", "close_reason": "US-003 init wizard + US-004 dashboard view landed; DashboardView ported to ratatui with envs/secrets two-pane layout, run_init_flow event loop wired.", "dependencies": [{"issue_id": "hm-rd2", "depends_on_id": "hm-djm", "type": "blocks", "created_at": "2026-04-12T18:15:19Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-rd2", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:12Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 2, "comment_count": 0} -{"_type": "issue", "id": "hm-b49", "title": "US-003: Init wizard view (ratatui)", "description": "Port the 3-step init wizard to ratatui: Step 1 data dir text input pre-filled from config::data_dir(); Step 2 remote slug validated with config::validate_remote_slug; Step 3 key provider select (hidden if macOS Keychain unavailable). Enter advances, Esc back, Ctrl-C aborts. On confirm calls init::run_init directly (not subprocess). Success screen shows pubkey and advances to dashboard. Replaces init::run_wizard cliclack path when TTY and no --json/--no-tui.", "acceptance_criteria": "three steps rendered; validation wired; calls init::run_init directly; cliclack path removed for TTY case; success screen advances to dashboard", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:18Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:52:37Z", "closed_at": "2026-04-13T01:52:37Z", "close_reason": "ratatui init wizard landed: 3-step flow (data dir/remote slug/key provider), validation wired, calls init::run_init directly, cliclack TTY path removed, success advances to dashboard", "dependencies": [{"issue_id": "hm-b49", "depends_on_id": "hm-djm", "type": "blocks", "created_at": "2026-04-12T18:15:18Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-b49", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:11Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-4y7", "title": "US-002: Remove legacy Bun/OpenTUI TUI", "description": "Delete the entire tui/ directory and remove himitsuTUI derivation + bun2nix input from flake.nix. Remove any himitsu-tui references from nativeBuildInputs and the HIMITSU_BIN env forwarding from launch_tui().", "acceptance_criteria": "tui/ directory deleted; flake.nix has no himitsuTUI/bun2nix refs; HIMITSU_BIN forwarding removed; nix build still succeeds", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:08Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:43:39Z", "closed_at": "2026-04-13T01:43:39Z", "close_reason": "Deleted tui/, dropped bun2nix input, removed himitsuTUI deriv. nix flake check + cargo test/clippy/fmt all green.", "dependencies": [{"issue_id": "hm-4y7", "depends_on_id": "hm-djm", "type": "blocks", "created_at": "2026-04-12T18:15:18Z", "created_by": "Cooper Maruyama", "metadata": "{}"}, {"issue_id": "hm-4y7", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:11Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-djm", "title": "US-001: Scaffold ratatui module and wire up entry point", "description": "Add ratatui/crossterm to rust/Cargo.toml and create rust/src/tui/ with mod.rs, app.rs, event.rs. Public entry himitsu::tui::run() installs/tears down raw mode + alternate screen, runs a no-op event loop that exits on q or Ctrl-C. Cli::launch_tui() in rust/src/cli/mod.rs calls tui::run() directly instead of spawning himitsu-tui. Terminal state restored on panic via drop guard or set_hook.", "acceptance_criteria": "ratatui+crossterm in Cargo.toml; rust/src/tui/{mod,app,event}.rs created; himitsu::tui::run() exists; launch_tui calls it in-process; raw mode restored on panic", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:14:01Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:17:48Z", "closed_at": "2026-04-13T01:17:48Z", "close_reason": "Closed", "dependencies": [{"issue_id": "hm-djm", "depends_on_id": "hm-nck", "type": "parent-child", "created_at": "2026-04-12T18:15:11Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 4, "comment_count": 0} -{"_type": "issue", "id": "hm-nck", "title": "Replace OpenTUI TUI with Rust/ratatui", "description": "Replace the Bun + @opentui/core TUI (in tui/, launched as a subprocess via himitsu-tui) with a pure-Rust TUI built on ratatui + crossterm, living inside the existing himitsu binary at rust/src/tui/. Running 'himitsu' with no subcommand will launch the new TUI in-process, eliminating the subprocess hop, the HIMITSU_BIN fallback, and the entire Bun toolchain from the build. Full PRD in commit history / conversation.", "status": "closed", "priority": 2, "issue_type": "epic", "owner": "demo@himitsu.dev", "created_at": "2026-04-13T01:13:45Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T02:10:40Z", "closed_at": "2026-04-13T02:10:40Z", "close_reason": "all steps complete", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-xx0", "title": "himitsu 'add' should behave like an alias for himitsu 'set'", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-12T23:51:12Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:17:38Z", "closed_at": "2026-04-13T01:17:38Z", "close_reason": "Added clap alias='add' to Set command in rust/src/cli/mod.rs", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-3w1", "title": "running 'himitsu' with no args should launch the tui", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-12T23:50:49Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:17:38Z", "closed_at": "2026-04-13T01:17:38Z", "close_reason": "Already implemented \u2014 cdf8757 feat: launch TUI when himitsu is run with no subcommand", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-457", "title": "running 'himitsu' with no args should launch the tui", "status": "closed", "priority": 2, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-12T13:39:46Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-12T13:47:48Z", "closed_at": "2026-04-12T13:47:48Z", "close_reason": "Implemented: running himitsu with no args now launches the TUI (himitsu-tui binary). Falls back to a helpful error if TUI not installed.", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-anf", "title": "himitsu 'add' should behave like an alias for himitsu 'set'", "status": "closed", "priority": 2, "issue_type": "task", "owner": "demo@himitsu.dev", "created_at": "2026-04-12T13:38:51Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-13T01:17:38Z", "closed_at": "2026-04-13T01:17:38Z", "close_reason": "Added clap alias='add' to Set command in rust/src/cli/mod.rs", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-3us", "title": "Document external import/export requirements", "description": "Add README notes for external tool requirements used by import/export/generate flows, including op for 1Password and sops for encrypted YAML/JSON output.", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:24Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:16Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-u7g", "title": "Add README demo recording instructions", "description": "Add a Demo or Recording section with the canonical local command to regenerate the VHS demo: cargo build --release && vhs demo/demo.tape, plus CI scratch-output behavior.", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:24Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:16Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-9lg", "title": "Clarify README demo GIF and tape roles", "description": "Update README demo references to use demo/demo-vhs.gif as the canonical polished demo and explain demo/tui-us-* tapes as per-story regression/demo artifacts.", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-05-01T00:51:23Z", "created_by": "Cooper Maruyama", "updated_at": "2026-05-03T22:01:09Z", "started_at": "2026-05-03T21:59:17Z", "closed_at": "2026-05-03T22:01:09Z", "close_reason": "Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync & Store Health section (auto-commit/push, --no-push, store health states), new Demo & Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-cfo", "title": "Distribute TUI footer actions evenly", "description": "Render footer action groups in evenly distributed segments instead of left/right weighted alignment.", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T11:54:15Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T11:56:36Z", "started_at": "2026-04-30T11:54:26Z", "closed_at": "2026-04-30T11:56:36Z", "close_reason": "Added shared distributed footer renderer and applied it to search and envs footers", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-7q6", "title": "Align TUI footer right actions", "description": "Use a split footer layout so the right-most footer actions render flush right instead of relying on spacer text.", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T10:57:00Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T10:58:24Z", "started_at": "2026-04-30T10:57:22Z", "closed_at": "2026-04-30T10:58:24Z", "close_reason": "Updated envs footer to split left actions from right-aligned help/back actions using a horizontal layout", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-cpf", "title": "TUI envs editor supports multi-entry envs", "description": "The envs edit flow currently supports single-entry env labels and returns an error for labels with multiple EnvEntry rows. Extend the editor state to hold multiple entry rows with add/remove controls so existing multi-entry envs can be edited in place.", "status": "closed", "priority": 3, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-30T10:45:06Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T23:23:27Z", "started_at": "2026-04-30T23:23:04Z", "closed_at": "2026-04-30T23:23:27Z", "close_reason": "Superseded by hm-6qj envs revamp, which redesigns the editor with full multi-entry/glob support from the ground up.", "dependencies": [{"issue_id": "hm-cpf", "depends_on_id": "hm-h9m", "type": "discovered-from", "created_at": "2026-04-30T03:45:06Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-h9m", "title": "TUI envs tab: edit existing env with live capture preview", "description": "Follow-up to hm-zzj. v1 ships read + delete; this adds `e` to edit an existing env's entries.\n\n## UX\n\n- `e` opens the same editor as the create flow (file separately as hm-zzj-followup-create) prefilled with the selected env's current entries.\n- Right pane continues to show the live `env_resolver::resolve` output as the user edits, so capture-ref expansions (`/$1/postgres-url` against actual store secrets) update in real time.\n- Save calls `envs_mut::upsert` (which is upsert semantics \u2014 replaces existing rows).\n\n## Notes\n\n- Depends on the create flow (shares the editor widget). Build the editor once, reuse it.\n- For wildcard envs, the live preview must enumerate `available_secrets` via `crate::remote::store::list_secrets` to compute capture matches.", "status": "closed", "priority": 3, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T21:58:07Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T10:45:15Z", "started_at": "2026-04-30T10:43:17Z", "closed_at": "2026-04-30T10:45:15Z", "close_reason": "Added env edit flow with prefilled editor, Ctrl-S upsert save, live preview while editing, and single-entry edit tests; filed hm-cpf for multi-entry editing", "dependencies": [{"issue_id": "hm-h9m", "depends_on_id": "hm-udz", "type": "blocks", "created_at": "2026-04-21T14:58:17Z", "created_by": "Cooper Maruyama", "metadata": "{}"}], "dependency_count": 1, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-udz", "title": "TUI envs tab: create new env flow", "description": "Follow-up to hm-zzj. v1 of the envs tab shipped read + delete only. This adds the `n` flow to create a new env from inside the tab.\n\n## UX\n\n- `n` opens an inline editor split with two fields:\n 1. Label input \u2014 validated live against `validate_env_label`. Show grammar hints (concrete vs `/*`).\n 2. Entries list \u2014 repeatable rows, each picking a kind (Single | Glob | Alias) and the path/key fields.\n- Save (`Ctrl+S` or `Enter` on a \"Save\" button) calls `envs_mut::upsert(label, entries, scope_hint, cwd)`; on success, reload and select the new label. On error, toast the message and keep the editor open.\n- `Esc` cancels with a confirmation if the form is dirty.\n\n## Wiring\n\n- Route through the same `View::Envs` slot or add a sub-modal \u2014 your call when implementing.\n- Reuse the keymap binding `keymap.envs` already defined; add a sub-binding under it for `n`.", "status": "closed", "priority": 3, "issue_type": "feature", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-21T21:58:07Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-30T10:41:14Z", "started_at": "2026-04-30T10:37:58Z", "closed_at": "2026-04-30T10:41:14Z", "close_reason": "Added TUI envs new-env flow with inline editor, live validation, save via envs_mut::upsert, toasts, and tests", "dependency_count": 0, "dependent_count": 1, "comment_count": 0} -{"_type": "issue", "id": "hm-79o", "title": "Docs: update README for search-as-root TUI flow", "description": "README still references the dashboard-root flow. Update screenshots/gifs to match search-as-root, refresh the key-binding table to list current shortcuts (Ctrl+N new, Ctrl+S store, Ctrl+Y copy, ? help, Esc quit, e edit, d delete with y/n confirm).", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T06:53:34Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T07:01:04Z", "closed_at": "2026-04-15T07:01:04Z", "close_reason": "Merged via parallel worktree agents; cargo test --bin himitsu tui::harness:: \u2192 3 passed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} -{"_type": "issue", "id": "hm-6ia", "title": "CI: run vhs against demo tapes to catch TUI flow regressions", "description": "Add a GitHub Actions job that installs vhs and runs the us-008..us-013 tapes (non-interactive, no gif diffing needed \u2014 just exit code) so broken tapes fail CI before they land. Cache cargo build, reuse release binary.", "status": "closed", "priority": 3, "issue_type": "task", "assignee": "Cooper Maruyama", "owner": "demo@himitsu.dev", "created_at": "2026-04-15T06:53:33Z", "created_by": "Cooper Maruyama", "updated_at": "2026-04-15T07:01:04Z", "closed_at": "2026-04-15T07:01:04Z", "close_reason": "Merged via parallel worktree agents; cargo test --bin himitsu tui::harness:: \u2192 3 passed", "dependency_count": 0, "dependent_count": 0, "comment_count": 0} +{"_type":"issue","id":"hm-csd","title":"floating description tooltip / row expansion for selected search result","description":"On the search view, the currently selected item should have a floating tooltip containing the whole description. Or, alternatively, the currently selected item should expand to take up multiple rows so that the entire description is always visible.","status":"open","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-10T02:31:59Z","created_by":"Cooper Maruyama","updated_at":"2026-05-10T02:31:59Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-634","title":"on search view, the currently selected item should have a floating tooltip containing the whole description. or, the currently selected item should expand to take up multiple rows so that the entire description is always visible","status":"open","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-09T01:40:16Z","created_by":"Cooper Maruyama","updated_at":"2026-05-09T01:40:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-68a","title":"introduce an element that is to be used anywhere where i am typing a reference to a secret. it should autocomplete as i type, showing me the closest match based on levenshtein distance. the same code should be used to also add a 'did you mean X' when himitsu search returns no results","status":"open","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-07T06:22:32Z","created_by":"Cooper Maruyama","updated_at":"2026-05-07T06:22:32Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-7ob","title":"Update command palette layout","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-07T06:06:18Z","created_by":"Cooper Maruyama","updated_at":"2026-05-07T06:10:27Z","started_at":"2026-05-07T06:06:18Z","closed_at":"2026-05-07T06:10:27Z","close_reason":"Updated command palette layout and copy","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-isi","title":"add support for TUI hints - a small bit of text that can render in subtle text on the bottom left corner, sort of like a floating element","status":"open","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-06T23:21:58Z","created_by":"Cooper Maruyama","updated_at":"2026-05-06T23:21:58Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-3w5","title":"all form fields should have placeholders containing an example","status":"open","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-06T23:20:58Z","created_by":"Cooper Maruyama","updated_at":"2026-05-06T23:20:58Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-3rr","title":"add a submit button to the new secret tui, and add a confirm dialog when clicking esc that lets you save or discard","status":"open","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-06T23:20:25Z","created_by":"Cooper Maruyama","updated_at":"2026-05-06T23:20:25Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6if","title":"Add tags support for secrets (search + env composition)","description":"## Motivation\n\nToday a secret's only addressable axis is its path. That works for hierarchies (`prod/api/STRIPE_KEY`) but not for crosscutting groupings — \"everything PCI\", \"everything the mobile team owns\", \"rotate-this-quarter\". Users currently fake it with path conventions, which collide with the env-prefix model.\n\nAdd free-form tags to secrets so that:\n- An item can carry many tags (`[\"pci\", \"stripe\", \"mobile\"]`).\n- `himitsu search` and `himitsu ls` can filter by tag.\n- The env DSL can select by tag, so an env like `pci-prod` can be authored as `tag: pci` instead of enumerating paths.\n\n## Data model\n\nTags live on `SecretValue` (the encrypted payload), as `repeated string tags = 9` in `proto/secrets.proto`. They travel with the value, are written/read through the existing age envelope, and stay atomic with the secret they describe.\n\n`SecretEntry.tags` (already declared at `proto/secrets.proto:89`) is repurposed as the manifest projection of the same field — populated by listing code that decrypts to surface metadata, identical to how descriptions are currently surfaced in `cli::search::search_core`.\n\nBackwards compatibility: empty tags is the default; legacy envelopes round-trip through `crypto::secret_value::decode` unchanged.\n\nTag grammar: `[A-Za-z0-9_.-]+`, 1–64 chars, no whitespace, case-sensitive. Validated at write time.\n\n## Authoring surfaces\n\n### CLI\n- `himitsu set \u003cpath\u003e \u003cvalue\u003e --tag \u003ct\u003e --tag \u003ct\u003e ...` — repeatable flag on `cli/set.rs`.\n- `himitsu write \u003cpath\u003e --tag \u003ct\u003e` — same flag wired through `cli/write.rs`.\n- `himitsu tag \u003cpath\u003e add \u003ctag\u003e...` / `rm \u003ctag\u003e...` / `list` — new subcommand at `rust/src/cli/tag.rs`, registered in `cli/mod.rs`. Mutates by decrypt → edit `tags` → re-encrypt → write.\n\n### TUI\n- `tui/views/new_secret.rs`: add a `Tags` step (comma-separated input, validated per char as the user types) between `Description` and `EnvKey`.\n- `tui/views/secret_viewer.rs`: render tags as chips in the metadata pane.\n- `tui/views/search.rs`: render tag chips next to each result row; clicking/pressing a chip refines the search.\n\n## Search \u0026 filter\n\n- `himitsu search \u003cquery\u003e [--tag t]...`: AND-semantics across tag flags; empty query + tags = \"all secrets with these tags\". Tags read off the same `Decoded` struct produced by `search_core` decrypt loop.\n- `himitsu ls --tag t [--tag t]...`: same AND filter, applied after path-prefix walk.\n- Query-string sugar (out of scope for this epic, file as follow-up): `tag:foo bar` syntax inside the freeform query.\n\n## Env DSL — new `Tag` entry\n\nExtend `EnvEntry` (`rust/src/config/mod.rs:191`) with a `Tag(String)` variant. YAML shapes the deserializer accepts (extending `EnvEntry::deserialize` at `mod.rs:215`):\n\n- Inline string with `tag:` prefix:\n ```yaml\n envs:\n pci-prod:\n - tag:pci\n - tag:prod\n ```\n AND-semantics across multiple `tag:` entries inside one env (consistent with CLI `--tag` repetition).\n\n- Map form for explicit alias-rename, parallel to existing alias:\n ```yaml\n envs:\n pci-prod:\n - { tag: pci } # all pci-tagged secrets, key = last path segment\n - { STRIPE: tag:stripe } # alias: env-key STRIPE := the (singular) stripe-tagged secret\n ```\n Map-form must error when more than one secret matches the tag (alias requires a single value).\n\nResolver (`rust/src/config/env_resolver.rs:63`) gains a parallel branch to `resolve_concrete` / `resolve_wildcard`: walk `available_secrets`, decrypt-or-cache to read `tags`, include those that match. Wildcard capture refs (`$1`) are not extended to tag entries in this epic — out of scope.\n\n## Out of scope\n- Query-string `tag:` sugar inside `search \u003cquery\u003e` (file as follow-up after this lands).\n- Sharing/inheriting tags across stores via policy.\n- Tag autocomplete in the TUI tag input (nice-to-have follow-up).\n- Capture-ref interpolation inside `Tag` env entries.\n- Renaming/merging tags as a first-class operation (do it manually via `tag rm` + `tag add`).\n\n## Success criteria\n- `himitsu set foo --tag a --tag b` round-trips: `himitsu get foo` shows tags in the metadata block, `himitsu tag foo list` prints `a\\nb`.\n- `himitsu search --tag a` returns only secrets carrying tag `a`; same for `ls --tag a`.\n- `himitsu codegen \u003cenv\u003e` against an env with `- tag:a` produces a SOPS bundle whose keys are exactly the secrets tagged `a`.\n- TUI new-secret form accepts comma-separated tags; secret-viewer renders them; search rows show tag chips.\n- All existing tests pass; new tests cover: tag validation, `EnvEntry` (de)serialization round-trip for both string and map forms, resolver behavior with tag entries, `search`/`ls` filter logic.\n- Legacy secrets (no tags field) decode unchanged.\n","status":"open","priority":2,"issue_type":"epic","owner":"noreply@anthropic.com","created_at":"2026-05-05T12:14:38Z","created_by":"Claude","updated_at":"2026-05-05T12:14:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-scm","title":"instead of adding a row and indenting items that have a common prefix, add a key to fold/unfold by 1 level, get rid of the extra row, and use a sutble accent to show when folded/unfolded","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-04T12:25:10Z","created_by":"Cooper Maruyama","updated_at":"2026-05-05T03:57:37Z","started_at":"2026-05-04T12:25:35Z","closed_at":"2026-05-05T03:57:37Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-bs1","title":"footer items are not spaced evenly - right-most item should be right-aligned","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-04T12:12:52Z","created_by":"Cooper Maruyama","updated_at":"2026-05-04T12:14:30Z","started_at":"2026-05-04T12:13:19Z","closed_at":"2026-05-04T12:14:30Z","close_reason":"render_distributed_footer now anchors first item Left and last item Right so the footer row reads as evenly distributed across the full width","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-dsa","title":"ctrl+P command menu should contain all valid commands from the stateless CLI","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-04T12:12:25Z","created_by":"Cooper Maruyama","updated_at":"2026-05-05T03:57:37Z","started_at":"2026-05-04T12:25:22Z","closed_at":"2026-05-05T03:57:37Z","close_reason":"Ctrl+P palette now lists every visible top-level CLI command. Wired commands (new/sync/rekey/join/add-remote/switch-store/envs/help/quit/toggle-store-column) keep their behavior. New entries (recipient ls/add/rm/show, remote list/remove/default, context show/set/clear, generate, export, check, docs, import, git) emit an info toast naming the equivalent CLI invocation; full TUI forms can be wired per-command as follow-up.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-8jm","title":"fix himitsu sync failing in the current state","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-04T12:09:41Z","created_by":"Cooper Maruyama","updated_at":"2026-05-04T12:17:24Z","started_at":"2026-05-04T12:11:24Z","closed_at":"2026-05-04T12:17:24Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-mw9","title":"add any compatible commands from the stateless CLI to the command picker in the TUI","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-04T01:15:29Z","created_by":"Cooper Maruyama","updated_at":"2026-05-04T01:18:27Z","started_at":"2026-05-04T01:15:44Z","closed_at":"2026-05-04T01:18:27Z","close_reason":"Added sync, rekey, and join commands to the TUI command palette","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-1wl","title":"himitsu import should convert SOME_KEY to some-key. it should also allow me to do a simple glob filter like foo/* and only import matches, and then it should also let me (unless it does already) set a prefix like --to some/target which would create some/target/{foo,bar,...} or --to github:org/repo#foo would put them in the foo directory. also, --to foo/bar should be equivalent to --to foo/bar/ (the trailing slash is ignored)","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-04T01:01:12Z","created_by":"Cooper Maruyama","updated_at":"2026-05-04T01:07:04Z","started_at":"2026-05-04T01:01:36Z","closed_at":"2026-05-04T01:07:04Z","close_reason":"Implemented key normalization (SOME_KEY→some-key), --filter glob, --to prefix, and --keep-names opt-out","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-u0v","title":"auto-detect if recipient list doesn't contain own age key","description":"## Context\n in that case, the sync status should glow orange and tell you to run 'himitsu join' which would simply add own public key to the stores recipient list, commit, and push. github actions should then rekey in a self-serve way if it is configured with a key in secrets. adding this key to secrets is also something that himitsu can do for you automatically. this task can be seen as idempotently ensuring those 3 conditions are met","notes":"Scope per description: idempotently ensure (1) own pubkey is in the store's recipient list, (2) GitHub Actions has a configured age key in repo secrets, (3) auto-rekey via Actions is wired up. Surface state via TUI store-health glow + 'himitsu join' command that performs (1)-(3). Significant product/UX design — defer until owner aligns on join command shape and Actions self-serve flow.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-03T21:56:09Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:12:52Z","started_at":"2026-05-03T22:06:52Z","closed_at":"2026-05-03T22:12:52Z","close_reason":"Implemented himitsu join command and TUI NotRecipient health indicator","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-bqk","title":"himitsu isn't automatically running init when user has no store","notes":"Investigation 2026-05-03: tui::run (rust/src/tui/mod.rs:33) auto-launches the wizard only on missing key (~/.local/share/himitsu/key). Gap: a user with a key but no resolved store gets an empty dashboard. Fix would be to also trigger run_init_flow when ctx.store.as_os_str().is_empty(). Risk: run_init_flow currently calls run() recursively after the wizard completes (mod.rs:126); init can complete-and-Success without creating a store if --name/--project are not used in the wizard, which would create an infinite loop. Either (a) require --name in the wizard's success path, (b) only re-enter run() when wizard reports a store was created, or (c) return Ok(()) from run() when store is still empty post-wizard. Needs interactive testing.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-03T21:52:55Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:07:19Z","started_at":"2026-05-03T22:05:14Z","closed_at":"2026-05-03T22:07:19Z","close_reason":"TUI now launches init flow when key exists but no store resolves, with post-wizard no-store guard.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-4y5","title":"Cross-repo ref: prompt to clone missing referenced store","description":"When a qualified cross-repo reference (github:org/repo#path) hits a store that isn't checked out locally, prompt for confirmation before cloning. Interactive: y/N prompt (default no). Non-interactive (no TTY): error with hint to run remote add. Add --yes to bypass confirmation for scripted use. TUI: small modal.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T10:55:51Z","created_by":"Cooper Maruyama","updated_at":"2026-05-01T10:57:55Z","started_at":"2026-05-01T10:56:38Z","closed_at":"2026-05-01T10:57:55Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-w6c","title":"Cross-repo ref: prefer github:org/repo#path syntax (accept legacy / form)","description":"Switch the canonical cross-repo reference syntax from github:org/repo/path/to/secret to github:org/repo#path/to/secret to disambiguate the slug from the path. Parser must accept both forms; the # form is the canonical one (used in docs, error messages, codegen). The slash form continues to parse as a fallback.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T10:55:51Z","created_by":"Cooper Maruyama","updated_at":"2026-05-01T10:57:55Z","started_at":"2026-05-01T10:55:55Z","closed_at":"2026-05-01T10:57:55Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-k4c","title":"init should have placeholder for config set to project if in a git repo, unless the user has not configured a global store yet","description":"In the init wizard's remote-store step, when inside a git repo (or parent is), default the placeholder to the current repo's origin slug and write to \u003cgit_root\u003e/himitsu.yaml as project-scoped config. Always run the global step first (skipped if already configured), labeled 'configure global store', then run a separate 'configure project' step (skipped if not in a git repo).","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-01T10:29:47Z","created_by":"Cooper Maruyama","updated_at":"2026-05-01T10:42:26Z","closed_at":"2026-05-01T10:42:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-anm","title":"Expand README sync and store health docs","description":"Document sync behavior, auto-commit/push, no-remote and not-pushed warnings, --no-push, auto_pull, and TUI store health states.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:23Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:14Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-1lt","title":"Make README quick start TUI-first","description":"Revise README Quick Start so the primary path launches the TUI first, then presents CLI commands as scripting/automation alternatives.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:22Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:15Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-qku","title":"Document TUI theme and key configuration","description":"Add README documentation for global config TUI settings, including tui.theme, tui.keys, accepted built-in themes, and HIMITSU_TUI_THEME override.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:22Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:15Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-o89","title":"Fix README search documentation","description":"Update README search feature and command docs to reflect current behavior: search reads store files directly and --refresh is retained as a no-op compatibility flag, not a SQLite index rebuild.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:21Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:16Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-1b7","title":"Review README and stabilize demo sync state","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:37:30Z","created_by":"Cooper Maruyama","updated_at":"2026-05-01T00:40:56Z","started_at":"2026-05-01T00:37:37Z","closed_at":"2026-05-01T00:40:56Z","close_reason":"Reviewed README and added demo sync-state override","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-2f9","title":"Refresh VHS demo tapes","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-30T23:45:18Z","created_by":"Cooper Maruyama","updated_at":"2026-05-01T00:03:22Z","started_at":"2026-04-30T23:45:23Z","closed_at":"2026-05-01T00:03:22Z","close_reason":"Refreshed VHS demo as sourced pure TUI tape and validated render","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-wqb","title":"allow editing name of items","description":"In the himitsu TUI, pressing Enter on an item opens an editor that lets you edit every field EXCEPT the item's name. Make the name field editable too.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T23:19:04Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T23:23:03Z","started_at":"2026-04-30T23:23:03Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6qj","title":"revamp envs feature.","description":"## Context\n envs should ultimately be stored like this:\n# config.yaml\n...\nenvs:\n my-env:\n - SOME_KEY: some-exact-match # optionally override env key\n - other-exact-match # uses env key set on item or fallsback to upper(replace(name, -,_)) (forward slash replaced with __)\n - arbitrary-glob/*\nmy-env-{dev,prod,stg}:\n - SOME_KEY: ref-requires-{} # requires specifying where to replace\n - some-group/item-name-{} # using the {} is not required if you have a better format\n\n\nTherefore, the env editor should have an initial browser where i can select an existing env or add a new one, then take me into a 2-panel layout where I am editing the left side as if it was hx and the right pane shows me a preview of what env would be generated based on matches and/or any errors. there should be fuzzy find support and some auto-complete like feature that lets me discover the right labels as im editing","status":"in_progress","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T23:16:44Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T23:23:04Z","started_at":"2026-04-30T23:23:04Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-5l4","title":"Highlight self in recipient ls","description":"Update himitsu recipient ls output so recipient rows whose full age public key matches the current user's own public key are highlighted in TTY output without changing non-TTY/plain output.","status":"closed","priority":2,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T12:26:21Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T12:28:59Z","started_at":"2026-04-30T12:26:43Z","closed_at":"2026-04-30T12:28:59Z","close_reason":"Highlighted recipient ls rows whose full public key matches the local self public key in TTY output, preserving plain non-TTY output","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-cak","title":"Fix search footer alignment and centering","description":"Apply split-layout footer alignment to the search view and correct small-screen search canvas centering.","status":"closed","priority":2,"issue_type":"bug","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T11:01:04Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T11:02:44Z","started_at":"2026-04-30T11:01:33Z","closed_at":"2026-04-30T11:02:44Z","close_reason":"Aligned search footer right actions with split layout and replaced Layout-based canvas centering with deterministic centered Rect math","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-774","title":"all screens should be the same size as the search view - apply whatever constraints the search view have globally","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T04:01:29Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T10:35:08Z","started_at":"2026-04-30T10:29:32Z","closed_at":"2026-04-30T10:35:08Z","close_reason":"Added shared standard_canvas margin/max-size helper and applied search view constraints across TUI screens and overlays","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-w6y","title":"Review and prune stale tests","description":"Review the current test suite and remove tests that cover deprecated behavior, stale expectations, or backwards-compatibility-only behavior no longer relevant to the Rust rewrite.","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-30T03:52:16Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T04:03:23Z","started_at":"2026-04-30T03:52:27Z","closed_at":"2026-04-30T04:03:23Z","close_reason":"Pruned stale/deprecated/backwards-compat test coverage and verified test suite passes","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-aqi","title":"Submodule-aware commit/push in dispatcher","description":"Enable single-store + git-submodule-for-includes workflow. Previously, himitsu set to a path inside a submodule would strand the file: submodule dirty, parent clean, nothing pushed. Dispatcher now commits submodules first, pushes them first, holds back parent push if any submodule push fails. auto_pull gains --recurse-submodules. Safe detached-HEAD auto-checkout only when HEAD matches origin default-branch tip.","status":"closed","priority":2,"issue_type":"feature","owner":"demo@himitsu.dev","created_at":"2026-04-24T09:18:44Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T09:19:01Z","closed_at":"2026-04-24T09:19:01Z","close_reason":"Landed: list_submodules/has_unpushed_commits/ensure_on_branch helpers in git.rs; Context::commit walks dirty submodules then parent, Context::push submodules-first with hold-back on failure, pull_if_remote recurses. 4 new tests covering round-trip with real local bare remotes. 460 tests green.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-fy3","title":"himitsu git status should never have unstaged / working changes. every action should commit, and make the git history be essentially an append-only log","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-21T22:13:42Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T22:40:35Z","closed_at":"2026-04-21T22:40:35Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-ic2","title":"himitsu codegen \u003cenv\u003e emits \u003cenv\u003e.sops.yaml","description":"Extend `himitsu codegen` (rust/src/cli/codegen.rs) to accept an env label and emit a valid SOPS file at `\u003cenv\u003e.sops.yaml`. Today codegen's `--env` flag is already threaded through (see rust/src/cli/codegen.rs:55-56) for language bindings; this task adds a new output mode where the env itself is the target.\n\n## CLI surface\n\n- `himitsu codegen \u003cenv\u003e` — shorthand for `--lang sops --env \u003cenv\u003e --output \u003cenv\u003e.sops.yaml`.\n- `himitsu codegen \u003cenv\u003e --output path.yaml` — override output path.\n- Accepts concrete labels (`foo/dev`) and wildcards (`foo/*`); wildcard form emits one file per matched child (`foo-dev.sops.yaml`, `foo-prod.sops.yaml`), or a single merged file if `--merge` is given (decide in review).\n\n## Output format\n\n- Top-level keys come from the resolved env entries (hm-qjz), each mapped to its decrypted secret value.\n- File is encrypted via `sops` rules: either the project's existing `.sops.yaml` rules file or an inline `creation_rules` block derived from himitsu recipients (TBD — call out in design doc).\n- Generated file contains the same `AUTO-GENERATED` header banner codegen already emits (rust/src/cli/codegen.rs:537).\n\n## Acceptance\n\n- `himitsu codegen foo/dev` produces `foo-dev.sops.yaml` that `sops -d` can decrypt with the configured recipients.\n- Wildcard case produces the expected file set.\n- Errors clearly when the env label is unknown or when captures fail to resolve.","notes":"## Integration decision (2026-04-21)\n\n**Shell out to the `sops` CLI.** Generate plaintext YAML from the resolved env tree, write it to the output path, then invoke `sops --encrypt --in-place \u003cpath\u003e`. Key/recipient resolution is delegated to the user's existing `.sops.yaml` rules file.\n\n- Requires `sops` on PATH — detect and emit a helpful error if missing.\n- No new crypto crate dependency.\n- Existing `AUTO-GENERATED` header banner applies to the plaintext emission before encryption.","status":"closed","priority":2,"issue_type":"feature","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:39:24Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T21:32:38Z","closed_at":"2026-04-21T21:32:38Z","close_reason":"Landed: codegen \u003cenv\u003e positional triggers sops path. Resolver-\u003edecrypt-\u003eYAML-\u003esops encrypt-in-place. Missing-sops error has install hint. 6 new tests green, 1 ignored smoke.","dependencies":[{"issue_id":"hm-ic2","depends_on_id":"hm-20e","type":"blocks","created_at":"2026-04-21T13:39:42Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-ic2","depends_on_id":"hm-r2e","type":"parent","created_at":"2026-04-21T13:39:45Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-20e","title":"Env resolver: expand wildcards and capture references","description":"Central pure-function resolver that both the TUI preview and `himitsu codegen \u003cenv\u003e` depend on. Given the full env map + a target label, return the concrete resolved entries (key → secret path, no wildcards, no captures remaining).\n\n## Behavior\n\n- Input: `\u0026Config.envs`, target label (e.g. `foo/*` or `foo/dev`).\n- Resolution:\n 1. If label is concrete, return its entries verbatim (Aliases already resolved to path refs).\n 2. If label ends in `/*`, enumerate sibling envs whose labels match `foo/\u003csegment\u003e` (or deeper — TBD in schema design), and for each child, substitute captures into the glob's entries (e.g. `\\$1` → the matched segment).\n 3. Merge overlapping keys: concrete child entries win over wildcard-inherited ones.\n- Output: `Vec\u003cResolvedEntry { key: String, secret_path: String }\u003e` plus diagnostics for unresolved captures or missing targets.\n\n## Why a dedicated resolver\n\nCodegen (hm-nm9) and the TUI preview (hm-kli) both need identical semantics. Putting it in one place means users never see a mismatch between TUI preview and generated SOPS output.\n\n## Acceptance\n\n- Table-driven tests for: concrete label, wildcard with 0/1/N children, nested wildcards (if allowed by schema), captures in Alias paths, capture index out of range.\n- Resolver returns deterministic order (sort by key) so generated SOPS files are diff-friendly.","notes":"## Output shape correction (2026-04-21)\n\nThe resolver returns a **nested tree**, not a flat `Vec\u003cResolvedEntry\u003e`. Shape (proposed):\n\n```rust\nenum EnvNode {\n Leaf { secret_path: String },\n Branch(BTreeMap\u003cString, EnvNode\u003e),\n}\n```\n\nReasoning: `foo/*` expansion over children `foo/bar` and `foo/bar/baz` must produce a single nested structure, not a flat list and not multiple resolver invocations. Captures (`\\$1`) still substitute path segments, but the output is always a tree rooted at the queried label.\n\nGlob entries reference secret variables only — never other env groups. The resolver does not follow cross-env references.\n\nDeterminism: `BTreeMap` gives sorted keys → diff-friendly downstream codegen.","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:39:15Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T21:12:58Z","closed_at":"2026-04-21T21:12:58Z","close_reason":"Wave 2 landed in commit. Resolver returns deterministic EnvNode trees for concrete + wildcard labels. Cache mirrors envs into SQLite with scope isolation, SHA-256 staleness, atomic refresh. 18 new tests green.","dependencies":[{"issue_id":"hm-20e","depends_on_id":"hm-5v4","type":"blocks","created_at":"2026-04-21T13:39:39Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-20e","depends_on_id":"hm-r2e","type":"parent","created_at":"2026-04-21T13:39:44Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"hm-zzj","title":"TUI Envs tab: list, create, edit, delete presets","description":"Add a dedicated Envs tab alongside the existing TUI views (see rust/src/tui/views/). It becomes the primary UX for authoring preset environments.\n\n## UX sketch\n\n- Left pane: tree of env labels grouped by scope (project first, then global). `foo`, `foo/bar`, `foo/*` render as a hierarchy.\n- Right pane: the selected env's resolved entries — Singles, Aliases, and the expanded form of any Glob (so users see what `foo/*` actually produces).\n- Keymap reuses the project's existing conventions (rust/src/tui/keymap.rs): `n` new env, `e` edit, `d` delete, `/` search, `Enter` drill in.\n- Inline editor for entry kinds (Single | Glob | Alias) with a live preview column showing capture-group expansions.\n\n## Wiring\n\n- Reads come from the SQLite cache (hm-8ht).\n- Writes go through the mutation layer (hm-d1m); optimistic UI with toast rollback on failure.\n- Scope indicator visible at all times; creation defaults to project scope inside a git repo, global otherwise.\n\n## Acceptance\n\n- Can create `foo`, `foo/bar`, and `foo/*` entries end-to-end from the TUI.\n- Editing an Alias with a capture ref renders the live expansion for each matched child env.\n- Deleting an env removes it from YAML + cache on disk.","notes":"## Scope reduction (2026-04-21)\n\n**First slice: read-only + delete.** Creation and inline editing are deferred to follow-up issues (to be filed when this one lands).\n\nIncluded in v1:\n- Tree/list view of envs grouped by scope (project/global).\n- Right pane shows resolved entries (Singles, Aliases, expanded Globs via hm-20e).\n- `d` deletes the selected env (with confirmation), routed through the mutation API (hm-d1m).\n- `/` search, scope indicator, navigation.\n\nDeferred to follow-ups:\n- `n` new env flow with inline editor.\n- `e` edit existing entries.\n- Live capture-reference preview editor.\n\nRationale: smallest slice that validates the cache + resolver + mutation stack end-to-end through real UI. Creation UX deserves its own design pass once read-path is proven.","status":"closed","priority":2,"issue_type":"feature","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:39:06Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T21:57:31Z","closed_at":"2026-04-21T21:57:31Z","close_reason":"Read+delete TUI tab landed. Epic complete: schema + cache + resolver + mutation + codegen sops + TUI all merged. 5 new view tests; 337 lib tests pass.","dependencies":[{"issue_id":"hm-zzj","depends_on_id":"hm-20e","type":"blocks","created_at":"2026-04-21T13:39:41Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-zzj","depends_on_id":"hm-8ht","type":"blocks","created_at":"2026-04-21T13:39:40Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-zzj","depends_on_id":"hm-d1m","type":"blocks","created_at":"2026-04-21T13:39:40Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-zzj","depends_on_id":"hm-r2e","type":"parent","created_at":"2026-04-21T13:39:45Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-d1m","title":"Config mutation layer: write envs to project or global scope","description":"Add a mutation API that the TUI and any future CLI subcommands call to create/update/delete env presets. Scope is inferred the same way `codegen` already does it (rust/src/cli/codegen.rs:118-127): walk up for `.himitsu.yaml`, else fall back to global config.\n\n## Requirements\n\n- Single entry point: `envs::upsert(label, entries, scope_hint) -\u003e Result\u003c()\u003e`; sibling `envs::delete(label, scope)`.\n- Preserve YAML formatting/comments where feasible (serde_yaml round-trip is lossy — evaluate `yaml-rust2` or string-level edits if comments matter here).\n- Atomic writes: temp file + rename so a crash mid-write does not corrupt config.\n- After every mutation, refresh the SQLite cache row for that scope.\n- Validation: reject labels that violate the grammar from the schema-design task.\n\n## Acceptance\n\n- Unit tests for project vs global routing, label validation, atomic write behavior.\n- Integration test: mutate via API → reload `Config` from disk → observe change.","notes":"## YAML fidelity decision (2026-04-21)\n\n**Accept lossy round-trip via `serde_yaml`.** Comments and custom formatting in `.himitsu.yaml` / global config will not survive a mutation. This matches the current codebase pattern and keeps the implementation small.\n\n- Document the lossy behavior in the TUI help panel + user-facing docs.\n- Atomic write (temp file + rename) still required to avoid partial writes.","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:38:54Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T21:23:55Z","closed_at":"2026-04-21T21:23:55Z","close_reason":"Landed: upsert/delete/read API with ScopeHint inference, atomic writes, validation, cache refresh. Config gained first-class envs field. 11 new tests green.","dependencies":[{"issue_id":"hm-d1m","depends_on_id":"hm-5v4","type":"blocks","created_at":"2026-04-21T13:39:37Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-d1m","depends_on_id":"hm-8ht","type":"blocks","created_at":"2026-04-21T13:39:38Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-d1m","depends_on_id":"hm-r2e","type":"parent","created_at":"2026-04-21T13:39:44Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"hm-8ht","title":"SQLite cache for env presets (project + global scope)","description":"Mirror `envs:` from YAML config into SQLite so the TUI can list/search without re-parsing YAML on every keystroke, and so other subsystems (codegen, check) have a uniform read API.\n\n## Requirements\n\n- Tables (proposed):\n - `envs(id INTEGER PK, label TEXT, scope TEXT CHECK(scope IN ('project','global')), config_path TEXT, mtime INTEGER)`\n - `env_entries(id INTEGER PK, env_id INTEGER REFERENCES envs(id) ON DELETE CASCADE, kind TEXT, value TEXT, alias_key TEXT)`\n- Rebuild strategy: on load, compare config file hash/mtime against cached row; rebuild if stale.\n- Scope handling: project rows keyed by git-root path so switching projects does not pollute.\n- YAML remains source of truth — every write path must rewrite YAML then refresh the cache; never the reverse.\n\n## Non-goals\n\n- No schema migrations framework yet — initial table ships with the feature.\n- No cross-device sync; the cache is purely local.\n\n## Acceptance\n\n- Round-trip test: load YAML → cache → query → matches parsed `Config.envs`.\n- Stale detection: mutating YAML on disk triggers cache rebuild on next read.","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:38:47Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T21:12:58Z","closed_at":"2026-04-21T21:12:58Z","close_reason":"Wave 2 landed in commit. Resolver returns deterministic EnvNode trees for concrete + wildcard labels. Cache mirrors envs into SQLite with scope isolation, SHA-256 staleness, atomic refresh. 18 new tests green.","dependencies":[{"issue_id":"hm-8ht","depends_on_id":"hm-5v4","type":"blocks","created_at":"2026-04-21T13:39:36Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-8ht","depends_on_id":"hm-r2e","type":"parent","created_at":"2026-04-21T13:39:43Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"hm-5v4","title":"Design env schema: wildcard semantics + capture references","description":"Formalize the env label/entry model before any TUI or codegen work lands. The current `EnvEntry` enum (rust/src/config/mod.rs:134) has `Single`, `Glob`, `Alias` — decide how capture references like `/\\$1/postgres-url` are expressed on-disk and in-memory.\n\n## Questions to resolve\n\n- Label grammar: confirm `foo`, `foo/bar`, `foo/*` are the only legal forms; reject `foo/*/bar`? Or allow mid-path wildcards?\n- Does a `foo/*` env enumerate its children by scanning other env labels in config, or by scanning the secret namespace? (Probably: scan sibling env labels that match `foo/\u003csomething\u003e`.)\n- Capture-ref syntax: `\\$1`, `\\${1}`, `{{1}}`? Which path segments are captured — everything matched by `*`?\n- Do captures only make sense inside `Glob` entries, or also inside `Alias.path`? Likely the latter: `Alias { key: DB_URL, path: \\\"/\\$1/postgres-url\\\" }` under a `foo/*` env.\n- Round-trip serde: new `EnvEntry` variant or reuse existing `Glob`/`Alias` with pattern detection?\n\n## Deliverable\n\nA short design doc (in this issue's `--design`) + updated serde tests in rust/src/config/mod.rs covering:\n- Capture-ref in Alias paths\n- Invalid label rejection\n- Expansion semantics (input env set → resolved concrete entries)","notes":"## Locked constraints (2026-04-21)\n\n- **Reject mid-path wildcards.** Only `foo`, `foo/bar`, `foo/*` are valid. `foo/*/bar`, `*/foo`, and similar are hard errors at config-parse time.\n- **Glob entries reference secret variables only, never other env groups.** Inside a `foo/*` env, entries describe secret paths (optionally with `\\$1` captures) — they do not pull in other env labels.\n- **Wildcards produce nested output, never multiple files.** `foo/*` expanded over concrete children `foo/bar`, `foo/bar/baz` yields a single tree:\n ```\n foo:\n bar:\n baz: \u003cvalue\u003e\n ```\n This mirrors how sops would naturally represent a hierarchical namespace.\n\nThese constraints must be enforced by the label grammar validator and reflected in the resolver's output shape (which is a tree, not a flat list — see hm-20e).","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:38:39Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T20:51:37Z","closed_at":"2026-04-21T20:51:37Z","close_reason":"Schema + validator landed: validate_env_label, parse_captures, validate_envs, ProjectConfig::validate. 15 new tests green, full workspace builds clean. Mid-path wildcards, bare-wildcard, bad chars, capture-in-concrete-env, and high-capture-index all rejected. Resolver (hm-20e) and codegen (hm-ic2) can now consume these helpers.","dependencies":[{"issue_id":"hm-5v4","depends_on_id":"hm-r2e","type":"parent","created_at":"2026-04-21T13:39:43Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} +{"_type":"issue","id":"hm-r2e","title":"Preset environment management (TUI tab + codegen integration)","description":"## Motivation\n\nUsers already declare environment presets in config under `envs:` as a `BTreeMap\u003cString, Vec\u003cEnvEntry\u003e\u003e` with `Single`, `Glob`, and `Alias` variants (see rust/src/config/mod.rs:116, :134). Today this config is only consumable by read paths — there is no UX for authoring, visualizing, or sharing envs, and `himitsu codegen` currently emits language bindings rather than deployable secret bundles.\n\nThis epic adds a first-class preset environment manager that:\n- Surfaces envs as a dedicated tab in the TUI\n- Treats env labels as a path namespace: `foo`, `foo/bar`, `foo/*` are all valid, where `foo/*` is a wildcard shortcut that expands across children\n- Supports capture-group references inside wildcard entries, e.g. `/$1/postgres-url` lets `foo/*` expand into `foo/dev/postgres-url`, `foo/prod/postgres-url`, etc.\n- Mutates the correct config file based on context (project `.himitsu.yaml` vs global config)\n- Mirrors the config into the SQLite cache so reads are fast and the TUI stays responsive\n- Extends `himitsu codegen \u003cenv\u003e` to emit `\u003cenv\u003e.sops.yaml` — a valid SOPS-encrypted secret bundle — so envs become deployable artifacts, not just labels\n\n## Data model\n\nConceptually this is a many-to-many relationship: envs ↔ secrets. In SQLite this is cached as (example, subject to design child):\n\n- `envs(id, label, scope)` — `scope` ∈ {project, global}; `label` stores the full namespaced path (`foo`, `foo/bar`)\n- `env_entries(env_id, kind, value, alias_key, capture_pattern)` — one row per EnvEntry, preserving Single/Glob/Alias semantics\n\nAuthoritative storage stays YAML; SQLite is a rebuilt cache keyed off config file mtime/hash.\n\n## Scope boundary\n\n- Scope (project vs global) is inferred the same way `codegen` already does it — walk up for `.himitsu.yaml`, else global.\n- All mutations are writes to the YAML config plus a cache-refresh; no new storage of truth in SQLite.\n\n## Out of scope\n\n- Changing how secrets themselves are stored/encrypted\n- Re-keying or rotation flows (separate surface)\n- Any non-SOPS codegen output format (typescript/go/etc. stay as-is)\n\n## Success criteria\n\n- TUI has an Envs tab that lists, creates, edits, and deletes env presets with wildcard + alias support\n- Wildcard capture references (`$1`, `$2`, ...) resolve correctly in both TUI preview and codegen\n- `himitsu codegen \u003cenv\u003e` produces a `\u003cenv\u003e.sops.yaml` that round-trips through `sops` without manual editing\n- SQLite cache stays consistent with YAML after every mutation; restart reflects on-disk state","status":"closed","priority":2,"issue_type":"epic","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:38:26Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T21:57:31Z","closed_at":"2026-04-21T21:57:31Z","close_reason":"Read+delete TUI tab landed. Epic complete: schema + cache + resolver + mutation + codegen sops + TUI all merged. 5 new view tests; 337 lib tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-vy9","title":"remove reference to legacy 'groups' command - use recipients in a path-based manner instead, e.g. team/person or team/*","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-21T20:26:12Z","created_by":"Cooper Maruyama","updated_at":"2026-04-21T20:34:51Z","closed_at":"2026-04-21T20:34:51Z","close_reason":"removed group references from README and CLI help, updated to path-based recipients","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-czj","title":"TUI details/edit should support arbitrary fields (key/value pairs)","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T19:31:36Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T19:41:28Z","closed_at":"2026-04-18T19:41:28Z","close_reason":"annotations wired through decode/encode, TUI viewer displays them, edit doc accepts custom key:value as annotations","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-tz3","title":"TUI should display status of remote store (if its out of sync/unhealthy)","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:53:59Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T16:26:20Z","closed_at":"2026-04-18T16:26:20Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6mo","title":"get rid of groups. recipients should allow for path-based names which would allow the equivalent of groups by just using foo/*","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:53:12Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T16:26:21Z","closed_at":"2026-04-18T16:26:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6nb","title":"add export command that lets you create SOPS-compatble files, mapping multiple keys to secrets, with support for foo/bar/* which would include all matching secrets with their default key","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:51:13Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T16:26:21Z","closed_at":"2026-04-18T16:26:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-jk1","title":"add support for SOPS to import","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:49:37Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T16:26:21Z","closed_at":"2026-04-18T16:26:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-far","title":"import command should allow importing the whole vault, including a dry run","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:49:05Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T16:26:21Z","closed_at":"2026-04-18T16:26:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-yxb","title":"search should show time ago format for created","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:38:47Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T15:54:48Z","closed_at":"2026-04-18T15:54:48Z","close_reason":"Implemented in TUI search view: humanized dates via humanize_age() and description column added","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-zzu","title":"search page should show excerpt of description.","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:38:26Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T15:54:48Z","closed_at":"2026-04-18T15:54:48Z","close_reason":"Implemented in TUI search view: humanized dates via humanize_age() and description column added","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-pmy","title":"every mutation should trigger a git sync to remote if configured","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-18T15:23:14Z","created_by":"Cooper Maruyama","updated_at":"2026-04-18T16:26:21Z","closed_at":"2026-04-18T16:26:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-bln","title":"himitsu 'get/read' completions should show available secrets, not files in the current directory","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T14:11:07Z","created_by":"Cooper Maruyama","updated_at":"2026-04-17T07:14:55Z","closed_at":"2026-04-17T07:14:55Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-p8g","title":"add import command, start with import from 1password via op cli","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T14:09:11Z","created_by":"Cooper Maruyama","updated_at":"2026-04-17T07:14:57Z","closed_at":"2026-04-17T07:14:57Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-t81","title":"use 'n days ago' format for date in search. include description, and put store as last column.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T14:08:28Z","created_by":"Cooper Maruyama","updated_at":"2026-04-17T07:14:58Z","closed_at":"2026-04-17T07:14:58Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-f6z","title":"TUI: integration test harness driving App via simulated key events","description":"Build a test harness that constructs App, feeds a sequence of crossterm::event::KeyEvent through the main loop tick, and asserts on rendered buffer state. Enables end-to-end coverage of search→viewer→new-secret flows without spawning a real terminal.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T06:53:32Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T07:01:04Z","closed_at":"2026-04-15T07:01:04Z","close_reason":"Merged via parallel worktree agents; cargo test --bin himitsu tui::harness:: → 3 passed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-3cm","title":"TUI: configurable keybindings via config file","description":"Allow users to remap TUI key bindings through the himitsu config file. Define a KeyMap struct, default to current bindings, let config override by action name (e.g. 'quit', 'new_secret', 'switch_store'). Threaded through App to each view's handle_key.","status":"closed","priority":2,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T06:53:30Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T07:19:59Z","closed_at":"2026-04-15T07:19:59Z","close_reason":"Merged to main; 231 tests pass (toast + configurable keymap). Conflicts in app.rs/search.rs/secret_viewer.rs resolved by keeping toast-based action returns on top of keymap dispatch","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-o15","title":"TUI: status-line toast for transient feedback","description":"Add a toast/status-line component that renders transient messages (saved, copied, deleted, error) for N seconds then clears. Currently feedback is inline-only in individual views. Should be globally mounted in App so every view can emit a toast.","status":"closed","priority":2,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T06:53:21Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T07:19:59Z","closed_at":"2026-04-15T07:19:59Z","close_reason":"Merged to main; 231 tests pass (toast + configurable keymap). Conflicts in app.rs/search.rs/secret_viewer.rs resolved by keeping toast-based action returns on top of keymap dispatch","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-64c","title":"record new demo after major tui changes","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T02:28:44Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T02:54:24Z","closed_at":"2026-04-15T02:54:24Z","close_reason":"Rewrote and re-recorded all 6 TUI tapes (us-008..us-013) against search-as-root + new-secret metadata + store picker flow","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-r4i","title":"new secret flow should alow setting metadata, not just edit","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-14T14:23:45Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T02:46:54Z","closed_at":"2026-04-15T02:46:54Z","close_reason":"metadata fields (description/url/totp/env_key/expires_at) added to new-secret form with tab nav, validation, 13 new tests","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-y6n","title":"provide alternative hotkey for ctrl+s to save, as it is commmonly set in tmux and conflicts","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-14T14:23:19Z","created_by":"Cooper Maruyama","updated_at":"2026-04-14T14:28:32Z","closed_at":"2026-04-14T14:28:32Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-72k","title":"get rid of 'env', its being used as a group which is incorrect. it shouldnt even show in the search tui. env is for codegen which I will spec out later","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-14T14:20:35Z","created_by":"Cooper Maruyama","updated_at":"2026-04-14T14:28:32Z","closed_at":"2026-04-14T14:28:32Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-8z8","title":"search view should have a hotkey for copying to clipboard","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T18:07:09Z","created_by":"Cooper Maruyama","updated_at":"2026-04-14T14:23:23Z","closed_at":"2026-04-14T14:23:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-57k","title":"tui should render all stores together, rendering the store as a column","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T18:06:03Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T18:11:43Z","closed_at":"2026-04-13T18:11:43Z","close_reason":"multi-store grouping with per-store header rows","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-rnx","title":"TUI secret viewer should render all decoded metadata","description":"The secret viewer in the TUI only shows path/env/created_at/lastmodified/recipients. It should also render description, url, totp, expires_at, and env_key from the decoded SecretValue so users can see everything they just edited without round-tripping through the editor.","status":"closed","priority":2,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T18:05:44Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T18:12:51Z","closed_at":"2026-04-13T18:12:51Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-ldq","title":"Add env_key metadata field to SecretValue","description":"Prepare for 'himitsu exec path/* -- cmd' by letting each secret carry a default environment variable name. When unset, callers derive one from the path (typically last segment uppercased). Description field already exists from hm-dvs/hm-de4.","acceptance_criteria":"proto field added; --env-key flag on set; validates as POSIX env var name; surfaced by himitsu get; round-trips through encode/decode","status":"closed","priority":2,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T17:20:50Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T17:21:05Z","closed_at":"2026-04-13T17:21:05Z","close_reason":"env_key field added end-to-end with validation + tests","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-tcp","title":"columns should have header labels in tui","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T17:17:56Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T18:12:51Z","closed_at":"2026-04-13T18:12:51Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-3ir","title":"the default tui should use a search bar with fuzzy find instead of the two-column navigator","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T04:04:48Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T04:11:38Z","closed_at":"2026-04-13T04:11:38Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-jjc","title":"add fuzzy find to search","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:14:50Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-yu4","title":"recipients should allow for adding metadata. adding a recipient should additionally prompt for a description (optional)","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:11:02Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-dvs","title":"add a field called totp that can be used to associate a TOTP key for any secret. also add a URL field that can be used to store the website it is associated with","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:10:15Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-de4","title":"add a expires_at field to metadata, can be optionally set to remind when the secret is about to expire","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:08:26Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6xd","title":".himitsu/recipients should contain a flat list of recipients. the group should not be determined by folder nesting, but rather by a mapping in .himitsu.yaml - this solves the issue where a recipient cannot belong to multiple groups without duplicating files","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:06:39Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-ftf","title":"implement himitsu read/write which are like get/set except they return just the plaintext secret, render no metadata, and are meant to be used in other tools","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:04:41Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-381","title":"implement completions","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T03:02:56Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-28r","title":"himitsu search should look similar to gh repo list","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:56:40Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:39:11Z","closed_at":"2026-04-13T03:39:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-iq6","title":"himitsu tui navigation should look more like a table that shows the path, description, last modified date, and store it belongs to. folders should be sorted first just like in the file explorer in most IDEs","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:40:26Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:51:25Z","closed_at":"2026-04-13T03:51:25Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb.6","title":"US-013: Store/env switch from dashboard","description":"Let the user switch the active store (recipients/data dir) without restarting the TUI. Add a store picker (e.g. 's' key) that lists configured stores from config and rebuilds the dashboard against the chosen store. Scope of 'env switch' is unclear — envs are derived from path prefixes, so selection already works; confirm with user whether this bead should also cover a true multi-context switch.","notes":"Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape → regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:51Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:16Z","closed_at":"2026-04-13T03:12:16Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-6vb.6","depends_on_id":"hm-6vb","type":"parent-child","created_at":"2026-04-12T19:14:50Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb.3","title":"US-011: Drill into secret from dashboard","description":"Pressing Enter on a secret in the dashboard's right-hand list currently does nothing. Wire it to open the SecretViewerView directly, skipping the search detour. Also add j/k/arrow navigation within the secret list (currently only env list navigates).","notes":"Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape → regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:40Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:15Z","closed_at":"2026-04-13T03:12:15Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-6vb.3","depends_on_id":"hm-6vb","type":"parent-child","created_at":"2026-04-12T19:14:39Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb.4","title":"US-012: Help overlay ('?')","description":"Add a modal help overlay bound to '?' (and maybe F1) that lists all keybindings for the current view. Should be view-aware: dashboard, search, and viewer each show their own bindings. Dismiss with Esc or '?'.","notes":"Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape → regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:40Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:15Z","closed_at":"2026-04-13T03:12:15Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-6vb.4","depends_on_id":"hm-6vb","type":"parent-child","created_at":"2026-04-12T19:14:39Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb.5","title":"US-008: Create secret from TUI","description":"Add a 'new secret' flow reachable from the dashboard (e.g. 'n' keybinding). Should prompt for env/path and value (value input hidden), then call the same internal path as 'himitsu set' — no subprocess. On success, refresh the dashboard list and select the new entry.","notes":"Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape → regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:40Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:15Z","closed_at":"2026-04-13T03:12:15Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-6vb.5","depends_on_id":"hm-6vb","type":"parent-child","created_at":"2026-04-12T19:14:39Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb.1","title":"US-009: Delete secret from TUI","description":"Add a delete action in the secret viewer and/or search result list (e.g. 'd' with a confirmation prompt). Wire to the existing store delete path. Refresh the parent view on success and surface errors as a status line.","notes":"Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape → regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:39Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:15Z","closed_at":"2026-04-13T03:12:15Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-6vb.1","depends_on_id":"hm-6vb","type":"parent-child","created_at":"2026-04-12T19:14:39Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb.2","title":"US-010: Edit secret value from TUI","description":"Currently 'e' on the secret viewer rekeys — rebind rekey and add a real edit flow that decrypts the current value, opens it in an in-TUI editor (or $EDITOR fallback with terminal suspend, matching the init wizard pattern), and re-encrypts on save. Decide with the user whether to preserve 'e' for edit and move rekey to a less prominent key.","notes":"Deliverable must include a demo artifact for the completed work: both a VHS tape (demo/*.tape → regenerated .gif) and an asciinema .cast recording that walks through the new flow end-to-end. Follow the existing demo/ conventions from the US-001..US-006 series.","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:39Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:15Z","closed_at":"2026-04-13T03:12:15Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-6vb.2","depends_on_id":"hm-6vb","type":"parent-child","created_at":"2026-04-12T19:14:39Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6vb","title":"TUI Phase 2: CRUD + UX polish","description":"Follow-on epic to hm-nck. The Rust/ratatui TUI shipped as a read-only explorer (dashboard → search → viewer). This epic adds the missing CRUD actions (create/edit/delete), first-class navigation from the dashboard, a help overlay, and store/env switching so the TUI stands on its own without shelling out to the CLI for common operations.","status":"closed","priority":2,"issue_type":"epic","owner":"demo@himitsu.dev","created_at":"2026-04-13T02:14:19Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T03:12:16Z","closed_at":"2026-04-13T03:12:16Z","close_reason":"all steps complete","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-c84","title":"US-007: Global navigation and keybindings","description":"Consistent global keybindings: q quits from dashboard (ignored in inputs), Ctrl-C always quits with terminal cleanup, Esc goes back one view, / enters search from dashboard. Footer hint bar on every view. Resize event re-lays out without panic. Event loop uses crossterm::event::poll with sane timeout (no busy spin).","acceptance_criteria":"q/Ctrl-C/Esc/'/' bindings wired; footer hint on each view; resize handled; poll-based event loop","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:48Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T02:10:40Z","closed_at":"2026-04-13T02:10:40Z","close_reason":"Global nav + keybindings wired via DashboardAction router in 02af8ab. q/Ctrl-C/Esc/'/' bindings live; footer on every view; poll-based loop with 100ms timeout; resize auto-re-lays out via ratatui frame draw each tick.","dependencies":[{"issue_id":"hm-c84","depends_on_id":"hm-djm","type":"blocks","created_at":"2026-04-12T18:15:19Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-c84","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:13Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-xe5","title":"US-006: Secret viewer","description":"Show path, env, recipients, created_at, lastmodified. Value hidden by default; 'r' reveals via existing get code path. 'y' copies to clipboard via arboard (graceful no-op if unavailable). 'e' triggers rekey for the current secret. Esc returns to previous view.","acceptance_criteria":"metadata shown; r reveals value; y copies via arboard; e triggers rekey; esc returns","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:39Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T02:09:54Z","closed_at":"2026-04-13T02:09:54Z","close_reason":"Implemented SecretViewerView with reveal/copy/rekey/esc, wired into router via SearchAction::OpenViewer; 6 tests passing","dependencies":[{"issue_id":"hm-xe5","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:13Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-xe5","depends_on_id":"hm-rd2","type":"blocks","created_at":"2026-04-12T18:15:20Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-pp9","title":"US-005: Search view","description":"/ from dashboard opens search with focused text input. Results update as query changes. Uses extracted core of crate::cli::search (refactor search::run to split IO from logic). Results show store, path, created_at. Enter on a result navigates to secret viewer. Esc returns to dashboard.","acceptance_criteria":"search view opens on /; live results; uses extracted search core; Enter opens viewer; Esc returns to dashboard","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:32Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T02:09:46Z","closed_at":"2026-04-13T02:09:46Z","close_reason":"Landed in 02af8ab","dependencies":[{"issue_id":"hm-pp9","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:12Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-pp9","depends_on_id":"hm-rd2","type":"blocks","created_at":"2026-04-12T18:15:19Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-rd2","title":"US-004: Dashboard view (envs + secrets)","description":"Two-pane layout: left = environments list, right = secrets for selected env. Header shows active store slug and env count. Arrow keys / j/k navigate envs. Right pane updates live on selection change. Data via internal Rust APIs (ls equivalent), not subprocess. Empty state message when no envs. Footer lists keybindings.","acceptance_criteria":"two-pane layout; live right-pane updates; uses internal APIs not subprocess; empty state handled; footer keybindings shown","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:25Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:52:00Z","closed_at":"2026-04-13T01:52:00Z","close_reason":"US-003 init wizard + US-004 dashboard view landed; DashboardView ported to ratatui with envs/secrets two-pane layout, run_init_flow event loop wired.","dependencies":[{"issue_id":"hm-rd2","depends_on_id":"hm-djm","type":"blocks","created_at":"2026-04-12T18:15:19Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-rd2","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:12Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"hm-b49","title":"US-003: Init wizard view (ratatui)","description":"Port the 3-step init wizard to ratatui: Step 1 data dir text input pre-filled from config::data_dir(); Step 2 remote slug validated with config::validate_remote_slug; Step 3 key provider select (hidden if macOS Keychain unavailable). Enter advances, Esc back, Ctrl-C aborts. On confirm calls init::run_init directly (not subprocess). Success screen shows pubkey and advances to dashboard. Replaces init::run_wizard cliclack path when TTY and no --json/--no-tui.","acceptance_criteria":"three steps rendered; validation wired; calls init::run_init directly; cliclack path removed for TTY case; success screen advances to dashboard","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:18Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:52:37Z","closed_at":"2026-04-13T01:52:37Z","close_reason":"ratatui init wizard landed: 3-step flow (data dir/remote slug/key provider), validation wired, calls init::run_init directly, cliclack TTY path removed, success advances to dashboard","dependencies":[{"issue_id":"hm-b49","depends_on_id":"hm-djm","type":"blocks","created_at":"2026-04-12T18:15:18Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-b49","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:11Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-4y7","title":"US-002: Remove legacy Bun/OpenTUI TUI","description":"Delete the entire tui/ directory and remove himitsuTUI derivation + bun2nix input from flake.nix. Remove any himitsu-tui references from nativeBuildInputs and the HIMITSU_BIN env forwarding from launch_tui().","acceptance_criteria":"tui/ directory deleted; flake.nix has no himitsuTUI/bun2nix refs; HIMITSU_BIN forwarding removed; nix build still succeeds","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:08Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:43:39Z","closed_at":"2026-04-13T01:43:39Z","close_reason":"Deleted tui/, dropped bun2nix input, removed himitsuTUI deriv. nix flake check + cargo test/clippy/fmt all green.","dependencies":[{"issue_id":"hm-4y7","depends_on_id":"hm-djm","type":"blocks","created_at":"2026-04-12T18:15:18Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"hm-4y7","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:11Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-djm","title":"US-001: Scaffold ratatui module and wire up entry point","description":"Add ratatui/crossterm to rust/Cargo.toml and create rust/src/tui/ with mod.rs, app.rs, event.rs. Public entry himitsu::tui::run() installs/tears down raw mode + alternate screen, runs a no-op event loop that exits on q or Ctrl-C. Cli::launch_tui() in rust/src/cli/mod.rs calls tui::run() directly instead of spawning himitsu-tui. Terminal state restored on panic via drop guard or set_hook.","acceptance_criteria":"ratatui+crossterm in Cargo.toml; rust/src/tui/{mod,app,event}.rs created; himitsu::tui::run() exists; launch_tui calls it in-process; raw mode restored on panic","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:14:01Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:17:48Z","closed_at":"2026-04-13T01:17:48Z","close_reason":"Closed","dependencies":[{"issue_id":"hm-djm","depends_on_id":"hm-nck","type":"parent-child","created_at":"2026-04-12T18:15:11Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":4,"comment_count":0} +{"_type":"issue","id":"hm-nck","title":"Replace OpenTUI TUI with Rust/ratatui","description":"Replace the Bun + @opentui/core TUI (in tui/, launched as a subprocess via himitsu-tui) with a pure-Rust TUI built on ratatui + crossterm, living inside the existing himitsu binary at rust/src/tui/. Running 'himitsu' with no subcommand will launch the new TUI in-process, eliminating the subprocess hop, the HIMITSU_BIN fallback, and the entire Bun toolchain from the build. Full PRD in commit history / conversation.","status":"closed","priority":2,"issue_type":"epic","owner":"demo@himitsu.dev","created_at":"2026-04-13T01:13:45Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T02:10:40Z","closed_at":"2026-04-13T02:10:40Z","close_reason":"all steps complete","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-xx0","title":"himitsu 'add' should behave like an alias for himitsu 'set'","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-12T23:51:12Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:17:38Z","closed_at":"2026-04-13T01:17:38Z","close_reason":"Added clap alias='add' to Set command in rust/src/cli/mod.rs","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-3w1","title":"running 'himitsu' with no args should launch the tui","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-12T23:50:49Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:17:38Z","closed_at":"2026-04-13T01:17:38Z","close_reason":"Already implemented — cdf8757 feat: launch TUI when himitsu is run with no subcommand","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-457","title":"running 'himitsu' with no args should launch the tui","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-12T13:39:46Z","created_by":"Cooper Maruyama","updated_at":"2026-04-12T13:47:48Z","closed_at":"2026-04-12T13:47:48Z","close_reason":"Implemented: running himitsu with no args now launches the TUI (himitsu-tui binary). Falls back to a helpful error if TUI not installed.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-anf","title":"himitsu 'add' should behave like an alias for himitsu 'set'","status":"closed","priority":2,"issue_type":"task","owner":"demo@himitsu.dev","created_at":"2026-04-12T13:38:51Z","created_by":"Cooper Maruyama","updated_at":"2026-04-13T01:17:38Z","closed_at":"2026-04-13T01:17:38Z","close_reason":"Added clap alias='add' to Set command in rust/src/cli/mod.rs","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-3us","title":"Document external import/export requirements","description":"Add README notes for external tool requirements used by import/export/generate flows, including op for 1Password and sops for encrypted YAML/JSON output.","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:24Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:16Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-u7g","title":"Add README demo recording instructions","description":"Add a Demo or Recording section with the canonical local command to regenerate the VHS demo: cargo build --release \u0026\u0026 vhs demo/demo.tape, plus CI scratch-output behavior.","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:24Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:16Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-9lg","title":"Clarify README demo GIF and tape roles","description":"Update README demo references to use demo/demo-vhs.gif as the canonical polished demo and explain demo/tui-us-* tapes as per-story regression/demo artifacts.","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-05-01T00:51:23Z","created_by":"Cooper Maruyama","updated_at":"2026-05-03T22:01:09Z","started_at":"2026-05-03T21:59:17Z","closed_at":"2026-05-03T22:01:09Z","close_reason":"Updated README: TUI-first quick start, search no-op --refresh wording, expanded import/export tool requirements (op, sops), new Configuration section (theme/keys/HIMITSU_TUI_THEME/auto_pull), new Sync \u0026 Store Health section (auto-commit/push, --no-push, store health states), new Demo \u0026 Recordings section (vhs regen, demo-vhs.gif vs tui-us-* roles, CI scratch behavior)","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-cfo","title":"Distribute TUI footer actions evenly","description":"Render footer action groups in evenly distributed segments instead of left/right weighted alignment.","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T11:54:15Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T11:56:36Z","started_at":"2026-04-30T11:54:26Z","closed_at":"2026-04-30T11:56:36Z","close_reason":"Added shared distributed footer renderer and applied it to search and envs footers","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-7q6","title":"Align TUI footer right actions","description":"Use a split footer layout so the right-most footer actions render flush right instead of relying on spacer text.","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T10:57:00Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T10:58:24Z","started_at":"2026-04-30T10:57:22Z","closed_at":"2026-04-30T10:58:24Z","close_reason":"Updated envs footer to split left actions from right-aligned help/back actions using a horizontal layout","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-cpf","title":"TUI envs editor supports multi-entry envs","description":"The envs edit flow currently supports single-entry env labels and returns an error for labels with multiple EnvEntry rows. Extend the editor state to hold multiple entry rows with add/remove controls so existing multi-entry envs can be edited in place.","status":"closed","priority":3,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-30T10:45:06Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T23:23:27Z","started_at":"2026-04-30T23:23:04Z","closed_at":"2026-04-30T23:23:27Z","close_reason":"Superseded by hm-6qj envs revamp, which redesigns the editor with full multi-entry/glob support from the ground up.","dependencies":[{"issue_id":"hm-cpf","depends_on_id":"hm-h9m","type":"discovered-from","created_at":"2026-04-30T03:45:06Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-h9m","title":"TUI envs tab: edit existing env with live capture preview","description":"Follow-up to hm-zzj. v1 ships read + delete; this adds `e` to edit an existing env's entries.\n\n## UX\n\n- `e` opens the same editor as the create flow (file separately as hm-zzj-followup-create) prefilled with the selected env's current entries.\n- Right pane continues to show the live `env_resolver::resolve` output as the user edits, so capture-ref expansions (`/$1/postgres-url` against actual store secrets) update in real time.\n- Save calls `envs_mut::upsert` (which is upsert semantics — replaces existing rows).\n\n## Notes\n\n- Depends on the create flow (shares the editor widget). Build the editor once, reuse it.\n- For wildcard envs, the live preview must enumerate `available_secrets` via `crate::remote::store::list_secrets` to compute capture matches.","status":"closed","priority":3,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-21T21:58:07Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T10:45:15Z","started_at":"2026-04-30T10:43:17Z","closed_at":"2026-04-30T10:45:15Z","close_reason":"Added env edit flow with prefilled editor, Ctrl-S upsert save, live preview while editing, and single-entry edit tests; filed hm-cpf for multi-entry editing","dependencies":[{"issue_id":"hm-h9m","depends_on_id":"hm-udz","type":"blocks","created_at":"2026-04-21T14:58:17Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-udz","title":"TUI envs tab: create new env flow","description":"Follow-up to hm-zzj. v1 of the envs tab shipped read + delete only. This adds the `n` flow to create a new env from inside the tab.\n\n## UX\n\n- `n` opens an inline editor split with two fields:\n 1. Label input — validated live against `validate_env_label`. Show grammar hints (concrete vs `\u003cprefix\u003e/*`).\n 2. Entries list — repeatable rows, each picking a kind (Single | Glob | Alias) and the path/key fields.\n- Save (`Ctrl+S` or `Enter` on a \"Save\" button) calls `envs_mut::upsert(label, entries, scope_hint, cwd)`; on success, reload and select the new label. On error, toast the message and keep the editor open.\n- `Esc` cancels with a confirmation if the form is dirty.\n\n## Wiring\n\n- Route through the same `View::Envs` slot or add a sub-modal — your call when implementing.\n- Reuse the keymap binding `keymap.envs` already defined; add a sub-binding under it for `n`.","status":"closed","priority":3,"issue_type":"feature","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-21T21:58:07Z","created_by":"Cooper Maruyama","updated_at":"2026-04-30T10:41:14Z","started_at":"2026-04-30T10:37:58Z","closed_at":"2026-04-30T10:41:14Z","close_reason":"Added TUI envs new-env flow with inline editor, live validation, save via envs_mut::upsert, toasts, and tests","dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"hm-79o","title":"Docs: update README for search-as-root TUI flow","description":"README still references the dashboard-root flow. Update screenshots/gifs to match search-as-root, refresh the key-binding table to list current shortcuts (Ctrl+N new, Ctrl+S store, Ctrl+Y copy, ? help, Esc quit, e edit, d delete with y/n confirm).","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T06:53:34Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T07:01:04Z","closed_at":"2026-04-15T07:01:04Z","close_reason":"Merged via parallel worktree agents; cargo test --bin himitsu tui::harness:: → 3 passed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hm-6ia","title":"CI: run vhs against demo tapes to catch TUI flow regressions","description":"Add a GitHub Actions job that installs vhs and runs the us-008..us-013 tapes (non-interactive, no gif diffing needed — just exit code) so broken tapes fail CI before they land. Cache cargo build, reuse release binary.","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"demo@himitsu.dev","created_at":"2026-04-15T06:53:33Z","created_by":"Cooper Maruyama","updated_at":"2026-04-15T07:01:04Z","closed_at":"2026-04-15T07:01:04Z","close_reason":"Merged via parallel worktree agents; cargo test --bin himitsu tui::harness:: → 3 passed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md index 96e94ad..584263b 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,14 @@ that takes precedence over the file. # Default store when neither -s nor -r is given. default_store: myorg/secrets # env: HIMITSU_DEFAULT_STORE -# Where age private keys live: "disk" or "macos-keychain". +# Where the age private key lives: +# "disk" — `/key` (the default). +# "macos-keychain" — macOS Keychain entry under +# `io.darkmatter.himitsu.agekey.byfp.v1`. Switching +# to keychain on an already-initialized machine +# auto-migrates the on-disk secret into the keychain +# and removes `/key`. The pubkey file +# always stays on disk for fingerprint discovery. key_provider: disk # env: HIMITSU_KEY_PROVIDER # When true, every store-touching command first runs `git fetch` and diff --git a/rust/src/cli/codegen.rs b/rust/src/cli/codegen.rs index 1c5f0b2..5ccdd8e 100644 --- a/rust/src/cli/codegen.rs +++ b/rust/src/cli/codegen.rs @@ -956,6 +956,7 @@ mod tests { state_dir: tmp.path().join("state"), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let args = CodegenArgs { @@ -983,6 +984,7 @@ mod tests { state_dir: tmp.path().join("state"), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let args = CodegenArgs { @@ -1011,6 +1013,7 @@ mod tests { state_dir: tmp.path().join("state"), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let args = CodegenArgs { @@ -1041,6 +1044,7 @@ mod tests { state_dir: tmp.path().join("state"), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let args = CodegenArgs { @@ -1177,6 +1181,7 @@ mod tests { state_dir: tmp.path().join("state"), store: project.clone(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let result = run_sops("ghost", None, &ctx); diff --git a/rust/src/cli/exec.rs b/rust/src/cli/exec.rs index e523958..000376d 100644 --- a/rust/src/cli/exec.rs +++ b/rust/src/cli/exec.rs @@ -20,7 +20,7 @@ use clap::Args; use super::Context; use crate::config::{self, env_resolver, validate_env_label}; -use crate::crypto::{age, secret_value, tags as tag_grammar}; +use crate::crypto::{secret_value, tags as tag_grammar}; use crate::error::{HimitsuError, Result}; use crate::reference::SecretRef; use crate::remote::store; @@ -74,7 +74,7 @@ pub fn run(args: ExecArgs, ctx: &Context) -> Result<()> { // Load the age identity once so we don't re-parse the key file per // resolved secret. `exec` is the first hot loop of decrypts and the // win is real. - let identity = age::read_identity(&ctx.key_path())?; + let identity = ctx.load_identity()?; let decrypted = decrypt_resolved(ctx, &identity, resolved)?; let env_map = build_env_map(decrypted, &args.tags)?; diff --git a/rust/src/cli/export.rs b/rust/src/cli/export.rs index ef514e9..a965fc0 100644 --- a/rust/src/cli/export.rs +++ b/rust/src/cli/export.rs @@ -36,7 +36,7 @@ pub struct ExportArgs { } pub fn run(args: ExportArgs, ctx: &Context) -> Result<()> { - let identity = crypto::read_identity(&ctx.key_path())?; + let identity = ctx.load_identity()?; // List all secrets in the store. let all_paths = store::list_secrets(&ctx.store, None)?; diff --git a/rust/src/cli/generate.rs b/rust/src/cli/generate.rs index 1d1ac9b..2112e56 100644 --- a/rust/src/cli/generate.rs +++ b/rust/src/cli/generate.rs @@ -43,7 +43,7 @@ pub fn run(args: GenerateArgs, ctx: &Context) -> Result<()> { } // Load age identity for decryption. - let identity = crypto::read_identity(&ctx.key_path())?; + let identity = ctx.load_identity()?; // Determine which envs to generate. let env_names: Vec = if let Some(ref env_name) = args.env { diff --git a/rust/src/cli/get.rs b/rust/src/cli/get.rs index dd66f63..51f7633 100644 --- a/rust/src/cli/get.rs +++ b/rust/src/cli/get.rs @@ -39,7 +39,7 @@ pub fn get_plaintext(ctx: &Context, path: &str) -> Result> { /// Decrypt and return the full decoded SecretValue for a secret reference. fn get_decoded(ctx: &Context, path: &str) -> Result { - let identity = age::read_identity(&ctx.key_path())?; + let identity = ctx.load_identity()?; get_decoded_with_identity(ctx, path, &identity) } diff --git a/rust/src/cli/import.rs b/rust/src/cli/import.rs index 8fcf55d..2616acc 100644 --- a/rust/src/cli/import.rs +++ b/rust/src/cli/import.rs @@ -739,6 +739,7 @@ mod tests { state_dir: std::path::PathBuf::from("/tmp"), store: std::path::PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let err = run(args, &ctx).unwrap_err(); assert!( @@ -765,6 +766,7 @@ mod tests { state_dir: std::path::PathBuf::from("/tmp"), store: std::path::PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let err = run(args, &ctx).unwrap_err(); assert!( @@ -792,6 +794,7 @@ mod tests { state_dir: std::path::PathBuf::from("/tmp"), store: std::path::PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let err = run(args, &ctx).unwrap_err(); assert!( diff --git a/rust/src/cli/init.rs b/rust/src/cli/init.rs index 6a0d173..e631462 100644 --- a/rust/src/cli/init.rs +++ b/rust/src/cli/init.rs @@ -70,6 +70,7 @@ pub fn run(args: InitArgs, ctx: &Context) -> Result<()> { state_dir: config::state_dir(), store: ctx.store.clone(), recipients_path: ctx.recipients_path.clone(), + key_provider: ctx.key_provider.clone(), }; return run_init(args, &patched_ctx); } @@ -83,41 +84,51 @@ pub(crate) fn run_init(args: InitArgs, ctx: &Context) -> Result<()> { let data_dir = &ctx.data_dir; let state_dir = &ctx.state_dir; - // ── 1. Ensure data_dir exists (keys, config) ────────────────────────── - let key_existed = data_dir.join("key").exists(); - + // ── 1. Ensure config exists, then resolve the active provider ──────── + // The provider must be settled BEFORE we write any key material, since + // it decides whether the secret lands on disk or in the keychain. With + // the old order (write key → set provider), `--key-provider macos-keychain` + // produced a config that pointed at the keychain while the secret sat + // in `data_dir/key` — fingers-crossed that no one read it. (Bug fix.) std::fs::create_dir_all(data_dir)?; - let key_path = data_dir.join("key"); - let pubkey_path = data_dir.join("key.pub"); - - let pubkey = if !key_path.exists() { - let (secret, public) = age::keygen(); - std::fs::write( - &key_path, - format!( - "# created: {}\n# public key: {public}\n{secret}\n", - timestamp() - ), - )?; - std::fs::write(&pubkey_path, format!("{public}\n"))?; - public - } else { - read_public_key(data_dir)? - }; - let config_path = config::config_path(); if !config_path.exists() { config::Config::write_default(&config_path)?; } - // ── 2. Handle --key-provider ────────────────────────────────────────── if let Some(ref provider_str) = args.key_provider { let provider: KeyProvider = provider_str.parse()?; let mut cfg = config::Config::load(&config_path)?; cfg.key_provider = provider; cfg.save(&config_path)?; } + let active_provider = config::Config::load(&config_path)?.key_provider; + + // If the user just switched to keychain on an already-initialized + // machine, move the existing on-disk secret into the keychain. The + // pubkey file stays in place — it's the provider-agnostic "is + // initialized" probe. + if crate::crypto::keystore::needs_disk_to_keychain_migration(&active_provider, data_dir)? { + crate::crypto::keystore::migrate_disk_to_keychain(data_dir)?; + eprintln!("✓ Migrated age key from disk to macOS Keychain"); + } + + // ── 2. Generate a fresh keypair if none exists yet ─────────────────── + let key_existed = crate::crypto::keystore::is_initialized(data_dir); + let pubkey = if !key_existed { + let (secret, public) = age::keygen(); + crate::crypto::keystore::store_new_key( + &active_provider, + data_dir, + &secret, + &public, + ×tamp(), + )?; + public + } else { + read_public_key(data_dir)? + }; // ── 3. Ensure state_dir exists (stores subdir) ──────────────────────── std::fs::create_dir_all(state_dir.join("stores"))?; diff --git a/rust/src/cli/join.rs b/rust/src/cli/join.rs index 50c4d7a..27c1f79 100644 --- a/rust/src/cli/join.rs +++ b/rust/src/cli/join.rs @@ -119,6 +119,7 @@ mod tests { state_dir: tmp.path().join("state"), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), } } diff --git a/rust/src/cli/ls.rs b/rust/src/cli/ls.rs index 1fbf9e2..6b2a6c7 100644 --- a/rust/src/cli/ls.rs +++ b/rust/src/cli/ls.rs @@ -76,7 +76,7 @@ pub fn run(args: LsArgs, ctx: &Context) -> Result<()> { let identity = if args.tag.is_empty() { None } else { - age::read_identity(&ctx.key_path()).ok() + ctx.load_identity().ok() }; // ── Resolve qualified references ────────────────────────────────────── diff --git a/rust/src/cli/mod.rs b/rust/src/cli/mod.rs index 847da27..6a22221 100644 --- a/rust/src/cli/mod.rs +++ b/rust/src/cli/mod.rs @@ -51,18 +51,31 @@ pub struct Context { /// from the project-level `himitsu.yaml` `store.recipients_path` field. /// When `None`, the default `.himitsu/recipients/` layout is used. pub recipients_path: Option, + /// Where the age private key lives. Resolved from `Config.key_provider` + /// at dispatcher boot so callers don't each re-read the config. + pub key_provider: crate::config::KeyProvider, } impl Context { - /// Path to the age private key file. + /// Path to the age private key file. Only valid for the + /// [`Disk`](crate::config::KeyProvider::Disk) provider — with the + /// keychain provider this path doesn't exist, so callers should + /// reach the secret through [`Self::load_identity`] instead of + /// reading the path directly. pub fn key_path(&self) -> PathBuf { - self.data_dir.join("key") + crate::crypto::keystore::disk_secret_path(&self.data_dir) } - /// Path to the age public key file. + /// Path to the age public key file. Always written (provider-agnostic). #[allow(dead_code)] pub fn pubkey_path(&self) -> PathBuf { - self.data_dir.join("key.pub") + crate::crypto::keystore::pubkey_path(&self.data_dir) + } + + /// Load the user's age identity through the active provider. This is + /// the chokepoint: every command that decrypts goes through it. + pub fn load_identity(&self) -> Result<::age::x25519::Identity> { + crate::crypto::keystore::load_identity(&self.key_provider, &self.data_dir) } /// Directory containing managed store checkouts. @@ -377,7 +390,7 @@ impl Cli { && !is_docs && !is_completions && !is_complete_paths - && !data_dir.join("key").exists() + && !crate::crypto::keystore::is_initialized(&data_dir) { eprintln!("First run — initializing himitsu..."); let ctx = Context { @@ -385,6 +398,7 @@ impl Cli { state_dir: state_dir.clone(), store: PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; init::run( init::InitArgs { @@ -463,11 +477,15 @@ impl Cli { } let recipients_path = load_recipients_path_override(&store); + let key_provider = crate::config::Config::load(&crate::config::config_path()) + .map(|c| c.key_provider) + .unwrap_or_default(); let ctx = Context { data_dir, state_dir, store, recipients_path, + key_provider, }; // Pre-dispatch: when `auto_pull` is on, fetch + fast-forward the @@ -568,12 +586,16 @@ impl Cli { // than erroring out. let store = crate::config::resolve_store(None).unwrap_or_default(); let recipients_path = load_recipients_path_override(&store); + let key_provider = crate::config::Config::load(&crate::config::config_path()) + .map(|c| c.key_provider) + .unwrap_or_default(); let ctx = Context { data_dir, state_dir, store, recipients_path, + key_provider, }; crate::tui::run(&ctx) } @@ -857,6 +879,7 @@ mod tests { state_dir: tmp, store: store.to_path_buf(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), } } diff --git a/rust/src/cli/recipient.rs b/rust/src/cli/recipient.rs index a8db309..62d1ca7 100644 --- a/rust/src/cli/recipient.rs +++ b/rust/src/cli/recipient.rs @@ -435,6 +435,7 @@ mod tests { state_dir: tmp.path().join("state"), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; (tmp, ctx) } diff --git a/rust/src/cli/rekey.rs b/rust/src/cli/rekey.rs index 9036721..9809e99 100644 --- a/rust/src/cli/rekey.rs +++ b/rust/src/cli/rekey.rs @@ -23,7 +23,7 @@ pub struct RekeyArgs { /// Note: `args.force` is accepted for forward-compat (future no-op detection) /// but currently all matched secrets are always re-encrypted. pub fn rekey_store(ctx: &Context, path_prefix: Option<&str>) -> Result { - let identity = age::read_identity(&ctx.key_path())?; + let identity = ctx.load_identity()?; let recipients = age::collect_recipients(&ctx.store, ctx.recipients_path.as_deref())?; if recipients.is_empty() { return Err(HimitsuError::Recipient("no recipients found".into())); diff --git a/rust/src/cli/schema.rs b/rust/src/cli/schema.rs index 329cd4d..e2ca122 100644 --- a/rust/src/cli/schema.rs +++ b/rust/src/cli/schema.rs @@ -257,6 +257,7 @@ mod tests { state_dir: tmp.path().join("state"), store: store.clone(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; cmd_refresh(&ctx).unwrap(); diff --git a/rust/src/cli/search.rs b/rust/src/cli/search.rs index 051b128..1fcc0ff 100644 --- a/rust/src/cli/search.rs +++ b/rust/src/cli/search.rs @@ -79,7 +79,7 @@ pub fn search_core(ctx: &Context, query: &str, tag_filter: &[String]) -> Result< // description from each secret's encrypted payload. If the identity // isn't available (fresh install, CI test fixture, missing key file) // we still return search results — just without descriptions. - let identity = age::read_identity(&ctx.key_path()).ok(); + let identity = ctx.load_identity().ok(); for (slug, store_path) in collect_stores(ctx)? { let paths = store::list_secrets(&store_path, None).unwrap_or_default(); diff --git a/rust/src/cli/sync.rs b/rust/src/cli/sync.rs index ef5be16..4e76b09 100644 --- a/rust/src/cli/sync.rs +++ b/rust/src/cli/sync.rs @@ -44,6 +44,7 @@ pub fn run(args: SyncArgs, ctx: &Context) -> Result<()> { state_dir: ctx.state_dir.clone(), store: store_path.clone(), recipients_path: None, + key_provider: ctx.key_provider.clone(), }; // Commit any pre-existing pending changes (e.g. from a prior sync that diff --git a/rust/src/cli/tag.rs b/rust/src/cli/tag.rs index e5c42d0..3285e4e 100644 --- a/rust/src/cli/tag.rs +++ b/rust/src/cli/tag.rs @@ -60,7 +60,7 @@ pub fn run(args: TagArgs, ctx: &Context) -> Result<()> { }; let ciphertext = store::read_secret(&effective_store, &secret_path)?; - let identity = age::read_identity(&ctx.key_path())?; + let identity = ctx.load_identity()?; let plaintext = age::decrypt(&ciphertext, &identity)?; let mut decoded = secret_value::decode(&plaintext); diff --git a/rust/src/crypto/keystore.rs b/rust/src/crypto/keystore.rs new file mode 100644 index 0000000..ded16ad --- /dev/null +++ b/rust/src/crypto/keystore.rs @@ -0,0 +1,234 @@ +//! Provider-aware persistence for the user's age private key. +//! +//! The private key can live in two places, picked by `Config.key_provider`: +//! +//! - [`Disk`](config::KeyProvider::Disk) — a file at `data_dir/key`. +//! - [`MacosKeychain`](config::KeyProvider::MacosKeychain) — the macOS +//! Keychain (a `generic-password` entry under +//! `io.darkmatter.himitsu.agekey.byfp.v1`). +//! +//! Either way the **public key** stays at `data_dir/key.pub` so other +//! commands can compute the fingerprint without unlocking the keychain. +//! The pubkey file is also the canonical "is himitsu initialized?" probe. +//! +//! Most callers don't reach in here directly — they go through +//! [`Context::load_identity`](crate::cli::Context::load_identity), which +//! is the chokepoint that resolves the active provider once. + +use std::path::Path; + +use ::age::x25519::Identity; + +use crate::config::KeyProvider as ProviderChoice; +use crate::crypto::age; +use crate::error::{HimitsuError, Result}; +use crate::keyring::macos::MacOSKeychain; +use crate::keyring::{fingerprint, KeyProvider}; + +/// Path to the on-disk public-key file. Always written, regardless of +/// which provider holds the secret. +pub fn pubkey_path(data_dir: &Path) -> std::path::PathBuf { + data_dir.join("key.pub") +} + +/// Path to the on-disk secret-key file. Only populated when the active +/// provider is [`ProviderChoice::Disk`]; with the keychain provider this +/// path doesn't exist. +pub fn disk_secret_path(data_dir: &Path) -> std::path::PathBuf { + data_dir.join("key") +} + +/// Has the user run `himitsu init`? True when a public-key file is +/// present — the secret can live on disk or in the keychain, but the +/// pubkey is always materialised so this is a provider-agnostic probe. +pub fn is_initialized(data_dir: &Path) -> bool { + pubkey_path(data_dir).exists() +} + +/// Persist a freshly-generated keypair under the given provider. +/// +/// `pubkey_path` always gets written. `secret`'s landing site depends on +/// `provider`: disk drops it next to the pubkey with the legacy +/// `# comment` header; keychain stores it under the public key's +/// fingerprint and never touches the disk secret file. +pub fn store_new_key( + provider: &ProviderChoice, + data_dir: &Path, + secret: &str, + pubkey: &str, + timestamp: &str, +) -> Result<()> { + std::fs::create_dir_all(data_dir)?; + std::fs::write(pubkey_path(data_dir), format!("{pubkey}\n"))?; + + match provider { + ProviderChoice::Disk => { + std::fs::write( + disk_secret_path(data_dir), + format!("# created: {timestamp}\n# public key: {pubkey}\n{secret}\n"), + )?; + } + ProviderChoice::MacosKeychain => { + ensure_keychain_available()?; + let fp = fingerprint(pubkey); + MacOSKeychain.store_key(&fp, secret)?; + } + } + Ok(()) +} + +/// Load the active identity for the configured provider. +/// +/// Disk: parses `data_dir/key`. Keychain: looks up the secret indexed +/// by the disk pubkey's fingerprint, then parses it. +pub fn load_identity(provider: &ProviderChoice, data_dir: &Path) -> Result { + match provider { + ProviderChoice::Disk => age::read_identity(&disk_secret_path(data_dir)), + ProviderChoice::MacosKeychain => { + ensure_keychain_available()?; + let pubkey = std::fs::read_to_string(pubkey_path(data_dir)) + .map_err(|e| { + HimitsuError::Keychain(format!( + "no public key file at {} (run `himitsu init`): {e}", + pubkey_path(data_dir).display() + )) + })? + .trim() + .to_string(); + let fp = fingerprint(&pubkey); + let secret = MacOSKeychain.load_key(&fp)?.ok_or_else(|| { + HimitsuError::Keychain(format!( + "no key for fingerprint {fp} in macOS Keychain — \ + run `himitsu init --key-provider macos-keychain` to migrate \ + from disk, or check that the entry under \ + io.darkmatter.himitsu.agekey.byfp.v1 / {fp} hasn't been deleted" + )) + })?; + age::parse_identity(&secret) + } + } +} + +/// Detect a disk-based key that should be migrated to the keychain. +/// +/// Returns `true` only when the active provider is keychain, the disk +/// secret file exists, and the keychain has no entry for the same +/// fingerprint yet — i.e. a one-shot migration is wanted, not idempotent +/// rewrites of an already-migrated key. +pub fn needs_disk_to_keychain_migration( + provider: &ProviderChoice, + data_dir: &Path, +) -> Result { + if !matches!(provider, ProviderChoice::MacosKeychain) { + return Ok(false); + } + if !disk_secret_path(data_dir).exists() { + return Ok(false); + } + if !MacOSKeychain::is_available() { + return Ok(false); + } + let pubkey = std::fs::read_to_string(pubkey_path(data_dir))? + .trim() + .to_string(); + let fp = fingerprint(&pubkey); + Ok(MacOSKeychain.load_key(&fp)?.is_none()) +} + +/// Move an existing on-disk secret into the keychain, then delete the +/// disk file. The pubkey file is left in place (still needed for +/// fingerprint discovery). +/// +/// No-op if the disk secret has already been migrated. Errors out +/// without touching state if the keychain write fails — callers can +/// safely retry. +pub fn migrate_disk_to_keychain(data_dir: &Path) -> Result<()> { + ensure_keychain_available()?; + let secret_path = disk_secret_path(data_dir); + if !secret_path.exists() { + return Ok(()); + } + let pubkey = std::fs::read_to_string(pubkey_path(data_dir))? + .trim() + .to_string(); + let fp = fingerprint(&pubkey); + let identity = age::read_identity(&secret_path)?; + let secret_str = secrecy::ExposeSecret::expose_secret(&identity.to_string()).to_string(); + MacOSKeychain.store_key(&fp, &secret_str)?; + // Only remove the disk file once the keychain write succeeded. + std::fs::remove_file(&secret_path)?; + Ok(()) +} + +fn ensure_keychain_available() -> Result<()> { + if !MacOSKeychain::is_available() { + return Err(HimitsuError::Keychain( + "macOS Keychain provider selected but this isn't macOS — \ + switch `key_provider` to `disk` in ~/.config/himitsu/config.yaml" + .into(), + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_initialized_tracks_pubkey_file_only() { + let dir = tempfile::tempdir().unwrap(); + assert!(!is_initialized(dir.path())); + + // Secret-only is NOT initialized — pubkey is the canonical probe. + std::fs::write(disk_secret_path(dir.path()), "AGE-SECRET-KEY-...").unwrap(); + assert!(!is_initialized(dir.path())); + + std::fs::write(pubkey_path(dir.path()), "age1pub").unwrap(); + assert!(is_initialized(dir.path())); + } + + #[test] + fn store_new_key_disk_writes_both_files() { + let dir = tempfile::tempdir().unwrap(); + store_new_key( + &ProviderChoice::Disk, + dir.path(), + "AGE-SECRET-KEY-1ABCDEF", + "age1publicfake", + "2026-05-09T12:00:00Z", + ) + .unwrap(); + + let pub_contents = std::fs::read_to_string(pubkey_path(dir.path())).unwrap(); + assert!(pub_contents.contains("age1publicfake")); + + let secret_contents = std::fs::read_to_string(disk_secret_path(dir.path())).unwrap(); + assert!(secret_contents.contains("AGE-SECRET-KEY-1ABCDEF")); + assert!(secret_contents.contains("# created: 2026-05-09T12:00:00Z")); + assert!(secret_contents.contains("# public key: age1publicfake")); + } + + #[test] + fn migration_predicate_false_for_disk_provider() { + let dir = tempfile::tempdir().unwrap(); + // Disk provider never migrates regardless of disk state. + std::fs::write(pubkey_path(dir.path()), "age1pub").unwrap(); + std::fs::write(disk_secret_path(dir.path()), "secret").unwrap(); + assert_eq!( + needs_disk_to_keychain_migration(&ProviderChoice::Disk, dir.path()).unwrap(), + false + ); + } + + #[test] + fn migration_predicate_false_when_disk_secret_missing() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(pubkey_path(dir.path()), "age1pub").unwrap(); + // Keychain provider + no on-disk secret = nothing to migrate. + assert_eq!( + needs_disk_to_keychain_migration(&ProviderChoice::MacosKeychain, dir.path()).unwrap(), + false + ); + } +} diff --git a/rust/src/crypto/mod.rs b/rust/src/crypto/mod.rs index 48c6b5f..b0993a3 100644 --- a/rust/src/crypto/mod.rs +++ b/rust/src/crypto/mod.rs @@ -1,3 +1,4 @@ pub mod age; +pub mod keystore; pub mod secret_value; pub mod tags; diff --git a/rust/src/tui/app.rs b/rust/src/tui/app.rs index 47d6fdd..547991a 100644 --- a/rust/src/tui/app.rs +++ b/rust/src/tui/app.rs @@ -572,6 +572,7 @@ fn clone_ctx(ctx: &Context) -> Context { state_dir: ctx.state_dir.clone(), store: ctx.store.clone(), recipients_path: ctx.recipients_path.clone(), + key_provider: ctx.key_provider.clone(), } } diff --git a/rust/src/tui/harness.rs b/rust/src/tui/harness.rs index 486188d..68393c2 100644 --- a/rust/src/tui/harness.rs +++ b/rust/src/tui/harness.rs @@ -237,6 +237,7 @@ mod tests { state_dir, store: alpha.clone(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; Self { diff --git a/rust/src/tui/mod.rs b/rust/src/tui/mod.rs index 8b82356..b70ffdd 100644 --- a/rust/src/tui/mod.rs +++ b/rust/src/tui/mod.rs @@ -92,6 +92,7 @@ pub fn run_init_flow() -> Result<()> { state_dir: crate::config::state_dir(), store: PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; let result = init::run_init(args, &ctx); @@ -120,11 +121,13 @@ pub fn run_init_flow() -> Result<()> { theme::set_theme(&tui.theme)?; icons::set_use_nerd_fonts(tui.nerd_fonts); + let cfg = Config::load(&config_path()).unwrap_or_default(); let ctx = Context { data_dir: crate::config::data_dir(), state_dir: crate::config::state_dir(), store: crate::config::resolve_store(None).unwrap_or_default(), recipients_path: None, + key_provider: cfg.key_provider, }; if !should_continue_to_dashboard_after_init(&ctx.store) { return Ok(()); @@ -133,7 +136,8 @@ pub fn run_init_flow() -> Result<()> { } fn should_launch_init_flow(ctx: &Context) -> bool { - !ctx.data_dir.join("key").exists() || ctx.store.as_os_str().is_empty() + !crate::crypto::keystore::is_initialized(&ctx.data_dir) + || ctx.store.as_os_str().is_empty() } fn should_continue_to_dashboard_after_init(store: &std::path::Path) -> bool { @@ -152,13 +156,17 @@ mod tests { state_dir: PathBuf::new(), store, recipients_path: None, + key_provider: crate::config::KeyProvider::default(), } } #[test] fn should_launch_init_flow_when_key_exists_but_store_is_missing() { + // The "is initialized" probe is the pubkey file, not the secret — + // both providers materialise the pubkey, but only the disk + // provider also drops the secret next to it. let data_dir = tempfile::tempdir().unwrap(); - std::fs::write(data_dir.path().join("key"), "AGE-SECRET-KEY").unwrap(); + std::fs::write(data_dir.path().join("key.pub"), "age1pub").unwrap(); let ctx = ctx_with(data_dir.path().to_path_buf(), PathBuf::new()); @@ -169,7 +177,7 @@ mod tests { fn should_not_launch_init_flow_when_key_and_store_exist() { let data_dir = tempfile::tempdir().unwrap(); let store = tempfile::tempdir().unwrap(); - std::fs::write(data_dir.path().join("key"), "AGE-SECRET-KEY").unwrap(); + std::fs::write(data_dir.path().join("key.pub"), "age1pub").unwrap(); let ctx = ctx_with(data_dir.path().to_path_buf(), store.path().to_path_buf()); diff --git a/rust/src/tui/views/envs.rs b/rust/src/tui/views/envs.rs index eec4fe6..bbb62cb 100644 --- a/rust/src/tui/views/envs.rs +++ b/rust/src/tui/views/envs.rs @@ -674,7 +674,9 @@ impl EnvsView { return EnvsAction::Quit; } - if matches!(key.code, KeyCode::Char('s')) && key.modifiers.contains(KeyModifiers::CONTROL) { + if (matches!(key.code, KeyCode::Char('s')) || matches!(key.code, KeyCode::Char('w'))) + && key.modifiers.contains(KeyModifiers::CONTROL) + { return self.perform_create(); } @@ -1559,6 +1561,7 @@ fn clone_ctx(ctx: &Context) -> Context { state_dir: ctx.state_dir.clone(), store: ctx.store.clone(), recipients_path: ctx.recipients_path.clone(), + key_provider: ctx.key_provider.clone(), } } @@ -1573,6 +1576,7 @@ impl EnvsView { ("y", "open YAML/DSL 2-pane editor"), ("d", "delete selected env (confirm y/N)"), ("ctrl-s", "save while editing"), + ("ctrl-w", "save while editing"), ("ctrl-space", "autocomplete in DSL editor"), ("?", "toggle this help"), ("esc / q", "back to search"), @@ -1642,6 +1646,7 @@ mod tests { state_dir: PathBuf::new(), store: store.to_path_buf(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), } } diff --git a/rust/src/tui/views/new_secret.rs b/rust/src/tui/views/new_secret.rs index e1ccc94..0870f8b 100644 --- a/rust/src/tui/views/new_secret.rs +++ b/rust/src/tui/views/new_secret.rs @@ -1008,6 +1008,7 @@ mod tests { state_dir: PathBuf::new(), store: PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), } } diff --git a/rust/src/tui/views/remote_add.rs b/rust/src/tui/views/remote_add.rs index e36970d..f68a14a 100644 --- a/rust/src/tui/views/remote_add.rs +++ b/rust/src/tui/views/remote_add.rs @@ -160,6 +160,7 @@ mod tests { state_dir: PathBuf::new(), store: PathBuf::new(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), } } diff --git a/rust/src/tui/views/search.rs b/rust/src/tui/views/search.rs index 290eb3b..9241022 100644 --- a/rust/src/tui/views/search.rs +++ b/rust/src/tui/views/search.rs @@ -185,6 +185,7 @@ impl SearchView { state_dir: ctx.state_dir.clone(), store: ctx.store.clone(), recipients_path: ctx.recipients_path.clone(), + key_provider: ctx.key_provider.clone(), }; let store_health = check_store_health(&ctx_owned); let env_index = build_env_index(); @@ -1202,7 +1203,7 @@ fn decrypt_value(ctx: &Context, result: &SearchResult) -> crate::error::Result crate::error::Result { let ciphertext = store::read_secret(&self.store_path, &self.path)?; - let identity = age::read_identity(&self.ctx.key_path())?; + let identity = self.ctx.load_identity()?; let plain = age::decrypt(&ciphertext, &identity)?; Ok(secret_value::decode(&plain)) } @@ -952,6 +953,7 @@ mod tests { state_dir, store: store.clone(), recipients_path: None, + key_provider: crate::config::KeyProvider::default(), }; (dir, ctx, "prod/API_KEY".to_string()) }