{"id":49449967,"url":"https://github.com/yukimemi/yui","last_synced_at":"2026-05-05T05:01:43.444Z","repository":{"id":354570443,"uuid":"1224144556","full_name":"yukimemi/yui","owner":"yukimemi","description":"Target-as-truth dotfiles manager. Edit your live configs, source repo updates automatically via hardlink/junction/symlink.","archived":false,"fork":false,"pushed_at":"2026-05-04T00:39:59.000Z","size":396,"stargazers_count":1,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T04:33:07.693Z","etag":null,"topics":["chezmoi-alternative","cli","command-line-tool","cross-platform","dotfiles","dotfiles-manager","hardlink","junction","linux","macos","rust","symlink","tera","tera-templates","windows"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yukimemi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T02:12:41.000Z","updated_at":"2026-05-03T17:00:41.000Z","dependencies_parsed_at":"2026-05-05T05:00:54.130Z","dependency_job_id":null,"html_url":"https://github.com/yukimemi/yui","commit_stats":null,"previous_names":["yukimemi/yui"],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/yukimemi/yui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukimemi%2Fyui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukimemi%2Fyui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukimemi%2Fyui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukimemi%2Fyui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yukimemi","download_url":"https://codeload.github.com/yukimemi/yui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yukimemi%2Fyui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32636108,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-04T10:08:07.713Z","status":"online","status_checked_at":"2026-05-05T02:00:06.033Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chezmoi-alternative","cli","command-line-tool","cross-platform","dotfiles","dotfiles-manager","hardlink","junction","linux","macos","rust","symlink","tera","tera-templates","windows"],"created_at":"2026-04-30T01:05:58.810Z","updated_at":"2026-05-05T05:01:43.408Z","avatar_url":"https://github.com/yukimemi.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/logo.svg\" width=\"560\" alt=\"yui — target-as-truth dotfiles manager\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cb\u003e結 — edit your live configs, the source repo updates itself.\u003c/b\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://crates.io/crates/yui-cli\"\u003e\u003cimg src=\"https://img.shields.io/crates/v/yui-cli.svg\" alt=\"crates.io\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/yukimemi/yui/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/yukimemi/yui/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"/\u003e\u003c/a\u003e\n  \u003ca href=\"./LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-blue.svg\" alt=\"License: MIT\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n`yui` flips the chezmoi flow: instead of editing your source repo and\nrunning `apply` to push changes out to `~`, you edit `~` directly and\nthe source follows automatically. The two sides share a backing inode\n(hardlink / junction / symlink), so an app's write to the target *is*\na write to source.\n\nIt exists to fix three chezmoi pain points the author hit running\nchezmoi for years:\n\n1. **The edit-source-then-apply tax** — every config tweak became a\n   two-step ceremony.\n2. **Source ↔ target drift** — apps overwrite the target directly,\n   and the user finds out at the next `chezmoi diff`.\n3. **Untracked new files** — apps that create new files inside a\n   managed directory aren't visible to chezmoi unless you remember\n   to `chezmoi add` them.\n\n## How it works\n\nYour dotfiles repo is a normal directory tree. `yui apply` walks it\nand links each file/directory into its target location:\n\n| platform | files | directories |\n|----------|-------|-------------|\n| Linux / macOS | symlink | symlink |\n| Windows (default) | **hardlink** | **junction** |\n| Windows (opt-in) | symlink | symlink (Developer Mode / admin) |\n\nThe Windows defaults are deliberate: hardlinks and junctions both\nwork without elevated permissions and survive most editors' \"atomic\nsave\" rename trick. When that trick *does* break the hardlink, the\n**absorb classifier** notices on the next `apply` / `status`:\n\n```\ntarget's file-id == source's file-id?            → InSync\ncontent identical, different file-id?            → RelinkOnly\ntarget newer + content differs?                  → AutoAbsorb (target wins)\nsource newer + content differs?                  → NeedsConfirm (anomaly)\ntarget missing?                                  → Restore\n```\n\n`AutoAbsorb` backs source up under `$DOTFILES/.yui/backup/` and\ncopies target's content into source before relinking — your local\nedit is preserved, even when an editor saved over the link.\n\nFor directories the same target-wins merge applies: target's files\nland in source (overwriting on conflict), source-only scaffolding\n(like `.yuilink` markers) survives, and the dir is then re-exposed\nvia a platform-appropriate link back to source — junction on\nWindows, symlink on Unix/macOS, or whatever the configured `dir_mode`\nresolves to. Non-regular entries inside the target — junctions,\nsymlinks, device files — are skipped with a warning since following\nthem safely is ill-defined.\n\nPer-file content collisions inside the merge run through the same\nabsorb classifier the file-level path uses: identical content is a\nno-op, target-newer copies through (AutoAbsorb), and source-newer +\ndiff defers to `[absorb] on_anomaly` (skip / force / ask). The\nmarker is consent for the *whole-tree* merge, but a single file\nwhere the source side is newer is still a real anomaly worth\nsurfacing.\n\n## Install\n\n```sh\ncargo install yui-cli\n```\n\nPre-built binaries for Linux x86_64, Windows x86_64, and macOS\n(Intel + Apple Silicon) are attached to every\n[GitHub Release](https://github.com/yukimemi/yui/releases).\n\n## Quick start\n\n```sh\n# Scaffold a source repo at the current directory and install git hooks.\nyui init --git-hooks\n\n# Edit $DOTFILES/config.toml to declare your mounts, then:\nyui apply        # render templates + link targets + auto-absorb drift\nyui list         # see every src→dst mapping at a glance\nyui status       # check what drifted\nyui doctor       # environment sanity check\n```\n\nSmallest useful `$DOTFILES/config.toml`:\n\n```toml\n[[mount.entry]]\nsrc = \"home\"\ndst = \"~\"          # ~ expands to $HOME / $USERPROFILE per OS\n\n[[mount.entry]]\nsrc  = \"appdata\"\ndst  = \"{{ env(name='APPDATA') }}\"\nwhen = \"yui.os == 'windows'\"\n```\n\nAdd files under `home/` and they'll link into `~`. Add a `.yuilink`\nfile to a directory to junction the whole directory as one unit (so\nfiles an app creates inside that dir land back in source\nautomatically).\n\n`src` is the path *to* yui's source for that mount; it accepts\nrelative paths (resolved against `$DOTFILES`), absolute paths, `~`\n/ `~/...`, and Tera tags. So a private clone outside `$DOTFILES`\ncan participate as its own mount:\n\n```toml\n[[mount.entry]]\nsrc = \"~/.dotfiles-private/home\"\ndst = \"~\"\n```\n\n## Templates (`*.tera`)\n\nFiles ending in `.tera` are rendered with [Tera] before linking; the\noutput is a sibling file with the `.tera` suffix dropped. `yui` adds\nthe rendered file to a managed `# \u003e\u003e\u003e yui rendered (auto-managed) \u003c\u003c\u003c`\nsection of `.gitignore` so it doesn't get committed.\n\n```\nhome/.gitconfig.tera   →  home/.gitconfig   →  ~/.gitconfig\n```\n\nTemplates have access to `yui.os` / `yui.host` / `yui.user` /\n`yui.arch` / `yui.source` and your `[vars]` table. Per-host overrides\ngo in `config.local.toml` (machine-local, gitignored), which `yui`\nloads after `config.*.toml` so its values win.\n\n[Tera]: https://keats.github.io/tera/\n\n## One source → many targets\n\nIf you want the same source directory linked to different places on\ndifferent OSes — common for editor configs (`~/.config/nvim` on Unix,\n`%LOCALAPPDATA%\\nvim` on Windows) — drop a `.yuilink` with content:\n\n```toml\n# $DOTFILES/home/.config/nvim/.yuilink\n[[link]]\ndst = \"~/.config/nvim\"\n\n[[link]]\ndst = \"{{ env(name='LOCALAPPDATA') }}/nvim\"\nwhen = \"yui.os == 'windows'\"\n```\n\n`yui list` shows each link and which `when` would activate it.\n\n### Stacking markers and file-level entries\n\nMarkers compose. A parent `.yuilink` no longer stops the walker, so\nyou can junction a whole `~/.config` and *also* layer extra dsts onto\nspecific subdirs:\n\n```toml\n# $DOTFILES/home/.config/.yuilink — junction the whole .config dir\n[[link]]\ndst = \"~/.config\"\n```\n\n```toml\n# $DOTFILES/home/.config/nvim/.yuilink — extra Windows-only dst\n[[link]]\ndst = \"{{ env(name='LOCALAPPDATA') }}/nvim\"\nwhen = \"yui.os == 'windows'\"\n```\n\nBoth links land — the parent takes care of the natural placement, the\nchild adds its OS-specific alternate.\n\nA `[[link]]` may also carry a `src = \"\u003cfilename\u003e\"` to scope the link to\na single sibling file rather than the directory itself. Useful for\npaths that don't follow `~/.config/\u003capp\u003e/` conventions, like the\nPowerShell profile on Windows:\n\n```toml\n# $DOTFILES/home/.config/powershell/.yuilink\n[[link]]\nsrc = \"Microsoft.PowerShell_profile.ps1\"\ndst = \"{{ env(name='USERPROFILE') }}/Documents/PowerShell/Microsoft.PowerShell_profile.ps1\"\nwhen = \"yui.os == 'windows'\"\n```\n\n`src` must be a single filename (no path separators); the file lives\nright next to the marker. The rest of the directory still falls\nthrough to whatever placement an ancestor (or the parent mount)\nprovides.\n\n## `.yuiignore` — exclude paths from being linked\n\nA `$DOTFILES/.yuiignore` file (gitignore syntax) keeps matched paths\nout of every yui flow — render skips them, list omits them, and apply\nwon't link them. Useful for editor lock-files, build artifacts, OS\njunk like `.DS_Store`, and anything else that lives next to your real\nconfig but shouldn't be propagated:\n\n```gitignore\n# $DOTFILES/.yuiignore\n**/.DS_Store\n**/lock.json\nhome/.config/nvim/lazy-lock.json     # exact path also works\n\n# Exclude all of build/ except the one file we DO want linked\nbuild/\n!build/result.toml\n```\n\nNested `.yuiignore` files inside subdirectories are honored too, with\nthe same rule-scoping semantics as `.gitignore`: deeper layers override\nshallower ones, `!negation` re-includes paths, and rules apply only to\nthe subtree below the file. Put repo-wide rules at `$DOTFILES/.yuiignore`\nand per-tree rules where they belong.\n\n## Secrets (`*.age` — opt-in)\n\nFiles ending in `.age` are encrypted with [age]; on every `apply` the\nciphertext is decrypted to a sibling file without the suffix, exactly\nlike `*.tera` rendering. The plaintext sibling is added to the managed\n`.gitignore` section so it never gets committed.\n\n```\nhome/.ssh/id_ed25519.age   →  home/.ssh/id_ed25519   →  ~/.ssh/id_ed25519\n       ↑ committed                  ↑ gitignored               ↑ linked\n```\n\n### Bootstrap\n\n```sh\nyui secret init        # generates ~/.config/yui/age.txt\n                       # appends the public key to $DOTFILES/config.local.toml\nyui secret encrypt home/.ssh/id_ed25519\n                       # produces home/.ssh/id_ed25519.age, ready to commit\n```\n\n`config.toml` after `init`:\n\n```toml\n[secrets]\nidentity = \"~/.config/yui/age.txt\"          # picked up automatically\nrecipients = [\n  \"age1abc...\",   # this machine's public key\n  # add more entries to grant other machines access\n]\n```\n\nThe feature is **off** until `recipients` has at least one entry — old\nrepos without `[secrets]` keep behaving exactly as before.\n\n### Multi-machine\n\nage supports multiple recipients per file. To grant a new machine\naccess:\n\n1. On the new machine: `yui secret init` → generates a per-machine key,\n   appends its public key to `config.local.toml` `[secrets].recipients`.\n   Move that public-key line to `config.toml` (the committed one) so\n   other machines see it too.\n2. On a machine that already has the secret: re-encrypt every `.age`\n   so its recipient list includes the new public key. (For now: run\n   `yui secret encrypt --force \u003cpath\u003e` per file. A `yui secret reencrypt`\n   helper is planned.)\n\n### Carrying the X25519 across machines (vault model)\n\n`apply` only ever uses the plain X25519 secret at\n`[secrets].identity` — no device prompts on the hot path. To\nferry that secret to a new machine, yui wraps a vault provider\nof your choice (**Bitwarden** or **1Password**) so the same\nauth you already use for that vault — master password, biometric,\npasskey unlock in the web vault, SSO — gates the unlock here.\n\n```toml\n[secrets]\nidentity   = \"~/.config/yui/age.txt\"   # X25519 plain, gitignored\nrecipients = [\"age1abc…\"]              # X25519 publics for *.age files\n\n[secrets.vault]\nprovider = \"bitwarden\"                 # or \"1password\"\n```\n\nThe vault item is stored under the fixed name\n`yui-x25519-identity` — yui doesn't expose a per-repo override\nyet (no one's hit the multi-yui-tree-on-one-vault collision in\npractice, so it's hardcoded).\n\n#### Setup (once on the first machine)\n\n```sh\nyui secret init                # generates ~/.config/yui/age.txt\nyui secret store               # pushes the file into the vault Secure Note\n```\n\n#### On each new machine\n\n```sh\ngit clone \u003cdotfiles\u003e\n# 1. Authenticate the vault CLI ONCE on this machine:\nbw login \u0026\u0026 bw unlock          # Bitwarden — or `op signin` for 1Password\n# 2. Pull the X25519 from the vault:\nyui secret unlock              # writes ~/.config/yui/age.txt\nyui apply                      # done\n```\n\nThe vault CLI itself is the auth boundary — yui shells out to\n`bw` / `op` and inherits whatever factor that CLI accepts.\nBitwarden's web vault supports passkey unlock; once you've used\nyour Pixel passkey to log into the BW web vault and the CLI\nsession is alive, `yui secret unlock` will quietly fetch the\nX25519 with no further prompts.\n\n#### Plugin recipients (advanced, unsupported)\n\n`[secrets].recipients` accepts plugin-flavoured public keys\n(`age1yubikey1…`, `age1fido2-hmac1…`, …) alongside the X25519\nones. yui doesn't ship first-class commands for plugin\nidentities — `apply` decrypts only with the X25519 in\n`[secrets].identity` — but encrypting `*.age` files to\nplugin-backed *recipients* works as long as the matching\n`age-plugin-*` binary is on `$PATH`, so a YubiKey holder can\ndecrypt the same file via `age` directly without yui in the\nloop. No support promises, but the path is open.\n\n[age]: https://age-encryption.org/\n\n## Hooks — run scripts around `apply`\n\nDrop a script under `$DOTFILES/.yui/bin/` and reference it with a\n`[[hook]]` entry. The script stays a normal executable (you can run\nit directly without yui); yui just decides *when* to invoke it.\n\n```toml\n[[hook]]\nname   = \"brew-bundle\"\nscript = \".yui/bin/brew-bundle.sh\"\n# Defaults: command=\"bash\", args=[\"{{ script_path }}\"], when_run=\"onchange\", phase=\"post\"\nwhen   = \"yui.os == 'macos'\"\n```\n\nSchema:\n\n| field | required? | default | meaning |\n|---|---|---|---|\n| `name` | ✓ | — | unique identifier (state-tracking key, `yui hooks run \u003cname\u003e`) |\n| `script` | ✓ | — | path to script, relative to `$DOTFILES` |\n| `command` | | `\"bash\"` | Tera-templated interpreter |\n| `args` | | `[\"{{ script_path }}\"]` | Tera-templated each |\n| `when_run` | | `\"onchange\"` | `once` \\| `onchange` \\| `every` |\n| `phase` | | `\"post\"` | `pre` \\| `post` (around `apply`) |\n| `when` | | _always_ | optional Tera bool predicate |\n\n`onchange` re-runs whenever the script's SHA-256 differs from the\nlast successful run. State is tracked in `$DOTFILES/.yui/state.json`\n(gitignored — it's per-machine).\n\n`command` and each `args` element are Tera-rendered with the standard\n`yui.*` / `vars.*` / `env(...)` plus these extras for the script:\n`script_path`, `script_dir`, `script_name`, `script_stem`,\n`script_ext`. Example with a Deno script:\n\n```toml\n[[hook]]\nname = \"denops-build\"\nscript = \".yui/bin/build.ts\"\ncommand = \"deno\"\nargs = [\"run\", \"-A\", \"{{ script_path }}\"]\nwhen_run = \"onchange\"\nphase = \"post\"\nwhen = \"yui.os != 'windows'\"\n```\n\nManual control:\n\n```sh\nyui hooks list                    # what's configured + last-run state\nyui hooks run                     # run all hooks per their rules\nyui hooks run brew-bundle         # run just this one (still honors `when`)\nyui hooks run brew-bundle --force # bypass when_run state check\n```\n\n`apply` runs `pre` hooks before render/link and `post` hooks after\nall linking. A failing hook stops `apply` immediately — fix the\nscript, then re-run.\n\n## Anomalies and the `[absorb]` policy\n\nWhen source AND target both diverge from each other, `yui` can't\nauto-merge. It defers to your `[absorb] on_anomaly` setting:\n\n```toml\n[absorb]\nauto              = true     # auto-absorb on any AutoAbsorb classification\nrequire_clean_git = true     # treat dirty source as anomaly\non_anomaly        = \"ask\"    # \"ask\" | \"skip\" | \"force\"\n```\n\n- `ask` — on a TTY, render the diff and prompt y/N; off-TTY, skip\n- `skip` — log a warning and leave both sides untouched\n- `force` — treat the anomaly as auto-absorb anyway (target wins)\n\nNeed to absorb a single file regardless of policy? `yui absorb\n\u003ctarget-path\u003e` does that — bypasses `auto`, `require_clean_git`, and\n`on_anomaly` for an explicit user-initiated pull.\n\n## Commands\n\n| | |\n|---|---|\n| `yui init [--git-hooks]` | scaffold `config.toml` + `.gitignore` in cwd |\n| `yui apply [--dry-run]` | render → link → auto-absorb |\n| `yui render [--check] [--dry-run]` | template-only pass; `--check` fails on drift |\n| `yui link [--dry-run]` | alias for apply (kept for muscle memory) |\n| `yui list [--all] [--icons MODE] [--no-color]` | every src→dst mapping |\n| `yui status [--icons MODE] [--no-color]` | drift overview, exits non-zero on any divergence |\n| `yui diff [--icons MODE] [--no-color]` | unified diff of every drifted entry (link or render) |\n| `yui absorb \u003ctarget\u003e [--dry-run] [--yes]` | pull one target into source — prints diff, confirms (`--yes` to skip) |\n| `yui unlink \u003cpath\u003e...` | tear down a specific link |\n| `yui update [--dry-run]` | `git pull --ff-only` source repo, then re-apply |\n| `yui unmanaged [--icons MODE] [--no-color]` | list source files no `[[mount.entry]]` claims |\n| `yui doctor` | environment sanity check |\n| `yui gc-backup [--older-than DUR] [--dry-run]` | survey or prune `.yui/backup/` snapshots by suffix age |\n| `yui hooks list` | show configured `[[hook]]` entries + last-run state |\n| `yui hooks run [\u003cname\u003e] [--force]` | run hooks on demand (bypassing `when_run` with `--force`) |\n| `yui completion \u003cshell\u003e` | print shell completion (bash / zsh / fish / powershell / elvish) |\n\n`--icons` accepts `unicode` (default), `nerd` (Nerd-Font glyphs),\n`ascii` (CI-log-safe). The `[ui] icons = \"...\"` config key sets it\nglobally.\n\n## Status\n\nUsed in production for the author's own ~/dotfiles. Known gaps:\n\n- no built-in encryption (use `pass` / `1password-cli` from a Tera\n  template instead)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyukimemi%2Fyui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyukimemi%2Fyui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyukimemi%2Fyui/lists"}