diff --git a/README.md b/README.md index 962cad07..62c6279e 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,20 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -macOS 環境を Nix flake で declarative に管理(nix-darwin + home-manager + sops-nix)。 +Declarative macOS configuration built on a Nix flake: nix-darwin, home-manager, sops-nix. -📖 **日常コマンドは [docs/CHEATSHEET.md](docs/CHEATSHEET.md) を参照** +Day-to-day commands live in [docs/CHEATSHEET.md](docs/CHEATSHEET.md). --- -## Fork して使う場合 +## Forking this ```bash -# 1. Fork ボタン → 自分の repo 名で clone +# 1. Fork, then clone under your own name git clone git@github.com:/dotfiles.git ~/.dotfiles cd ~/.dotfiles -# 2. nix/user.nix を編集(これだけで全 nix モジュールに反映) +# 2. Edit nix/user.nix. Every nix module reads from here. $EDITOR nix/user.nix # { # username = ""; @@ -24,65 +24,64 @@ $EDITOR nix/user.nix # dotfilesRepo = "https://github.com//dotfiles.git"; # } -# 3. age 鍵を生成して .sops.yaml の public key を差し替え +# 3. Generate an age key and replace the public key in .sops.yaml mkdir -p ~/.config/sops/age age-keygen -o ~/.config/sops/age/keys.txt -# 出力された "# public key: age1..." の行を .sops.yaml の age1... に貼り換え +# Copy the "# public key: age1..." line into .sops.yaml $EDITOR .sops.yaml -# 4. 元の所有者の secrets は復号できないので削除して空から始める +# 4. The previous owner's secrets cannot be decrypted, so start empty rm secrets/secrets.yaml -# 必要な secret を追加していく(例) -sops secrets/secrets.yaml -# (sops が新しいエディタ画面を開く → 自分の secret を YAML で記述 → 保存) +sops secrets/secrets.yaml # opens an editor; write your own secrets as YAML -# 5. 個人 brew tap を整理(任意) +# 5. Optional: drop the personal brew taps $EDITOR nix/hosts/darwin.nix -# - "gapul/openutau", "gapul/zrythm" は 作者個人の fork → 削除可 -# - 不要な GUI cask も削っていい(gimp, blender 等) +# "gapul/openutau" and "gapul/zrythm" are personal forks and can go. +# GUI casks you do not want (gimp, blender, ...) can go too. -# 6. bootstrap 実行 +# 6. Bootstrap bash scripts/bootstrap.sh ``` -クローン後にエディタで `nix/user.nix`, `.sops.yaml`, `nix/hosts/darwin.nix` を編集すれば、他は触らずに動く設計。 +After cloning, `nix/user.nix`, `.sops.yaml` and `nix/hosts/darwin.nix` are the only files +you need to touch. Everything else works unchanged. - -## 構成 +## Layout ``` nix/ -├── flake.nix # entry point (darwin/nixos/home-manager 各構成 + devShell) -├── user.nix # ユーザー名・メール等 (最初に書き換える) -├── hosts/ # マシン別: darwin.nix (メイン Mac) / macmini.nix / nixos-laptop.nix / wsl.nix -├── home/ # home-manager: common.nix + OS 別 (darwin/linux/wsl/hyprland) + backup 系 -├── lib/ # テーマ (palettes.json を SSO とする rose-pine dark/light) -└── pkgs/ # 自前パッケージ -configs/ # 各アプリの実 config (ghostty/tmux/sketchybar/nvim/karabiner/yazi/...) -secrets/secrets.yaml # SOPS で age 暗号化 -.sops.yaml # 受信者 (age pubkey) -scripts/bootstrap.sh # 新 Mac 用 0 → 1 セットアップ (Linux/WSL 版もあり) -windows/ # Windows 側セットアップ (winget/scoop/AutoHotkey/...) -mobile/ # iOS / Android (アプリ宣言・adb 設定・構成プロファイル) -esphome/ # ESP チップに載るデバイス設定 (水やり機) -tailscale/ # tailnet のポリシー (ACL / split DNS) ※fetch して作る -nextdns/ # NextDNS のプロファイル設定 ※fetch して作る -templates/ # direnv 用 dev shell テンプレ (node/python/rust) -Justfile # 普段使うコマンド集 +|-- flake.nix entry point: darwin / nixos / home-manager configurations and devShells +|-- user.nix username, email and so on. Edit this first. +|-- hosts/ per machine: darwin.nix (main Mac), macmini.nix, nixos-laptop.nix, wsl.nix +|-- home/ home-manager: common.nix plus per-OS (darwin/linux/wsl/hyprland) and backup +|-- lib/ theming. palettes.json is the single source for rose-pine dark and light. +`-- pkgs/ packages built here rather than taken from nixpkgs +configs/ real application configs (ghostty, tmux, sketchybar, nvim, karabiner, yazi, ...) +secrets/secrets.yaml age-encrypted through SOPS +.sops.yaml recipients (age public keys) +scripts/bootstrap.sh zero-to-one setup for a new Mac. Linux and WSL variants live alongside it. +windows/ Windows-side setup (winget, scoop, AutoHotkey, ...) +mobile/ iOS and Android: app declarations, adb settings, configuration profiles +esphome/ device configs that run on ESP chips (the plant waterer) +tailscale/ tailnet policy: ACLs and split DNS. Fetched, not committed by hand. +nextdns/ NextDNS profile settings. Fetched, not committed by hand. +templates/ direnv dev-shell templates (node, python, rust) +Justfile the commands used day to day ``` -## 起動 +## Getting started + +On a new Mac: -新しい Mac: ```bash curl -fsSL https://raw.githubusercontent.com/gapul/dotfiles/main/scripts/bootstrap.sh | bash ``` -## コマンド一覧 +## Commands -### 🟢 Justfile レシピ +### Justfile recipes -以下は `just --list` から自動生成(レシピを変えたら `just docs` で再生成)。 +Generated from `just --list`. Run `just docs` after changing a recipe. ```text @@ -171,106 +170,123 @@ curl -fsSL https://raw.githubusercontent.com/gapul/dotfiles/main/scripts/bootstr ``` -### 🟪 検索 +### Search -| コマンド | 説明 | +| Command | What it does | |---|---| -| `nh search ` | nixpkgs から package 検索(例: `nh search firefox`) | +| `nh search ` | Search nixpkgs, e.g. `nh search firefox` | -### 🟫 リモート +### Remote -| コマンド | 説明 | +| Command | What it does | |---|---| -| `nssh user@host` | rootless Nix(`nix-portable`)で nvim/yazi/tmux(自分の設定)を使う | -| `just ssh ` | `nssh` のショート | -| `herdr --remote user@host` | 同じ下準備を済ませてから herdr で繋ぐ(zsh 関数が `configs/bin/remote-bootstrap` を先に流す) | +| `nssh user@host` | nvim, yazi and tmux with your own config, through rootless Nix (`nix-portable`) | +| `just ssh ` | Shorthand for `nssh` | +| `herdr --remote user@host` | Same preparation, then connect with herdr. A zsh function runs `configs/bin/remote-bootstrap` first. | -### 🟥 コード品質 (git-hooks.nix + treefmt) +### Code quality -pre-commit フック・フォーマッタは `nix/flake.nix` で **宣言的に管理**(git-hooks.nix / treefmt-nix)。 +Hooks and formatters are declared in `nix/flake.nix` through git-hooks.nix and treefmt-nix. -| コマンド | 説明 | +| Command | What it does | |---|---| -| `nix fmt` | nix(nixfmt) + shell(shfmt) を整形。`nix/` 配下で実行 | -| `nix develop ./nix` | devShell 入室。`.pre-commit-config.yaml` を生成し `.git/hooks` に導入 | -| `nix flake check ./nix` | `checks.pre-commit` で nix/ を検査(CI と同じ) | -| `nix develop ./nix -c pre-commit run --all-files` | リポ全体にフックを実行 | -| `nix develop ./nix -c shellcheck scripts/*.sh` | シェルの手動チェック(任意) | -| `nix run nixpkgs#statix -- check nix` | nix lint(手動。enforced 外) | - -**commit 時に自動実行されるフック**(`.git/hooks/pre-commit`): +| `nix fmt` | Format nix (nixfmt) and shell (shfmt). Run it inside `nix/`. | +| `nix develop ./nix` | Enter the devShell. Generates `.pre-commit-config.yaml` and installs `.git/hooks`. | +| `nix flake check ./nix` | Run `checks.pre-commit` over `nix/`, the same check CI runs | +| `nix develop ./nix -c pre-commit run --all-files` | Run the hooks across the whole repo | +| `nix develop ./nix -c shellcheck scripts/*.sh` | Check shell by hand | +| `nix run nixpkgs#statix -- check nix` | Lint nix by hand. Not enforced. | -以下は `nix/flake.nix` の `preCommit.hooks` から自動生成(フックを変えたら `just docs` で再生成)。 +Hooks that run on commit, from `.git/hooks/pre-commit`. Generated from `preCommit.hooks` +in `nix/flake.nix`; run `just docs` after changing one. -| フック | 対象 | 除外 | 内容 | +| Hook | Target | Excluded | What it does | |---|---|---|---| -| `deadnix` | `*.nix` | — | 未使用コード検出 (モジュール引数 `{ lib, ... }` は許容) | -| `gitleaks` | 全 staged | — | 機密 leak 検出 | -| `nixfmt` | `*.nix` | — | 整形チェック (未整形なら fail) | -| `shellcheck` | 全 staged | `configs/wm/sketchybar/.*`、`\.envrc$`、`\.zsh$`、`configs/macmini/bin/.*`、`configs/macmini/client/.*`、`configs/macmini/setup-scripts/.*` | shell lint (.shellcheckrc に従う) | +| `deadnix` | `*.nix` | — | Finds unused code. Module arguments like `{ lib, ... }` are allowed. | +| `gitleaks` | all staged | — | Secret detection | +| `nixfmt` | `*.nix` | — | Formatting check. Fails on anything unformatted. | +| `shellcheck` | all staged | `configs/wm/sketchybar/.*`, `\.envrc$`, `\.zsh$`, `configs/macmini/bin/.*`, `configs/macmini/client/.*`, `configs/macmini/setup-scripts/.*` | Shell lint, following .shellcheckrc | -メモ: -- フックを編集するには `nix/flake.nix` の `preCommit.hooks` を変更 → `nix develop` で再生成 -- `.pre-commit-config.yaml` は **生成物**(store パス依存)。`.gitignore` 済・非追跡。fork 後は `nix develop ./nix` で生成 -- **flake は `nix/` サブディレクトリ**にあるため、`treefmt` フックは git ルートから root 検出に失敗する。整形フックは per-file の `nixfmt-rfc-style` を使い、`treefmt` は `nix fmt` 専用 -- `shellcheck` は enforced 済。除外中の sketchybar 設定群は `nix develop ./nix -c shellcheck configs/wm/sketchybar/...` で手動チェック可 -- CI は `om ci`(omnix)で回す。`checks.pre-commit` 出力(git-hooks.nix)をビルドすることでリポ全体に同じフックが走る。設定は `om.yaml` + flake 出力が単一の真実で、`.github/workflows/ci.yml` は system→runner を割り当てて `om ci run` を呼ぶだけの薄いアダプタ +Notes: -### 🟨 復旧 / メンテ (生コマンド) +- To change a hook, edit `preCommit.hooks` in `nix/flake.nix`, then re-enter `nix develop` to regenerate. +- `.pre-commit-config.yaml` is generated and depends on store paths. It is gitignored and untracked; + run `nix develop ./nix` after forking to produce it. +- The flake lives in `nix/`, not at the repo root, so the `treefmt` hook cannot find the root from + there. Formatting uses the per-file `nixfmt-rfc-style` hook instead, and `treefmt` is reserved + for `nix fmt`. +- `shellcheck` is enforced. The excluded sketchybar configs can still be checked by hand with + `nix develop ./nix -c shellcheck configs/wm/sketchybar/...`. +- CI runs through `om ci` (omnix). Building the `checks.pre-commit` output from git-hooks.nix + applies the same hooks to the whole repo. `om.yaml` and the flake outputs are the single source + of truth; `.github/workflows/ci.yml` is a thin adapter that maps system to runner and calls + `om ci run`. -| コマンド | 説明 | +### Recovery and maintenance + +| Command | What it does | |---|---| -| `just doctor` | 環境ヘルスチェック(/nix マウント・Login Items・fstab 状態) | -| `sudo /usr/local/bin/determinate-nixd init` | macOS update 後 `/nix` が見えない時 | -| `sudo /usr/local/bin/determinate-nixd upgrade` | Determinate Nix runtime 本体を更新(数ヶ月に1回) | -| `nh darwin switch` | システムだけ rebuild(`just rebuild` の半分) | -| `nh home switch` | ユーザーだけ rebuild | -| `nh clean all` | 古い世代を一括削除 | +| `just doctor` | Health check: `/nix` mount, Login Items, fstab state | +| `sudo /usr/local/bin/determinate-nixd init` | When `/nix` is missing after a macOS update | +| `sudo /usr/local/bin/determinate-nixd upgrade` | Update the Determinate Nix runtime. Every few months. | +| `nh darwin switch` | Rebuild the system only, half of `just rebuild` | +| `nh home switch` | Rebuild the user configuration only | +| `nh clean all` | Delete old generations | + +## Things worth knowing + +- The Nix runtime is Determinate Nix. nix-darwin coexists with it through `nix.enable = false`. +- nix-darwin and home-manager are kept separate to avoid the USER check bug, + [nix-darwin#1462](https://github.com/nix-darwin/nix-darwin/issues/1462). +- After editing anything under `configs/`, remember to `git add` it. A Nix flake only sees + git-tracked files. +- Configs that applications write back to, such as nvim and karabiner, use `mkOutOfStoreSymlink` + so GUI and CLI edits land in the repo. +- Secrets are decrypted by sops-nix and need `~/.config/sops/age/keys.txt`. Keep a copy in + Bitwarden. +- For per-language dev shells, copy `templates//` into a project and run `direnv allow`. -## ポイント +## Why `/nix` is decrypted and `noauto` is removed from fstab -- **Nix runtime**: Determinate Nix(`nix.enable = false` で共存) -- **nix-darwin と home-manager は分離**: [#1462](https://github.com/nix-darwin/nix-darwin/issues/1462) (USER check bug)回避のため -- **configs 編集 → `git add` 忘れずに**: Nix flake は git-tracked しか見ない -- **動的設定 (nvim/karabiner) は `mkOutOfStoreSymlink`**: GUI/CLI の書き戻しが dotfiles に反映 -- **secrets は SOPS-nix で復号**: `~/.config/sops/age/keys.txt` が必要。Bitwarden に backup 推奨 -- **direnv**: `templates//` をプロジェクトにコピーして `direnv allow` で言語別 dev shell +Determinate Nix ships this arrangement by default: -## 設計判断: `/nix` を復号化 + `fstab` から `noauto` 削除 +1. the `/nix` volume is encrypted with FileVault, +2. `/etc/fstab` carries `noauto`, and +3. the launchd daemon `org.nixos.darwin-store` mounts it lazily. -Determinate Nix のデフォルト構成は: -1. `/nix` ボリュームを FileVault 暗号化 -2. `/etc/fstab` に `noauto` を付与 -3. launchd デーモン `org.nixos.darwin-store` で遅延マウント +That assumes everything starts from a launchd daemon, which nix-darwin's PR #1052 wraps in +`wait4path`. Login Items, GUI session restore and restored Terminal windows are outside that +wrapper. With Ghostty, AeroSpace and sketchybar running as Login Items, `/nix` is not mounted +yet when they start, and they fail to read their configs. -この設計は **launchd daemon ベースの起動**(`nix-darwin` の PR #1052 で `wait4path` 自動付与)を前提にしているが、**Login Items / GUI 自動復元 / restoring Terminal は wait4path 範囲外**。Ghostty・AeroSpace・sketchybar を Login Items で常駐させてる構成では、boot 直後に /nix がまだマウントされず config 読み込み失敗する。 +This is a long-standing community problem rather than a local mistake; see +[LnL7/nix-darwin#774](https://github.com/LnL7/nix-darwin/issues/774), where one user wraps the +login shell in a C program and another gave up on installing yabai through Nix. -[deep-research](https://github.com/LnL7/nix-darwin/issues/774) によると、これはコミュニティで何年も解決してない有名な問題。 `lilyball` は login shell を C wrapper で包んでる、`astratagem` は「Nix で yabai 入れるのやめた」と発言してる。 +What is done here instead: -**対処** (現状の構成): -- `/nix` ボリュームを `diskutil apfs decryptVolume "Nix Store"` で復号化 -- `/etc/fstab` から `noauto` を削除 → macOS の `automountd` が起動序盤にマウント -- Login Items が起動するときには /nix は既にマウント済 → config 読める +- decrypt the volume with `diskutil apfs decryptVolume "Nix Store"`, +- remove `noauto` from `/etc/fstab` so macOS `automountd` mounts it early in boot, +- and by the time Login Items start, `/nix` is already there. -**トレードオフ**: -- ✅ Login Items 問題が完全解決 -- ✅ `wait4path` 経路で漏れる GUI / 復元 Terminal も解決 -- ⚠️ Determinate 公式サポート外設定(upgrade で fstab 書き戻される可能性 → bootstrap.sh で自動修正) -- ⚠️ ボリューム暗号化が外れる - - 実害は無い(/nix の中身は nixpkgs 公開バイナリ、Mac 本体は FileVault でカバー済) +The trade-off is that this is outside Determinate's supported configuration, so an upgrade may +write `noauto` back — `bootstrap.sh` corrects that — and the volume is no longer encrypted. +Losing that encryption costs nothing here: `/nix` holds public nixpkgs binaries, and the machine +itself is covered by FileVault. -**自動修復**: `bootstrap.sh` が新規 install 時に自動で復号化 + `noauto` 削除する。`just doctor` で日常チェック可能。Determinate upgrade 後はとくに `just doctor` 推奨。 +`bootstrap.sh` performs the decryption and the fstab edit on a fresh install. `just doctor` +checks both, and is worth running after a Determinate upgrade in particular. -## トラブルシューティング +## Troubleshooting -| 症状 | 対処 | +| Symptom | What to do | |---|---| -| 再起動後に sketchybar / Ghostty / launcher が config 読まない | `just doctor` で /nix マウント状態確認、`fstab` に `noauto` 戻ってたら `sudo sed -i '' 's/,noauto//' /etc/fstab` | -| `/nix` が見えない / shell でエラー | `sudo determinate-nixd init` または `sudo diskutil mount "Nix Store"` | -| `nh: more values required` | 新ターミナル開き直す(`__HM_SESS_VARS_SOURCED` の継承で env が古い) | -| `git push` できない | dotfiles の remote が SSH 化済 → `~/.ssh/config` 確認 | -| pre-commit hook で commit blocked | leak は redact 表示、嘘陽性なら `.gitleaks.toml` の allowlist 追記 | -| `darwin-rebuild switch` で USER エラー | nix-darwin#1462 の bug。`just rebuild`(nh)で回避 | -| Ghostty config の一部設定が無視される(`quick-terminal-position` 等が default のまま) | Ghostty 1.3.1 は invalid な行(例: `quick-terminal-screen = mouse`、`global:f18=...`)で**parse 中断**。+show-config で適用状態確認、config の上から 1 行ずつ消して原因特定 | +| sketchybar, Ghostty or the launcher ignore their config after a reboot | Check the `/nix` mount with `just doctor`. If `noauto` came back, `sudo sed -i '' 's/,noauto//' /etc/fstab` | +| `/nix` is missing, or the shell errors out | `sudo determinate-nixd init`, or `sudo diskutil mount "Nix Store"` | +| `nh: more values required` | Open a new terminal. The old environment is inherited through `__HM_SESS_VARS_SOURCED`. | +| `git push` fails | The dotfiles remote uses SSH. Check `~/.ssh/config`. | +| A pre-commit hook blocks the commit | Leaks are shown redacted. For a false positive, add an allowlist entry to `.gitleaks.toml`. | +| `darwin-rebuild switch` fails with a USER error | nix-darwin#1462. Use `just rebuild`, which goes through nh. | +| Some Ghostty settings are ignored and stay at their defaults, such as `quick-terminal-position` | Ghostty 1.3.1 stops parsing at the first invalid line, for example `quick-terminal-screen = mouse` or `global:f18=...`. Check what actually applied with `+show-config`, then delete config lines from the top until the culprit shows up. | diff --git a/configs/apps/README.md b/configs/apps/README.md index 34fe045b..594653a3 100644 --- a/configs/apps/README.md +++ b/configs/apps/README.md @@ -1,33 +1,36 @@ -# GUI アプリ plist 管理 +# GUI application plists -menubar / 入力系ユーティリティの設定を home-manager activation で復元する。 -Puddle は websites(壁紙)と security-scoped bookmark を壊さないよう、 -enforce したいキーのみ `defaults write`(surgical)。 +Restoring the settings of menubar and input utilities through home-manager activation. -## 管理対象 +Puddle is handled surgically — only the keys worth enforcing are written with +`defaults write` — so that its websites, meaning the wallpapers, and its security-scoped +bookmarks are not destroyed. -| App | 方式 | 説明 | +## What is managed + +| App | Method | Notes | |---|---|---| -| **Puddle** | surgical write (`nix/home/darwin.nix`) | 動的壁紙。websites/bookmark はライブ保持し全置換しない。behavior 3 キー(deactivateOnBattery / extendPuddleBelowMenuBar / showOnAllSpaces)のみ enforce | +| Puddle | Surgical writes, in `nix/home/darwin.nix` | Dynamic wallpaper. websites and bookmarks stay live and are never wholesale replaced. Only three behaviour keys are enforced: deactivateOnBattery, extendPuddleBelowMenuBar and showOnAllSpaces | -## menubar アイコンの表示/非表示 +## Hiding and showing menubar icons -`NSStatusItem VisibleCC Item-*` = false で「アイコンを隠す」状態を管理対象に含める -(Maccy)。rebuild の import で隠し状態が表示に戻らないよう明示保持する。 -位置キー `NSStatusItem Preferred Position*` は端末固有なので除外する。 +`NSStatusItem VisibleCC Item-*` set to false is what "hide this icon" means, and it is managed +here for Maccy. Keeping it explicit stops a rebuild's import from making hidden icons visible +again. The position keys, `NSStatusItem Preferred Position*`, are machine-specific and are +excluded. -## 新 Mac での復元 +## Restoring on a new Mac -`just rebuild`(home-manager activation)で Puddle の behavior 3 キーが -`defaults write` される。以下は GUI で手動: +`just rebuild`, through home-manager activation, writes Puddle's three behaviour keys. The rest +is done by hand in the GUI. -### Puddle -- 壁紙は `puddle apply` で入る(`~/.config/puddle/install.toml` の `wallpapers`)。 - シェーダ本体は dotfiles ではなく gapul/puddle-shaders にあり、 - `~/Library/Application Support/Puddle/Wallpapers/` へ展開される。 - ここに残るのはワークスペース連動の inputs(実行時状態)だけ。 +For Puddle, the wallpapers come from `puddle apply`, driven by `wallpapers` in +`~/.config/puddle/install.toml`. The shaders themselves are not in dotfiles; they are in +gapul/puddle-shaders and are unpacked into +`~/Library/Application Support/Puddle/Wallpapers/`. What stays here is only the +workspace-linked inputs, which are runtime state. -## 設定変更後の capture +## Capturing a change -Puddle は plist を持たない(surgical write)。behavior を変えたら -`nix/modules/home/darwin-apps.nix` の puddlePrefs 内 `defaults write` を直接編集する。 +Puddle has no plist here, since it is written surgically. After changing a behaviour, edit the +`defaults write` calls inside `puddlePrefs` in `nix/modules/home/darwin-apps.nix` directly. diff --git a/configs/apps/codexbar/README.md b/configs/apps/codexbar/README.md index e1eeb607..18d7cdc8 100644 --- a/configs/apps/codexbar/README.md +++ b/configs/apps/codexbar/README.md @@ -1,8 +1,10 @@ -# CodexBar 設定 +# CodexBar configuration -`~/.config/codexbar/config.json` の実体です。out-of-store symlink で繋いであるので、 -CodexBar の UI 操作がそのままこのファイルに書き込まれる。[omniwm](../../wm/omniwm/README.md) と同じ扱い。 +This is the real `~/.config/codexbar/config.json`, linked in as an out-of-store symlink, so +anything changed through CodexBar's UI is written straight back here. Handled the same way as +[omniwm](../../wm/omniwm/README.md). -- 配線は `nix/home/darwin.nix`。新しい mac では rebuild すればリンクが張られる。 -- `providers[].codexActiveSource` に Codex アカウントの UUID が入る。このリポジトリは public だが、 - 識別子だけで資格情報ではないのでそのまま追跡している。 +The wiring is in `nix/home/darwin.nix`, so a rebuild on a new Mac creates the link. + +`providers[].codexActiveSource` holds the UUID of the Codex account. This repository is public, +but that is an identifier rather than a credential, so it stays tracked. diff --git a/configs/apps/obsidian/README.md b/configs/apps/obsidian/README.md index 146af71f..31c97515 100644 --- a/configs/apps/obsidian/README.md +++ b/configs/apps/obsidian/README.md @@ -1,19 +1,26 @@ -# Obsidian 設定スナップショット (追跡専用ミラー) +# Obsidian configuration snapshot, a tracking mirror only -このディレクトリは Obsidian の `.obsidian` 設定の **片方向スナップショット** です。 +This directory is a one-way snapshot of Obsidian's `.obsidian` configuration. -- **本体は vault 側** (`~/Documents/notes/.obsidian`)。ここは履歴・差分閲覧用の**読み取りミラー**。 -- 更新は `just obsidian-snapshot` で **vault → dotfiles の一方通行**。 - 逆向き (dotfiles → vault) には**絶対に**戻さない(戻すと二重オーナーになり同期が壊れる)。 -- 日々の同期は Obsidian Git / Self-hosted LiveSync が担当。dotfiles は中身を所有しない。 +The real thing lives in the vault, at `~/Documents/notes/.obsidian`. What is here is a +read-only mirror, for history and diffs. -## 公開リポジトリ前提の安全設計 +It is updated with `just obsidian-snapshot`, which goes from the vault into dotfiles and never +the other way. Copying back would create two owners and break the sync. -このリポジトリは public。よって: +Day-to-day syncing is Obsidian Git and Self-hosted LiveSync's job. dotfiles does not own the +contents. -- **ホワイトリスト方式**: 安全と確認した json のみ収録 - (`app` `appearance` `hotkeys` `community-plugins` `core-plugins` `graph` `daily-notes` `types` `canvas`)。 -- **絶対に入れない**: `plugins/*/data.json`(LiveSync の CouchDB 認証・各種 API キーが入りうる)、 - `workspace*.json`(端末状態)、`copilot-index-*` / `.smart-env`(キャッシュ)、プラグイン本体。 -- `just obsidian-snapshot` は非空の秘密値を検出すると**中止**する。さらに commit 前に `gitleaks` を通す。 -- 秘密ごと版管理したい設定は **sops 暗号化** (`just secrets` / `.sops.yaml`) してから置く。 +## Designed for a public repository + +This repository is public, so: + +- Only files confirmed to be safe are included, by whitelist: `app`, `appearance`, `hotkeys`, + `community-plugins`, `core-plugins`, `graph`, `daily-notes`, `types` and `canvas`. +- Some things are never included: `plugins/*/data.json`, which can hold LiveSync's CouchDB + credentials and various API keys; `workspace*.json`, which is per-device state; + `copilot-index-*` and `.smart-env`, which are caches; and the plugins themselves. +- `just obsidian-snapshot` aborts if it finds a non-empty secret value, and `gitleaks` runs + before the commit as well. +- Configuration that has to be versioned along with its secrets is encrypted with sops first, + through `just secrets` and `.sops.yaml`. diff --git a/configs/cli/claude/README.md b/configs/cli/claude/README.md index 2366a1d8..a454da6f 100644 --- a/configs/cli/claude/README.md +++ b/configs/cli/claude/README.md @@ -1,108 +1,115 @@ # Claude Code -母艦とリモート (nssh 先) で持ち方が違う。 +Held differently on the Mac and on remote machines reached through nssh. -## 母艦 — まるごと symlink +## On the Mac, symlinked wholesale -`nix/home/workstation.nix` が out-of-store symlink を張っていて、ここにあるファイルが実体。 +`nix/home/workstation.nix` creates out-of-store symlinks, so the files here are the real thing. -| 実体 | リンク先 | +| The real file | Linked to | | --- | --- | | `settings.json` | `$CLAUDE_CONFIG_DIR/settings.json` | | `CLAUDE.md` | `$CLAUDE_CONFIG_DIR/CLAUDE.md` | -| `hooks/` `output-styles/` `bin/` | 同名のディレクトリ | -| `skills/{english-vocab,gapul-writing-voice,step-by-step-tutor}` | `skills/` の下 | +| `hooks/`, `output-styles/`, `bin/` | The directories of the same name | +| `skills/{english-vocab,gapul-writing-voice,step-by-step-tutor}` | Under `skills/` | -`settings.json` をまるごと持てるのは、母艦が `defaultMode: bypassPermissions` で -`permissions.allow` が育たないから。TUI からの書き戻しはそのままこのファイルに落ちるので、 -`git diff` に出てきたらコミットすればいい。 +Keeping the whole `settings.json` works because the Mac runs with +`defaultMode: bypassPermissions`, so `permissions.allow` never grows. Anything the TUI writes +back lands in this file, and when it shows up in `git diff` it can simply be committed. -セッション履歴・`.claude.json` (資格情報)・`settings.local.json` は state なので持たない。 -vendored な skill (cloudflare/* など) も上流から取り直せるので管理しない。 +Session history, `.claude.json`, which holds credentials, and `settings.local.json` are state +and are not kept. Vendored skills, such as cloudflare's, can be fetched again from upstream and +are not managed either. -## リモート (nssh 先) — 管理キーだけ merge +## On remote machines, merging only the managed keys -`settings.remote.json` の**管理キーだけ**を、ホストの既存 JSON へ -`scripts/merge-claude-settings.py` が上書き merge する。`~/.bashrc` に `bashrc.remote` を読む -行だけ足すのと同じ考え方で、管理外のキーはホスト側にそのまま残す。 +`scripts/merge-claude-settings.py` merges only the managed keys from `settings.remote.json` +over whatever JSON the host already has. It is the same idea as adding a single line to +`~/.bashrc` that sources `bashrc.remote`: anything unmanaged stays as the host had it. -| | キー | +| | Keys | | --- | --- | -| 管理対象 | `permissions.defaultMode` / `theme` / `effortLevel` / `editorMode` / `verbose` / `preferredNotifChannel` / `skipDangerousModePermissionPrompt` / `skipWorkflowUsageWarning` / `environmentVariables` | -| 管理外 (ホスト所有) | `permissions.allow` / `permissions.additionalDirectories` / `enabledPlugins` / `hooks` | - -管理対象は**母艦の `settings.json` が実際に持っているキー**に限る。母艦が書いていない -キーを配ると、母艦は既定値・リモートだけ明示値という食い違いが生まれ、下の「母艦が正」 -が成り立たなくなる。`tui` / `inputNeededNotifEnabled` / `agentPushNotifEnabled` は -リモート側の値から起こしてしまったもので、母艦に無いので外した (2026-08-13)。 -既に配ってしまったホストの値は残るが、以後はホスト所有として扱う。 - -### なぜ母艦のように丸ごと symlink にしないのか - -`defaultMode: bypassPermissions` を配った時点で `permissions.allow` は育たなくなるので、 -「symlink にすると承認済みの許可が毎回消える」という元の理由は消えた (2026-08-15)。 -それでも merge のままなのは、母艦の `settings.json` に**リモートへ持って行くと壊れる/ -意味が無いキー**が混ざっているため。 - -- `hooks` — `/Users/gapul/.config/claude/hooks/*.sh` という絶対パス。Linux では存在しない - ので毎回失敗する。リモートは自前のフック (herdr 連携) を持っている -- `enabledPlugins` — 母艦は clangd / swift LSP。この箱に要るのは rust-analyzer で、 - 配ると入れ替わってしまう。ツールチェーンはホストごとに違うので触らない -- `extraKnownMarketplaces` — 上の plugin とセットでしか意味が無いので外す -- `disableDeepLinkRegistration` — macOS の `~/Applications` URL ハンドラの話で Linux には無い -- `defaultModel` — 母艦は `claude-sonnet-4-20250514` を固定している。これを配ると - リモートの既定モデルまで巻き戻る。**意図的に外している**ので、揃えたくなったら - ここに書き足すのではなく先に母艦側の固定を見直すこと - -### 入れ子の扱い - -`permissions` は `defaultMode` だけを管理する。merge は両側が object のキーだけ再帰する -ので、ホストが育てた `allow` / `additionalDirectories` は残る。配列は再帰しない -(`allow` を要素ごとに混ぜたいわけではない)。`--check` と `--adopt` も同じ構造をなぞる。 - -### bypassPermissions を配ることの意味 - -リモートでも Claude Code が権限確認なしで動く。共有機に入れる場合は、その箱で -Claude に許されることが自分のアカウントでできること全部になる、という前提で使う。 - -`CLAUDE.md` と自作 skill は書き換わらないので、リモートでも普通に symlink する。 -`hooks/` `output-styles/` `bin/` は母艦のデスクトップ前提 (osascript 通知 / herdr / -Notion MCP) なので持ち込まない。 - -このスクリプトを母艦の `settings.json` へ向けてはいけない。書き込みが tmp+rename なので、 -nix が張った symlink を実ファイルで置き換えて追跡を切る。母艦では `--adopt` (読むだけ) の -向きでのみ使う。 - -## どちらが勝つか — クライアント端末が正 - -管理キーの値は接続元の母艦を正とする。リモートで設定をいじっても、それはそのホスト限りの -一時的なものとして次の merge で上書きされる。値の更新は母艦から吸い上げる向きで行う。 +| Managed | `permissions.defaultMode`, `theme`, `effortLevel`, `editorMode`, `verbose`, `preferredNotifChannel`, `skipDangerousModePermissionPrompt`, `skipWorkflowUsageWarning`, `environmentVariables` | +| Unmanaged, owned by the host | `permissions.allow`, `permissions.additionalDirectories`, `enabledPlugins`, `hooks` | + +The managed set is limited to keys the Mac's own `settings.json` actually contains. +Distributing a key the Mac does not set would leave the Mac on the default while the remote had +an explicit value, which breaks the rule below that the Mac is authoritative. `tui`, +`inputNeededNotifEnabled` and `agentPushNotifEnabled` had been lifted from a remote machine's +values rather than the Mac's, and were removed on 2026-08-13. The values already distributed +stay where they are, and are treated as host-owned from now on. + +### Why not symlink it the way the Mac does + +Once `defaultMode: bypassPermissions` was distributed, `permissions.allow` stopped growing, so +the original reason — that symlinking would wipe accumulated approvals every time — went away +on 2026-08-15. It stays a merge because the Mac's `settings.json` contains keys that either +break or mean nothing on a remote machine. + +- `hooks` — absolute paths like `/Users/gapul/.config/claude/hooks/*.sh`, which do not exist on + Linux and fail every time. Remote machines have their own hooks, integrated with herdr. +- `enabledPlugins` — the Mac has clangd and the Swift LSP. What a given box needs is + rust-analyzer, and distributing this would replace it. Toolchains differ per host, so it is + left alone. +- `extraKnownMarketplaces` — only meaningful together with those plugins, so it goes too. +- `disableDeepLinkRegistration` — a macOS `~/Applications` URL handler thing, with no Linux + equivalent. +- `defaultModel` — the Mac pins `claude-sonnet-4-20250514`. Distributing it would roll the + remote's default model back too. Leaving it out is deliberate; if they should match, revisit + the pin on the Mac first rather than adding it here. + +### Nesting + +Only `defaultMode` inside `permissions` is managed. The merge recurses only where both sides +have an object, so `allow` and `additionalDirectories` as the host grew them survive. Arrays are +not recursed into, since merging `allow` element by element is not the intent. `--check` and +`--adopt` follow the same structure. + +### What distributing bypassPermissions means + +Claude Code runs on the remote machine without asking for permission. On a shared machine, use +it on the understanding that whatever Claude is allowed to do there is everything your account +can do. + +`CLAUDE.md` and the hand-written skills are never rewritten, so they are symlinked on remote +machines as normal. `hooks/`, `output-styles/` and `bin/` assume the Mac's desktop — +notifications through osascript, herdr, the Notion MCP — and are not carried across. + +Never point this script at the Mac's own `settings.json`. It writes through a temporary file and +a rename, which would replace the symlink nix created with a real file and break tracking. On +the Mac, use it only in the `--adopt` direction, which just reads. + +## Which side wins: the client machine + +For the managed keys, the Mac you connect from is authoritative. Changing a setting on a remote +machine is temporary, local to that host, and gets overwritten by the next merge. Updates travel +in the other direction, pulled up from the Mac: ``` -just claude-settings-adopt # 母艦の現在値を settings.remote.json へ取り込む +just claude-settings-adopt # pull the Mac's current values into settings.remote.json ``` -吸い上げるのは `settings.remote.json` が既に持っている管理キーだけで、キーの集合は増えない -(`permissions` を巻き込まないため)。新しく管理したいキーがあるときは、先に -`settings.remote.json` へそのキーを手で足してから `adopt` を走らせる。 +It pulls only the managed keys `settings.remote.json` already has; the set never grows, so that +`permissions` is not swept in. To manage a new key, add it to `settings.remote.json` by hand +first, then run `adopt`. -## theme = auto とライト/ダーク追従 +## theme = auto, and following light and dark -`auto` は TUI 上の表示が "Auto (match terminal)" で、**OS ではなく端末**を見る。 -実装は端末への OSC 11 (背景色問い合わせ) の応答 `rgb:RRRR/GGGG/BBBB` を読み、 -取れないときは `COLORFGBG` にフォールバックする。 +`auto` appears in the TUI as "Auto (match terminal)", and it watches the terminal rather than +the OS. It sends OSC 11 to ask for the background colour, reads the `rgb:RRRR/GGGG/BBBB` reply, +and falls back to `COLORFGBG` when there is no answer. -つまり ssh 越しでも、応答するのは母艦の ghostty なので**そのまま追従する**。 -`nix/lib/theme.nix` の dark/light 2 端点や `theme-watch` のような side channel は -Claude には要らない。 +That means it follows correctly even over ssh, because what answers is ghostty on the Mac. +Claude needs neither the two dark and light endpoints in `nix/lib/theme.nix` nor a side channel +like `theme-watch`. -ただし tmux の内側では OSC 11 に応答するのが tmux 自身になるため、外側の ghostty まで -問い合わせが届くかは tmux のバージョン依存。ここは nvim の `&background` 自動判定と同じ -制約で、`configs/editors/nvim/lua/plugins/auto-dark-mode.lua` の OSC 111 に関する -コメントも参照。 +Inside tmux, though, tmux itself answers the OSC 11, so whether the query reaches the outer +ghostty depends on the tmux version. This is the same constraint nvim's automatic `&background` +detection has; see the comments about OSC 111 in +`configs/editors/nvim/lua/plugins/auto-dark-mode.lua`. -## 適用 +## Applying it -- 母艦: `just rebuild` -- リモート: `nssh ` -- 手で: `python3 scripts/merge-claude-settings.py ~/.claude/settings.json` +- On the Mac: `just rebuild` +- On a remote machine: `nssh ` +- By hand: `python3 scripts/merge-claude-settings.py ~/.claude/settings.json` diff --git a/configs/homelab/README.md b/configs/homelab/README.md index 8443fb49..84578d45 100644 --- a/configs/homelab/README.md +++ b/configs/homelab/README.md @@ -1,22 +1,26 @@ # configs/homelab -> **注記(2026-08-09)**: この構成はハイパーバイザ無しの NixOS 1台へ置き換え中。 -> 設定の実体は `nix/hosts/homeserver.nix` と `nix/homelab/` に移っている。 -> 当日の手順は [HOMESERVER_MIGRATION.md](../../docs/HOMESERVER_MIGRATION.md)。 -> 以下は移行元の記録。 +> **Superseded (2026-08-09).** This setup was replaced by a single NixOS machine with no +> hypervisor. The configuration now lives in `nix/hosts/homeserver.nix` and `nix/homelab/`, and +> the steps taken that day are in +> [HOMESERVER_MIGRATION.md](../../docs/HOMESERVER_MIGRATION.md). What follows is a record of +> what was replaced. -自宅セルフホスト基盤の設定一式。 +The configuration for the self-hosting setup at home. How it was operated, how it was put +together and what went wrong are in [`docs/HOMELAB.md`](../../docs/HOMELAB.md). -📖 **運用手順・構成・トラブルシュートは [`docs/HOMELAB.md`](../../docs/HOMELAB.md) を参照。** +## One directory per service -## サービス別ディレクトリ -| ディレクトリ | 内容 | +| Directory | Contents | |------|------| -| `adguard/` | AdGuard Home 二重化(主系 Pi / 副系 CT101 / 同期)+ runbook | -| `caddy/` | リバースプロキシ Caddyfile(CT103・Tailscale 限定) | -| `raspberrypi/` | ラズパイ初期化 `bootstrap.sh`(Docker/zram/log2ram/Tailscale・SD延命) | -| `forgejo/` | セルフホスト Git(GitHub ミラー・`git.gapul.net`・稼働中) | +| `adguard/` | The two AdGuard Home instances — primary on the Pi, secondary on CT101, plus the sync — and the runbook | +| `caddy/` | The reverse proxy Caddyfile, on CT103, listening on Tailscale only | +| `raspberrypi/` | `bootstrap.sh` for the Pi: Docker, zram, log2ram, Tailscale, and the tweaks that keep the SD card alive | +| `forgejo/` | Self-hosted git, mirroring GitHub, at `git.gapul.net` | -## ホスト早見 -- pve `.100` / dockge(CT101) `.65` / caddy(CT103) `.119` / hermes(CT104) `.120` / HA(VM100) `.88` / rpi4 `.53` -- CT へは pve から `pct exec -- ...` +## The hosts + +pve `.100`, dockge on CT101 `.65`, caddy on CT103 `.119`, hermes on CT104 `.120`, Home +Assistant on VM100 `.88`, rpi4 `.53`. + +Containers are reached from pve with `pct exec -- ...`. diff --git a/configs/homelab/adguard/README.md b/configs/homelab/adguard/README.md index 1b5fae49..125dc43d 100644 --- a/configs/homelab/adguard/README.md +++ b/configs/homelab/adguard/README.md @@ -1,74 +1,86 @@ -# AdGuard Home 二重化 (A構成) +# Two AdGuard Home instances -ローカル DNS を主系(Raspberry Pi 4)/ 副系(.65)で冗長化する。 -**主系のラズパイは制作用途でいつでも停止でき、止めても副系が名前解決を継続する。** +Local DNS made redundant with a primary on a Raspberry Pi 4 and a secondary on `.65`. The Pi can +be switched off at any time for creative work, and the secondary keeps resolving names while it +is gone. -## 構成 +## The pieces -| 役割 | ホスト | DNS | 管理UI | 外部URL | +| Role | Host | DNS | Admin UI | External URL | |---|---|---|---|---| -| 主系 | ラズパイ4 `192.168.116.53` | :53 | :3000 | `dns.gapul.net` | -| 副系 | Docker母艦 `192.168.116.65` | :53 | :3080 | `dns2.gapul.net` | -| 同期 | `.65` (adguardhome-sync) | — | — | 主系→副系 10分毎 | +| Primary | Raspberry Pi 4, `192.168.116.53` | :53 | :3000 | `dns.gapul.net` | +| Secondary | The Docker host, `192.168.116.65` | :53 | :3080 | `dns2.gapul.net` | +| Sync | `.65`, adguardhome-sync | — | — | Primary to secondary, every 10 minutes | ``` -クライアント DNS 設定: - primary = 192.168.116.53 ← 主系(Pi)。止めてもOK - secondary = 192.168.116.65 ← 副系。常時稼働でフェイルオーバー +client DNS settings: + primary = 192.168.116.53 the Pi. Safe to switch off + secondary = 192.168.116.65 always up, and takes over ``` -## セットアップ手順 +## Setting it up -### 1. ラズパイの準備 -- Raspberry Pi OS Lite (64bit) を microSD に焼く -- 固定IP `192.168.116.53` を割当(ルーターのDHCP予約 or dhcpcd 設定) -- Docker + compose plugin を導入 -- microSD延命: `log2ram` 導入、`zram` 有効化 +### 1. Preparing the Pi + +- Flash Raspberry Pi OS Lite, 64-bit, to a microSD card. +- Give it the fixed address `192.168.116.53`, either as a DHCP reservation on the router or + through dhcpcd. +- Install Docker and the compose plugin. +- To make the card last: install `log2ram` and enable `zram`. + +### 2. Starting the primary, on the Pi -### 2. 主系を起動 (ラズパイ上) ```bash cd configs/homelab/adguard/primary-pi docker compose up -d -# http://192.168.116.53:3000 で初期セットアップ → admin ログイン作成 +# then run the first-time setup at http://192.168.116.53:3000 and create the admin login ``` -### 3. 副系を起動 (.65 / Dockge から or CLI) +### 3. Starting the secondary, on `.65`, through Dockge or the CLI + ```bash cd configs/homelab/adguard/secondary docker compose up -d -# http://192.168.116.65:3080 で初期セットアップ → 同じ admin で作成 +# first-time setup at http://192.168.116.65:3080, with the same admin ``` -### 4. 同期を起動 (.65) +### 4. Starting the sync, on `.65` + ```bash cd configs/homelab/adguard/sync -cp .env.example .env # 主系/副系の admin パスワードを記入 (sops管理推奨) +cp .env.example .env # fill in both admin passwords; sops is preferable docker compose up -d ``` -以後、主系で編集したフィルタ・書換ルール・設定が副系へ自動複製される。 -### 5. クライアントへ配布 -ルーターの DHCP 配布 DNS を `192.168.116.53` / `192.168.116.65` の2つに設定。 -(Tailscale 経由は既存の Split DNS `gapul.net→Cloudflare` 設定と併用) +From then on, filters, rewrite rules and settings edited on the primary are copied to the +secondary automatically. -## 制作用途でラズパイを止める / 戻す +### 5. Handing it to the clients + +Set the router's DHCP-distributed DNS to both `192.168.116.53` and `192.168.116.65`. Over +Tailscale this coexists with the existing split DNS that sends `gapul.net` to Cloudflare. + +## Switching the Pi off and back on ```bash -# 止める (ラズパイ上) — 副系(.65)が自動でDNSを引き継ぐ +# stop it, on the Pi. The secondary at .65 takes over DNS automatically. docker compose -f primary-pi/compose.yaml down -sudo systemctl disable docker # 制作中はDockerごと止めてリソース解放する場合 +sudo systemctl disable docker # also stop Docker entirely, to free resources during creative work -# 制作 ... CPU/メモリ/IO がフルに使える +# ... CPU, memory and IO are all yours -# 戻す +# bring it back sudo systemctl enable --now docker docker compose -f primary-pi/compose.yaml up -d -# 復帰後、次の sync サイクルで副系との差分が再同期される +# the next sync cycle reconciles it with the secondary ``` -`conf/` は volume に残るので、停止しても設定・ログイン情報は失われない。 +`conf/` lives in a volume, so settings and logins survive being stopped. + +## Notes -## 注意 -- `work/` `conf/` `.env` は Git 管理外(`.gitignore` 済み)。設定実体と秘密情報はコミットしない -- 主系/副系で **DHCPサーバー機能は使わない**(同期対象外。ルーターのDHCPを継続利用) -- Tailscale は制作中も起動したままで可(軽量・リモート用) +- `work/`, `conf/` and `.env` are outside git, already in `.gitignore`. Neither the real + configuration nor the secrets get committed. +- Do not use the DHCP server feature on either instance. It is not synced, and the router keeps + doing DHCP. +- Tailscale can stay running during creative work; it is light and it is how you get in. diff --git a/configs/homelab/forgejo/README.md b/configs/homelab/forgejo/README.md index e90c395a..f8c9dc3c 100644 --- a/configs/homelab/forgejo/README.md +++ b/configs/homelab/forgejo/README.md @@ -1,64 +1,73 @@ -# Forgejo (GitHub 以外の自宅 git ホスト) +# Forgejo, a git host at home that is not GitHub -GitHub の代替/冗長リモート。GitHub 障害・アカウント凍結時にも dotfiles やコードが自宅に残る。 +A redundant remote, so dotfiles and the rest of the code are still at home if GitHub goes down +or the account is frozen. -- 公開: `https://git.gapul.net/` (Caddy・**Tailscale 限定**・Cloudflare DNS-01 TLS) -- git は **HTTPS のみ**(SSH 無効)。push/pull は token 認証 +Published at `https://git.gapul.net/`, through Caddy, on Tailscale only, with TLS from +Cloudflare DNS-01. git is HTTPS only, with SSH disabled, and push and pull authenticate with a +token. -## デプロイ (CT101 = dockge / .65) +## Deploying, on CT101, dockge, at .65 ```bash -ssh proxmox # root@192.168.116.100 (flap したら再試行) +ssh proxmox # root@192.168.116.100; retry if the path flaps pct enter 101 mkdir -p /opt/stacks/forgejo/data && cd /opt/stacks/forgejo -# compose.yaml を配置 (dotfiles の configs/homelab/forgejo/compose.yaml)。Dockge UI 推奨 +# put compose.yaml in place, from configs/homelab/forgejo/compose.yaml. The Dockge UI is easier. docker compose up -d -docker compose logs -f # "Starting new Web server" を確認 +docker compose logs -f # wait for "Starting new Web server" ``` -## Caddy ルート反映 (CT103 = caddy) +## Applying the Caddy route, on CT103 -dotfiles の `configs/homelab/caddy/Caddyfile` に `git.gapul.net → .65:3003` を追加済み。 -caddy CT に配ってリロード: +`configs/homelab/caddy/Caddyfile` already routes `git.gapul.net` to `.65:3003`. Distribute it to +the Caddy container and reload: ```bash ssh proxmox 'pct exec 103 -- caddy reload --config /etc/caddy/Caddyfile' ``` -## 初期設定 (ブラウザ) +## First-time setup, in the browser -1. `https://git.gapul.net/` を開く → 初回セットアップ画面 -2. admin ユーザー(gapul)を作成。DB は内蔵 SQLite で十分 -3. 設定 → Applications → **Generate New Token**(scope: repo)。token を控える(Bitwarden へ) +1. Open `https://git.gapul.net/` and you get the setup screen. +2. Create the admin user, `gapul`. The built-in SQLite is plenty. +3. Under Settings, Applications, choose Generate New Token with the repo scope, and put the + token in Bitwarden. -## リポジトリを冗長化する 2 方式 +## Two ways to make a repository redundant -### 方式1: Pull Mirror (推奨・Mac の操作を変えない) +### Pull mirror, recommended, because nothing on the Mac changes -Forgejo が GitHub から定期的に pull して自宅へ複製し続ける。Mac 側は一切変更不要。 +Forgejo pulls from GitHub periodically and keeps a copy at home. Nothing on the Mac needs +changing. -1. Forgejo 右上 + → **New Migration** → **GitHub** -2. URL に GitHub repo (例 `https://github.com/gapul/dotfiles`) -3. **「This repository will be a mirror」にチェック** → Migrate -4. (private repo の場合のみ) GitHub の token を入力欄に -5. 以後、設定した間隔で自動同期。`git.gapul.net/gapul/dotfiles` に常に最新の複製 +1. Plus, top right, then New Migration, then GitHub. +2. Give the GitHub repo URL, for instance `https://github.com/gapul/dotfiles`. +3. Tick "This repository will be a mirror", then Migrate. +4. For a private repository, paste a GitHub token. +5. From then on it syncs on the configured interval, and + `git.gapul.net/gapul/dotfiles` always holds a current copy. -→ dotfiles・notes 等をこの方式で登録すれば、push 先は GitHub のままで自宅にも残る。 +Registering dotfiles, notes and the like this way means pushes still go to GitHub while a copy +stays at home. -### 方式2: 両方へ push (自宅をライブの相互リモートに) +### Pushing to both, making home a live mirror -Mac の repo で push 先を GitHub と Forgejo の両方にする: +Point the Mac's repository at both remotes: ```bash cd ~/.dotfiles -# まず Forgejo 側に空 repo を作成 (UI の New Repository) してから: +# create an empty repository in Forgejo first, through New Repository, then: git remote set-url --add --push origin https://git.gapul.net/gapul/dotfiles.git -git remote set-url --add --push origin https://github.com/gapul/dotfiles.git # 既存分も再追加 -# 以後 `git push` で両方へ飛ぶ。token は git credential helper / .netrc に保存 +git remote set-url --add --push origin https://github.com/gapul/dotfiles.git # re-add the existing one +# from now on `git push` goes to both. The token lives in the git credential helper or .netrc git config credential.https://git.gapul.net.username gapul ``` -## メンテ -- バックアップ: `./data`(SQLite + リポジトリ実体) を別途保全。Mac の restic 対象には無いので、 - homelab 側で `./data` を定期 dump するか、重要 repo は GitHub が原本なので Forgejo 側は再構築可。 -- 更新: wud が新バージョンを通知 → Dockge で pull & redeploy。 +## Maintenance + +Back up `./data`, which holds the SQLite database and the repositories themselves, separately. +It is not covered by the Mac's restic, so either dump it periodically from the homelab side, or +accept that GitHub holds the original of anything important and Forgejo can be rebuilt. + +For updates, wud reports a new version and Dockge pulls and redeploys. diff --git a/configs/homelab/restic/README.md b/configs/homelab/restic/README.md index 97e607d9..1e2bfcee 100644 --- a/configs/homelab/restic/README.md +++ b/configs/homelab/restic/README.md @@ -1,59 +1,77 @@ -# restic オフサイトバックアップ (共有リポジトリ) +# Offsite backups with restic, in a shared repository -母艦Mac / homeserver / macmini / rpi4 が **同一の暗号化 restic リポジトリ**を共有する: -`rclone:google-drive:restic-backup` (Google Drive 上、rclone 経由)。host 名で相乗り・重複排除。 +The Mac, homeserver, the mac mini and rpi4 all share one encrypted restic repository, +`rclone:google-drive:restic-backup`, held on Google Drive through rclone. They are separated by +host name and deduplicated against each other. -| ホスト | 対象 | スケジュール | 実装 | 秘密の場所 | +| Host | What it backs up | Schedule | Implementation | Where the secrets live | |---|---|---|---|---| -| 母艦 (MacBook-Mini) | Documents/Pictures/Downloads/Movies/Music/Minecraft | 日次 13:00 | home-manager `nix/home/restic-backup.nix` (launchd) | sops-nix | -| homeserver | `/var/lib` (全サービスの状態。`/srv` のメディアと attic は対象外) | 日次 03:00 | NixOS `services.restic.backups.homeserver` (`nix/homelab/backup.nix`) | `/var/lib/secrets/` に手動配置 (age 鍵未導入のため) | -| macmini | `~/Developer` + `~/.config` (除外: node_modules/.venv/target/.git/objects/モデルの重み) | 日次 05:00 | home-manager `nix/home/macmini-backup.nix` (launchd) | 手動配置の生ファイル (sops 非導入) | -| rpi4 | `/home/pi` (docker サービスデータ) | 日次 04:30 | `restic-rpi-offsite.sh` + systemd timer | `/root/.config/rclone/rclone.conf` + `/root/.restic.pw` | +| The Mac, MacBook-Mini | Documents, Pictures, Downloads, Movies, Music, Minecraft | Daily at 13:00 | home-manager, `nix/home/restic-backup.nix`, through launchd | sops-nix | +| homeserver | `/var/lib`, the state of every service. The media in `/srv` and attic are excluded | Daily at 03:00 | NixOS, `services.restic.backups.homeserver` in `nix/homelab/backup.nix` | Placed by hand in `/var/lib/secrets/`, since there is no age key yet | +| The mac mini | `~/Developer` and `~/.config`, excluding node_modules, .venv, target, .git/objects and model weights | Daily at 05:00 | home-manager, `nix/home/macmini-backup.nix`, through launchd | Raw files placed by hand; sops is not set up there | +| rpi4 | `/home/pi`, the docker services' data | Daily at 04:30 | `restic-rpi-offsite.sh` and a systemd timer | `/root/.config/rclone/rclone.conf` and `/root/.restic.pw` | -- 秘密はどれも `rclone.conf`(GDrive トークン) と restic パスワードで、**このリポジトリには含めない**(sops 経由 or 手動配置)。 -- **共有リポジトリなので `restic forget` は必ず `--host <自ホスト>` スコープ**にする。**prune は母艦の日次のみ**が実行し、他ホストは prune しない(排他ロック競合を避ける)。 -- Google OAuth は Production 公開済みでトークンは失効しない(以前は Testing のため約7日で失効し全ホスト停止した罠あり)。 +The secrets are always the same two things — `rclone.conf`, holding the Google Drive token, and +the restic password — and neither is in this repository; they arrive through sops or by hand. -## デプロイ手順 +Because the repository is shared, `restic forget` is always scoped with `--host `, +and only the Mac's daily run prunes. Nothing else prunes, to avoid fighting over the exclusive +lock. + +The Google OAuth client is published as Production, so the token does not expire. It used to be +in Testing, where it expired roughly weekly and stopped every host at once. + +## Deploying + +### rpi4, Debian on aarch64 -### rpi4 (Debian/aarch64) ```sh sudo apt-get install -y restic rclone -# 秘密を配置: /root/.config/rclone/rclone.conf , /root/.restic.pw (母艦 sops から) -# ntfy 通知用: /root/.config/ntfy/{url,token} +# place the secrets: /root/.config/rclone/rclone.conf and /root/.restic.pw, from the Mac's sops +# and for notifications: /root/.config/ntfy/{url,token} sudo install -m755 restic-rpi-offsite.sh /usr/local/bin/restic-rpi-offsite.sh sudo install -m644 restic-rpi-offsite.service restic-rpi-offsite.timer /etc/systemd/system/ sudo systemctl daemon-reload && sudo systemctl enable --now restic-rpi-offsite.timer ``` -### macmini / homeserver (宣言済み) -スクリプトも launchd/systemd も nix が生成するので、ここに手順は無い。 -秘密だけ手で置く(sops/age を入れていない機械のため)。 +### The mac mini and homeserver, both declared -- macmini: `nix/home/macmini-backup.nix`。秘密は `~/.config/rclone/rclone.conf` と - `~/.config/restic/password`、通知用に `~/.config/ntfy/{url,token}`。`just rebuild` で反映。 -- homeserver: `nix/homelab/backup.nix`。秘密は `/var/lib/secrets/{rclone.conf,restic.password}`。 +nix generates the script and the launchd or systemd unit, so there is no procedure here. Only +the secrets go in by hand, because neither machine has sops or age. -2026-08-12 まで macmini は `restic-macmini-offsite.sh` + `local.restic-macmini.plist` の -imperative 構成だった。両方消したので、この README を見て手で置き直さないこと。 +- The mac mini: `nix/home/macmini-backup.nix`. The secrets are + `~/.config/rclone/rclone.conf` and `~/.config/restic/password`, plus + `~/.config/ntfy/{url,token}` for notifications. `just rebuild` applies it. +- homeserver: `nix/homelab/backup.nix`. The secrets are + `/var/lib/secrets/{rclone.conf,restic.password}`. -## スマホから中身を閲覧 (files.gapul.net) +Until 2026-08-12 the mac mini used an imperative setup, `restic-macmini-offsite.sh` and +`local.restic-macmini.plist`. Both are gone, so do not put them back by reading this file. -暗号化リポジトリは Google Drive 上では中身が見えない。プレビューは homeserver 上の -**restic mount (read-only FUSE) + Filebrowser** で行う。定義は `nix/homelab/restic-view.nix` -(pve 時代は手書き systemd unit だった。移行時に宣言へ移し、ここの unit ファイルは消した)。 +## Browsing the contents from a phone, at files.gapul.net -- restic mount は **`--no-lock` 必須**(常駐マウントのロックが日次 prune を塞ぐのを防ぐ)。 -- Filebrowser の待ち受けは **8085**。8082 は pve 時代のポートで、homeserver では ntfy の - コンテナが使っている(1台に畳んだことで生まれた衝突)。 -- **CF DNS**: `files.gapul.net` の A レコードが個別に要る(ワイルドカードは無い)。 -- tailnet 限定・認証なし。 +An encrypted repository shows nothing useful on Google Drive. Previewing happens on homeserver, +through a read-only FUSE restic mount plus Filebrowser, declared in +`nix/homelab/restic-view.nix`. In the pve days these were hand-written systemd units; the +migration moved them into the declaration and the unit files here were deleted. -## 復元テスト (2026-07-20 実施・全ホスト合格) +- The restic mount needs `--no-lock`, so a permanently mounted repository does not block the + daily prune. +- Filebrowser listens on 8085. 8082 was the port in the pve days, and on homeserver the ntfy + container has it — a collision created by folding everything onto one machine. +- Cloudflare needs its own A record for `files.gapul.net`; there is no wildcard. +- It is on the tailnet only, with no authentication. + +## Restore test, run on 2026-07-20, passed on every host + +For each host, the SHA256 of the restored file matched the live one: -各ホストで「復元したファイルの SHA256 がライブと一致」を確認済み。手順: ```sh -# 例 (対象ホストで、そのホストの restic 環境を export した状態): -restic dump --host latest | sha256sum # ← ライブの sha256sum と一致すれば OK +# on the target host, with that host's restic environment exported: +restic dump --host latest | sha256sum # must match the live sha256sum ``` -- gdrive 越しの `restic ls`/`stats` は**2分でタイムアウトして誤った欠損判定をしがち**。件数確認は `restic find`(ピンポイント) か homeserver の `/mnt/restic-view` マウント、または backup run サマリの「processed N files」を使う。macOS に `timeout` は無い(gtimeout)。 + +`restic ls` and `restic stats` over Google Drive tend to time out after two minutes and make +things look missing when they are not. To count files, use `restic find` for a specific path, +the `/mnt/restic-view` mount on homeserver, or the "processed N files" line in the backup run's +summary. Note that macOS has no `timeout`; it is `gtimeout`. diff --git a/configs/ime/skk/README.md b/configs/ime/skk/README.md index 694bfd40..62b7a0cb 100644 --- a/configs/ime/skk/README.md +++ b/configs/ime/skk/README.md @@ -1,62 +1,72 @@ -# SKK 環境 (macSKK + skkeleton + azooKey skkserv) +# SKK: macSKK, skkeleton and the azooKey skkserv -## 構成 +## The pieces -| 要素 | 場所 | 役割 | +| Piece | Where | Role | |---|---|---| -| **macSKK** | InputMethod (Container sandboxed) | GUI アプリで日本語入力 | -| **skkeleton** | nvim plugin | エディタ内 SKK | -| **azooKey skkserv** | `/Applications/azooKey skkserv.app` | skkserv (localhost:1178) | -| **公開辞書** | `~/.skk/SKK-JISYO.{L,geo,jinmei,propernoun,station}` | skkeleton が直接読む | -| **公開辞書 (macSKK 用 copy)** | `~/Library/Containers/net.mtgto.../Documents/Dictionaries/` | macSKK が sandbox 内で読む | -| **user dict (macSKK)** | 同上の `skk-jisyo.utf8` | macSKK 専用学習 | -| **user dict (skkeleton)** | `~/.skk/skkeleton-user-dict` | skkeleton 専用学習 | - -## 管理対象 (dotfiles) - -- `macSKK.plist` — macSKK の workarounds (Ghostty/VSCode) + UI 設定 + skkserv 接続情報 -- `azoo-key-skkserv.plist` — host/incomingCharset/startServerAtLaunch -- `kana-rule.conf` — ローマ字変換ルール (skkeleton と役物対応を揃える: `! ? :` を追加全角化、`()` は半角維持、`#!use-default` で既定継承) - -plist 2つは home.nix の `home.activation.skkPlistImport` で `defaults import` され、`killall cfprefsd` で flush される。 -`kana-rule.conf` は plist ではなく Container 内 Documents のファイルなので、`home.activation.skkKanaRule` で `install` 実コピーして配置する (symlink 不可は辞書と同様)。 - -## sandbox の制限事項 - -1. **辞書は symlink 不可**: macSKK sandbox は `~/.skk/` への symlink を読めない (NSPOSIX EPERM) → real file copy が必須。9MB 二重持ち。 -2. **plist 直書きは効かない**: macSKK は `dictionaries[]` 配列を起動時に load せず、NSFilePresenter で実行時イベントから登録する設計。`defaults import` でも dict 一覧は再現されない。 -3. **辞書登録は手動**: macSKK 起動中に「削除 → 配置」で NSFilePresenter イベントを発火させる必要あり。 - -## 初回セットアップ (新 Mac) +| macSKK | An input method, sandboxed in a container | Japanese input in GUI applications | +| skkeleton | An nvim plugin | SKK inside the editor | +| azooKey skkserv | `/Applications/azooKey skkserv.app` | An skkserv on localhost:1178 | +| Public dictionaries | `~/.skk/SKK-JISYO.{L,geo,jinmei,propernoun,station}` | Read directly by skkeleton | +| Public dictionaries, macSKK's copy | `~/Library/Containers/net.mtgto.../Documents/Dictionaries/` | What macSKK can read from inside its sandbox | +| User dictionary, macSKK | `skk-jisyo.utf8` in the same place | macSKK's own learning | +| User dictionary, skkeleton | `~/.skk/skkeleton-user-dict` | skkeleton's own learning | + +## What dotfiles manages + +- `macSKK.plist` — macSKK's workarounds for Ghostty and VS Code, its UI settings, and the + skkserv connection details. +- `azoo-key-skkserv.plist` — host, incomingCharset and startServerAtLaunch. +- `kana-rule.conf` — the romaji conversion rules, aligned with skkeleton's handling of + punctuation: `!`, `?` and `:` become full width, `()` stay half width, and + `#!use-default` inherits the rest. + +The two plists are applied by `home.activation.skkPlistImport` in home.nix, through +`defaults import` followed by `killall cfprefsd` to flush. + +`kana-rule.conf` is not a plist but a file inside the container's Documents, so +`home.activation.skkKanaRule` copies it in with `install`. As with the dictionaries, a symlink +does not work. + +## What the sandbox will not allow + +1. **Dictionaries cannot be symlinked.** macSKK's sandbox cannot read a symlink into `~/.skk/`; + it gets NSPOSIX EPERM. A real copy is required, which means keeping 9 MB twice. +2. **Writing the plist directly does not work.** macSKK does not load the `dictionaries[]` + array at startup; it registers them from runtime events through NSFilePresenter. Even + `defaults import` will not reproduce the dictionary list. +3. **Registering a dictionary is a manual step.** With macSKK running, the file has to be + deleted and put back to fire the NSFilePresenter event. + +## First-time setup on a new Mac ```bash -# 1. 公開辞書を ~/.skk/ に入れる (skkeleton 用) +# 1. put the public dictionaries in ~/.skk/, for skkeleton bash ~/.dotfiles/scripts/install-skk-dicts.sh -# 2. macSKK を一度起動(menubar から日本語入力切替) -# Container ディレクトリが作成されるまで待つ +# 2. start macSKK once, by switching to Japanese input from the menu bar, +# and wait for its container directory to be created -# 3. macSKK 用 Container にも辞書をコピー -# (macSKK 起動中に削除→ditto で NSFilePresenter を発火) +# 3. copy the dictionaries into macSKK's container too. +# With macSKK running, delete and ditto them in, to fire NSFilePresenter. bash ~/.dotfiles/scripts/install-skk-dicts-macskk.sh -# 4. macSKK 設定画面 → Dictionaries で各辞書を Toggle ON +# 4. in macSKK's settings, under Dictionaries, toggle each dictionary on -# 5. (オプション) skkserv を有効化 -# macSKK 設定 → Dictionaries → SKKServ Toggle ON +# 5. optionally enable skkserv, in macSKK's settings under Dictionaries ``` -## 設定変更後の capture +## Capturing a change -GUI で macSKK / skkserv の設定を変えたら、dotfiles に反映するには: +After changing macSKK or skkserv through their GUIs, get it back into dotfiles with: ```bash -# macSKK plist 抽出 → XML 化 (git diff が読めるように) +# extract macSKK's plist and convert it to XML, so git diff is readable cp ~/Library/Containers/net.mtgto.inputmethod.macSKK/Data/Library/Preferences/net.mtgto.inputmethod.macSKK.plist \ ~/.dotfiles/configs/ime/skk/macSKK.plist plutil -convert xml1 ~/.dotfiles/configs/ime/skk/macSKK.plist -# azooKey skkserv plist 抽出 → UI state 除外 → XML 化 +# extract azooKey skkserv's plist, drop the UI state, convert to XML cp ~/Library/Containers/io.github.gitusp.azoo-key-skkserv/Data/Library/Preferences/io.github.gitusp.azoo-key-skkserv.plist \ ~/.dotfiles/configs/ime/skk/azoo-key-skkserv.plist python3 -c " @@ -70,18 +80,22 @@ with open(p, 'wb') as f: plistlib.dump(d, f) plutil -convert xml1 ~/.dotfiles/configs/ime/skk/azoo-key-skkserv.plist ``` -注: `defaults import` は XML/binary 両対応なので、dotfiles 側は XML 固定で OK。 -macSKK が GUI 操作で binary に書き戻しても、capture 時に再度 XML 化すれば履歴は綺麗。 +`defaults import` accepts both XML and binary, so keeping the dotfiles copy as XML is fine. +macSKK writing binary back through its GUI does not matter, because converting again at capture +time keeps the history clean. -kana-rule を変えたら (実ファイルなので単純コピー): +After changing kana-rule, which is a real file, a plain copy is enough: ```bash cp ~/Library/Containers/net.mtgto.inputmethod.macSKK/Data/Documents/Settings/kana-rule.conf \ ~/.dotfiles/configs/ime/skk/kana-rule.conf ``` -## 共有していないもの +## What is not shared + +**Learning history**, the user dictionaries, is separate between macSKK and skkeleton. Sharing +them is a problem for another day: pointing both at one file risks losing it to a race, and the +formats are not guaranteed compatible. -- **学習履歴 (user dict)**: macSKK と skkeleton で**別々**。同期は将来課題。 - - 同じファイルにすると race condition で消える可能性 + 書式互換性未保証 -- **macSKK の Dictionary file 一覧**: GUI で都度 toggle 要(plist 経由では再現されない) +**macSKK's list of dictionary files** has to be toggled through the GUI each time, since it +cannot be reproduced through the plist. diff --git a/configs/keyboard/keyball39/README.md b/configs/keyboard/keyball39/README.md index b07e4ad8..c1e9c364 100644 --- a/configs/keyboard/keyball39/README.md +++ b/configs/keyboard/keyball39/README.md @@ -1,82 +1,87 @@ -# Keyball39 QMK keymap (gapul) +# Keyball39 QMK keymap -Remap/VIA で運用していた Keyball39 の設定を、コード管理の QMK キーマップに移行したもの。 -`keymap-export.json` が実機(Remap 経由でライブ読み込み)から吸い出した現行設定、 -`keymap.c` はそれを `gen_keymap.py` で機械変換したもの。VIA は有効のままなので、 -このコードをベースにしつつ Remap/VIA でキーマップ・ライトの微調整も引き続き可能。 +The Keyball39 configuration, moved out of Remap and VIA and into a QMK keymap kept in code. +`keymap-export.json` is the configuration as it was on the keyboard, read out live through +Remap, and `keymap.c` is that converted mechanically by `gen_keymap.py`. VIA stays enabled, so +this code is the base but the keymap and lighting can still be tweaked through Remap and VIA. -## ファイル +## The files -- `keymap.c` … 生成物(手編集も可)。編集して自分好みにしてよい。 -- `keymap-export.json` … 移行元データ(バックアップ。実機から吸い出した全レイヤーのキーコード+ライト設定)。 -- `gen_keymap.py` … `keymap-export.json` → `keymap.c` を再生成するスクリプト。 -- `config.h` / `rules.mk` … 純正 keyball39 `via` と同等(VIA_ENABLE / RGBLIGHT / OLED)。 +- `keymap.c` — the generated result, and fine to edit by hand however you like. +- `keymap-export.json` — the source data and a backup: every layer's keycodes plus the lighting + settings, read off the keyboard. +- `gen_keymap.py` — regenerates `keymap.c` from `keymap-export.json`. +- `config.h` and `rules.mk` — equivalent to the stock keyball39 `via` keymap, with VIA_ENABLE, + RGBLIGHT and OLED. -## レイヤー構成(移行時点) +## The layers, as of the migration -- L0: QWERTY + ホームロー・モッド(左 A=Shift S=Ctrl D=Alt F=Gui G=Ctrl+Alt+Gui / 右対称)+ 親指 Tab=LT3 Space=LT1 Esc=LT2 -- L1: 数字・記号・矢印・マウスボタン・画面輝度 -- L2: ファンクションキー・数字・記号・音量 -- L3: RGB/アンダーグロウ操作・トラックボール(CPI/スクロール/オートマウス)・Bootloader +- L0: QWERTY with home row mods — on the left, A is Shift, S is Ctrl, D is Alt, F is Gui and G + is Ctrl+Alt+Gui, mirrored on the right — and on the thumbs, Tab is LT3, Space is LT1 and Esc + is LT2. +- L1: numbers, symbols, arrows, mouse buttons and screen brightness. +- L2: function keys, numbers, symbols and volume. +- L3: RGB and underglow, the trackball (CPI, scroll, auto mouse), and the bootloader. -ライト(アンダーグロウ)は移行時 Effect Mode = All Off(実質オフ)。 +The underglow was at effect mode All Off at migration time, which is effectively off. -## ビルド方法(実際に通った手順) +## Building it, the way that actually worked -Keyball は QMK **0.22.14** で検証されている。本物の qmk_firmware(0.22.14) に -keyball をシンボリックリンクで重ねてビルドする。brew版qmkはpython環境が壊れやすいので、 -`avr-gcc@12`(brew, keg-only) + uv venv(Python 3.11) + pip版qmkランチャーを使う。 +Keyball is verified against QMK 0.22.14. The build overlays keyball onto a real qmk_firmware +0.22.14 checkout through symlinks. Homebrew's qmk breaks its Python environment easily, so this +uses `avr-gcc@12` from brew, which is keg-only, plus a uv venv on Python 3.11 and the pip +version of the qmk launcher. ```sh -# 0) 前提: brew で avr-gcc@12 を導入済み(keg-only) -brew install qmk/qmk/qmk # avr-gcc@12 等が入る。qmk本体は使わずavr-gccだけ使う +# 0) install avr-gcc@12 through brew first; it is keg-only +brew install qmk/qmk/qmk # brings in avr-gcc@12 and friends. Only avr-gcc gets used. -# 1) keyball リポジトリ +# 1) the keyball repository git clone https://github.com/Yowkees/keyball.git ~/repos/keyball -# 2) qmk_firmware 0.22.14(submodule込み) +# 2) qmk_firmware 0.22.14, with submodules git clone https://github.com/qmk/qmk_firmware.git \ --depth 1 --recurse-submodules --shallow-submodules -b 0.22.14 ~/repos/qmk -# 3) keyball を重ねる + この keymap を gapul として結合 +# 3) overlay keyball, and link this keymap in as "gapul" ln -sfn ../../keyball/qmk_firmware/keyboards/keyball ~/repos/qmk/keyboards/keyball ln -sfn ~/.dotfiles/configs/keyboard/keyball39 \ ~/repos/keyball/qmk_firmware/keyboards/keyball/keyball39/keymaps/gapul -# 4) uv venv(3.11) に依存 + qmk ランチャー +# 4) dependencies in a 3.11 uv venv, plus the qmk launcher cd ~/repos/qmk uv venv .venv --python 3.11 uv pip install --python .venv/bin/python -r requirements.txt qmk -# 5) ビルド +# 5) build export PATH="$PWD/.venv/bin:/opt/homebrew/opt/avr-gcc@12/bin:$PATH" export QMK_HOME=$PWD qmk config user.qmk_home=$PWD qmk compile -kb keyball/keyball39 -km gapul -# => ~/repos/qmk/keyball_keyball39_gapul.hex (約26.8KB / 28KB, 余裕あり) +# produces ~/repos/qmk/keyball_keyball39_gapul.hex, about 26.8 KB of 28 KB ``` -keymap を編集したら 5) だけ再実行。`keymap-export.json` から作り直したいときは -`python3 gen_keymap.py`(このフォルダ)で `keymap.c` を再生成。 +After editing the keymap, only step 5 needs repeating. To rebuild from +`keymap-export.json` instead, run `python3 gen_keymap.py` in this directory. -### 別解: Remap の Firmware Workbench(クラウドビルド, ローカル環境不要) +There is also Remap's Firmware Workbench, which builds in the cloud: it compiles the source in +the browser, gives you a hex, and can flash it, with no local toolchain at all. -ソースをブラウザ上でビルド → hex 取得 → そのまま書き込みも可能。 +## Flashing -## 書き込み - -分割キーボードなので**左右それぞれに同じ hex を書き込む**(片方ずつ)。 +It is a split keyboard, so the same hex goes onto both halves, one at a time. ```sh cd ~/repos/qmk export PATH="$PWD/.venv/bin:/opt/homebrew/opt/avr-gcc@12/bin:$PATH" export QMK_HOME=$PWD -# 書き込む側の半分だけ USB 接続 → 実行 → プロンプトが出たらリセットボタンを2回押す +# connect only the half you are flashing, run this, and press reset twice when prompted qmk flash -kb keyball/keyball39 -km gapul -# 終わったらもう片方を USB 接続して、同じコマンドをもう一度 +# then connect the other half and run the same command again ``` -QMK Toolbox や Remap の Flash 機能で `keyball_keyball39_gapul.hex` を書いてもよい -(ProMicro / ATmega32U4 / Caterina を選択)。書き込みで EEPROM は初期化され、 -キーマップは keymap.c の内容が既定になる。ライトは All Off に戻るので必要なら Remap で再設定。 -VIA は有効なので Remap での微調整は引き続き可能。 +QMK Toolbox or Remap's flash feature work equally well with +`keyball_keyball39_gapul.hex`; choose ProMicro, ATmega32U4 and Caterina. Flashing clears the +EEPROM, so the keymap falls back to whatever `keymap.c` says. The lighting returns to All Off, +so set it again through Remap if you want it. VIA is still enabled, so adjusting things in Remap +keeps working. diff --git a/configs/macmini/README.md b/configs/macmini/README.md index cc66828e..51b61109 100644 --- a/configs/macmini/README.md +++ b/configs/macmini/README.md @@ -1,34 +1,52 @@ -# macmini ローカルAIスタック +# The local AI stack on the mac mini -M4 Mac mini(24GB, ヘッドレス)上のローカルAI一式。gapul.net 経由でスマホから操作。 +Everything AI-related on the headless M4 Mac mini with 24 GB, reachable from a phone through +gapul.net. -## 層構成(nix宣言 vs imperative) +## What is declared in nix and what is not -| 層 | 管理 | 場所 | +| Layer | Managed by | Where | |---|---|---| -| brew(ffmpeg/uv/aria2/socat/container/tailscale) | **nix宣言** | `nix/hosts/macmini.nix` の `homebrew.brews` | -| ラッパーCLI(transcribe/tts/describe等) | 本リポ管理 | `configs/macmini/bin/` → `~/.local/bin/` | -| サービス/スクリプト(ai-stack.sh 等) | 本リポ管理 | `configs/macmini/services/` → `~/` | -| launchd supervisor | 本リポ管理 | `configs/macmini/launchd/` → `~/Library/LaunchAgents/` | -| Python venv(ML) | **imperative** | `bootstrap.sh` で uv 再構築(torch/mlx/pyannote等はnix化困難) | -| モデル(重い) | **imperative** | `models.txt` manifest + `bootstrap.sh`(hf-mirror/GitHub直DL) | +| brew: ffmpeg, uv, aria2, socat, container, tailscale | Declared in nix | `homebrew.brews` in `nix/hosts/macmini.nix` | +| Wrapper CLIs: transcribe, tts, describe and the rest | This repository | `configs/macmini/bin/` linked into `~/.local/bin/` | +| Services and scripts, such as ai-stack.sh | This repository | `configs/macmini/services/` linked into `~/` | +| The launchd supervisor | This repository | `configs/macmini/launchd/` linked into `~/Library/LaunchAgents/` | +| Python venvs for ML | Imperative | Rebuilt by `bootstrap.sh` with uv; torch, mlx and pyannote are hard to express in nix | +| The models, which are large | Imperative | The `models.txt` manifest plus `bootstrap.sh`, downloading from hf-mirror and GitHub | -## 再現手順(まっさら macmini から) +## Reproducing it from a clean mac mini -1. `darwin-rebuild switch`(brew群・SSH/sleep設定が入る) -2. `bash configs/macmini/bootstrap.sh`(venv再構築 → モデル取得 → スクリプト配置 → コンテナ起動 → launchd登録) -3. Caddy(別ホスト `caddy`)に tools のブロック追記(`docs/`参照) +1. `darwin-rebuild switch`, which brings in the brew packages and the SSH and sleep settings. +2. `bash configs/macmini/bootstrap.sh`, which rebuilds the venvs, fetches the models, places the + scripts, starts the containers and registers the launchd agents. +3. Add the tools block to Caddy, which runs on a different host; see `docs/`. -## サービス一覧(全てLAN 192.168.116.91) +## The services, all on 192.168.116.91 -- 埋め込み+リランク(Ruri)サーバー :8900 / AIツールWebパネル :8901 -- コンテナ: Minecraft :25565(apple container、socatでhost公開) - - Open WebUI と AnythingLLM は 2026-08-12 に homeserver へ移し、2026-08-20 に廃止した。AIパネル(:8901)と用途が重なっていたため。 - - Ollama(:11434)と `ask` は 2026-08-28 に撤去。独自 blob ストアに 47GB を抱えたまま、推論の実体は MLX 系と claude-bridge に移っていたため。 +- The embedding and reranking server, Ruri, on :8900, and the AI tools web panel on :8901. +- Containers: Minecraft on :25565, through apple container, published to the host with socat. -## 重要な運用上の罠(詳細は各スクリプトのコメント / Claudeメモ参照) +Open WebUI and AnythingLLM moved to homeserver on 2026-08-12 and were retired on 2026-08-20, +because they overlapped with the AI panel on :8901. -- **モデルDLは母艦を経由せず macmini 直**。HF Xetは絞られる(2.9KB/s〜)ので **hf-mirror.com**(8MB/s)or **GitHub**(TRvlvr等)を優先。aria2 self-healing + zip検証。 -- **量子化×日本語**: VLMの4bitは日本語生成を壊しうる(EMNLP2024で査読済)。Ruri は日本語堅牢。 -- **apple container**: 初回 `container system start` + `container system kernel set --recommended`。DNS死ぬので `--dns 1.1.1.1`。**ホストポート公開(-p)がHTTPで壊れる → socatで host→containerIP 転送**。 -- **SSH**: macminiはBitwardenエージェントがrefuseしがち → `ControlMaster` 多重化必須。 +Ollama, on :11434, and the `ask` command were removed on 2026-08-28. It was holding 47 GB in its +own blob store while the actual inference had moved to the MLX stack and claude-bridge. + +## Things that catch you out + +The details are in the comments in each script. + +**Download models directly on the mac mini**, not through the Mac. HF Xet gets throttled, down +to a few kilobytes per second, so prefer hf-mirror.com, which manages about 8 MB/s, or GitHub, +for things like TRvlvr's releases. aria2 is used with self-healing plus zip verification. + +**Quantisation interacts badly with Japanese.** Four-bit VLMs can break Japanese generation; +this is a peer-reviewed result from EMNLP 2024. Ruri holds up well in Japanese. + +**apple container** needs `container system start` and +`container system kernel set --recommended` the first time. DNS breaks, so pass +`--dns 1.1.1.1`. Publishing a host port with `-p` breaks over HTTP, so socat forwards from the +host to the container's address instead. + +**SSH to the mac mini** is often refused by the Bitwarden agent, so `ControlMaster` multiplexing +is effectively required. diff --git a/configs/macmini/hermes/README.md b/configs/macmini/hermes/README.md index 6bdaf21e..e9d03ff6 100644 --- a/configs/macmini/hermes/README.md +++ b/configs/macmini/hermes/README.md @@ -1,55 +1,61 @@ # hermes -macmini の Hermes エージェント(専用ユーザー `hermes`、サンドボックスは `hsandbox`)のうち、 -**自分で書いたコードだけ**をここに置く。設定と秘密は置かない。 +The parts of the Hermes agent on the mac mini — running as a dedicated user, `hermes`, with its +sandbox as `hsandbox` — that were written here. No configuration and no secrets. -## claude-acp(別リポジトリへ移動) +## claude-acp, moved to its own repository -Hermes の `copilot-acp` プロバイダは、外部プロセスを spawn して stdio 上で -JSON-RPC 2.0 を話す。`claude-acp` はその相手役を最小限だけ実装したアダプタで、 -`session/prompt` を受けるとサブスクの `claude -p` を呼び、返答をそのまま流し返す。 -これで Anthropic の API 課金なしに Claude を推論バックエンドにできる。 +Hermes's `copilot-acp` provider spawns an external process and speaks JSON-RPC 2.0 over stdio. +`claude-acp` is a minimal adapter for the other end of that: it takes `session/prompt`, calls +`claude -p` on the subscription, and passes the answer straight back. That makes Claude the +inference backend without paying for Anthropic's API. -内側の `claude` はツールを無効にして走らせる。ツール実行は Hermes の仕事で、 -Hermes が返答中の `` ブロックを自分のツール層で処理する。 +The inner `claude` runs with tools disabled. Executing tools is Hermes's job, and Hermes handles +the `` blocks in the reply through its own tool layer. -実体は **[gapul/claude-acp](https://github.com/gapul/claude-acp)** に移した。マシンの設定では -なく Hermes を動かすための部品なので、dotfiles が持つ理由が無い。ここは flake input として -参照し、`nix/hosts/macmini.nix` の activation が store から -`/Users/hermes/.local/bin/claude-acp` へ敷く(別ユーザーのホームなので home-manager では -届かない)。`.env` の `HERMES_COPILOT_ACP_COMMAND` がそのパスを指す。 +The code now lives in [gapul/claude-acp](https://github.com/gapul/claude-acp). It is a component +for running Hermes rather than a setting for this machine, so dotfiles has no reason to hold it. +It is referenced here as a flake input, and the activation in `nix/hosts/macmini.nix` lays it +down from the store at `/Users/hermes/.local/bin/claude-acp` — home-manager cannot reach another +user's home. `HERMES_COPILOT_ACP_COMMAND` in `.env` points at that path. -### まなび側だけの上乗せ(HOME が `manabi-home` のとき) +### What is added only on the manabi side, when HOME is `manabi-home` -- **毎ターン先頭に日時を付ける**。内側の `claude` には日付しか渡らず、時刻を - 聞かれると推測で答える(実測で1時間半ずれた)。曜日も渡して数え違えを防ぐ。 -- **やり取りのあとにサンドボックスの `~/study/after_turn.py` を呼ぶ**。向こうで - 学習記録を git にコミットし、計画が変わったのに画像が無ければ作って `MEDIA:` を返す。 - 返信に `MEDIA:` が無いときだけ貼る(コミットは毎回したいので呼び出し自体は毎回)。 - スクリプトが無くても黙って何もしないだけなので、片方だけ古くても壊れない。 +- **Prefix every turn with the date and time.** The inner `claude` only receives the date, so + asked for the time it guesses, and in practice was an hour and a half out. The day of the week + is passed too, to stop it miscounting. +- **Call the sandbox's `~/study/after_turn.py` after each exchange.** That commits the study + record to git on the far side and, if the plan changed and there is no image, generates one + and returns `MEDIA:`. The image is attached only when the reply contains `MEDIA:`, but the + call happens every time because the commit should. If the script is missing it quietly does + nothing, so one side being out of date does not break anything. -**壊れやすさ**: 実装が合わせているのは Hermes 内部の `agent/copilot_acp_client.py` -の契約で、公開 API ではない。Hermes を上げたら会話が通ることを必ず確かめること。 +This is fragile. What the implementation matches is the contract in Hermes's internal +`agent/copilot_acp_client.py`, which is not a public API. After upgrading Hermes, always check +that a conversation still goes through. -## まなび(別リポジトリへ移動) +## manabi, moved to its own repository -学習チューターの一式は **[gapul/manabi](https://github.com/gapul/manabi)**(private)に移した。 -gateway の起動シム、ダッシュボード、サンドボックスのスクリプト、SOUL とチャンネルプロンプト。 -この機械には `/Users/Shared/manabi` に clone してあり、launchd のユニットはそこを exec する。 -private なので flake input にはできない(CI が fetch できない)から、パス参照になっている。 -サービス側の更新は向こうで `git pull` すれば済み、dotfiles の rebuild は要らない。 +The whole study tutor now lives in [gapul/manabi](https://github.com/gapul/manabi), which is +private: the gateway's startup shim, the dashboard, the sandbox scripts, SOUL and the channel +prompts. On this machine it is cloned at `/Users/Shared/manabi`, and the launchd units exec from +there. Being private it cannot be a flake input, since CI could not fetch it, hence the path +reference. Updating the service is a `git pull` over there; no dotfiles rebuild is involved. -## ここに無いもの +## What is deliberately not here -- `config.yaml`(`/Users/hermes/.hermes/` と `/Users/hermes/manabi-home/.hermes/`)は - Hermes 自身が実行中に書き換える。git に置くと常に差分が出るうえ、まなび側は - Discord のチャンネル ID と本人の学習状況が入るので公開リポジトリには置かない。 -- `.env`(トークン類)も同じ理由で置かない。 +`config.yaml`, in `/Users/hermes/.hermes/` and `/Users/hermes/manabi-home/.hermes/`, is +rewritten by Hermes while it runs. Keeping it in git would mean a permanent diff, and the +manabi one holds Discord channel IDs and someone's actual study record, which does not belong in +a public repository. -## 撤去したもの +`.env`, holding the tokens, is absent for the same reason. -`claude-bridge`(OpenAI 互換 :9180 経由で `claude -p` を叩く旧経路)は 2026-08-12 に -撤去した。`model.provider` が `copilot-acp` に移ってから一度も使われておらず、最後に -推論を捌いたのは 2026-07-17。gapul のホームの claude バイナリに依存していたのも、 -ユーザー分離を締められない理由になっていた。戻すなら niski84/claude-bridge を入れ直し、 -`model-providers/claude-cli` プラグインを有効化して `model.provider: claude-cli` に戻す。 +## What was removed + +`claude-bridge`, the old path that called `claude -p` through an OpenAI-compatible endpoint on +:9180, was removed on 2026-08-12. Nothing had used it since `model.provider` moved to +`copilot-acp`; the last inference it served was on 2026-07-17. It also depended on the claude +binary in gapul's home, which was one of the things preventing proper user separation. To bring +it back, reinstall niski84/claude-bridge, enable the `model-providers/claude-cli` plugin and set +`model.provider: claude-cli`. diff --git a/configs/macmini/minecraft/README.md b/configs/macmini/minecraft/README.md index 7606d3ef..d5d8be8a 100644 --- a/configs/macmini/minecraft/README.md +++ b/configs/macmini/minecraft/README.md @@ -1,12 +1,13 @@ # minecraft -macmini で動かしているマイクラのサーバーの起動まわり。どのインスタンスを立てるかは -`nix/hosts/macmini.nix` の `minecraftServers` が持ち、ここに置いてあるのは -その表から呼ばれるスクリプトだけ。 +Everything to do with starting the Minecraft servers that run on the mac mini. Which instances +exist is decided by `minecraftServers` in `nix/hosts/macmini.nix`; what lives here is only the +scripts that table calls. -- `run.sh` — Paper と Fabric を起動する。版が変わっていれば世界を退避し、宣言された - jar を `plugins/` `mods/` に張り直してから上げる -- `run-modded.sh` — NeoForge を起動する。libraries が無ければインストーラを1回走らせる -- `backup.sh` — 毎晩 4:40、全インスタンスを1本ずつ止めて固める +- `run.sh` — starts Paper and Fabric. If the version has changed it moves the world aside, then + relinks the declared jars into `plugins/` and `mods/` before starting. +- `run-modded.sh` — starts NeoForge, running the installer once if the libraries are missing. +- `backup.sh` — every night at 04:40, stops each instance in turn and archives it. -📖 **構成・世界の増やし方・復元手順は [`docs/minecraft.md`](../../../docs/minecraft.md) を参照。** +How it is all put together, how to add a world and how to restore one are in +[`docs/minecraft.md`](../../../docs/minecraft.md). diff --git a/configs/macmini/setup-scripts/README.md b/configs/macmini/setup-scripts/README.md index 0273850d..f2fab1db 100644 --- a/configs/macmini/setup-scripts/README.md +++ b/configs/macmini/setup-scripts/README.md @@ -1,14 +1,14 @@ # setup-scripts -macmini AIスタック構築時(2026-07)に使った一回きりのセットアップ/モデルDLスクリプト置き場。 -常駐や日常コマンドからは参照されていない(参照確認済み)。 +The one-off setup and model-download scripts used to build the mac mini's AI stack in July +2026. Nothing resident or in daily use refers to them; that has been checked. -- dl_* / fetch_* / pull_* : モデル・イメージのダウンロード -- finalize_* / *_setup.sh / setup_* : 各サービスの初期構築 -- rag_test.py : RAGサーバー動作確認 -- popo-remaining-tasks.md : 構築時の残タスクメモ +- `dl_*`, `fetch_*`, `pull_*` — downloading models and images +- `finalize_*`, `*_setup.sh`, `setup_*` — the initial build of each service +- `rag_test.py` — a smoke test for the RAG server +- `popo-remaining-tasks.md` — notes on what was left over during the build -稼働中の実体はホーム直下に残してある: -ai-stack.sh(launchd), -rag_server.py, ai_panel.py, diarize_merge.py, sbv2_tts.py, llm_ask.py, -各 *-venv / *-models / *-data ディレクトリ(venvは絶対パスのため移動不可) +The things that actually run stayed in the home directory: `ai-stack.sh`, driven by launchd; +`rag_server.py`, `ai_panel.py`, `diarize_merge.py`, `sbv2_tts.py` and `llm_ask.py`; and the +`*-venv`, `*-models` and `*-data` directories, which cannot be moved because the venvs hold +absolute paths. diff --git a/configs/macmini/setup-scripts/popo-remaining-tasks.md b/configs/macmini/setup-scripts/popo-remaining-tasks.md index 07995260..90c76c17 100644 --- a/configs/macmini/setup-scripts/popo-remaining-tasks.md +++ b/configs/macmini/setup-scripts/popo-remaining-tasks.md @@ -1,27 +1,58 @@ -# popo-agent chat-provider 作業の引き継ぎ(2026-07-13) - -別マシン(gapul mac + Windows実機 ispc)で進めた作業の残タスクです。可能なものから進めてください。 -リポジトリ: github.com/post-urban/popo-agent(デフォルト/統合ブランチは main のみ、GitHub Flow)。 - -## 完了済み(origin にある状態) -5 本の feature ブランチを origin/main(#401) に rebase 済み。各 PR に provider 固有の制限・マイグレーション注意を明記済み。 -- Chatwork **PR #403 Open**(markdown変換 + ファイル送受信 + 5 migrations) -- Telegram **PR #404 Open**(Bot API full + 5 migrations) -- Windows(runtime) **PR #364 Open**(Windowsネイティブ互換 + fcntl修正 + CI Windows matrix) -- LINE **PR #405 Closed(保留)** — ブランチ feat/chat-line は rebase済み(5e883bed)、再開は `gh pr reopen 405` -- teams(feat/chat-teams, 5commits)— 未 rebase / 未 PR - -## 残タスク -1. **teams ブランチ**: origin/main へ rebase → 検証(typecheck/vitest)→ PR。他 provider 同様、共有ファイル(chat-egress.ts / inbound.ts / identity.ts / route.ts)で加算的衝突、`buildChatLinkReassignedEmail`・`resolveSeatForSlackSender` の provider 拡張が必要になる想定。 -2. **マージ順の衝突対応**: chatwork/telegram/line/teams は同じ共有ファイルを触るため、1本マージ後に残りは main 追従 rebase(軽微な衝突解決)が必要。 -3. **token 解決の DRY 化**(任意): 各 provider egress の「apiToken解決 → resolveRuntimeSecret → http_401 なら refresh して1回リトライ」が複数箇所に重複。`resolveXxxToken` に集約可能。 -4. **provider diff の深い監査**(任意): 過剰実装/dead code。chatwork の未配線 interactivity-handler は削除済み。line/teams は未監査。 - -## 実機(ispc / Windows, ssh ispc)依存で保留中のもの -- **Chatwork ファイル送受信**: 既存 install に OAuth scope `rooms.files:read/write` の**再認可**が必要(再認可まで API が 400、実機 E2E 未検証)。scopes.ts には追加済み。 -- **LINE 実機 E2E 未実施**: LINE の DB は空。LINE Messaging API チャネル作成 + channel token/secret + webhook(line-test.mugen404.com) + tenant/install/連携 + LINE アプリからの送信、が必要。ispc の line worktree は古い commit(39d1893a) で未同期、.env の ENTITLEMENT_ISSUER が host.docker.internal:3000 に誤設定。 -- **Windows 追従リスク**: 今後 main に fcntl/OS固有の新規コードが増えたら Windows-guard 追従が必要(追加した Windows CI matrix が検出想定)。 - -## 注意 -- data/ や .env を git に add しない。vault ノートを直接削除しない。.claude/plans/ に書かない。 -- 実機 ispc は Windows。認証情報の入力やアカウント作成は人間側の操作。 +# Handover: popo-agent chat providers, 2026-07-13 + +What is left from work done on other machines, the gapul Mac and the ispc Windows machine. Pick +up whatever is workable. The repository is github.com/post-urban/popo-agent, which uses GitHub +Flow with main as the only default and integration branch. + +## Already done and on origin + +Five feature branches are rebased onto origin/main at #401. Each pull request spells out its +provider-specific limitations and migration caveats. + +- Chatwork, PR #403, open: markdown conversion, file transfer both ways, five migrations. +- Telegram, PR #404, open: the full Bot API, five migrations. +- Windows runtime, PR #364, open: native Windows compatibility, an fcntl fix, and a Windows CI + matrix. +- LINE, PR #405, closed and on hold. The branch feat/chat-line is rebased, at 5e883bed; reopen + with `gh pr reopen 405`. +- Teams, feat/chat-teams, five commits, neither rebased nor opened as a PR. + +## What is left + +1. **The teams branch.** Rebase onto origin/main, verify with typecheck and vitest, and open a + PR. Like the other providers, expect additive conflicts in the shared files — + chat-egress.ts, inbound.ts, identity.ts, route.ts — and expect + `buildChatLinkReassignedEmail` and `resolveSeatForSlackSender` to need extending for the new + provider. +2. **Conflicts between merges.** chatwork, telegram, line and teams all touch the same shared + files, so after the first one merges the rest need rebasing onto main, with minor conflicts + to resolve. +3. **Deduplicating token resolution**, optional. Every provider's egress repeats the same + sequence: resolve apiToken, call resolveRuntimeSecret, and on http_401 refresh and retry + once. It could collapse into a `resolveXxxToken`. +4. **A deeper audit of the provider diffs**, optional, looking for over-implementation and dead + code. Chatwork's unwired interactivity-handler has already been deleted. line and teams have + not been audited. + +## Blocked on the ispc Windows machine, reachable as `ssh ispc` + +**Chatwork file transfer** needs the existing install re-authorised for the OAuth scopes +`rooms.files:read/write`. Until it is, the API returns 400, and the end-to-end test on real +hardware has not been done. The scopes are already in scopes.ts. + +**LINE has not been tested end to end.** Its database is empty. It needs a LINE Messaging API +channel created, a channel token and secret, a webhook at line-test.mugen404.com, a tenant, +install and link, and a message sent from the LINE app. The line worktree on ispc is out of +date, at commit 39d1893a, and its `.env` has `ENTITLEMENT_ISSUER` wrongly set to +host.docker.internal:3000. + +**Keeping Windows working** will need attention whenever new fcntl or OS-specific code lands on +main, since the Windows guards have to follow. The Windows CI matrix that was added should catch +it. + +## Ground rules + +Do not `git add` `data/` or `.env`. Do not delete vault notes directly. Do not write into +`.claude/plans/`. + +ispc is a Windows machine, and entering credentials or creating accounts is a human's job. diff --git a/configs/wm/omniwm/README.md b/configs/wm/omniwm/README.md index 7b92689b..21a462ed 100644 --- a/configs/wm/omniwm/README.md +++ b/configs/wm/omniwm/README.md @@ -1,12 +1,16 @@ -# OmniWM 設定 - -`~/.config/omniwm/settings.toml` の実体です。out-of-store symlink で繋いであるので、 -OmniWM の GUI で設定を変えるとこのファイルが直接書き換わる。 - -- 設定を nix で生成はしない。OmniWM 自身が書き戻す(キーはソート済み、float はフル精度、 - バージョンが上がるとキーが増減する)ので、store の読み取り専用シンボリックリンクだと保存できない。 - out-of-store symlink なら書き込みがそのままリポジトリに届く。 -- 配線は `nix/home/darwin.nix`。新しい mac では rebuild すればリンクが張られる。 -- 差分は GUI をいじったときに出るので、`git diff` に現れたら普通にコミットすればいい。 -- OmniWM が tmp+rename で書くと symlink が実ファイルに置き換わって追跡が切れる。 - その場合は次の rebuild で `.hm-bak` が残るので気づける(変更は失われるがファイルは無事)。 +# OmniWM configuration + +This is the real `~/.config/omniwm/settings.toml`, linked in as an out-of-store symlink, so +changing a setting through OmniWM's GUI rewrites this file directly. + +The configuration is not generated from nix. OmniWM writes it back itself — keys sorted, floats +at full precision, and keys appearing and disappearing across versions — and a read-only +symlink into the store cannot be saved to. An out-of-store symlink means those writes land in +the repository. + +The wiring is in `nix/home/darwin.nix`, so a rebuild on a new Mac creates the link. Changes show +up in `git diff` whenever the GUI is touched, and can simply be committed. + +If OmniWM writes through a temporary file and a rename, the symlink is replaced by a real file +and tracking breaks. The next rebuild leaves a `.hm-bak` behind, which is how you notice: the +change is lost but the file is intact. diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 47180d96..5da5021e 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -1,64 +1,66 @@ -# Daily-Use Cheatsheet +# Daily-use cheatsheet -このマシンで現代モダン CLI 環境を回すためのコマンド集。 -詳細な背景は [README.md](../README.md) を参照。 +The commands that keep this machine's CLI environment running. For the reasoning behind any of +it, see [README.md](../README.md). --- -## 🎯 まず覚えるショートカット 6 つ +## The six worth memorising first -| キー / コマンド | 何 | +| Key or command | What it does | |---|---| -| **Ctrl+G** | fzf で全 ghq repo から fuzzy 選択 → cd | -| **Ctrl+R** | atuin で履歴 fuzzy 検索 | -| **``** | fzf-tab で fuzzy 補完(全コマンドで効く) | -| `just rebuild` | 設定変えたら呼ぶ | -| `gita ll` | 全 repo の brunch + dirty 状態を 1 画面 | -| `tldr ` | 分からない時の即席 man page | +| Ctrl+G | Fuzzy-pick any ghq repo through fzf and cd into it | +| Ctrl+R | Fuzzy-search history through atuin | +| `` | Fuzzy completion through fzf-tab, on every command | +| `just rebuild` | Run this after changing any configuration | +| `gita ll` | Branch and dirty state for every repo, on one screen | +| `tldr ` | An instant man page when you have forgotten how something works | -これだけ覚えれば日常はだいたい回る。 +Those six cover most days. --- -## 🔄 システム管理 (Justfile) +## System management, through the Justfile -| コマンド | 何 | +| Command | What it does | |---|---| -| `just` | レシピ一覧 | -| `just rebuild` | nh darwin switch + nh home switch | -| `just update` | flake input 更新 → rebuild | -| `just upgrade` | brew + cask(`--greedy`)+ mas + Nix 全部最新化 | -| `just gc` | 古い世代削除 + nix store gc(`--keep 5 --keep-since 7d`) | -| `just check` | 構文/型チェック(ビルドはしない) | -| `just diff` | 現在のシステムと flake の差分 | -| `just doctor` | 環境ヘルスチェック(再起動後 / Determinate upgrade 後) | +| `just` | List the recipes | +| `just rebuild` | `nh darwin switch` and `nh home switch` | +| `just update` | Update the flake inputs, then rebuild | +| `just upgrade` | Bring brew, casks with `--greedy`, mas and Nix all up to date | +| `just gc` | Delete old generations and run the store GC, `--keep 5 --keep-since 7d` | +| `just check` | Syntax and type checking, without building | +| `just diff` | The difference between the running system and the flake | +| `just doctor` | Health check, worth running after a reboot or a Determinate upgrade | --- -## 📂 ファイル操作 +## Files -eza / bat が ls / cat を置き換え済(home-manager が auto-alias)。 +eza and bat have already replaced ls and cat; home-manager aliases them automatically. -| 入力 | 中身 | +| What you type | What runs | |---|---| -| `cat ` | bat — シンタックスハイライト + line number | +| `cat ` | bat, with syntax highlighting and line numbers | | `ls` | eza --icons=auto --git | | `ll` | eza -l --icons=auto --git | | `la` | eza -a --icons=auto --git | | `lt` | eza tree | -| `bottom` / `btm` | システムモニタ TUI(top 置換) | -| `gdu` / `dust` / `diskonaut` | ディスク使用量可視化 | -| `yazi` | ファイラ TUI | +| `bottom`, `btm` | A system monitor TUI, replacing top | +| `gdu`, `dust`, `diskonaut` | Visualise disk usage | +| `yazi` | File manager TUI | -上の表は覚える用の抜粋。実際に定義されている全 alias は下の自動生成表を参照。 +That table is the memorable subset. Every alias that is actually defined is in the generated +table below. -### 🔤 全 alias 一覧(自動生成) +### Every alias, generated -`nix/modules/home/shell.nix` の `shellAliases` と `configs/shell/zshrc.common` から生成 -(alias を変えたら `just docs` で再生成)。後者は母艦と nssh 先が共有する可搬な alias。 +Generated from `shellAliases` in `nix/modules/home/shell.nix` and from +`configs/shell/zshrc.common`. Regenerate with `just docs` after changing an alias. The latter +file holds the portable aliases shared between this machine and anything reached through nssh. -| alias | 展開先 | +| alias | Expands to | |---|---| | `..` | `cd ..` | | `...` | `cd ../..` | @@ -82,340 +84,350 @@ eza / bat が ls / cat を置き換え済(home-manager が auto-alias)。 --- -## 🚀 移動 +## Getting around -| キー / コマンド | 何 | +| Key or command | What it does | |---|---| -| **Ctrl+G** | fzf で repo 横断選択(ghq + 自作 widget) | -| `cd ` | fzf-tab で fuzzy + preview(eza が出る) | -| `z ` | zoxide(履歴頻度で cd) | -| `ghq look ` | 該当 repo の subshell に移動 | +| Ctrl+G | Pick across repos through fzf, using ghq and a hand-written widget | +| `cd ` | fzf-tab, fuzzy with an eza preview | +| `z ` | zoxide, cd by how often you go there | +| `ghq look ` | Open a subshell in that repo | --- -## 🌳 git repo 管理(ghq + gita) +## Managing repos, with ghq and gita -### ghq(配置) +### ghq, for placement -| コマンド | 何 | +| Command | What it does | |---|---| -| `ghq get owner/repo` | `~/ghq///` に clone | -| `ghq get ` | URL 渡しても OK | -| `ghq list` | 管理下の全 repo を相対パスで一覧 | -| `ghq list -p` | フルパスで一覧(スクリプト用) | -| `ghq look ` | 該当 repo に subshell で移動 | -| `ghq root` | ghq.root の値 (~/ghq) | +| `ghq get owner/repo` | Clone into `~/ghq///` | +| `ghq get ` | A URL works too | +| `ghq list` | Every managed repo, by relative path | +| `ghq list -p` | The same with full paths, for scripting | +| `ghq look ` | A subshell in that repo | +| `ghq root` | The value of ghq.root, `~/ghq` | -### gita(横断操作) +### gita, for working across all of them -| コマンド | 何 | +| Command | What it does | |---|---| -| `gita ll` | 全 repo の brunch / dirty / 最終 commit | -| `gita ls` | repo 名一覧 | -| `gita add ` | 個別追加 | -| `gita-sync` | ghq list の全 repo を gita に再登録 | -| `gita super pull` | 全 repo まとめて pull | -| `gita super -- repoA repoB pull` | 一部のみ | -| `gita super exec ls` | 任意コマンドを横断実行 | -| `gita group add -n work repoA repoB` | グループ作成 | -| `gita super -g work pull` | グループだけ操作 | -| `gita info ` | 該当 repo のパス | +| `gita ll` | Branch, dirty state and last commit for every repo | +| `gita ls` | Just the names | +| `gita add ` | Add one | +| `gita-sync` | Re-register everything in `ghq list` with gita | +| `gita super pull` | Pull every repo | +| `gita super -- repoA repoB pull` | Only some of them | +| `gita super exec ls` | Run any command across all of them | +| `gita group add -n work repoA repoB` | Make a group | +| `gita super -g work pull` | Operate on a group | +| `gita info ` | The path to that repo | --- -## 🔍 検索 / 補完 +## Searching and completion -| コマンド | 何 | +| Command | What it does | |---|---| -| `` | fzf-tab 起動(git/cd/kill/checkout 等は preview 付き) | -| **Ctrl+R** | atuin 履歴 fuzzy 検索(複数端末同期、global がデフォルト) | -| **Ctrl+R(検索中もう一度)** | filter mode 切替: global → host → session → directory → workspace | -| **Ctrl+S(検索中)** | search mode 切替: fuzzy → prefix → fulltext | -| **Tab(検索中)** | 候補を確定して shell に挿入(編集モード、誤爆防止) | -| **Enter(検索中)** | 候補を即実行 | -| `fd ` | 高速ファイル名検索 | -| `rg ` | 高速 grep(ripgrep) | -| `fzf` | 標準入力から fuzzy 選択 | +| `` | fzf-tab, with previews for git, cd, kill, checkout and others | +| Ctrl+R | atuin's fuzzy history search, synced across machines, global by default | +| Ctrl+R again, while searching | Cycle the filter: global, host, session, directory, workspace | +| Ctrl+S while searching | Cycle the search mode: fuzzy, prefix, fulltext | +| Tab while searching | Accept the candidate into the shell for editing, rather than running it | +| Enter while searching | Run the candidate immediately | +| `fd ` | Fast filename search | +| `rg ` | Fast grep, ripgrep | +| `fzf` | Fuzzy-pick from standard input | --- -## 🔧 git 体験 +## git -| コマンド | 何 | +| Command | What it does | |---|---| -| `git diff` | delta で side-by-side + line-number(自動 pager) | -| `g status` / `gs` | エイリアス | -| `ga` `gc` `gp` `gl` | add/commit/push/pull のエイリアス | +| `git diff` | delta, side by side with line numbers, paged automatically | +| `g status`, `gs` | Aliases | +| `ga`, `gc`, `gp`, `gl` | add, commit, push, pull | | `lazygit` | git TUI | -| `lazyjj` | jj(jujutsu) TUI | -| `just dev install` | pre-commit hook を `.git/hooks/pre-commit` に (新 Mac 初回) | -| `just dev` | devShell に入る (shellcheck/statix 使用可) | +| `lazyjj` | jujutsu TUI | +| `just dev install` | Install the pre-commit hook into `.git/hooks/pre-commit`, once per new Mac | +| `just dev` | Enter the devShell, which has shellcheck and statix | --- -## 📝 知りたい時 +## Looking things up -| コマンド | 何 | +| Command | What it does | |---|---| -| `tldr ` | 1 画面の使用例(`tldr tar`、`tldr ffmpeg`) | -| `man ` | 詳細マニュアル | -| `nh search ` | nixpkgs から package 検索 | -| `, ` | install せず試す(`, asciinema rec`) | +| `tldr ` | One screen of examples, as in `tldr tar` or `tldr ffmpeg` | +| `man ` | The full manual | +| `nh search ` | Search nixpkgs | +| `, ` | Try something without installing it, as in `, asciinema rec` | --- -## 🔒 機密管理 (SOPS) +## Secrets, through SOPS -| コマンド | 何 | +| Command | What it does | |---|---| -| `just secrets-edit` | sops で `secrets/secrets.yaml` を透過的に編集 | -| `just secrets-rekey` | `.sops.yaml` を変更後の再暗号化 | +| `just secrets-edit` | Edit `secrets/secrets.yaml` transparently through sops | +| `just secrets-rekey` | Re-encrypt after changing `.sops.yaml` | -`~/.config/sops/age/keys.txt` が age 秘密鍵。Bitwarden に backup 推奨。 +The age private key is `~/.config/sops/age/keys.txt`. Back it up in Bitwarden. --- -## 💾 バックアップ / アーカイブ / クラウド (Justfile) +## Backups, archives and cloud storage -4 層のストレージ階層。実装は `nix/home/restic-backup.nix` / `rclone-mount.nix` のコメント参照。 +Four layers of storage. The reasoning is in the comments in `nix/home/restic-backup.nix` and +`rclone-mount.nix`. -| 層 | 用途 | 実体 | +| Layer | For | Where | |---|---|---| -| GitHub + Forgejo | コード(再現可能) | git。GitHub 原本 + 自宅 Forgejo ミラー([HOMELAB.md](./HOMELAB.md)) | -| restic warm | 現役ファイル(自動・日次) | `google-drive:restic-backup`(暗号化) | -| restic cold | 使わなくなったファイル(永久保持) | 同リポジトリ `--tag archive` | -| rclone mount | 他者と共有する平文クラウド | `~/Sync/google-drive-{personal,school}`(KEXT 不要) | +| GitHub and Forgejo | Code, which is reproducible | git. GitHub is the original, with a Forgejo mirror at home; see [HOMELAB.md](./HOMELAB.md) | +| restic warm | Files still in use, automatically and daily | `google-drive:restic-backup`, encrypted | +| restic cold | Files no longer in use, kept forever | The same repository, under `--tag archive` | +| rclone mount | Plaintext cloud storage shared with other people | `~/Sync/google-drive-{personal,school}`, no kernel extension needed | -### warm(現役ファイル・日次自動 + 手動) +### Warm, for files in use, daily and on demand -| コマンド | 何 | +| Command | What it does | |---|---| -| `just backup` | warm を今すぐ実行(launchd kickstart → ログ追尾) | -| `just backup-ls` | 全スナップショット一覧(Tags 列で warm/archive 区別) | -| `just backup-check` | リポジトリ整合性検証(restic check) | +| `just backup` | Run the warm backup now, kickstarting launchd and following the log | +| `just backup-ls` | Every snapshot; the Tags column separates warm from archive | +| `just backup-check` | Verify repository integrity with restic check | -### cold(アーカイブ・使わなくなった物をローカルから退避し永久保持) +### Cold, for archiving things off the local disk and keeping them permanently -| コマンド | 何 | +| Command | What it does | |---|---| -| `just archive ` | restic へ退避 → 確認 → ローカル削除(`--tag archive`) | -| `just archive-ls` | アーカイブした snapshot 一覧(ID/日付/元パス) | -| `just archive-stats` | アーカイブ総容量・ファイル数 | -| `just archive-find ` | アーカイブ内をファイル名検索(restic find・FUSE 不要) | +| `just archive ` | Push to restic, verify, then delete locally, under `--tag archive` | +| `just archive-ls` | The archived snapshots, with ID, date and original path | +| `just archive-stats` | Total archived size and file count | +| `just archive-find ` | Search filenames inside the archive with restic find; no FUSE needed | -### 復元 / 共有マウント +### Restoring, and the shared mounts -| コマンド | 何 | +| Command | What it does | |---|---| -| `just restore [dest]` | snapshot から復元(既定は元の絶対パス。alias: `unarchive`) | -| `just gdrive` | `~/Sync/google-drive-*` のマウント状態確認 | -| `just gdrive remount` / `open` | 再マウント / Finder で開く | +| `just restore [dest]` | Restore from a snapshot, defaulting to the original absolute path. Also `unarchive` | +| `just gdrive` | Check whether `~/Sync/google-drive-*` are mounted | +| `just gdrive remount`, `open` | Remount, or open in Finder | -- cold は append でなく restic の `--keep-tag archive` で永久保持(`forget` で間引かれない)。 -- 共有マウントは **fuse-t(NFS)** なので macFUSE の KEXT/リカバリー不要。`restic`/`google-drive:restic-backup` フォルダはマウントから除外済(誤削除防止)。 +Cold storage is kept permanently through restic's `--keep-tag archive` rather than by appending, +so `forget` never thins it out. The shared mounts use fuse-t over NFS, so macFUSE's kernel +extension and the recovery-mode dance are not needed, and the `restic` and +`google-drive:restic-backup` folders are excluded from the mount so they cannot be deleted by +accident. --- -## 🛰 リモート +## Remote machines -remote の種類別に 4 つの戦略を使い分け: +Four strategies, depending on what the remote is: -| シチュエーション | コマンド | 何 | +| Situation | Command | What it does | |---|---|---| -| **計算ノード**(non-root、ephemeral) | `nssh user@host` | rootless Nix(`nix-portable`)で nvim/yazi/tmux、起動毎に展開 | -| **同上を herdr で** | `herdr --remote user@host` | zsh 関数が `configs/bin/remote-bootstrap` を先に流すので下準備は nssh と同じ。tmux の代わりに herdr | -| **長期 Linux サーバー**(root、persistent) | リモートで `bash <(curl -sL ...bootstrap-linux.sh)` | full Nix install + dotfiles clone + home-manager(`.#-linux`) | -| **WSL2 環境**(Windows + WSL) | WSL 内で `bash <(curl ...bootstrap-wsl.sh)` | Linux 共通 + WSL interop(clipboard, /mnt/c/) | -| **制限環境**(Nix 不可、append 程度) | local から `sync-configs-rsync.sh user@host [--full]` | nvim + zsh.local + git config を rsync、何も install しない | +| A compute node, non-root and ephemeral | `nssh user@host` | nvim, yazi and tmux through rootless Nix (`nix-portable`), unpacked on each start | +| The same, but through herdr | `herdr --remote user@host` | The zsh function runs `configs/bin/remote-bootstrap` first, so the preparation matches nssh. herdr instead of tmux | +| A long-lived Linux server, root and persistent | On the remote: `bash <(curl -sL ...bootstrap-linux.sh)` | A full Nix install, a dotfiles clone, and home-manager as `.#-linux` | +| WSL2 | Inside WSL: `bash <(curl ...bootstrap-wsl.sh)` | The Linux common set plus WSL interop: clipboard, `/mnt/c/` | +| A restricted environment where Nix is impossible and appending is about all you get | From the local machine: `sync-configs-rsync.sh user@host [--full]` | rsync nvim, zsh.local and the git config across. Installs nothing | -ssh agent forwarding で private repo 取得対応済。 +Fetching private repos works, through ssh agent forwarding. -### 該当 home-manager attr -- `.#` … macOS (= 現在 mac) -- `.#-wsl` … WSL2 -- `.#-linux` … 純 Linux x86_64 -- `.#-linux-aarch64` … 純 Linux ARM (Raspberry Pi 等) +### The home-manager attributes + +- `.#` — macOS, which is this Mac +- `.#-wsl` — WSL2 +- `.#-linux` — plain Linux, x86_64 +- `.#-linux-aarch64` — plain Linux on ARM, such as a Raspberry Pi --- -## 🧰 プロジェクト開発 +## Project development -### direnv + devenv +### direnv and devenv ```bash -# 新プロジェクトに devenv.nix 雛形 +# scaffold devenv.nix in a new project cd my-project devenv init -# devenv.nix を編集して使いたい言語/サービスを宣言 +# edit devenv.nix and declare the languages and services you want nvim devenv.nix -# 例: +# for example: # { pkgs, ... }: { # packages = [ pkgs.nodejs_22 pkgs.postgresql ]; # languages.javascript.enable = true; # services.postgres.enable = true; # } -# direnv で auto-load +# auto-load through direnv echo "use devenv" > .envrc direnv allow -# cd するだけで Node + postgres が起動、抜けると停止 +# cd in and Node and postgres start; leave and they stop ``` -### Nix-init(flake.nix 雛形生成) +### nix-init, for scaffolding a flake.nix ```bash -# nixpkgs に無い OSS を Nix flake 化したい時 +# when you want to package something that is not in nixpkgs nix-init --url https://github.com/nosarthur/gita -# 対話で description / version / builder を入力 -# → 動く flake.nix が生成される +# it asks for a description, version and builder +# and writes a flake.nix that works ``` -### ブラウザエンジンのビルド(Ladybird / Servo) +### Building browser engines: Ladybird and Servo -WebKit 以外のエンジンを母艦でビルドするための devShell。どちらも Xcode の clang と -macOS SDK を使うので darwin 限定。定義は `nix/shells/browser-engines.nix`。 +devShells for building engines other than WebKit on this machine. Both use Xcode's clang and the +macOS SDK, so they are darwin only. Defined in `nix/shells/browser-engines.nix`. ```bash -# Ladybird(CMake + vcpkg) +# Ladybird, CMake and vcpkg nix develop ~/.dotfiles/nix#ladybird cd ~/Developer/github.com/LadybirdBrowser/ladybird python3 Meta/ladybird.py build ./Build/release/bin/Ladybird.app/Contents/MacOS/Ladybird https://example.com -# Servo(mach + cargo) +# Servo, mach and cargo nix develop ~/.dotfiles/nix#servo cd ~/Developer/github.com/servo/servo ./mach build --release --media-stack dummy ./target/release/servoshell --headless --exit -o out.png https://example.com ``` -踏みやすい罠: - -- `./mach bootstrap` は Homebrew を叩く唯一の経路なので実行しない。必要な cmake / - pkg-config は devShell 側にある。`MACH_USE_NIX` も立てない(mach が darwin で - 評価できない `shell.nix` に再突入する) -- Servo の音声/動画を有効にするには GStreamer 公式 pkg (本体 + devel の2つ) を sudo で - システムに入れる必要があり、これは宣言管理の外に出る。macmini 側にだけ入れてあるので、 - media 有効ビルドは macmini で行う。`./mach package` が GStreamer dylib を .app に - 同梱するので、母艦は Servo.app を受け取るだけでよく何も入れなくてよい。 - 入れずにビルドするなら `--media-stack dummy`(再生なし) -- Ladybird の headless は 2026-08 時点の master で壊れている(Compositor プロセスが - 起動しないまま WebContent が接続を叩いて落ちる)。GUI は正常 -- devShell に依存ライブラリを足さない。vcpkg が自前で建てるものと衝突すると nix 側が - 勝ち、成果物が `/nix/store` を参照する。その store path は誰も root していないので - 次の GC で消え、ある日突然 dyld の "Library not loaded" で起動しなくなる。疑ったら - `otool -L` で `/nix/store` 参照が無いことを確認する -- ビルドが重いときは macmini に投げる(10 コア / 24GB / 空きが多い)。home が両機とも - `/Users/gapul` なので、`Build/release` を同じパスに rsync すればそのまま動く +Things that catch people out: + +- Do not run `./mach bootstrap`; it is the one path that reaches for Homebrew. The cmake and + pkg-config it wants are already in the devShell. Do not set `MACH_USE_NIX` either, or mach + re-enters a `shell.nix` that cannot be evaluated on darwin. +- Enabling audio and video in Servo needs the official GStreamer packages, both the runtime and + the devel one, installed system-wide with sudo, which puts them outside declarative + management. They are installed on the mac mini only, so media-enabled builds happen there. + `./mach package` bundles the GStreamer dylibs into the .app, so this machine only has to + receive Servo.app and needs nothing installed. To build here without them, use + `--media-stack dummy`, which means no playback. +- Ladybird's headless mode is broken on master as of August 2026: the Compositor process never + starts, and WebContent crashes trying to connect to it. The GUI is fine. +- Do not add dependency libraries to the devShell. When they collide with what vcpkg builds for + itself, nix wins, and the result references `/nix/store`. Nothing roots those store paths, so + the next GC deletes them and the binary stops starting one day with a dyld "Library not + loaded". If you suspect this, check with `otool -L` that there are no `/nix/store` + references. +- Send heavy builds to the mac mini, which has 10 cores, 24 GB and more headroom. Both machines + use `/Users/gapul` as home, so rsyncing `Build/release` to the same path is enough to run it. --- -## 🩺 探索 / トラブル +## Poking around and troubleshooting -| コマンド | 何 | +| Command | What it does | |---|---| -| `just doctor` | 環境チェック | -| `nix-tree ~/.nix-profile` | 依存関係 TUI(「あれ何で入ってる?」) | -| `nix-tree /run/current-system` | システムレベルの依存関係 | -| `nh clean all` | 古い世代を一括削除 | +| `just doctor` | Check the environment | +| `nix-tree ~/.nix-profile` | Dependency TUI, for "why is this even installed?" | +| `nix-tree /run/current-system` | The same at the system level | +| `nh clean all` | Delete old generations in one go | --- -## 🐛 緊急復旧 +## Emergency recovery -| 症状 | コマンド | +| Symptom | Command | |---|---| -| `/nix` が見えない | `sudo diskutil mount "Nix Store"` | -| 同上で直らない | `sudo determinate-nixd init` | -| `fstab` に `noauto` 戻った | `sudo sed -i '' 's/,noauto//' /etc/fstab` | -| Determinate 本体 update | `sudo determinate-nixd upgrade` | -| dotfiles 全部復元(新 Mac) | `bash scripts/bootstrap.sh` | +| `/nix` is not there | `sudo diskutil mount "Nix Store"` | +| That did not fix it | `sudo determinate-nixd init` | +| `noauto` came back in `fstab` | `sudo sed -i '' 's/,noauto//' /etc/fstab` | +| Updating Determinate itself | `sudo determinate-nixd upgrade` | +| Restore everything on a new Mac | `bash scripts/bootstrap.sh` | --- -## 🎯 一日の流れ(典型例) +## A typical day ```bash -# 朝、ターミナル開く -Ctrl+R # atuin で前日の続き +# morning, open a terminal +Ctrl+R # pick up where yesterday left off, through atuin -# プロジェクト移動 -Ctrl+G # fzf で repo 選択 → cd +# go to a project +Ctrl+G # choose a repo through fzf and cd into it -# 状態確認 -gita ll # 全 repo の brunch 状態俯瞰 -gs # 今いる repo の status +# see where things stand +gita ll # branch state across every repo +gs # status of the repo you are in -# 編集 -nvim src/ # fzf-tab で fuzzy 補完 -cat README.md # bat で読む +# edit +nvim src/ # fuzzy completion through fzf-tab +cat README.md # read it through bat # diff -git diff # delta で side-by-side +git diff # side by side, through delta # commit ga . && gc -m "feat: ..." gp -# 別 repo へ -Ctrl+G # 別 repo +# on to another repo +Ctrl+G -# 知らないコマンド試したい -, asciinema rec demo.cast # install せず実行 -tldr ffmpeg # 使い方確認 +# try a command you do not have +, asciinema rec demo.cast # run it without installing +tldr ffmpeg # check how it works -# 設定変えた +# after changing configuration just rebuild -# 週末: 全 repo を最新化 -gita super pull # 横断 pull -just upgrade # システム全体 update +# weekend: bring everything up to date +gita super pull # pull every repo +just upgrade # update the whole system ``` --- -## 🪟 Windows (ハイブリッド: ネイティブ pwsh + WSL) +## Windows: native pwsh plus WSL -詳細ロードマップは [windows-roadmap.md](./windows-roadmap.md)。 -日常コマンドの macOS 等価: +The full roadmap is in [windows-roadmap.md](./windows-roadmap.md). The everyday commands, next +to their macOS equivalents: -| macOS | Windows ネイティブ | +| macOS | Windows, native | |---|---| -| `bash scripts/bootstrap.sh` | `pwsh -File windows/bootstrap.ps1` (= `just win-bootstrap`) | -| `just rebuild` | `git pull` で済む (Nix 不使用、configs symlink は bootstrap 再実行で更新) | +| `bash scripts/bootstrap.sh` | `pwsh -File windows/bootstrap.ps1`, or `just win-bootstrap` | +| `just rebuild` | `git pull` is enough. There is no Nix; config symlinks update by re-running bootstrap | | `just upgrade` | `winget upgrade --all` | | `just secrets edit` | `sops $env:USERPROFILE\dotfiles\secrets\secrets.yaml` | -| `just check` | `just win-verify` (apps.json ID 実在検証) + `just win-fmt` (PSScriptAnalyzer) | -| `pbcopy < file` (WSL) | `Get-Content file \| Set-Clipboard` | -| `xdg-open file` (WSL) | `start file` (pwsh) / `explorer file` | +| `just check` | `just win-verify`, which checks the apps.json IDs exist, plus `just win-fmt` for PSScriptAnalyzer | +| `pbcopy < file` in WSL | `Get-Content file \| Set-Clipboard` | +| `xdg-open file` in WSL | `start file` in pwsh, or `explorer file` | -ネイティブ pwsh 専用関数 (profile.ps1 で定義): +Functions that only exist in native pwsh, defined in profile.ps1: -| 関数 | 用途 | +| Function | What it does | |---|---| -| `Find-DotfilesToolOverlap` | scoop ↔ winget の重複ツールを可視化 | -| `Get-DotfilesSecret ` | secrets.yaml から SOPS 復号して値を取り出す | -| `Copy-DotfilesSecret ` | 同上 → クリップボードへ | -| `Add-SshKey [path]` | Windows ssh-agent に鍵登録 (WSL とも共有) | -| `wsl-here` | 現在ディレクトリで WSL に入る | +| `Find-DotfilesToolOverlap` | Show tools installed through both scoop and winget | +| `Get-DotfilesSecret ` | Decrypt a value out of secrets.yaml through SOPS | +| `Copy-DotfilesSecret ` | The same, into the clipboard | +| `Add-SshKey [path]` | Register a key with the Windows ssh-agent, shared with WSL | +| `wsl-here` | Enter WSL in the current directory | + +SKK, the Japanese input method, is a different implementation on each system: + +- macOS: macSKK with azoo-key-skkserv, in `configs/ime/skk/` +- Windows: CorvusSKK, installed automatically from `nathancorvussolis.corvusskk` in apps.json -SKK (日本語 IME) は macOS と Windows で実装が別物: -- macOS: macSKK + azoo-key-skkserv (`configs/ime/skk/`) -- Windows: CorvusSKK (`nathancorvussolis.corvusskk` を apps.json で auto install) - 設定の同期は未対応(辞書だけ macOS から同期可) +Their settings are not synced; the dictionary can be copied from macOS. --- -## 🧬 内部実装メモ(future-self 向け) +## Implementation notes, for future reference -- **eza/bat/delta/tealdeer**: `home-manager` の `programs.*` で declarative 管理 -- **fzf-tab**: `programs.zsh.plugins` で `pkgs.zsh-fzf-tab` 投入 -- **ghq.root**: `programs.git.extraConfig.ghq.root` で永続化 -- **Ctrl+G widget**: `programs.zsh.initContent` 内の `ghq-fzf` 関数 -- **gita-sync**: 同じく `initContent` の関数(`gita add` を ghq list で全件) -- **gita 本体**: `uv tool install gita`(`bootstrap.sh` で自動化) -- **nom 統合**: `nix build` の wrapper 関数で `--log-format internal-json -v |& nom --json` を挟む -- **nh**: 自前 TUI 持ってるので nom ラップしない(rev. `5d8bdac` で alias 撤回) +- eza, bat, delta and tealdeer are managed declaratively through home-manager's `programs.*`. +- fzf-tab comes in through `programs.zsh.plugins` as `pkgs.zsh-fzf-tab`. +- `ghq.root` is persisted through `programs.git.extraConfig.ghq.root`. +- The Ctrl+G widget is the `ghq-fzf` function inside `programs.zsh.initContent`. +- `gita-sync` is another function there, running `gita add` over everything in `ghq list`. +- gita itself comes from `uv tool install gita`, automated in `bootstrap.sh`. +- nom is wired in through a wrapper function around `nix build` that inserts + `--log-format internal-json -v |& nom --json`. +- nh is not wrapped in nom, because it has its own TUI. The alias was withdrawn in `5d8bdac`. -設定変更したい場合は `~/.dotfiles/nix/home/common.nix` を編集して `just rebuild`。 +To change any of this, edit `~/.dotfiles/nix/home/common.nix` and run `just rebuild`. diff --git a/docs/HOMELAB.md b/docs/HOMELAB.md index aeaa10ad..8ff4a22d 100644 --- a/docs/HOMELAB.md +++ b/docs/HOMELAB.md @@ -1,320 +1,428 @@ -# 自宅サーバー運用ガイド (Homelab Operations) +# Homelab operations -自宅 Proxmox ベースのセルフホスト基盤の構成・運用手順をまとめたドキュメント。 -設定の実体は `configs/homelab//`、ホスト構成は本書を参照。 +How the Proxmox-based self-hosting setup at home was built and run. The configuration itself +lives in `configs/homelab//`; the host layout is described here. -> **この構成は解体中。** ハイパーバイザ無しの NixOS 1台への置き換えが進行中で、 -> 設定の実体は `nix/hosts/homeserver.nix` と `nix/homelab/` に移っている。 -> 当日の手順は [HOMESERVER_MIGRATION.md](HOMESERVER_MIGRATION.md)。 -> 以下は移行元の記録として読むこと。 +> **Superseded.** This setup was replaced by a single NixOS machine with no hypervisor. The +> configuration now lives in `nix/hosts/homeserver.nix` and `nix/homelab/`, and the steps taken +> that day are in [HOMESERVER_MIGRATION.md](HOMESERVER_MIGRATION.md). What follows is a record +> of what was replaced. -- LAN: `192.168.116.0/24` / ゲートウェイ: `192.168.116.254` -- ハイパーバイザ: **Proxmox VE 9.1**(`pve` = `192.168.116.100`) -- tailnet: `tail079f44.ts.net`(MagicDNS 有効) +- LAN `192.168.116.0/24`, gateway `192.168.116.254` +- Hypervisor: Proxmox VE 9.1, `pve` at `192.168.116.100` +- tailnet `tail079f44.ts.net`, MagicDNS on --- -## 1. 構成(CT / VM 一覧) +## 1. Containers and VMs -| ID | 名前 | LAN IP | tailnet | 役割 | +| ID | Name | LAN IP | tailnet | Role | |----|------|--------|---------|------| -| pve | pve | `.100` | `100.101.225.43` | Proxmox ホスト | -| CT101 | dockge | `.65` | — | Docker 母艦。Dockge(:5001)管理。スタックは `/opt/stacks//` | -| CT102 | tailscale-router | (dhcp) | `100.107.201.72` | subnet router(`192.168.116.0/24` 広告・承認済) | -| CT103 | caddy | `.119` | `100.64.125.107` | リバースプロキシ(Caddy / Tailscale 限定待受) | -| CT104 | hermes | `.120` | — | Discord×Claude エージェント | +| pve | pve | `.100` | `100.101.225.43` | The Proxmox host | +| CT101 | dockge | `.65` | — | The Docker machine, managed through Dockge on :5001. Stacks live in `/opt/stacks//` | +| CT102 | tailscale-router | dhcp | `100.107.201.72` | Subnet router, advertising and approved for `192.168.116.0/24` | +| CT103 | caddy | `.119` | `100.64.125.107` | Reverse proxy: Caddy, listening on Tailscale only | +| CT104 | hermes | `.120` | — | The Discord agent backed by Claude | | VM100 | haos | `.88` | — | Home Assistant OS | -| — | rpi4 | `.53` | `100.69.79.75` | サブサーバー(AdGuard 主系)※**制作で停止する前提のノード** | +| — | rpi4 | `.53` | `100.69.79.75` | Secondary server, primary AdGuard. Expected to be switched off during creative work | --- -## 2. アクセス早見表 +## 2. How to reach things -| 目的 | コマンド / URL | +| Purpose | Command or URL | |------|----------------| -| Proxmox | `ssh root@192.168.116.100` / `https://pve.gapul.net` | -| CT へ入る | pve から `pct enter ` または `pct exec -- ` | -| ラズパイ | `ssh pi@192.168.116.53` | -| Home Assistant SSH | `ssh hassio@192.168.116.88`(add-on / ed25519 鍵) | -| HA Web | `https://home.gapul.net` | -| Dockge | `https://dockge.gapul.net`(= `.65:5001`) | -| Git (Forgejo) | `https://git.gapul.net`(= `.65:3003`)。GitHub のセルフホスト・ミラー | -| AdGuard 主系/副系 | `https://dns.gapul.net` / `https://dns2.gapul.net` | -| 監視 | `https://status.gapul.net`(Uptime Kuma) | +| Proxmox | `ssh root@192.168.116.100`, `https://pve.gapul.net` | +| Enter a container | From pve: `pct enter ` or `pct exec -- ` | +| Raspberry Pi | `ssh pi@192.168.116.53` | +| Home Assistant over SSH | `ssh hassio@192.168.116.88`, through the add-on with an ed25519 key | +| Home Assistant web | `https://home.gapul.net` | +| Dockge | `https://dockge.gapul.net`, which is `.65:5001` | +| Git (Forgejo) | `https://git.gapul.net`, `.65:3003`. A self-hosted mirror of GitHub | +| AdGuard primary and secondary | `https://dns.gapul.net`, `https://dns2.gapul.net` | +| Monitoring | `https://status.gapul.net`, Uptime Kuma | -- SSH 認証は **Bitwarden agent の ed25519 鍵**(`SHA256:2WG8EZOQ47X+XFzjXtuoytt8e3K8qsJd7r/FdbFKmM4`)。 -- CT(.65/caddy 等)へは Mac から直接 SSH 不可 → **pve 経由(`pct`)**で操作する。 -- `*.gapul.net` は **Tailscale 接続時のみ**到達可(Caddy が tailnet 限定待受のため)。 +SSH authenticates with the ed25519 key in the Bitwarden agent +(`SHA256:2WG8EZOQ47X+XFzjXtuoytt8e3K8qsJd7r/FdbFKmM4`). -### アカウント / 認証情報 +Containers such as `.65` and caddy cannot be reached over SSH from the Mac; go through pve with +`pct`. And `*.gapul.net` only resolves while Tailscale is connected, because Caddy listens on +the tailnet alone. -> ⚠️ **パスワードはこのリポジトリに書かない**(git 管理のため)。実パスワードは **Bitwarden** で管理し、ここには「ユーザー名」と「保管場所」のみ記載する。 +### Accounts -| サービス | ユーザー名 | 認証方法 / パスワード保管 | -|----------|-----------|---------------------------| -| Proxmox (SSH) | `root` | ed25519 鍵(Bitwarden agent) | -| Proxmox (Web `pve.gapul.net`) | `root@pam` | パスワード → Bitwarden | -| ラズパイ rpi4 | `pi` | ed25519 鍵(焼き込み時に投入)+ 緊急用パスワード(Bitwarden) | -| Home Assistant (Web) | `gapul` | パスワード → Bitwarden | -| Home Assistant (SSH add-on) | `hassio` | ed25519 鍵 / passwordless sudo | -| AdGuard 主系・副系 | `gapul` | パスワード → Bitwarden(同期 `adguardhome-sync` も同一資格を使用。実体は CT101 `/opt/stacks/adguardhome-sync/compose.yaml` の env にのみ存在) | -| Dockge (`.65:5001`) | (要確認) | パスワード → Bitwarden。忘失時は CT101 で `docker exec -it dockge npm run reset-password` | -| Uptime Kuma (`status.gapul.net`) | (初回設定で作成) | パスワード → Bitwarden | -| Forgejo (`git.gapul.net`) | `gapul` | admin。パスワード + API token → Bitwarden | -| Cloudflare API (Caddy DNS-01) | — | トークンは CT103 `/etc/caddy/cf.env`(git 管理外) | - -- 秘密情報を dotfiles に入れる場合は **SOPS**(`.sops.yaml`)で暗号化し、平文でコミットしない。`work/ conf/ .env` は `.gitignore` 済み。 - -### SSH 鍵 +Passwords do not go in this repository, since it is in git. Real passwords live in Bitwarden; +what follows is the username and where to find the password. -秘密鍵は **Bitwarden Desktop の SSH agent** が保持(Mac にファイルとして秘密鍵は置かない。`~/.ssh/*.bak` はバックアップ)。agent は 2 鍵を提供: - -| ラベル | 種別 | フィンガープリント | 用途 | +| Service | Username | Authentication and where the password lives | +|----------|-----------|---------------------------| +| Proxmox over SSH | `root` | ed25519 key in the Bitwarden agent | +| Proxmox web, `pve.gapul.net` | `root@pam` | Password, in Bitwarden | +| Raspberry Pi rpi4 | `pi` | ed25519 key, written at imaging time, plus an emergency password in Bitwarden | +| Home Assistant web | `gapul` | Password, in Bitwarden | +| Home Assistant SSH add-on | `hassio` | ed25519 key, passwordless sudo | +| AdGuard primary and secondary | `gapul` | Password, in Bitwarden. `adguardhome-sync` uses the same credentials, and they exist only in the env of CT101's `/opt/stacks/adguardhome-sync/compose.yaml` | +| Dockge, `.65:5001` | to be confirmed | Password, in Bitwarden. If it is lost, run `docker exec -it dockge npm run reset-password` on CT101 | +| Uptime Kuma, `status.gapul.net` | created on first run | Password, in Bitwarden | +| Forgejo, `git.gapul.net` | `gapul` | Admin. Password and API token in Bitwarden | +| Cloudflare API, for Caddy's DNS-01 | — | The token is in CT103's `/etc/caddy/cf.env`, outside git | + +Anything secret that does go into dotfiles is encrypted with SOPS through `.sops.yaml` and never +committed in the clear. `work/`, `conf/` and `.env` are already in `.gitignore`. + +### SSH keys + +The private keys are held by Bitwarden Desktop's SSH agent; no private key sits on the Mac as a +file, and `~/.ssh/*.bak` are backups. The agent offers two keys: + +| Label | Type | Fingerprint | Use | |--------|------|--------------------|------| -| `GitHub` | ed25519 | `SHA256:2WG8EZOQ47X+XFzjXtuoytt8e3K8qsJd7r/FdbFKmM4` | **homelab 標準**。下記すべてに登録 | -| `mvrx-dev` | RSA | `SHA256:4WQSmfgnETVJxL6I7R13l5b8Qu6mGpx65FTs7ELov0U` | 別途(業務系)。homelab では未使用 | +| `GitHub` | ed25519 | `SHA256:2WG8EZOQ47X+XFzjXtuoytt8e3K8qsJd7r/FdbFKmM4` | The homelab standard, registered everywhere below | +| `mvrx-dev` | RSA | `SHA256:4WQSmfgnETVJxL6I7R13l5b8Qu6mGpx65FTs7ELov0U` | Work, unused in the homelab | + +The public key to register: -**登録に使う公開鍵(ed25519)**: ``` -公開鍵は `secrets/secrets.yaml` の `ssh_authorized_keys` をSOPS経由で配布する。 +The public key is distributed from `ssh_authorized_keys` in `secrets/secrets.yaml` through SOPS. ``` -**この公開鍵の登録先**: +Where it is registered: -| ホスト | ユーザー | 登録方法 | +| Host | User | How | |--------|---------|---------| -| pve `.100` | `root` | 既存(`~/.ssh/authorized_keys`) | -| rpi4 `.53` | `pi` | Raspberry Pi Imager の焼き込み時に投入 | -| HA `.88` | `hassio` | add-on Configuration の `ssh.authorized_keys` | -| CT102 tailscale-router | `root` | 既存 | +| pve `.100` | `root` | Already in `~/.ssh/authorized_keys` | +| rpi4 `.53` | `pi` | Written by Raspberry Pi Imager at flash time | +| HA `.88` | `hassio` | `ssh.authorized_keys` in the add-on configuration | +| CT102 tailscale-router | `root` | Already present | -- CT101(dockge) / CT103(caddy) / CT104(hermes) には **この鍵は未登録** → pve から `pct enter/exec` でアクセスする。 -- 新ホストに登録する1行: - ```bash - mkdir -p ~/.ssh && install -m 600 /path/to/decrypted/authorized_keys ~/.ssh/authorized_keys - ``` +The key is not registered on CT101 (dockge), CT103 (caddy) or CT104 (hermes); reach those from +pve with `pct enter` or `pct exec`. To register it on a new host: -**参考: 各ホストのサーバ公開鍵(known_hosts 検証用)** -- HA add-on `.88`: `SHA256:AxGmsu9vVDDMjp9+xKcZtIwTq7fePBlx3ruPUOgNzho`(add-on 再作成で変わることがある→ `ssh-keygen -R 192.168.116.88` で更新) +```bash +mkdir -p ~/.ssh && install -m 600 /path/to/decrypted/authorized_keys ~/.ssh/authorized_keys +``` + +Host keys, for known_hosts: the HA add-on at `.88` is +`SHA256:AxGmsu9vVDDMjp9+xKcZtIwTq7fePBlx3ruPUOgNzho`. Recreating the add-on can change it, in +which case `ssh-keygen -R 192.168.116.88`. --- -## 3. DNS(AdGuard 二重化 + Tailscale 配布) +## 3. DNS: two AdGuards, distributed by Tailscale -ローカル DNS を主系(ラズパイ)/副系(CT101)で冗長化し、広告ブロックを全 tailnet 端末へ配布する。 +Local DNS is made redundant with a primary on the Pi and a secondary on CT101, and ad blocking +is pushed to every device on the tailnet. ``` -全 tailnet 端末 - │ DNS(Tailscale Global nameservers / Override ON, 上から優先): - │ 1. 100.69.79.75 (主系 Pi) ← 通常 - │ 2. 192.168.116.65 (副系 CT101) ← 主系停止時のフェイルオーバー(CT102 subnet router 経由で外からも到達) - │ 3. Quad9 9.9.9.9 (最終保険) ← 両系ダウン時のみ +every tailnet device + │ DNS, from Tailscale's global nameservers with Override on, in order: + │ 1. 100.69.79.75 primary, the Pi ← normally + │ 2. 192.168.116.65 secondary, CT101 ← failover, reachable from outside through the CT102 subnet router + │ 3. Quad9 9.9.9.9 ← only if both are down ▼ - AdGuard 主系(.53) ──[adguardhome-sync 10分毎]──▶ AdGuard 副系(.65) - └ gapul.net だけ Split DNS → Cloudflare(1.1.1.1) → Caddy(tailnet IP) + AdGuard primary (.53) ──[adguardhome-sync, every 10 min]──▶ AdGuard secondary (.65) + └ gapul.net alone goes through split DNS to Cloudflare (1.1.1.1) and on to Caddy's tailnet IP ``` -- 主系: `configs/homelab/adguard/primary-pi/`(ラズパイ上 `~/adguard/primary-pi/`) -- 副系・同期: CT101 `/opt/stacks/adguard-secondary/`、`/opt/stacks/adguardhome-sync/`(Dockge 管理) -- AdGuard 管理ユーザー: `gapul` -- **`gapul.net` の Split DNS(→Cloudflare)は必須**。消すと他端末で `*.gapul.net` が解決不能になる(公開DNSが CGNAT(100.64.x) を rebinding 保護で弾くため)。 -- **公開 DNS をグローバルに常設しない**こと。LAN内IP(.65)を入れると外出端末でタイムアウト→激遅になる(→ subnet router 経由で到達させる現構成が正解)。 -- Tailscale DNS 設定は管理コンソール `https://login.tailscale.com/admin/dns`。 +- Primary: `configs/homelab/adguard/primary-pi/`, deployed to `~/adguard/primary-pi/` on the Pi. +- Secondary and sync: `/opt/stacks/adguard-secondary/` and `/opt/stacks/adguardhome-sync/` on + CT101, managed by Dockge. +- The AdGuard admin user is `gapul`. +- The split DNS for `gapul.net` to Cloudflare is required. Remove it and `*.gapul.net` stops + resolving on every other device, because public DNS rejects CGNAT addresses (100.64.x) as + rebinding. +- Do not leave a LAN address in the global DNS list permanently. Putting `.65` there makes + devices away from home time out and everything crawl, which is why reaching it through the + subnet router is the right shape. +- Tailscale's DNS settings are at `https://login.tailscale.com/admin/dns`. --- -## 4. 運用手順:ラズパイの停止 / 復帰(制作モード) +## 4. Stopping and restarting the Pi, for creative work + +The Pi is expected to be switched off from time to time, for TouchDesigner and similar. The +secondary at `.65` keeps DNS running while it is off. -ラズパイは「制作(TouchDesigner 等)で一時的に止める」前提のノード。止めても副系(.65)が DNS を継続する。 +To stop it, always gracefully, to protect the SD card: -**止める**(SDカード保護のため必ず graceful に): ```bash ssh pi@192.168.116.53 'sudo poweroff' -# 緑LED(ACT)が消えたら電源を抜いてOK(赤LED=給電中の表示なので点いたままで正常) -# Uptime Kuma の「AdGuard Primary Pi (.53)」モニターは Pause しておくと誤報が出ない +# once the green ACT LED goes out it is safe to pull the power; the red LED just means power is present +# pause the "AdGuard Primary Pi (.53)" monitor in Uptime Kuma to avoid a false alarm ``` -**戻す**: -```bash -# 電源を挿すだけ → 自動起動。AdGuard 主系も restart:unless-stopped で自動復帰 -ssh pi@192.168.116.53 'docker ps' # 復帰確認 -# Kuma のモニターを Resume -``` +To bring it back, plug it in — it boots on its own, and AdGuard comes back with +`restart:unless-stopped`. Confirm with `ssh pi@192.168.116.53 'docker ps'` and resume the +monitor. -停止中の挙動(検証済み): 副系(.65)で名前解決・広告ブロック継続、`gapul.net` も Cloudflare split で生存。 +While it is off, verified behaviour: the secondary at `.65` keeps resolving names and blocking +ads, and `gapul.net` survives through the Cloudflare split. --- -## 5. リバースプロキシ(Caddy / CT103) +## 5. Reverse proxy: Caddy on CT103 + +Caddy runs natively under systemd on CT103. Its configuration is `/etc/caddy/Caddyfile`, from +`configs/homelab/caddy/Caddyfile` in dotfiles. It listens on Tailscale only, with ports 80 and +443 closed on the LAN, and gets its TLS through Cloudflare DNS-01, with `CF_API_TOKEN` in +`/etc/caddy/cf.env`. -- Caddy は CT103 で **native systemd** 稼働。設定: `/etc/caddy/Caddyfile`(dotfiles: `configs/homelab/caddy/Caddyfile`)。 -- **Tailscale 限定待受**(LAN の 80/443 は閉)。TLS は Cloudflare DNS-01(`CF_API_TOKEN` は `/etc/caddy/cf.env`)。 +To publish a new service: + +1. Add a block to the Caddyfile on CT103: -**新サービスを公開する手順**: -1. CT103 の Caddyfile にブロックを追加: ``` newsvc.gapul.net { tls { dns cloudflare {env.CF_API_TOKEN} } reverse_proxy 192.168.116.65:PORT } ``` -2. Cloudflare の A レコードは手で足さない。`just dns` が - Caddy の vhost 一覧と突き合わせて足りないものを出し、`just dns --apply` で作る - (proxied=false, ttl=60、宛先は tailnet の homeserver)。公開側(トンネル経由)の - CNAME も同じコマンドが `homelab/cloudflared.nix` の ingress と突き合わせる。 + +2. Do not add the Cloudflare A record by hand. `just dns` compares Caddy's vhost list against + what exists and lists what is missing; `just dns --apply` creates them, with proxied false, + ttl 60, pointing at homeserver's tailnet address. The same command checks the public CNAMEs, + the ones through the tunnel, against the ingress in `homelab/cloudflared.nix`. 3. `pct exec 103 -- systemctl reload caddy` --- -## 6. 監視(Uptime Kuma / status.gapul.net) +## 6. Monitoring: Uptime Kuma at status.gapul.net -- CT101 の `/opt/stacks/uptime-kuma/`。`status.gapul.net` で UI。 -- モニター(DNS 監視 = 実際に解決できるかを毎分チェック): - - **AdGuard Secondary (.65)** … 常時稼働前提=**本気でアラート** - - **AdGuard Primary Pi (.53)** … 制作で止めるので**情報用**。停止前に Pause - - **Home Assistant (.88)** … HTTP 監視 -- ⚠️ **通知が未設定**。Discord webhook を登録すると実アラートが飛ぶ(既存の Discord 連携と相性◎)。 +Runs from `/opt/stacks/uptime-kuma/` on CT101, with its UI at `status.gapul.net`. The DNS +monitors check every minute that a name actually resolves. + +- AdGuard secondary (.65) is supposed to be up permanently, so it alerts for real. +- AdGuard primary on the Pi (.53) gets switched off deliberately, so it is informational. + Pause it before stopping the Pi. +- Home Assistant (.88) is an HTTP check. + +No notification target is configured. Registering a Discord webhook would make the alerts +actually arrive, and fits the Discord integration that already exists. --- -## 7. Home Assistant(VM100 / .88) - -- **SSH**: `ssh hassio@192.168.116.88`(Advanced SSH & Web Terminal add-on、ed25519 鍵承認済、非rootだが passwordless sudo 可)。 - - add-on は `ssh.password` / `ssh.authorized_keys` のどちらか未設定だと**起動拒否→再起動ループ**するので注意。 - - 弾かれる時: ユーザー名は `hassio`(自分のMacユーザーではない)。ホスト鍵変更時は `ssh-keygen -R 192.168.116.88`。 -- **リバプロ整合**: Caddy 配下なので `configuration.yaml` に設定済み: - ```yaml - http: - use_x_forwarded_for: true - trusted_proxies: - - 192.168.116.119 # Caddy(CT103) - ``` - (Caddy 側の `header_up -X-Forwarded-For` ハックは撤去済み=実クライアントIPが記録される) -- ⚠️ **HA Core の再起動は「設定 → システム」の電源アイコンから**行う。Developer Tools 経由の再起動は失敗して設定が読み込まれないことがあった。 +## 7. Home Assistant, VM100 at .88 + +SSH in with `ssh hassio@192.168.116.88`, through the Advanced SSH & Web Terminal add-on, with +the ed25519 key authorised. The user is not root but has passwordless sudo. + +The add-on refuses to start and loops if neither `ssh.password` nor `ssh.authorized_keys` is +set. If you are refused, check that the username is `hassio` rather than your Mac username. If +the host key changed, `ssh-keygen -R 192.168.116.88`. + +Because it sits behind Caddy, `configuration.yaml` has: + +```yaml +http: + use_x_forwarded_for: true + trusted_proxies: + - 192.168.116.119 # Caddy on CT103 +``` + +The `header_up -X-Forwarded-For` hack on the Caddy side has been removed, so real client +addresses are recorded. + +Restart Home Assistant Core from Settings, System, through the power icon. Restarting through +Developer Tools has failed in a way that left the configuration unread. --- -## 8. 運用上のハマりどころ(既知) +## 8. Things that have caught us out -| 事象 | 対処 | +| Symptom | What to do | |------|------| -| Docker Hub の pull 上限(自宅公開IP) | `docker pull --platform linux/amd64 mirror.gcr.io/:` → `docker tag` で元名に。デーモン再起動不要 | -| 別アーキのイメージ流用で `exec format error` | arm64(Pi)↔amd64(CT) は混在不可。アーキを合わせて pull | -| pve への SSH が時々 `Permission denied` | 経路が flap する。数秒おいて再試行で繋がる | -| zram が二重化して `failed`(Pi/trixie) | OS標準 `systemd-zram-generator` に一本化(zram-tools は撤去)。`bootstrap.sh` 反映済み | -| この Mac で広告ブロックが効かない | Mac の Tailscale クライアントが古いDNSを固着 → **Mac再起動**で解消 | +| Docker Hub pull limits on the house's public IP | `docker pull --platform linux/amd64 mirror.gcr.io/:`, then `docker tag` back to the original name. No daemon restart needed | +| `exec format error` from reusing an image built for another architecture | arm64 on the Pi and amd64 on the containers do not mix. Pull the matching architecture | +| SSH to pve occasionally refused with `Permission denied` | The path flaps. Wait a few seconds and retry | +| zram doubled up and failed, on the Pi under trixie | Standardise on the OS's own `systemd-zram-generator` and drop zram-tools. Already reflected in `bootstrap.sh` | +| Ad blocking not working on this Mac | The Mac's Tailscale client is stuck on old DNS. Rebooting the Mac fixes it | --- -## 9. Git ホスト(Forgejo / git.gapul.net) +## 9. Git hosting: Forgejo at git.gapul.net + +A git remote that is not GitHub, so the code is still at home if GitHub goes down or the +account is frozen. -GitHub 以外の git リモート。GitHub 障害・アカウント凍結時にもコードが自宅に残る冗長化。 +A Docker stack on CT101 in `/opt/stacks/forgejo/`, from `configs/homelab/forgejo/` in dotfiles, +at `.65:3003` behind `git.gapul.net`. git is HTTPS only, with `DISABLE_SSH=true`, and push and +pull authenticate with a token. The admin is `gapul`, and the store is SQLite. `INSTALL_LOCK=true` +in the compose env skips the web installer, and the admin was created from the CLI with +`docker exec -u git forgejo forgejo admin user create --admin ...`. -- CT101 の Docker スタック(`/opt/stacks/forgejo/`、dotfiles: `configs/homelab/forgejo/`)。`.65:3003` → `git.gapul.net`。 -- git は **HTTPS のみ**(`DISABLE_SSH=true`)。push/pull は token 認証。admin = `gapul`。SQLite。 -- `INSTALL_LOCK=true`(compose env)で Web インストーラを飛ばし、admin は CLI で作成 - (`docker exec -u git forgejo forgejo admin user create --admin ...`)。 +It runs as a pull mirror: Forgejo pulls from GitHub every 15 minutes. Nothing about the Mac's +git usage changes — pushes still go to GitHub and get copied home. Because the pull is outbound, +the redundancy works even without Caddy, which is only there for the web UI, clone and push. -**運用は Pull Mirror 方式**: Forgejo が GitHub から定期 pull(15分毎)。Mac の git 操作は変えず、 -push 先は GitHub のまま自宅へ複製され続ける。**外向き pull なので Caddy 無しでも冗長化は機能** -(Caddy は Web UI / clone / push 用)。 +State: -**状態**: -- ✅ `dotfiles`(public)を Pull Mirror 登録済・稼働中。 -- ⏳ `obsidian-vault`(private)は GitHub の read token が必要 → Forgejo UI の Migration で token を貼って追加。 -- ⏳ Caddy 公開: Caddyfile に `git.gapul.net → .65:3003` 追加済。§5 の手順で Cloudflare A レコード追加 + - `pct exec 103 -- systemctl reload caddy` が必要(未実施でもミラーは動く)。 +- `dotfiles`, public, is registered as a pull mirror and working. +- `obsidian-vault`, private, needs a GitHub read token. Add it through Migration in the Forgejo + UI, pasting the token. +- Publishing through Caddy: `git.gapul.net` to `.65:3003` is in the Caddyfile. It still needs + the Cloudflare A record from section 5 and `pct exec 103 -- systemctl reload caddy`. The + mirror works without this. -**ミラー追加手順**(UI): + → New Migration → GitHub → repo URL → **「This repository will be a mirror」** に -チェック → Migrate(private repo は GitHub token を入力)。 +To add a mirror through the UI: plus, New Migration, GitHub, the repo URL, tick "This repository +will be a mirror", Migrate. Private repositories need a GitHub token. --- -## 10. バックアップ(Proxmox ゲスト) +## 10. Backups of the Proxmox guests -2段構え。vzdump で全ゲストをローカルに固め、restic で Google Drive へオフサイト退避(既存 Mac と**同一 restic リポジトリ**に host=pve タグで相乗り=重複排除)。 +Two stages. vzdump packs every guest locally, and restic ships that offsite to Google Drive, +sharing the same restic repository as the Mac under a `host=pve` tag so deduplication works +across both. ``` -02:00 vzdump(全ゲスト, snapshotモード, zstd, keep-last=2)→ /var/lib/vz/dump -03:00 restic backup /var/lib/vz/dump → rclone:google-drive:restic-backup (host=pve, tag=pve-vzdump) +02:00 vzdump, all guests, snapshot mode, zstd, keep-last=2, into /var/lib/vz/dump +03:00 restic backup /var/lib/vz/dump → rclone:google-drive:restic-backup, host=pve, tag=pve-vzdump restic forget --host pve --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune ``` -- vzdump ジョブ: PVE の `/etc/pve/jobs.cfg`(`pvesh get /cluster/backup`)。`local` ストレージへ。 -- restic 層(pve は Debian なので Nix管理外の自前構成): - - スクリプト: `/usr/local/bin/restic-pve-offsite.sh` - - timer: `/etc/systemd/system/restic-pve-offsite.{service,timer}`(毎日 03:00, `Persistent=true`) - - 秘密: `/root/.config/rclone/rclone.conf`(GDriveトークン)/ `/root/.restic.pw`(restic パスワード)= SOPS 復号値を pve root専用に配置(git管理外) -- **手動実行**: `systemctl start restic-pve-offsite.service` -- **確認**: `RCLONE_CONFIG=/root/.config/rclone/rclone.conf RESTIC_REPOSITORY=rclone:google-drive:restic-backup RESTIC_PASSWORD_FILE=/root/.restic.pw restic snapshots --host pve` -- **リストア**: `restic restore --target /tmp/r` で取り出し → PVE UI / `pct restore` / `qmrestore` で復元。 -- ⚠️ 通知未整備: PVE バックアップ通知先が `mail-to-root`(実質届かない)。失敗検知のため **Discord 通知**に向けるのが次の改善。restic 層は母艦/macmini/rpi4 と同じ ntfy 通知に寄せてもよい。 - -### 共有 restic リポジトリの全体像 / 母艦・macmini・rpi4 も相乗り(2026-07-20) -pve だけでなく **母艦Mac / macmini / rpi4** も同一 restic リポジトリ `rclone:google-drive:restic-backup` を共有する(host 名で相乗り・重複排除)。各ホストの対象・スケジュール・デプロイ手順・秘密の場所・**復元テスト手順**は → [`configs/homelab/restic/README.md`](../configs/homelab/restic/README.md)。成果物(スクリプト/systemd/launchd)も同ディレクトリに収録。 -- 共有リポジトリのため `restic forget` は必ず `--host` スコープ、**prune は母艦の日次のみ**(排他ロック競合回避)。pve の後継 homeserver も同リポジトリに host=homeserver で相乗り(`nix/homelab/backup.nix`、03:00)。 -- 母艦の restic-monitor は `nix/lib/restic-common.nix` の `monitoredHosts` を**ホスト別**に見る。共有リポジトリでは「リポジトリ全体の最新1本」を見ても他の1台が生きている限り警告が出ず、1台だけ止まった状態を検知できない。**ホストを増やしたら monitoredHosts に足す**(足し忘れるとそのホストは無監視、退役ホストは外さないと毎日誤報)。 -- 書き込む側が居なくなったホストのスナップショット(pve の vzdump、移行時の cold/warm pass)は `archive` タグを付けて保持期限から外してある。外すと keep-monthly 6 の期限切れで消える。 -- Google OAuth は Production 公開済でトークン失効しない(旧: Testing で約7日ごとに失効し全ホスト同時停止の罠)。失敗時は ntfy 通知(母艦/macmini/rpi4)。 -- **スマホから中身プレビュー**: `files.gapul.net` = pve 上の restic mount(read-only FUSE, `--no-lock`) + Filebrowser を Caddy 公開(tailnet 限定・認証なし)。CF は `files.gapul.net` の個別 A レコード(→ caddy tailnet IP)が必要。 -- 復元テスト(2026-07-20): 母艦/pve/macmini/rpi4 全ホストで、復元ファイルの SHA256 がライブと一致することを確認済み。 -- dash(Homepage) の Backup セクションに Backrest(閲覧/リストア)と Filebrowser(中身プレビュー)を登録済み。 - -### HA 自動バックアップ(TODO) -VM100 全体は上記 vzdump で取得済み。HA 内蔵の自動バックアップ(設定単位の復元用・暗号化パスワード要設定)は未設定。 +The vzdump job is in PVE's `/etc/pve/jobs.cfg`, visible through `pvesh get /cluster/backup`, +writing to `local` storage. ---- +The restic layer is hand-built, since pve runs Debian and is outside nix: -## 11. Proxmox 運用改善(2026-06-29) +- Script: `/usr/local/bin/restic-pve-offsite.sh` +- Timer: `/etc/systemd/system/restic-pve-offsite.{service,timer}`, daily at 03:00 with + `Persistent=true` +- Secrets: `/root/.config/rclone/rclone.conf` for the Google Drive token and `/root/.restic.pw` + for the restic password, both decrypted from SOPS and placed for pve's root alone, outside git -**実施済み** -- CT102 `onboot=1`(再起動後も subnet router 復帰) -- `swappiness=10`、`fail2ban`(sshd jail 有効) -- **SSH 鍵のみ化**(pve: `PasswordAuthentication no` / `PermitRootLogin prohibit-password`、`/etc/ssh/sshd_config.d/99-hardening.conf`) -- バックアップ(§10)、CT101 rootfs 96%→51%(未使用イメージ prune + `pct resize 101 rootfs +8G`) -- `apt dist-upgrade` 157件適用(subscription nag も自動除去) +Run it by hand with `systemctl start restic-pve-offsite.service`. Check it with: -**未実施 TODO(要・判断/枠/秘密)** -1. **カーネル再起動**: `7.0.12-1-pve` 導入済だが稼働中は `6.17.4`。`ssh root@192.168.116.100 reboot` で反映(全ゲスト数分停止/DNS主系は Pi のため継続)。 -2. **PVE ファイアウォール**: リモート有効化は見送り。理由=CT102 が `firewall=1`(要調整)/ pve 自身が Tailscale ノード(UDP 41641 等の許可が要)/ LAN 経由だと tailnet 到達性を検証できずロックアウト時はコンソール復旧のみ。pve は外部非公開+SSH鍵のみ+fail2ban で実用十分。やるなら **コンソール attended** で。 -3. **root@pam の 2FA**: 未設定(Web UI で TOTP 登録)。 -4. **バックアップ/障害の Discord 通知**: 現状 `mail-to-root`(届かない)。PVE 通知ターゲットに Discord webhook を登録。 -5. **メモリ微オーバーコミット**: 割当 15872MB > 物理 15360MB。CT104(4G→2G)/CT101(6G→4G) で解消可(任意・緊急でない)。 +```bash +RCLONE_CONFIG=/root/.config/rclone/rclone.conf \ +RESTIC_REPOSITORY=rclone:google-drive:restic-backup \ +RESTIC_PASSWORD_FILE=/root/.restic.pw \ +restic snapshots --host pve +``` + +To restore, `restic restore --target /tmp/r` and then bring it back through the PVE UI, +`pct restore` or `qmrestore`. + +Notifications are not sorted: PVE's backup notification target is `mail-to-root`, which in +practice never arrives. Pointing it at Discord so failures are noticed is the next improvement. +The restic layer could equally move to the same ntfy path the Mac, mac mini and Pi use. + +### The shared restic repository, 2026-07-20 + +The Mac, the mac mini and the Pi share the same repository, +`rclone:google-drive:restic-backup`, alongside pve, separated by host name and deduplicated +across all of them. What each host backs up, on what schedule, how it is deployed, where its +secrets live and how to test a restore are in +[`configs/homelab/restic/README.md`](../configs/homelab/restic/README.md), which also holds the +scripts, systemd units and launchd plists. + +- Because the repository is shared, `restic forget` is always scoped with `--host`, and pruning + happens only in the Mac's daily run, to avoid fighting over the exclusive lock. homeserver, + which replaced pve, uses the same repository as `host=homeserver` from + `nix/homelab/backup.nix` at 03:00. +- The Mac's restic-monitor looks at `monitoredHosts` in `nix/lib/restic-common.nix` per host. + In a shared repository, looking at the single newest snapshot across the whole repository + never warns as long as one machine is still writing, so a single dead host goes unnoticed. + Add new hosts to `monitoredHosts` — forget and that host is unmonitored; leave a retired host + in and it produces a false alarm every day. +- Snapshots from hosts that no longer write, meaning pve's vzdump and the cold and warm passes + from the migration, are tagged `archive` and exempted from retention. Remove the tag and + keep-monthly 6 will expire them. +- The Google OAuth client is published as Production, so the token does not expire. It used to + be in Testing, where it expired roughly weekly and stopped every host at once. Failures + notify through ntfy on the Mac, the mac mini and the Pi. +- To browse the contents from a phone, `files.gapul.net` is a read-only FUSE restic mount on + pve with `--no-lock`, plus Filebrowser, published through Caddy on the tailnet without + authentication. Cloudflare needs its own A record for `files.gapul.net` pointing at Caddy's + tailnet address. +- Restore test, 2026-07-20: on the Mac, pve, the mac mini and the Pi, the SHA256 of the + restored files matched the live ones. +- Homepage's Backup section lists Backrest, for browsing and restoring, and Filebrowser, for + previewing contents. + +### Home Assistant's own backups, still to do + +The whole of VM100 is covered by vzdump above. Home Assistant's built-in automatic backup, for +restoring at the configuration level, is not set up and needs an encryption password chosen. --- -## 関連 -- 各サービス設定: `configs/homelab/{adguard,caddy,forgejo,raspberrypi}/` -- ラズパイ初期化: `configs/homelab/raspberrypi/bootstrap.sh` -- 汎用チートシート: `docs/CHEATSHEET.md` +## 11. Proxmox improvements, 2026-06-29 + +Done: + +- CT102 set to `onboot=1`, so the subnet router comes back after a reboot +- `swappiness=10` and fail2ban with the sshd jail enabled +- Keys only for SSH: `PasswordAuthentication no` and `PermitRootLogin prohibit-password` in + `/etc/ssh/sshd_config.d/99-hardening.conf` on pve +- The backups in section 10, and CT101's rootfs brought from 96% to 51% by pruning unused + images and `pct resize 101 rootfs +8G` +- 157 packages through `apt dist-upgrade`, which also removed the subscription nag + +Still to do, each needing a decision, capacity or a secret: + +1. **Reboot for the kernel.** `7.0.12-1-pve` is installed but `6.17.4` is running. + `ssh root@192.168.116.100 reboot` picks it up, stopping every guest for a few minutes; DNS + continues because the primary is on the Pi. +2. **The PVE firewall.** Enabling it remotely was ruled out: CT102 has `firewall=1` and would + need adjusting, pve is itself a Tailscale node and needs UDP 41641 and friends allowed, and + over the LAN there is no way to verify tailnet reachability, so a lockout would mean console + recovery only. pve is not exposed publicly and has keys-only SSH plus fail2ban, which is + adequate. If it is done, it should be done at the console. +3. **2FA for root@pam**, not configured. Register a TOTP through the web UI. +4. **Discord notifications for backups and failures.** Currently `mail-to-root`, which does not + arrive. Register a Discord webhook as a PVE notification target. +5. **Slight memory overcommit**, 15872 MB allocated against 15360 MB physical. Reducing CT104 + from 4 G to 2 G and CT101 from 6 G to 4 G resolves it. Optional, not urgent. --- -## 12. メディアサーバー (Jellyfin / Navidrome / Samba) +## Related -CT101(dockge) に Docker スタックで配置。メディアは Proxmox `local-lvm` から切り出した専用ボリューム。 +- Per-service configuration: `configs/homelab/{adguard,caddy,forgejo,raspberrypi}/` +- Bootstrapping the Pi: `configs/homelab/raspberrypi/bootstrap.sh` +- General cheatsheet: `docs/CHEATSHEET.md` -### ストレージ -- CT101 `mp0`: `local-lvm:vm-101-disk-1` (200G) → `/mnt/jellyfin-media`(`pct set 101 -mp0 local-lvm:200,mp=/mnt/jellyfin-media`、稼働中ホットプラグ可) -- 構成: `/mnt/jellyfin-media/{movies,tv,music}` / リサイズ: `pct resize 101 mp0 +NNG` +--- -### サービス -| サービス | URL | ポート | スタック | +## 12. Media servers: Jellyfin, Navidrome and Samba + +Docker stacks on CT101, with the media on a dedicated volume carved out of Proxmox's +`local-lvm`. + +### Storage + +CT101's `mp0` is `local-lvm:vm-101-disk-1`, 200 G, mounted at `/mnt/jellyfin-media`, created +with `pct set 101 -mp0 local-lvm:200,mp=/mnt/jellyfin-media` and hot-pluggable while running. +Inside it are `movies`, `tv` and `music`. Resize with `pct resize 101 mp0 +NNG`. + +### Services + +| Service | URL | Port | Stack | |---|---|---|---| -| Jellyfin(動画) | https://jellyfin.gapul.net | 8096 | `/opt/stacks/jellyfin/` | -| Navidrome(音楽) | https://navidrome.gapul.net | 4533 | `/opt/stacks/navidrome/` | -| Samba(共有) | `smb://192.168.116.65/media` | 445 | `/opt/stacks/samba/` | +| Jellyfin, video | https://jellyfin.gapul.net | 8096 | `/opt/stacks/jellyfin/` | +| Navidrome, music | https://navidrome.gapul.net | 4533 | `/opt/stacks/navidrome/` | +| Samba, file sharing | `smb://192.168.116.65/media` | 445 | `/opt/stacks/samba/` | + +Caddy on CT103 maps each `*.gapul.net` to `192.168.116.65:`, with Cloudflare A records +pointing at `100.64.125.107`. + +Samba's user is `gapul`, with the password in CT101's `/opt/stacks/samba/.smb-pass`, outside +git. From the Mac, Finder, Command-K, `smb://192.168.116.65/media`; from the tailnet it goes +through the CT102 subnet router. + +### Hardware transcoding on the Intel iGPU, Alder Lake-N + +`pct set 101 -dev0 /dev/dri/renderD128,gid=993 -dev1 /dev/dri/card1,gid=44`, hot-pluggable. +Jellyfin's compose gets `devices: [/dev/dri:/dev/dri]`, and in the UI, Dashboard, Playback, +Hardware acceleration, enable VAAPI on `/dev/dri/renderD128`. -- Caddy(CT103) で各 `*.gapul.net` → `192.168.116.65:`、Cloudflare A → `100.64.125.107`。 -- Samba: user `gapul` / パスワードは CT101 `/opt/stacks/samba/.smb-pass`(git管理外)。Mac は Finder `⌘K` → `smb://192.168.116.65/media`(tailnetからはCT102 subnet router経由)。 +### Dashboard and monitoring -### HWトランスコード (Intel iGPU / Alder Lake-N) -- `pct set 101 -dev0 /dev/dri/renderD128,gid=993 -dev1 /dev/dri/card1,gid=44`(ホットプラグ可) -- Jellyfin compose に `devices: [/dev/dri:/dev/dri]`。UI: Dashboard→Playback→Hardware acceleration→**VAAPI**(`/dev/dri/renderD128`)を有効化。 +Homepage's `services.yaml` has a Media group with Jellyfin and Navidrome. Adding HTTP monitors +for each URL in Uptime Kuma is worth doing. -### dash(Homepage) / 監視 -- Homepage `services.yaml` に Media グループ(Jellyfin/Navidrome)追加済み。 -- Uptime Kuma に HTTP モニター追加推奨(各URL)。 +### Note -### メモ -- Docker Hub 制限回避: Jellyfin=lscr.io / Navidrome・Samba=mirror.gcr.io。 +To avoid the Docker Hub limits, Jellyfin comes from lscr.io and Navidrome and Samba from +mirror.gcr.io. diff --git a/docs/HOMESERVER_MIGRATION.md b/docs/HOMESERVER_MIGRATION.md index c9a76d15..3ba4c1e3 100644 --- a/docs/HOMESERVER_MIGRATION.md +++ b/docs/HOMESERVER_MIGRATION.md @@ -1,76 +1,81 @@ -# homeserver 移行手順(Proxmox → NixOS) +# Migrating homeserver from Proxmox to NixOS -pve(Proxmox 単一ノード)を、ハイパーバイザ無しの NixOS 1台に置き換える当日の手順。 -設定側は `nix/hosts/homeserver.nix` と `nix/homelab/` に揃っていて、CI の VM テストが -起動まで確認済み。残りはデータの移送と、手で置く秘密と、一度きりの認証。 +The day's runbook for replacing pve, a single Proxmox node, with one NixOS machine and no +hypervisor. The configuration side was already finished in `nix/hosts/homeserver.nix` and +`nix/homelab/`, with the CI VM test confirming it boots. What was left was moving the data, +placing the secrets by hand, and the one-time authentications. -段階移行ではないので、サービスごとの切り戻しは無い。pve を消した瞬間に戻り先が消える。 +This is not a staged migration, so there is no per-service rollback. The moment pve is gone, +there is nothing to go back to. -判断待ちと手作業の一覧は [HOMESERVER_TODO.md](HOMESERVER_TODO.md)。本書は当日の手順のみ。 +Decisions and manual steps still outstanding are in [HOMESERVER_TODO.md](HOMESERVER_TODO.md). +This page is the runbook alone. --- -## 0. 消える前に理解しておくこと +## 0. What to understand before anything disappears -いまの vzdump のバックアップは `/var/lib/vz/dump` にある。これは置き換える NVMe の -同じディスクの上なので、フォーマットした瞬間に一緒に消える。**箱の外にコピーするまで、 -バックアップはバックアップとして機能していない。** +The current vzdump backups are in `/var/lib/vz/dump`, which sits on the same NVMe that is about +to be replaced, so formatting takes them with it. Until a copy exists outside the box, the +backup is not functioning as a backup. -切り戻しが必要になった場合の道は「pve を入れ直して、箱の外に退避したものから戻す」だけ。 -数時間かかる。 +If a rollback becomes necessary, the only route is reinstalling pve and restoring from whatever +was copied out. That takes hours. -作業中も家のインターネットは生きる。DNS 主系は Raspberry Pi にあり、この箱には無い。 +The house keeps its internet throughout. The primary DNS is on the Raspberry Pi, not on this +machine. --- -## 1. 前日までにやること +## 1. The day before -### 1.1 Home Assistant の最後のバックアップ +### 1.1 A final Home Assistant backup ```sh -ssh -J root@100.101.225.43 root@192.168.116.88 # 使えなければ pve のコンソールから +ssh -J root@100.101.225.43 root@192.168.116.88 # or from pve's console if that does not work ha backups new ``` -Container 版には組み込みバックアップが無い。Supervisor が作る最後のフルバックアップになる。 +The container version has no built-in backup, so this is the last full backup Supervisor will +ever make. -### 1.2 平文パスワードのローテーション +### 1.2 Rotating the plaintext passwords -以下2つは compose ファイルに平文で入っていた。samba のほうは `command:` にあるので -`ps` にも出る。移行を機に変える。 +Two passwords were in the compose files in the clear, and samba's is in `command:`, which means +it shows up in `ps` as well. The migration is a good moment to change them: -- archivebox の管理者パスワード -- samba の共有ユーザー `gapul` のパスワード +- ArchiveBox's admin password +- The samba share user `gapul`'s password -新しい値は移行後の `/var/lib/secrets/archivebox.env` と `smbpasswd` に入れる。 +The new values go into `/var/lib/secrets/archivebox.env` and `smbpasswd` after the migration. -### 1.3 Cloudflare の A レコード +### 1.3 Cloudflare A records -2種類ある。**追加**が2件と、**付け替え**が20件超。 +Two kinds of change: two records to add, and more than twenty to repoint. -追加は `esphome.gapul.net` と `nodered.gapul.net`。この2つは HA のアドオン ingress -経由で開いていたもので、Supervisor が無くなると入口が消えるため独立した vhost にした。 -既存と同形(type A / proxied=false / ttl=60)。 +The additions are `esphome.gapul.net` and `nodered.gapul.net`. Both were previously reached +through Home Assistant add-on ingress, and with Supervisor gone that entrance disappears, so +they become independent vhosts. Same shape as the existing ones: type A, proxied false, ttl 60. -付け替えのほうが本体で、**移行後でないとできない**。`*.gapul.net` は全件 Caddy の -tailnet IP(現在 `100.64.125.107` = CT103)を指しているが、Caddy が新ホストへ移ると -tailnet IP が変わる。20件超を手で直すと必ず取りこぼし、「そのサービスだけ繋がらない」を -後日踏む。専用スクリプトを用意した。 +The repointing is the bigger half, and it can only be done after the migration. Every +`*.gapul.net` currently points at Caddy's tailnet address, `100.64.125.107` on CT103, and moving +Caddy to the new host changes that address. Editing twenty-odd records by hand guarantees +missing one and finding out later that a single service does not connect, so there is a script. ```sh export CF_API_TOKEN=... # Zone:DNS:Edit -scripts/cf-repoint-records.sh --from 100.64.125.107 --to <新ホストの tailnet IP> -# 一覧を確認してから -scripts/cf-repoint-records.sh --from 100.64.125.107 --to <新ホストの tailnet IP> --apply +scripts/cf-repoint-records.sh --from 100.64.125.107 --to +# check the listing, then +scripts/cf-repoint-records.sh --from 100.64.125.107 --to --apply ``` -既定は dry-run。新ホストの tailnet IP は `tailscale ip -4` で分かる。 -ワイルドカード証明書は取るが、DNS レコードはワイルドカードではないので1件ずつ必要。 +It is a dry run by default. `tailscale ip -4` gives the new host's address. The certificate is a +wildcard, but the DNS records are not, so each one is needed individually. -### 1.4 restic の疎通確認 +### 1.4 Checking restic still works -rclone の Google Drive トークンは1週間ほど放置すると失効し、両ホストとも黙って止まる。 -当日に気づくと退避先が無い。 +The rclone Google Drive token expires after about a week of disuse, and both hosts then stop +silently. Finding that out on the day means having nowhere to put the data. ```sh restic -r rclone:google-drive:restic-backup snapshots | tail -5 @@ -78,52 +83,54 @@ restic -r rclone:google-drive:restic-backup snapshots | tail -5 --- -## 2. データ退避(35GB) +## 2. Getting the data out, 35 GB -退避先は Google Drive の restic リポジトリか母艦 Mac。**200GB のマウントは同じ NVMe の上 -なので退避先にならない。** +It goes either to the restic repository on Google Drive or to the Mac. The 200 GB mount is on +the same NVMe, so it is not a destination. -### 2.0 先にサービスを止める +### 2.0 Stop the services first -稼働中の postgres / couchdb / SQLite のデータディレクトリをそのままコピーすると、 -書き込み途中の状態が取れて復元時に壊れている。ここが移行で一番静かに失敗する場所。 +Copying a running postgres, couchdb or SQLite data directory captures a half-written state that +turns out to be corrupt when restored. This is the quietest way for a migration to fail. ```sh ssh pve 'pct exec 101 -- sh -c "cd /opt/stacks && for d in */; do (cd \$d && docker compose down); done"' ssh pve 'qm shutdown 100' # HAOS ``` -止めずに済ませたい場合は、少なくとも DB だけ論理ダンプを取る(`pg_dump`、CouchDB は -レプリケーション)。ただし止められる状況なら止めるのが確実で速い。 +If stopping is not an option, at least take logical dumps of the databases: `pg_dump`, and +replication for CouchDB. But when stopping is possible it is both safer and faster. -**この時点から家のサービスは落ちる。** DNS 主系は Pi なのでインターネットは生きる。 +From this point the house's services are down. The internet keeps working, since primary DNS is +on the Pi. -### 2.0.1 先に確保しておくもの(止める前でもよい) +### 2.0.1 Grab the irreplaceable things first, which can happen before stopping -失うと復元不可能で、かつ小さいもの。時間のかかる本番コピーとは別に、単独で先に取る。 -**2026-08-09 に取得済み**、母艦の `~/tmp/homeserver-migration/`: +Small things that cannot be reconstructed if lost. Worth taking separately from the slow main +copy. These were taken on 2026-08-09, into `~/tmp/homeserver-migration/` on the Mac: -| ファイル | 中身 | +| File | Contents | |---|---| -| `haos-backup-dc879ac6.tar`(28MB) | HAOS のフルバックアップ。HA config + Matter ファブリック + Node-RED のフロー | -| `syncthing-identity.tar`(20KB) | Syncthing の cert.pem / key.pem / config.xml | -| `CHECKSUMS.txt` | 上2つの sha256 | +| `haos-backup-dc879ac6.tar`, 28 MB | The HAOS full backup: HA config, the Matter fabric, the Node-RED flows | +| `syncthing-identity.tar`, 20 KB | Syncthing's cert.pem, key.pem and config.xml | +| `CHECKSUMS.txt` | sha256 of both | -これらは移行日までに中身が変わりうる(HA の DB は動き続ける)ので、当日にもう一度取り直す。 -それでも先に取っておく価値があるのは、ファブリックと Syncthing の身元だけは -「壊れていても古くても、無いよりはるかにマシ」だから。 +Their contents can change before migration day — HA's database keeps running — so take them +again on the day. Taking them early is still worth it, because for the fabric and Syncthing's +identity, damaged or stale beats absent by a wide margin. -### 2.1 バインドマウントとホスト側ディレクトリ +### 2.1 Bind mounts and host directories ```sh ssh pve 'pct exec 101 -- tar -C /opt -czf - stacks' > ~/migration/ct101-stacks.tar.gz ssh pve 'pct exec 101 -- tar -C /mnt -czf - jellyfin-media' > ~/migration/bulk.tar.gz ``` -### 2.2 名前付きボリューム(コピーでは移らない) +### 2.2 Named volumes, which copying does not move -docker は `/var/lib/docker/volumes`、podman は `/var/lib/containers/storage/volumes` に -置く。ディレクトリを持っていくのではなく export/import する。名前は新旧で一致している。 +docker keeps them in `/var/lib/docker/volumes` and podman in +`/var/lib/containers/storage/volumes`. Rather than carrying the directory across, export and +import them. The names match on both sides. ```sh for v in dawarich_dawarich_db_data dawarich_dawarich_public dawarich_dawarich_shared \ @@ -133,129 +140,132 @@ for v in dawarich_dawarich_db_data dawarich_dawarich_public dawarich_dawarich_sh done ``` -### 2.3 Home Assistant 一式(フルバックアップ1本で済む) +### 2.3 All of Home Assistant, in one full backup -HAOS からは個別にディレクトリを吸い出すのではなく、Supervisor のフルバックアップを -1本作るのが早い。config もアドオンのデータも全部その中に入る。 +Rather than pulling directories out of HAOS individually, one Supervisor full backup is faster +and contains everything, config and add-on data alike. -**作成はシリアルコンソールから。** SSH アドオンは protection mode が有効で、中から -`ha` を叩くと `unauthorized: missing or invalid API token` で拒否される。 -一方 SSH アドオンからは `/backup` が見えるので、作成はシリアル、取り出しは SSH と -役割を分ける。 +Create it from the serial console. The SSH add-on runs with protection mode on, so running `ha` +from inside it is refused with `unauthorized: missing or invalid API token`. The SSH add-on can +see `/backup` though, so create over serial and fetch over SSH. ```sh -# 作成(pve から HAOS のシリアルコンソールへ。login: root でパスワード無し) +# create, from pve into HAOS's serial console. Login is root with no password. ssh pve { printf "\n"; sleep 3; printf "root\n"; sleep 6; \ printf "nohup ha backups new --name pre-nixos-migration > /tmp/bk.log 2>&1 &\n"; sleep 5; \ printf "exit\n"; sleep 2; } | timeout 35 socat - UNIX-CONNECT:/var/run/qemu-server/100.serial0 -# 取り出し(母艦から) +# fetch, from the Mac ssh hassio@192.168.116.88 'ls -lh /backup/' ssh hassio@192.168.116.88 'cat /backup/.tar' > ~/tmp/homeserver-migration/haos-backup.tar ``` -中身と復元先の対応: +What is inside and where it goes: -| バックアップ内の tar | 中身 | 復元先 | +| tar inside the backup | Contents | Restore to | |---|---|---| -| `homeassistant.tar.gz` の `data/` | config 一式(`configuration.yaml` / `.storage` / `custom_components`(HACS) / `home-assistant_v2.db` / `esphome/`) | `/var/lib/hass` | -| `core_matter_server.tar.gz` の `data/` | **Matter ファブリック**(`certificates/`) | `/var/lib/matter-server` | -| `a0d7b954_nodered.tar.gz` の `config/` | Node-RED のフロー | `/var/lib/node-red` | -| `core_mosquitto.tar.gz` | mosquitto の永続データ | `/var/lib/mosquitto` | -| `5c53de3b_esphome.tar.gz` | `addon.json` のみで**中身は無い** | 不要 | +| `data/` in `homeassistant.tar.gz` | The whole config: `configuration.yaml`, `.storage`, `custom_components` (HACS), `home-assistant_v2.db`, `esphome/` | `/var/lib/hass` | +| `data/` in `core_matter_server.tar.gz` | The Matter fabric, in `certificates/` | `/var/lib/matter-server` | +| `config/` in `a0d7b954_nodered.tar.gz` | The Node-RED flows | `/var/lib/node-red` | +| `core_mosquitto.tar.gz` | mosquitto's persistent data | `/var/lib/mosquitto` | +| `5c53de3b_esphome.tar.gz` | `addon.json` and nothing else | Not needed | -ESPHome の yaml はアドオンのデータではなく HA の config 側(`data/esphome/`)にある。 -アドオンの tar を探しても空なので注意。 +The ESPHome yaml is not add-on data; it is on the HA config side, in `data/esphome/`. Searching +the add-on's tar finds nothing, which is confusing. -Matter のファブリックを失うと全 Matter デバイスを工場出荷リセットして再ペアリングする -ことになる。取り出したら `tar -tzf` で `data/certificates/` が入っていることを必ず確認する。 +Losing the Matter fabric means factory-resetting and re-pairing every Matter device. After +extracting, always confirm with `tar -tzf` that `data/certificates/` is in there. -### 2.3.1 VM105 のディスクイメージ(保険) +### 2.3.1 VM105's disk image, as insurance -会社の L2TP トンネルは `nix/homelab/vpn-relay.nix` に宣言してあるが、**CI では検証できない** -(サンドボックスから会社の終端に繋げない)。ネイティブ版が初回で上がらなかった場合に -仕事が止まるので、旧 VM のディスクを丸ごと持っておく。 +The company L2TP tunnel is declared in `nix/homelab/vpn-relay.nix`, but CI cannot verify it — +the sandbox cannot reach the company endpoint. If the native version does not come up first +time, work stops, so keep the whole old VM disk. ```sh ssh pve 'qm stop 105; dd if=/dev/pve/vm-105-disk-0 bs=4M status=progress | zstd -T0' > ~/migration/vm105.img.zst ``` -10GB のうち実使用は少ないので圧縮すれば小さい。新ホストで `libvirt` に食わせれば -数分で元の中継が復活する。トンネルが新環境で一度でも上がったら捨ててよい。 +Only a little of the 10 GB is actually used, so it compresses well. Feeding it to `libvirt` on +the new host brings the old relay back in minutes. Once the tunnel has come up even once in the +new environment, it can be thrown away. -### 2.4 Syncthing の身元 +### 2.4 Syncthing's identity -`/opt/stacks/syncthing/config/config/` にある cert.pem と key.pem。これが device ID の -実体で、再生成すると Mac から見て別のマシンになり、全フォルダを再スキャンする。 +cert.pem and key.pem in `/opt/stacks/syncthing/config/config/`. They are the device ID; +regenerating them makes the Mac see a different machine and rescan every folder. -### 2.5 退避物の検証 +### 2.5 Verify what was copied out -戻せないバックアップを取っても意味がないので、tar を1本 test 展開して中身を確認する。 +A backup that cannot be restored is not worth taking, so extract one tar as a test and look +inside it. --- -## 3. インストール +## 3. Installing -### 3.1 ISO +### 3.1 The ISO -このリポジトリの recovery ISO を使う。母艦は aarch64-darwin なので手元ではビルドできない。 -CI の Recovery ISO ジョブが毎回ビルドして artifact に上げているので、それを落とす。 +Use this repository's recovery ISO. The Mac is aarch64-darwin and cannot build it locally, but +CI's Recovery ISO job builds one every time and uploads it as an artifact. ```sh gh run download --name "nixos-recovery-" --dir ~/tmp/iso -# 中の SHA256 と付属の checksum を照合してから書き込む +# check the SHA256 against the bundled checksum before writing it ``` -**インストールする世代と同じコミットの ISO を使うこと。** zpool は作成時の ZFS が -feature flag を有効にするので、ISO 側が新しすぎると、インストールした側が -プールを import できない状態になりうる。同一コミットなら同じ nixpkgs なので一致する。 +Use the ISO from the same commit as the generation being installed. A zpool enables feature +flags from the ZFS that created it, so an ISO that is too new can leave the installed system +unable to import the pool. The same commit means the same nixpkgs, so they agree. -この ISO には以下が入っている。 +The ISO contains: -- ZFS(`zpool` / `zfs`。これが無いと disko がプールを作れない) -- disko、git、neovim、sops、age、cryptsetup -- flakes 有効化済み。**これが無いと `disko --flake` も `nixos-install --flake` も - 最初の一手で "experimental Nix feature 'nix-command' is disabled" で止まる** -- 自前 cachix を substituter に登録済み。CI がホストのクロージャを push しているので、 - インストールはビルドではなくダウンロードになる -- この手順書そのもの。`homeserver-guide` で読める(tailscale 認証前でネットが無い状態でも読める) +- ZFS, `zpool` and `zfs`. Without them disko cannot create the pool. +- disko, git, neovim, sops, age, cryptsetup. +- flakes, already enabled. Without that, both `disko --flake` and `nixos-install --flake` stop + on the very first command with "experimental Nix feature 'nix-command' is disabled". +- Our own cachix registered as a substituter. CI pushes the host's closure, so installing is a + download rather than a build. +- This runbook. `homeserver-guide` opens it, readable before Tailscale authentication and + therefore without a network. -### 3.2 ディスクを切る +### 3.2 Partitioning -**ここから不可逆。** 実行前に、2章の退避物が箱の外にあることをもう一度確認する。 +Nothing after this is reversible. Before running it, confirm once more that everything from +section 2 is outside the box. ```sh sudo disko --mode destroy,format,mount --flake github:gapul/dotfiles?dir=nix#homeserver ``` -GPT を切り直し、1GB の ESP と、残り全部の zpool `rpool` を作る。データセットは -root / nix / var-lib / srv / home。srv だけスナップショット対象外。 +This lays down a fresh GPT with a 1 GB ESP and gives the rest to a zpool called `rpool`. The +datasets are root, nix, var-lib, srv and home; only srv is excluded from snapshots. -### 3.3 インストール +### 3.3 Installing ```sh sudo nixos-install --flake github:gapul/dotfiles?dir=nix#homeserver sudo nixos-enter --root /mnt -c 'passwd gapul' -# 鍵を置く (再起動後に入る唯一の手段。/home は独立データセットなので -# マウントされていることを findmnt で確認してから置く) +# place the key, the only way in after the reboot. /home is its own dataset, +# so confirm it is mounted with findmnt before writing to it. findmnt /mnt/home sudo install -d -m 700 -o 1000 -g 100 /mnt/home/gapul/.ssh curl -sL https://github.com/gapul.keys | sudo tee /mnt/home/gapul/.ssh/authorized_keys sudo chown 1000:100 /mnt/home/gapul/.ssh/authorized_keys sudo chmod 600 /mnt/home/gapul/.ssh/authorized_keys -# ★必須: プールを明け渡してから再起動する -# これを飛ばすとプールにインストーラの hostId が残り、新システムは -# forceImportRoot=false のため import を拒否して起動しない。 +# Required: hand the pool back before rebooting. +# Skip this and the pool keeps the installer's hostId, and the new system, +# with forceImportRoot=false, refuses to import it and will not boot. sudo umount -R /mnt sudo zpool export rpool reboot ``` -**`zpool export` を忘れた場合の復旧**: 起動が initrd の緊急シェルに落ちる。 -`emergencyAccess = true` にしてあるのでパスワード無しで入れるので、そこで +If `zpool export` was forgotten, the boot drops into the initrd emergency shell. +`emergencyAccess = true` is set, so it lets you in without a password: ```sh zpool import -f rpool @@ -265,59 +275,60 @@ reboot --- -## 4. 秘密と一度きりの認証 +## 4. Secrets and one-time authentication -すべて root 所有の 0400。sops-nix はこのホストの age 鍵ができるまで使えないので、 -最初は手で置く。鍵ができたら `secrets/secrets.yaml` に移す。 +Everything is root-owned and 0400. sops-nix is unusable until this host has an age key, so the +first placement is by hand; once the key exists they move into `secrets/secrets.yaml`. -| パス | 中身 | +| Path | Contents | |---|---| -| `/var/lib/secrets/acme-cloudflare.env` | `CF_DNS_API_TOKEN=...`(旧 `/etc/caddy/cf.env` と同じトークン。**変数名が違う**) | -| `/var/lib/secrets/restic.password` | restic リポジトリのパスワード | -| `/var/lib/secrets/rclone.conf` | rclone の設定(google-drive リモート) | -| `/var/lib/secrets/mosquitto-ha.password` | mosquitto_passwd 形式のハッシュ部分のみ | -| `/var/lib/secrets/gatus.env` | `NTFY_TOPIC` と `NTFY_TOKEN`(これが無いと gatus が起動しない) | -| `/var/lib/secrets/.env` × 8 | キーの一覧は `nix/homelab/README.md` | -| `/var/lib/secrets/mvrx/` 6ファイル | 会社トンネル一式。同じく `nix/homelab/README.md` | +| `/var/lib/secrets/acme-cloudflare.env` | `CF_DNS_API_TOKEN=...`, the same token as the old `/etc/caddy/cf.env`, but under a different variable name | +| `/var/lib/secrets/restic.password` | The restic repository password | +| `/var/lib/secrets/rclone.conf` | The rclone configuration, with the google-drive remote | +| `/var/lib/secrets/mosquitto-ha.password` | Just the hash part, in mosquitto_passwd format | +| `/var/lib/secrets/gatus.env` | `NTFY_TOPIC` and `NTFY_TOKEN`. gatus does not start without them | +| `/var/lib/secrets/.env`, eight of them | The keys are listed in `nix/homelab/README.md` | +| `/var/lib/secrets/mvrx/`, six files | The company tunnel, also in `nix/homelab/README.md` | -`.env` のキー名は旧 `.env` と一致しないものがある。paperless の `PAPERLESS_SECRET` -はコンテナ側では `PAPERLESS_SECRET_KEY`、miniflux はパスワード単体ではなく `DATABASE_URL` -全体が必要。ここを間違えるとサービスは失敗せずに間違った認証情報で起動する。 +Some of the `.env` key names differ from the old `.env` files. paperless's +`PAPERLESS_SECRET` is `PAPERLESS_SECRET_KEY` inside the container, and miniflux needs the whole +`DATABASE_URL` rather than the password alone. Getting these wrong does not make the service +fail; it starts with the wrong credentials. -mosquitto のハッシュ: +The mosquitto hash: ```sh -mosquitto_passwd -c /tmp/p ha # 対話でパスワード入力 +mosquitto_passwd -c /tmp/p ha # prompts for the password cut -d: -f2 /tmp/p > /var/lib/secrets/mosquitto-ha.password ``` -一度きりの認証: +One-time authentication: ```sh -sudo tailscale up --advertise-routes=192.168.116.0/24,192.168.1.0/24 # 管理画面でルート承認 -tailscale ip -4 # この IP に 1.3 の DNS 付け替えを行う +sudo tailscale up --advertise-routes=192.168.116.0/24,192.168.1.0/24 # approve the routes in the admin console +tailscale ip -4 # this is the address the DNS repointing in 1.3 uses sudo smbpasswd -a gapul -# AdGuard の管理者アカウントは https://dns2.gapul.net の初回画面で作る +# AdGuard's admin account is created on the first-run screen at https://dns2.gapul.net ``` --- -## 5. データ復元 +## 5. Restoring the data -**所有者に注意。** 旧環境は全部 root で動く docker コンテナだったが、ネイティブサービスは -それぞれ専用ユーザーで動く。 +Watch the ownership. Everything in the old setup ran as root in docker containers; the native +services each run as their own user. -| 復元先 | 所有者 | +| Destination | Owner | |---|---| -| `/var/lib/homelab//` | root(コンテナが root で動くため) | -| `/srv/`(旧 /mnt/jellyfin-media) | root | +| `/var/lib/homelab//` | root, since the containers run as root | +| `/srv/`, formerly /mnt/jellyfin-media | root | | `/var/lib/hass`, `/var/lib/matter-server` | root | -| `/var/lib/syncthing/.config/syncthing/`(cert.pem, key.pem) | `syncthing` | +| `/var/lib/syncthing/.config/syncthing/`, cert.pem and key.pem | `syncthing` | | `/var/lib/esphome` | `esphome` | | `/var/lib/node-red` | `node-red` | -| `/var/lib/AdGuardHome` | root(大文字に注意) | +| `/var/lib/AdGuardHome` | root, and note the capitals | -名前付きボリュームの取り込み: +Importing the named volumes: ```sh for v in dawarich_dawarich_db_data ... ; do @@ -326,43 +337,42 @@ for v in dawarich_dawarich_db_data ... ; do done ``` -Home Assistant の設定を1行直す。`/var/lib/hass/configuration.yaml` の +One line of Home Assistant's configuration needs changing. In +`/var/lib/hass/configuration.yaml`: ```yaml http: - trusted_proxies: [192.168.116.119] # 旧 Caddy コンテナ + trusted_proxies: [192.168.116.119] # the old Caddy container ``` -を `127.0.0.1` にする。Caddy が同じホストになったため。直さないと全リクエストが 400 で -弾かれ、痕跡は HA のログにしか出ない。 +becomes `127.0.0.1`, since Caddy is now on the same host. Leave it and every request is rejected +with a 400, with no trace of why outside HA's own log. ---- - -### 5.1 アドレスが変わるので直すもの +### 5.1 Things that break because an address changed -旧ホストの IP を直書きしている設定が何箇所かある。サービスは起動するので、 -気づくのは「使おうとしたとき」になる。 +A few places hardcode the old host's address. The services still start, so the discovery +happens when someone tries to use them. -| どこ | 何を | +| Where | What | |---|---| -| `/var/lib/hass/configuration.yaml` | `trusted_proxies` を `127.0.0.1` に(前述) | -| `/var/lib/homelab/homepage/config/services.yaml` | 19箇所の IP 直書き。`192.168.116.100`(pve、消滅)、`.88`(HAOS→localhost)、`.65`(CT101→localhost)。`.53`(Pi)と `.91` はそのまま | -| スマホの OwnTracks | Dawarich の宛先が旧 CT101 の `:3005`。新ホストのアドレスへ | -| MQTT クライアント | mosquitto の認証が HA ユーザー依存から独自ユーザーに変わる | +| `/var/lib/hass/configuration.yaml` | `trusted_proxies` to `127.0.0.1`, as above | +| `/var/lib/homelab/homepage/config/services.yaml` | 19 hardcoded addresses. `192.168.116.100` (pve, gone), `.88` (HAOS, now localhost), `.65` (CT101, now localhost). `.53` (the Pi) and `.91` stay | +| OwnTracks on the phone | Points at Dawarich on the old CT101's `:3005`. Change it to the new host | +| MQTT clients | mosquitto's authentication moves from the HA user to its own | -### 5.1.1 復元に必要なものはバックアップの中に入れない +### 5.1.1 What restoring needs must not live inside the backup -`/var/lib/secrets` は restic のバックアップ対象に入っている。つまり **restic の -パスワードと rclone の設定だけは、バックアップの外**(Bitwarden)に無いと、 -バックアップを開けられない。移行前に手元にあることを確認しておく。 +`/var/lib/secrets` is included in the restic backup, which means the restic password and the +rclone configuration have to exist outside the backup — in Bitwarden — or the backup cannot be +opened at all. Confirm they are to hand before migrating. -母艦の `~/.ssh/config` は sops 管理で、`pve` などのエントリが入っている。 -`homeserver` のエントリ追加はそちら側の作業になる。 +The Mac's `~/.ssh/config` is managed through sops and contains entries such as `pve`. Adding a +`homeserver` entry is work on that side. -### 5.2 旧スナップショットの掃除(移行後でよい) +### 5.2 Cleaning up the old snapshots, which can wait -pve は `--host pve --tag pve-vzdump` で Google Drive に vzdump を送っていた。 -その host はもう存在しないので、放っておくと永久に残る。 +pve sent its vzdump to Google Drive under `--host pve --tag pve-vzdump`. That host no longer +exists, so left alone they stay forever. ```sh restic forget --host pve --tag pve-vzdump --keep-last 1 --prune @@ -370,40 +380,41 @@ restic forget --host pve --tag pve-vzdump --keep-last 1 --prune --- -## 6. 検証 +## 6. Verifying ```sh systemctl --failed journalctl -p err -b --no-pager | tail -40 ``` -- https://status.gapul.net が緑になるか(gatus が20件を監視している) -- Matter デバイスが Home Assistant でオンラインに戻るか。戻らない場合はまずホストの - IPv6 を疑う。Matter は Wi-Fi 機器でも IPv6 を要求する -- Syncthing の device ID が `Y72TVZZ-...` のままか。変わっていたら 2.4 の復元に失敗している -- Mac から `restic snapshots` に homeserver タグの新しいスナップショットが出るか -- `dig @<新ホスト> example.com` が引けるか(53番は AdGuard が持つ) -- 会社トンネル: `systemctl status mvrx-vpn` と `ip -4 addr show ppp0`、tailnet 越しに - `ssh mvrx-nolang-dev` が通るか。**ここだけは CI で検証できていない唯一の箇所**なので、 - 駄目なら 2.3.1 のディスクイメージを libvirt で起動して仕事を優先する -- Jellyfin のハードウェアトランスコードが効くか(`/dev/dri` はメタルなら素直に見える) +- Does https://status.gapul.net go green? gatus watches twenty checks. +- Do the Matter devices come back online in Home Assistant? If not, suspect the host's IPv6 + first — Matter needs IPv6 even for Wi-Fi devices. +- Is Syncthing's device ID still `Y72TVZZ-...`? If it changed, the restore in 2.4 failed. +- Does `restic snapshots` from the Mac show a new snapshot tagged homeserver? +- Does `dig @ example.com` answer? AdGuard owns port 53. +- The company tunnel: `systemctl status mvrx-vpn`, `ip -4 addr show ppp0`, and whether + `ssh mvrx-nolang-dev` works over the tailnet. This is the one thing CI could not verify, so + if it fails, boot the disk image from 2.3.1 under libvirt and get work moving first. +- Does Jellyfin's hardware transcoding work? On bare metal `/dev/dri` shows up without effort. -コンテナは初回だけイメージ取得で時間がかかる。docker.io は mirror.gcr.io 経由に -宣言済みなのでレート制限は踏まないはず。 +Containers take a while the first time, pulling images. docker.io is already declared to go +through mirror.gcr.io, so the rate limit should not come up. -データベースを持つスタック(attic / dawarich / miniflux / paperless)は、初回起動で -アプリ側が一度死んで再起動する。compose の health 待ちが systemd の順序依存に変わり、 -「起動した」までしか見ないため。`Restart=always` が拾うので放っておいてよい。 +The stacks with databases — attic, dawarich, miniflux, paperless — die once on first start and +restart. The compose health wait becomes a systemd ordering dependency, which only knows that +the dependency started. `Restart=always` catches it, so leave it alone. --- -## 7. 切り戻し +## 7. Rolling back -pve は既に存在しないので、戻すなら Proxmox を入れ直して 2章の退避物から復元する。 -数時間コース。 +pve no longer exists, so going back means reinstalling Proxmox and restoring from section 2. +Several hours. -現実的な保険は NixOS 側の世代で、設定の問題であれば再起動1回で前の世代を選べる。 -ZFS のスナップショットは自動で取られているので、データの問題なら `zfs rollback`。 +The realistic insurance is NixOS generations: if the problem is configuration, one reboot +selects the previous generation. ZFS snapshots are taken automatically, so if the problem is +data, `zfs rollback`. -コンソールという保険は無くなる。pve の Web UI から noVNC で中に入る道が消えるので、 -起動しなくなったら物理アクセスになる。BIOS へは `systemctl reboot --firmware-setup`。 +What is gone is the console. There is no more noVNC into the machine from pve's web UI, so if it +stops booting, that means physical access. `systemctl reboot --firmware-setup` gets to the BIOS. diff --git a/docs/HOMESERVER_TODO.md b/docs/HOMESERVER_TODO.md index 4a40e915..2d996bd7 100644 --- a/docs/HOMESERVER_TODO.md +++ b/docs/HOMESERVER_TODO.md @@ -1,102 +1,111 @@ -# homeserver 移行の残り +# What is left after the homeserver migration -移行そのものは 2026-08-11 に完了した。コンテナ30個とネイティブサービス一式が稼働、 -メモリは 9.7GB → 6.3GB、tailnet IP は `100.127.129.31`。当日の手順と実地の知見は -[HOMESERVER_MIGRATION.md](HOMESERVER_MIGRATION.md) にある。 +The migration itself finished on 2026-08-11. Thirty containers and the native services are +running, memory went from 9.7 GB to 6.3 GB, and the tailnet address is `100.127.129.31`. The +steps taken that day, and what was learned doing it, are in +[HOMESERVER_MIGRATION.md](HOMESERVER_MIGRATION.md). -ここに残すのは、移行後に「使おうとして初めて壊れているとわかった」もの。移行前の -判断待ちと当日手順は役目を終えたので落とした。 +What stays here is the things that turned out to be broken only when someone tried to use them. +Pre-migration decisions and the day's runbook have served their purpose and are gone. -2026-08-16 に実機を一通り当たり直した結果を反映してある。 +Rechecked against the running machine on 2026-08-16. --- -## 見つけて直したもの (2026-08-16) +## Found and fixed on 2026-08-16 -記録として残す。どれも「サービスは起動しているのに中身が死んでいる」形で、 -gatus からも podman からも健全に見えていた。 +Kept as a record. Every one of these had the same shape: the service was up and the contents +were dead. gatus and podman both reported them healthy. -- **Home Assistant が 8/11 の起動以降ずっと recovery mode だった**。移行後に - `trusted_proxies` を `127.0.0.1` へ直したとき、`[127.0.0.1, ::1]` と書いたのが原因。 - YAML のフローシーケンスでは `::1` を引用符なしに置けない。5日間、Matter も自動化も - MQTT も動いていなかったのに、コンテナは `Up` でヘルスチェックも通っていた。 -- **MQTT が HAOS のアドオンを向いたままだった**。接続先が `core-mosquitto`、ユーザーが - `homeassistant`。mosquitto 側の `ha` は新規ユーザーなので、パスワードを作り直して - `/var/lib/secrets/mosquitto-ha.password` と Home Assistant の両方に入れ直した。 -- **Matter が `use_addon: true` のままだった**。接続先も `ws://core-matter-server:5580/ws`。 - この設定だと Home Assistant が Supervisor のアドオン管理を呼びに行き、 - `KeyError: 'hassio'` で統合ごと落ちる。ここから `backup` → `cloud` → `default_config` と - 連鎖して default_config が丸ごと立たなくなっていた。`ws://127.0.0.1:5580/ws` に変更。 - ファブリック (`/var/lib/matter-server`) は無事なので再ペアリングは不要だった。 -- **tailnet のサブネット経路が5日間ずっと未承認だった**。homeserver は 3 本 - (`192.168.116.0/24` / `192.168.1.0/24` / `10.80.1.0/24`) を広告していたのに承認されて - おらず、さらに移行で消えたはずの `tailscale-router` と `mvrx-relay` が primary を - 握ったままだった。承認より先に死んだノードを消す順でないと、承認してもそちらへ吸われる。 - 移行で物理的に無くなった 5 台 (`caddy` / `pve` / `tailscale-router` / `mvrx-relay` / - `mullvad-exit`) を削除してから 3 本を承認し、母艦から会社の開発機まで直接届くことを - 確認した (`192.168.1.36:22` と `10.80.1.36:22` の両方)。ProxyJump の迂回はもう要らない。 -- **homepage が services.yaml を読めていなかった**。中身の無いグループが残っていて - `null.forEach` を踏み、ダッシュボードにブックマークしか出ていなかった。中身も - 移行で嘘になっていた (削除済みの AdGuard、ネイティブ化して消えたコンテナ名、 - 旧 CT101 の IP を向いた glances) ので実機に合わせて書き直した。 -- **samba に `gapul` が居なかった**。インストール当日に手で置くはずの - `sudo smbpasswd -a gapul` が抜けていて、`pdbedit -L` が空だった。共有 `media` は - 匿名で一覧はできるので「見えている = 使える」と誤解しやすい。パスワードを作って - 登録し、認証が通ることと誤ったパスワードが `NT_STATUS_LOGON_FAILURE` で弾かれる - ことを確認、値は Bitwarden へ。 -- **Dawarich が tailnet のアドレスを拒否していた**。`dawarich.env` の - `APPLICATION_HOSTS` が `localhost,::1,127.0.0.1` のままで、`100.127.129.31:3005` に - 投げると Rails の host authorization が 403 を返す。localhost からしか触っていな - かったので気付かなかった。スマホの OwnTracks が動き出す前に踏むところだった。 - tailnet と LAN のアドレスを足して、母艦から OwnTracks 形式の POST を実際に通し、 - points が増えることまで確認した。 -- **fgc の ntfy 通知は一度も届いていなかった**。`NOTIFY` が - `ntfy://fgc:@127.0.0.1:8082/games` で、この 127.0.0.1 はコンテナ自身を指す。 - fgc は `podman` ネットワーク、ntfy は `ntfy_default` にいるので名前でも届かない。 - 「失敗時に apprise がコマンド全体をログに吐く」= 平文パスワードが見えていたのは、 - 毎回失敗していたから。宛先を `host.containers.internal:8082` に直し、認証は - `games` への write-only トークンに変更(パスワードより漏れたときに切りやすい)。 - apprise から1通通ることを確認済み。 +- **Home Assistant had been in recovery mode since it started on 8/11.** When + `trusted_proxies` was corrected to `127.0.0.1` after the migration, it was written as + `[127.0.0.1, ::1]`. In a YAML flow sequence `::1` cannot appear unquoted. For five days + Matter, automations and MQTT were all dead while the container reported `Up` and passed its + health check. +- **MQTT still pointed at the HAOS add-on**, connecting to `core-mosquitto` as user + `homeassistant`. The `ha` user on the new mosquitto is new, so the password was regenerated + and written into both `/var/lib/secrets/mosquitto-ha.password` and Home Assistant. +- **Matter was still set to `use_addon: true`**, pointing at + `ws://core-matter-server:5580/ws`. With that setting Home Assistant calls into Supervisor's + add-on management and the whole integration dies with `KeyError: 'hassio'`. That cascaded + through `backup` and `cloud` until `default_config` failed to load at all. Changed to + `ws://127.0.0.1:5580/ws`. The fabric in `/var/lib/matter-server` was intact, so nothing had + to be paired again. +- **The tailnet subnet routes had gone unapproved for five days.** homeserver advertised three + (`192.168.116.0/24`, `192.168.1.0/24`, `10.80.1.0/24`) and none were approved, while + `tailscale-router` and `mvrx-relay` — both supposedly gone in the migration — still held + primary. Deleting the dead nodes has to come before approving, or the traffic is pulled back + to them anyway. Five machines that no longer physically exist (`caddy`, `pve`, + `tailscale-router`, `mvrx-relay`, `mullvad-exit`) were removed, then the three routes were + approved, and reaching the work development machine directly from the Mac was confirmed on + both `192.168.1.36:22` and `10.80.1.36:22`. The ProxyJump detour is no longer needed. +- **homepage could not read services.yaml.** An empty group was left in it, which hit + `null.forEach`, and the dashboard showed nothing but bookmarks. The contents had also gone + stale in the migration — a deleted AdGuard, container names that had become native services, + a glances pointing at the old CT101 address — so it was rewritten against the running + machine. +- **samba had no `gapul` user.** The `sudo smbpasswd -a gapul` that should have been run on + install day was missed, and `pdbedit -L` was empty. The `media` share lists anonymously, + which makes it easy to assume that seeing it means it works. A password was created and + registered, authentication was confirmed, a wrong password was confirmed to fail with + `NT_STATUS_LOGON_FAILURE`, and the value went into Bitwarden. +- **Dawarich rejected its own tailnet address.** `APPLICATION_HOSTS` in `dawarich.env` was + still `localhost,::1,127.0.0.1`, so a request to `100.127.129.31:3005` got a 403 from Rails + host authorization. Nobody noticed because it had only ever been used from localhost. It + would have bitten the moment OwnTracks on the phone started sending. The tailnet and LAN + addresses were added, and an OwnTracks-shaped POST from the Mac was run through to confirm + the point count went up. +- **fgc's ntfy notifications had never once arrived.** `NOTIFY` was + `ntfy://fgc:@127.0.0.1:8082/games`, and that `127.0.0.1` means the container + itself. fgc is on the `podman` network and ntfy is on `ntfy_default`, so the name would not + resolve either. The reason the plaintext password was visible in the logs — apprise prints + the whole command on failure — is that it failed every single time. The destination is now + `host.containers.internal:8082`, and authentication moved from a password to a write-only + token for `games`, which is easier to revoke. One message from apprise was confirmed to + arrive. -Home Assistant の `.storage` と `/var/lib/hass` は可変状態なのでリポジトリには入らない。 -壊れたら `.storage/core.config_entries.bak-claude` と `configuration.yaml.bak-recovery` が -同じディレクトリにある。 +Home Assistant's `.storage` and `/var/lib/hass` are mutable state and are not in the +repository. If something breaks, `.storage/core.config_entries.bak-claude` and +`configuration.yaml.bak-recovery` sit in the same directory. -## 残っているもの +## Still open -- [ ] **Dawarich のスマホ側**。サーバ側は済み(ログインは `gapul@homeserver.local`、 - API キー発行済み、`APPLICATION_HOSTS` 修正済み)。残りは iPhone の OwnTracks を - `http://100.127.129.31:3005/api/v1/owntracks/points?api_key=…` に向けるところ。 - 家の外でも記録するならスマホの Tailscale が常時オンである必要がある -- [ ] **ブリッジの部屋が旧 server_name のまま**。discord の portal は 18 室が - `!…:matrix.gapul.net` で、join が 404 になる。7月のドメイン変更の取りこぼしで、 - 移行とは無関係。直すなら portal を作り直すことになるので判断が要る。telegram 側は - `No user logins found` でそもそもログインが無い -- [ ] **HAOS と旧ホスト由来のゴミ**。消すかどうかの判断待ち。`core.entity_registry` に - `platform: hassio` のエンティティが 63 件残っていて、これは永久に unavailable の - まま。`/var/lib/homelab` にも廃止したスタックの残骸ディレクトリが合計 17MB ある - (backrest 785K / uptime-kuma 649K / adguard-secondary 16M / adguardhome-sync / - wud / stirling-pdf.bak)。容量としては無視できるので、消す動機は見通しの良さだけ +- [ ] **Dawarich on the phone.** The server side is done: the login is + `gapul@homeserver.local`, an API key exists, and `APPLICATION_HOSTS` is fixed. What + remains is pointing OwnTracks on the iPhone at + `http://100.127.129.31:3005/api/v1/owntracks/points?api_key=…`. Recording away from home + requires Tailscale to stay on on the phone. +- [ ] **Bridge rooms still carry the old server_name.** Eighteen discord portals are + `!…:matrix.gapul.net` and joining them returns 404. This is fallout from the July domain + change, unrelated to the migration. Fixing it means recreating the portals, so it needs a + decision. The telegram side reports `No user logins found` and has no login at all. +- [ ] **Leftovers from HAOS and the old host.** Waiting on a decision to delete. + `core.entity_registry` still holds 63 entities with `platform: hassio`, permanently + unavailable. `/var/lib/homelab` holds 17 MB of directories from retired stacks + (backrest 785K, uptime-kuma 649K, adguard-secondary 16M, adguardhome-sync, wud, + stirling-pdf.bak). The space is negligible; the only reason to delete is clarity. -## 落ち着いてからやること +## Worth doing once things settle -急がないが、やると効くもの。 +Not urgent, but each of these pays off. -- [ ] **2本目の NVMe を足して ZFS ミラーに**。いまは単騎で冗長ゼロ (`rpool` 472G、使用 5%)。 - `zpool attach` で再インストール無しに変換できる。2台目のノードより先に効く -- [ ] **秘密を sops に移す**。ホストの age 鍵ができたら、`/var/lib/secrets` の - 手置きファイルを `secrets/secrets.yaml` へ。restic の ntfy 通知もこれ待ち -- [ ] **イメージをダイジェスト固定 + Renovate**。いまは `:latest` のままで、 - 再現性としては中途半端 -- [ ] **Raspberry Pi も NixOS に**。AdGuard の主系と副系が1つの定義から生成できる -- [ ] **gatus の死角を埋める**。ntfy とホスト自体が落ちたときは通知が飛ばない。 - Pi から homeserver を見る監視を置くのが素直。今回の recovery mode のように - 「HTTP 200 は返るが中身が死んでいる」も抜けるので、そこも考える価値がある -- [ ] **Mullvad exit node**(欲しければ)。CT106 は中身が無かったので新規構築。 - ホストの routing table を汚さないよう独自 netns で -- [ ] **deploy-rs か colmena**。ホストが増えてきたら -- [ ] **コンテナのネイティブ化**。forgejo / navidrome / miniflux などはモジュールがある。 - データ移行の手間に見合うと思ったものだけ。Conduit は 2026-08-11 に移行済み - (`services.matrix-conduit`)。ブリッジは見送った理由を `nix/homelab/matrix.nix` の - 冒頭に書いてある -- [ ] **attic を `services.atticd` へ**。postgres の移行が絡むので単独でやる +- [ ] **Add a second NVMe and make it a ZFS mirror.** There is currently one disk and no + redundancy (`rpool` 472 G, 5% used). `zpool attach` converts it without reinstalling. + This matters more than a second node. +- [ ] **Move secrets into sops.** Once the host has an age key, the hand-placed files under + `/var/lib/secrets` can go into `secrets/secrets.yaml`. restic's ntfy notification is + waiting on this too. +- [ ] **Put the Raspberry Pi on NixOS too**, so the primary and secondary AdGuard can be + generated from one definition. +- [ ] **Close gatus's blind spots.** No notification goes out when ntfy or the host itself is + down. Watching homeserver from the Pi is the obvious answer. It also misses the shape + seen above, where HTTP returns 200 but the contents are dead, which is worth thinking + about. +- [ ] **A Mullvad exit node**, if it turns out to be wanted. CT106 was empty, so this is a + fresh build. Give it its own netns rather than dirtying the host routing table. +- [ ] **deploy-rs or colmena**, once there are more hosts. +- [ ] **Turn containers into native services.** forgejo, navidrome and miniflux have modules. + Only the ones where the data migration is worth it. The bridges' reasons for staying as + they are are written at the top of `nix/homelab/matrix-bridges.nix`. +- [ ] **Move attic to `services.atticd`.** A postgres migration is involved, so do it on its + own. diff --git a/docs/NIXOS_DUALBOOT.md b/docs/NIXOS_DUALBOOT.md index ee65646d..0b69e3e1 100644 --- a/docs/NIXOS_DUALBOOT.md +++ b/docs/NIXOS_DUALBOOT.md @@ -1,369 +1,422 @@ -# NixOS / Windows デュアルブート 構築手順 (最大セキュリティ構成) +# Dual-booting NixOS alongside Windows, with everything locked down + +How to shrink a single SSD that already has a full Windows installation on it and put NixOS in +the free space. Assumes the Intel integrated GPU is the only graphics. Both systems end up +encrypted and signed: + +- The NixOS root is fully encrypted with LUKS. +- Booting goes through lanzaboote with Secure Boot enabled, signed with our own keys, keeping + the Microsoft keys so Windows still boots. +- Windows keeps BitLocker; it is only suspended, never decrypted. + +The configuration is already part of this repository's flake, under `nix/`: + +- `nix/hosts/nixos-laptop.nix` — the system: lanzaboote, LUKS with TPM2, zram, the Intel GPU, + Hyprland, fcitx5-mozc, tlp, fprintd, podman, tailscale, fwupd. +- `nixosConfigurations."nixos-laptop"` in `nix/flake.nix` — wires up lanzaboote, disko and + home-manager, pulling in `home/common.nix`, `home/linux.nix`, `home/hyprland.nix`, + `home/dev.nix` and `home/restic-backup-linux.nix`. +- `nix/hosts/nixos-laptop-disk.nix` — the declarative disko layout, covering the LUKS root + only, also exposed through `diskoConfigurations`. +- `scripts/install-nixos-laptop.sh` — a guarded install helper, the procedural alternative to + disko. +- `nix/hosts/nixos-laptop-hardware.nix` — the machine-specific file, generated on the real + hardware and added afterwards. The LUKS device UUID lives here. + +Do not delete Windows, and do not format the EFI system partition — it gets reused. Check every +disk name (`nvme0n1`, `sda` and so on) against `lsblk` on the actual machine before running +anything. -現在 Windows がフル構築済みの 1 台の SSD を**縮小して空き領域に NixOS を入れる**手順。 -GPU は Intel 内蔵のみ前提。**両 OS とも暗号化 + 署名ブート**にする: +--- -- NixOS root … **LUKS 全ディスク暗号化** -- ブート … **lanzaboote で Secure Boot 有効**(自分の鍵で署名、Microsoft 鍵も残して Windows 共存) -- Windows … BitLocker を**一時中断するだけ**(復号しない)で維持 +## Phase 0. Preparing Windows -設定は本 dotfiles の flake (`nix/`) に統合済み: +Skipping this before touching partitions is how Windows ends up unbootable. -- `nix/hosts/nixos-laptop.nix` … システム設定 (lanzaboote / LUKS+TPM2 / zram / Intel GPU / Hyprland / - fcitx5-mozc / tlp / fprintd / podman / tailscale / fwupd) -- `nix/flake.nix` の `nixosConfigurations."nixos-laptop"` … lanzaboote + disko + home-manager を接続 - (`home/common.nix` + `home/linux.nix` + `home/hyprland.nix` リック + `home/dev.nix` + `home/restic-backup-linux.nix`) -- `nix/hosts/nixos-laptop-disk.nix` … disko 宣言的レイアウト (LUKS root のみ。`diskoConfigurations` でも公開) -- `scripts/install-nixos-laptop.sh` … ガード付きインストール補助 (disko を使わない手続き型の選択肢) -- `nix/hosts/nixos-laptop-hardware.nix` … **実機で生成して後から追加**するマシン固有ファイル (LUKS デバイス UUID もここに入る) +1. Back up anything that matters. +2. Suspend BitLocker, without decrypting. In an administrator PowerShell: -> ⚠️ Windows を消さないこと。ESP (EFI システムパーティション) は**フォーマットせず流用**する。 -> 各コマンドのディスク名 (`nvme0n1` / `sda` 等) は必ず `lsblk` で実機を確認してから実行する。 + ```powershell + manage-bde -status # check the encryption state + (Get-BitLockerVolume -MountPoint C:).KeyProtector # write down the recovery key. Required. + manage-bde -protectors -disable C: -RebootCount 0 # stay suspended until re-enabled + ``` ---- + The data stays encrypted. Switching Secure Boot and adding a boot loader changes the TPM + measurements, so Windows may ask for the recovery key once on its next start. Have it to + hand. -## Phase 0. Windows 側の事前準備 (最重要) +3. Turn off Fast Startup: Control Panel, Power Options, "Choose what the power buttons do", + "Change settings that are currently unavailable", then untick "Turn on fast startup". + Leaving it on means Windows hibernates with NTFS locked and unfinalised, and touching it + from Linux corrupts it. -パーティション操作前にこれを怠ると Windows が起動不能になる。 + This is temporary, for the installation. This setup never mounts the Windows NTFS from NixOS + — `boot.supportedFilesystems` is commented out — so it can go back on afterwards, as + described in 9-2. Leave it off only if you want to share C: read-write with NixOS. -1. **重要データのバックアップ**を取る。 -2. **BitLocker を中断**(復号は不要)。管理者 PowerShell で: - ```powershell - manage-bde -status # 暗号化状態を確認 - (Get-BitLockerVolume -MountPoint C:).KeyProtector # 回復キーを控える(必須) - manage-bde -protectors -disable C: -RebootCount 0 # 再有効化まで中断したままにする - ``` - > データは暗号化されたまま保持される。Secure Boot 切替やブートローダ追加で TPM 測定値が変わり、 - > 初回 Windows 起動時に**一度だけ回復キーを要求される**ことがあるので、回復キーは必ず手元に。 -3. **高速スタートアップを無効化** (コントロールパネル → 電源オプション → 電源ボタンの動作 → - 「現在利用可能でない設定を変更します」→「高速スタートアップを有効にする」のチェックを外す)。 - 切らないと Windows 休止中の NTFS がロック/未確定状態のままになり、Linux から触ると破損する。 - > これは**インストール作業中の一時的な措置**。本構成は NixOS から Windows NTFS を自動マウント - > しない (`boot.supportedFilesystems` はコメントアウト) ので、**インストール後は再有効化してよい** - > (Phase 9-2 参照)。C: を NixOS から読み書き共有する場合のみ OFF のまま運用する。 -4. **休止状態を無効化** (任意だが推奨)。管理者 PowerShell で: - ```powershell - powercfg /h off - ``` -5. **C: を縮小**。`diskmgmt.msc` (ディスクの管理) → C: を右クリック →「ボリュームの縮小」→ - NixOS 用に空けたい容量 (例: 150000 MB ≒ 150GB) を指定。 - 生成された**未割り当て領域はそのまま**にしておく (NixOS インストーラ側で切る)。 +4. Turn off hibernation, optional but recommended: `powercfg /h off` in an administrator + PowerShell. +5. Shrink C:. In `diskmgmt.msc`, right-click C:, Shrink Volume, and give the amount you want + for NixOS, for instance 150000 MB for about 150 GB. Leave the resulting unallocated space + alone; the NixOS installer will carve it up. -## Phase 1. BIOS / UEFI 設定 +## Phase 1. BIOS and UEFI -再起動して BIOS に入る (起動時 `Del` / `F2` 等)。 +Reboot into the BIOS, usually with Del or F2. -- **Secure Boot は一旦 OFF のまま**。鍵を自分で登録するまでは NixOS が起動できないため、 - インストール〜鍵登録 (Phase 8) の後で ON にする。 -- **UEFI モードを維持** (CSM / Legacy にしない)。Windows は元々 UEFI なのでそのまま。 -- USB から起動できるよう、必要なら Fast Boot を一時的に無効化。 +- Leave Secure Boot off for now. NixOS cannot boot until our own keys are enrolled, which + happens in phase 8. +- Stay in UEFI mode; do not switch to CSM or Legacy. Windows is already UEFI. +- If necessary, turn off Fast Boot temporarily so the machine will boot from USB. -## Phase 2. インストーラ USB を作る (Mac から) +## Phase 2. Making the installer USB, from the Mac -インストール作業は全て CLI (cryptsetup / nixos-install --flake / sbctl) で、デスクトップ環境は -インストール後に flake から入るため、**minimal ISO** を使う (軽い・無駄が無い)。 +The whole installation is command line — cryptsetup, `nixos-install --flake`, sbctl — and the +desktop comes from the flake afterwards, so the minimal ISO is enough and is lighter. + +1. Download the ISO and check the SHA256. The "latest" link on `channels.nixos.org` tracks the + newest build. -1. ISO をダウンロードして SHA256 を照合 (`channels.nixos.org` の "latest" は最新ビルドに追従): ```sh cd ~/Downloads curl -L -o nixos-minimal-26.05-x86_64.iso \ "https://channels.nixos.org/nixos-26.05/latest-nixos-minimal-x86_64-linux.iso" curl -sL "https://channels.nixos.org/nixos-26.05/latest-nixos-minimal-x86_64-linux.iso.sha256" - shasum -a 256 nixos-minimal-26.05-x86_64.iso # 上の値と一致を確認 + shasum -a 256 nixos-minimal-26.05-x86_64.iso # must match ``` -2. USB を挿し、書き込む (macOS の `dd` は `status=progress` 非対応。進捗は `Ctrl+T`): + +2. Insert the USB stick and write to it. macOS's `dd` has no `status=progress`; press Ctrl-T + for progress. + ```sh - diskutil list external physical # USB の diskN を特定 (容量・Removable で判断) + diskutil list external physical # find the USB's diskN, by size and Removable diskutil unmountDisk /dev/diskN sudo dd if=~/Downloads/nixos-minimal-26.05-x86_64.iso of=/dev/rdiskN bs=4m sync && diskutil eject /dev/diskN ``` - > `rdiskN` (raw) を使うと速い。`diskN` を間違えると別ディスクを破壊するので必ず確認。 - > 書込後に「The disk you attached was not readable」が出たら **Ignore** (Initialize は押さない)。 - -## Phase 3. インストーラを起動 - -USB を挿して PC を起動 → 起動メニュー (`F12` / `F8` / `Esc` / `F11` 等) で **UEFI: USB...** を選択。 -minimal はテキストのログイン画面。`sudo -i` で root になり、`nmtui` 等で有線/Wi-Fi を接続しておく。 - -## Phase 4. パーティション作成 + LUKS 暗号化 - -> ⚠️ **前提**: 本リポジトリの NixOS 関連ファイルを **commit & push 済み**にしておくこと -> (インストーラは github から clone する)。未 push のローカル変更は実機に届かない。 -> -> **やり方は 3 通り**。いずれも先に「空き領域に `cfdisk` で Linux パーティションを 1 つ作る」まで同じ。 -> -> **(a) disko (宣言的・推奨)** — ディスクレイアウトをリポジトリで管理。LUKS root **1 パーティションのみ** -> 管理し GPT/Windows/ESP には触れない (安全)。 -> ```sh -> nix-shell -p git --run 'git clone https://github.com/gapul/dotfiles.git /tmp/df' -> # nix/hosts/nixos-laptop-disk.nix の device を実機の Linux パーティションに置換 -> # (lsblk -o NAME,SIZE,FSTYPE,PATH で確認。絶対にディスク全体/Windows/ESP にしない) -> sudo disko --mode destroy,format,mount --flake /tmp/df/nix#nixos-laptop # LUKS+ext4 を /mnt に -> mount /dev/nvme0n1p1 /mnt/boot # ESP は disko 管理外。手動マウント (フォーマット禁止) -> ``` -> 以降は Phase 6 (`nixos-generate-config --root /mnt` …) へ。 -> -> **(b) 補助スクリプト (ガード付き手続き)** — disko を使わず、デバイス取り違え/NTFS・ESP 誤消去を -> 機械的に拒否しつつ Phase 4-6 を自動化: -> ```sh -> bash /tmp/df/scripts/install-nixos-laptop.sh /dev/nvme0n1p5 /dev/nvme0n1p1 -> ``` -> -> **(c) 手動** — 下記の詳細手順を 1 つずつ (中身を理解したい場合)。 + + `rdiskN`, the raw device, is much faster. Getting `diskN` wrong destroys another disk, so + check. If macOS says "The disk you attached was not readable" afterwards, choose Ignore — + never Initialize. + +## Phase 3. Booting the installer + +Plug the stick in and boot, choosing "UEFI: USB..." from the boot menu, usually F12, F8, Esc or +F11. The minimal image gives a text login. Become root with `sudo -i` and get networking up, +wired or wireless, with `nmtui`. + +## Phase 4. Partitioning and LUKS + +Before starting, the NixOS files in this repository must be committed and pushed, because the +installer clones from GitHub. Local changes that are not pushed never reach the machine. + +There are three ways to do this. All of them start the same way: create one Linux partition in +the free space with `cfdisk`. + +**(a) disko, declarative, recommended.** The disk layout lives in the repository. It manages the +LUKS root partition alone and never touches the GPT, Windows or the ESP, which is what makes it +safe. + +```sh +nix-shell -p git --run 'git clone https://github.com/gapul/dotfiles.git /tmp/df' +# replace the device in nix/hosts/nixos-laptop-disk.nix with the real Linux partition +# check with lsblk -o NAME,SIZE,FSTYPE,PATH. Never the whole disk, Windows, or the ESP. +sudo disko --mode destroy,format,mount --flake /tmp/df/nix#nixos-laptop # LUKS and ext4 onto /mnt +mount /dev/nvme0n1p1 /mnt/boot # the ESP is outside disko. Mount by hand, never format. +``` + +Then continue at phase 6, with `nixos-generate-config --root /mnt`. + +**(b) The helper script**, a guarded procedure without disko, which automates phases 4 through 6 +while mechanically refusing to touch the wrong device or wipe NTFS or the ESP: + +```sh +bash /tmp/df/scripts/install-nixos-laptop.sh /dev/nvme0n1p5 /dev/nvme0n1p1 +``` + +**(c) By hand**, following the steps below one at a time, if you want to understand what is +happening. ```sh sudo -i lsblk -o NAME,SIZE,FSTYPE,PARTTYPENAME,MOUNTPOINT ``` -典型的な既存構成 (NVMe の例): +A typical existing layout on NVMe: -| パーティション | 用途 | 触り方 | +| Partition | Purpose | What to do | |---|---|---| -| `nvme0n1p1` | ESP (vfat, ~100–300MB) | **流用** (フォーマット禁止) | -| `nvme0n1p2` | Microsoft 予約 (MSR) | 触らない | -| `nvme0n1p3` | Windows C: (ntfs) | 触らない | -| `nvme0n1p4` | 回復 (ntfs) | 触らない | -| 末尾の空き | Phase 0 で空けた領域 | ここに NixOS root を作る | +| `nvme0n1p1` | ESP, vfat, 100-300 MB | Reuse. Never format | +| `nvme0n1p2` | Microsoft Reserved | Leave alone | +| `nvme0n1p3` | Windows C:, ntfs | Leave alone | +| `nvme0n1p4` | Recovery, ntfs | Leave alone | +| free space at the end | what phase 0 freed | The NixOS root goes here | + +Create the new partition in the free space, with type `Linux filesystem`: -空き領域に新パーティションを作成 (タイプは `Linux filesystem`): ```sh -cfdisk /dev/nvme0n1 # [New] → 全空き容量 → [Type: Linux filesystem] → [Write] → yes → [Quit] +cfdisk /dev/nvme0n1 # New, all free space, Type: Linux filesystem, Write, yes, Quit ``` -作られた番号を `lsblk` で再確認 (ここでは `nvme0n1p5` と仮定)。**LUKS で暗号化**してから ext4 を作る: +Check the resulting number with `lsblk` — assume `nvme0n1p5` here — then encrypt it with LUKS +before making the filesystem: + ```sh -cryptsetup luksFormat /dev/nvme0n1p5 # パスフレーズを設定 (YES と大文字確認あり) -cryptsetup open /dev/nvme0n1p5 cryptroot # /dev/mapper/cryptroot として開く +cryptsetup luksFormat /dev/nvme0n1p5 # set a passphrase; it asks for an uppercase YES +cryptsetup open /dev/nvme0n1p5 cryptroot # opens as /dev/mapper/cryptroot mkfs.ext4 -L nixos /dev/mapper/cryptroot ``` -> スワップは**平文パーティションを作らない**。host 設定で `zramSwap.enable = true;` 済み -> (メモリ内・暗号化された RAM 上)。物理スワップが要る場合のみ、別途暗号化スワップを構成する。 +Do not create a plaintext swap partition. The host configuration already has +`zramSwap.enable = true`, which is in RAM and encrypted. If real swap turns out to be needed, +set up an encrypted swap separately. -## Phase 5. マウント (ESP は流用) +## Phase 5. Mounting, reusing the ESP ```sh mount /dev/disk/by-label/nixos /mnt mkdir -p /mnt/boot -mount /dev/nvme0n1p1 /mnt/boot # ← 既存 Windows ESP。フォーマットしない! +mount /dev/nvme0n1p1 /mnt/boot # the existing Windows ESP. Do not format it. ``` -> ESP が 100MB しかない場合、署名 UKI で埋まりやすい。`nix/hosts/nixos-laptop.nix` の -> `boot.lanzaboote.configurationLimit` を 3〜5 に下げておくと安全 (260MB 以上あれば 8 のままで可)。 +If the ESP is only 100 MB it fills up quickly with signed UKIs. Lowering +`boot.lanzaboote.configurationLimit` in `nix/hosts/nixos-laptop.nix` to 3 to 5 is the safe move; +with 260 MB or more, 8 is fine. -## Phase 6. ハードウェア設定を生成し、flake を取り込む +## Phase 6. Generating the hardware configuration and pulling in the flake ```sh nixos-generate-config --root /mnt ``` -`/mnt/etc/nixos/hardware-configuration.nix` が生成される。`cryptroot` を開いた状態で実行したので、 -中に **`boot.initrd.luks.devices."cryptroot".device = "/dev/disk/by-uuid/…";`** が自動で入る。 -あわせて `fileSystems."/boot"` が ESP (vfat)、`fileSystems."/"` が `/dev/mapper/cryptroot` を -指していることを確認する。 +This writes `/mnt/etc/nixos/hardware-configuration.nix`. Because `cryptroot` is open, it +automatically contains +`boot.initrd.luks.devices."cryptroot".device = "/dev/disk/by-uuid/…";`. Check that +`fileSystems."/boot"` points at the vfat ESP and `fileSystems."/"` at `/dev/mapper/cryptroot`. + +Then pull in dotfiles and copy the generated hardware configuration to the name the repository +expects: -dotfiles を取り込み、生成したハード設定をリポジトリ内の所定名にコピー: ```sh nix-shell -p git git clone https://github.com/gapul/dotfiles.git /mnt/etc/nixos/dotfiles cp /mnt/etc/nixos/hardware-configuration.nix \ /mnt/etc/nixos/dotfiles/nix/hosts/nixos-laptop-hardware.nix -git -C /mnt/etc/nixos/dotfiles add -A # flake は git 追跡ファイルしか含めないため必須 +git -C /mnt/etc/nixos/dotfiles add -A # required: a flake only sees git-tracked files ``` -> この `nixos-laptop-hardware.nix` が存在して初めて flake に `nixosConfigurations."nixos-laptop"` が -> 生える設計 (Mac 側 `nix flake check` を壊さないため)。 +`nixosConfigurations."nixos-laptop"` only appears in the flake once +`nixos-laptop-hardware.nix` exists, which is deliberate — otherwise `nix flake check` on the Mac +would break. -## Phase 7. インストール +## Phase 7. Installing -この時点では **Secure Boot はまだ OFF**。lanzaboote の鍵が未登録なので OFF のまま入れる。 +Secure Boot is still off at this point, since the lanzaboote keys are not enrolled yet. -> ⚠️ **先に署名鍵を作る**。lanzaboote はブートローダー設置時に `/var/lib/sbctl` の鍵で UKI を -> 署名するため、鍵が無いと `nixos-install` の最後で -> `Failed to read public key from /var/lib/sbctl/keys/db/db.pem` と言って**ブートローダー設置だけ失敗**する -> (システム本体のビルドは終わっているので、鍵を作って再実行すれば続きから進む)。 -> 鍵の *作成* は BIOS の Setup Mode を必要としない。BIOS 操作が要るのは Phase 8 の *登録* から。 -> ```sh -> nixos-install --no-bootloader --flake /mnt/etc/nixos/dotfiles/nix#nixos-laptop -> nixos-enter --root /mnt -c 'sbctl create-keys' -> ``` +Create the signing keys first. lanzaboote signs the UKI with the keys in `/var/lib/sbctl` when it +installs the boot loader, and without them `nixos-install` fails at the very end with +`Failed to read public key from /var/lib/sbctl/keys/db/db.pem` — only the boot loader +installation fails; the system itself is already built, so creating the keys and re-running +picks up where it stopped. Creating keys does not require the BIOS to be in Setup Mode; that is +only needed to enrol them, in phase 8. + +```sh +nixos-install --no-bootloader --flake /mnt/etc/nixos/dotfiles/nix#nixos-laptop +nixos-enter --root /mnt -c 'sbctl create-keys' +``` + +Then: ```sh nixos-install --flake /mnt/etc/nixos/dotfiles/nix#nixos-laptop ``` -- 途中で **root パスワード**を聞かれる。 -- 通常ユーザー (`gapul`) のパスワードを設定: - ```sh - nixos-enter --root /mnt -c 'passwd gapul' - ``` +It asks for a root password along the way. Set the normal user's password too: -完了したら再起動: ```sh -reboot # USB を抜く +nixos-enter --root /mnt -c 'passwd gapul' ``` -## Phase 8. 初回起動後 → Secure Boot を有効化 +Then `reboot`, removing the USB stick. + +## Phase 8. After the first boot: enabling Secure Boot -systemd-boot ベースのメニューに **NixOS** と **Windows Boot Manager** が並ぶ。 -起動時に LUKS パスフレーズを聞かれる → NixOS にログイン。 +The systemd-boot menu lists NixOS and Windows Boot Manager. Booting asks for the LUKS +passphrase, then logs into NixOS. -### 8-1. Secure Boot 鍵を作って署名・登録 +### 8-1. Create, sign with and enrol the Secure Boot keys ```sh -# 鍵を生成 (host 設定の pkiBundle = /var/lib/sbctl と一致) +# generate the keys, matching pkiBundle = /var/lib/sbctl in the host configuration sudo sbctl create-keys -# lanzaboote は既に有効なので、再ビルドで UKI が自分の鍵で署名される +# lanzaboote is already enabled, so a rebuild signs the UKI with our keys sudo nixos-rebuild switch --flake ~/.dotfiles/nix#nixos-laptop -sudo sbctl verify # 署名済みファイルが ✓ で並ぶことを確認 +sudo sbctl verify # every signed file should be ticked ``` -### 8-2. BIOS を「Setup Mode」にして鍵を登録 +### 8-2. Put the BIOS in Setup Mode and enrol + +1. Reboot into the BIOS and clear the Secure Boot keys, or put it in Setup Mode. Depending on + the vendor this is "Erase all Secure Boot keys", "Clear keys" or "Setup Mode". +2. Back in NixOS, enrol our keys while keeping the Microsoft ones: -1. 再起動して BIOS へ。**Secure Boot の鍵をクリア / Setup Mode に**する - (メーカーにより "Erase all Secure Boot keys" / "Clear keys" / "Setup Mode")。 -2. NixOS に戻り、Microsoft 鍵を**残したまま**自分の鍵を登録: ```sh - sudo sbctl enroll-keys --microsoft # -m: MS 鍵を残し Windows の起動を維持 + sudo sbctl enroll-keys --microsoft # keeps the MS keys so Windows still boots ``` -### 8-3. Secure Boot を ON +### 8-3. Turn Secure Boot on -1. 再起動して BIOS で **Secure Boot を Enabled** に戻す。 -2. NixOS 起動後に確認: - ```sh - bootctl status # "Secure Boot: enabled (user)" になっていれば成功 - ``` - Windows も Boot Manager から起動できることを確認 (MS 鍵を残したので通る)。 +Reboot into the BIOS and set Secure Boot to Enabled. Once NixOS is up, `bootctl status` should +say `Secure Boot: enabled (user)`. Check that Windows still boots from the boot manager; it will, +because the Microsoft keys are still there. -### 8-4. その他 +### 8-4. Loose ends -- **時計ズレ対策** (NixOS は RTC=UTC、Windows は既定でローカル時刻)。Windows の管理者 PowerShell で: - ```powershell - reg add "HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation" /v RealTimeIsUniversal /t REG_DWORD /d 1 /f - ``` -- **BitLocker を再有効化**。Phase 0 で中断したので Windows 側で戻す: - ```powershell - manage-bde -protectors -enable C: - ``` -- **age 鍵の配置**: home-manager に `sops-nix` を入れているため、暗号化シークレットを使う場合は - `~/.config/sops/age/keys.txt` (または `SOPS_AGE_KEY_FILE`) に鍵を置く。鍵が無く復号で詰まる場合は、 - 当面 `nix/flake.nix` の該当 `imports` から `sops-nix.homeManagerModules.sops` を一時的に外してもよい。 +The clocks will disagree, since NixOS keeps the RTC in UTC and Windows defaults to local time. +In an administrator PowerShell on Windows: -## Phase 9. 以降の運用 +```powershell +reg add "HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation" /v RealTimeIsUniversal /t REG_DWORD /d 1 /f +``` + +Re-enable BitLocker, suspended back in phase 0, with `manage-bde -protectors -enable C:`. + +Place the age key. home-manager includes `sops-nix`, so using encrypted secrets needs a key at +`~/.config/sops/age/keys.txt`, or wherever `SOPS_AGE_KEY_FILE` points. If there is no key yet +and decryption blocks you, temporarily removing `sops-nix.homeManagerModules.sops` from the +relevant `imports` in `nix/flake.nix` is a reasonable stopgap. + +## Phase 9. Living with it + +Configuration changes go through the flake, the same as on the Mac. lanzaboote re-signs every +time, so Secure Boot stays intact: -設定変更は Mac と同じく flake から。lanzaboote が**毎回自動で署名**するので Secure Boot は維持される: ```sh sudo nixos-rebuild switch --flake ~/.dotfiles/nix#nixos-laptop ``` -`nix/hosts/nixos-laptop-hardware.nix` をリポジトリに**コミット**しておけば以後そのまま再構築できる -(LUKS デバイスの UUID とデバイスパスのみ。鍵そのものは含まれないので公開リポでも問題は小さいが、 -気になるなら `.gitignore` してローカル保持も可)。 - -### 9-1. 両 OS を日常的に使うための小ワザ - -Windows と NixOS を両方使う前提なので、切替コストを下げる運用を入れておく。 - -- **NixOS から直接 Windows へ再起動**(systemd-boot のメニューを待たずに済む): - ```sh - # 次回だけ Windows で起動して再起動 - sudo bootctl set-oneshot auto-windows && systemctl reboot - # もしくは - sudo systemctl reboot --boot-loader-entry=auto-windows - ``` - エントリ名は `bootctl list` で確認できる (`auto-windows` が標準)。 -- **メニュー表示時間**は `boot.loader.timeout = 5;` 済み。取り逃したらもう一度再起動でOK。 -- **Secure Boot 鍵をバックアップ**しておくと、BIOS 更新等で鍵が飛んでも復旧が速い: - ```sh - sudo tar czf ~/sbctl-keys-backup.tar.gz -C /var/lib sbctl # 安全な場所/USB に退避 - ``` -- **共有ファイル**は NTFS を Linux から書き込むより、両 OS が安全に読み書きできる - **exFAT の共有パーティション**を 1 つ用意する方が破損リスクが無い (休止状態の影響も受けない)。 - 必要になったら空き領域に `mkfs.exfat` で作る。 - -### 9-2. 高速スタートアップを戻す (任意) - -Phase 0-3 で切った高速スタートアップは、**NixOS から Windows C: (NTFS) を触らない運用なら戻してよい**。 -危険なのは「Windows 休止状態の NTFS を Linux から書き込む」ケースだけで、本構成はそれを避けている。 -Windows の管理者 PowerShell で: +Commit `nix/hosts/nixos-laptop-hardware.nix` and the machine can be rebuilt from the repository +from then on. It contains only the LUKS device UUID and device paths, no key material, so a +public repository is a small risk; if that still bothers you, gitignore it and keep it locally. + +### 9-1. Making both systems pleasant to use daily + +Since both get used, it is worth lowering the cost of switching. + +Reboot straight into Windows without waiting for the systemd-boot menu: + +```sh +# boot Windows next time only, then reboot +sudo bootctl set-oneshot auto-windows && systemctl reboot +# or +sudo systemctl reboot --boot-loader-entry=auto-windows +``` + +`bootctl list` shows the entry names; `auto-windows` is the standard one. + +The menu timeout is already `boot.loader.timeout = 5`. If you miss it, just reboot again. + +Back up the Secure Boot keys, so a BIOS update that loses them is quick to recover from: + +```sh +sudo tar czf ~/sbctl-keys-backup.tar.gz -C /var/lib sbctl # keep it somewhere safe, or on a USB stick +``` + +For files shared between the two, a single exFAT partition both systems can safely read and +write beats writing NTFS from Linux: no corruption risk, and hibernation does not affect it. +Create one with `mkfs.exfat` in free space when the need comes up. + +### 9-2. Turning Fast Startup back on, optional + +The Fast Startup turned off in phase 0 can go back on, as long as NixOS never touches the +Windows C: NTFS. The dangerous case is writing to a hibernated Windows NTFS from Linux, and this +setup avoids it. In an administrator PowerShell: + ```powershell powercfg /h on reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Power" /v HiberbootEnabled /t REG_DWORD /d 1 /f ``` -※ 将来 C: を NixOS から読み書き共有したくなったら、再び OFF にすること。 -### 9-3. インストール後の初回セットアップ (一度だけ) +If you later want to share C: read-write with NixOS, turn it off again. + +### 9-3. One-time setup after installing -flake に組み込んだ各機能の有効化に必要な手動ステップ。 +Manual steps needed to switch on the features the flake already contains. ```sh -# 指紋を登録 (fprintd)。以後 sudo / hyprlock で指紋が使える +# enrol a fingerprint (fprintd). sudo and hyprlock accept it afterwards sudo fprintd-enroll $USER -# Tailscale に参加 (homelab *.gapul.net) +# join Tailscale, for the homelab's *.gapul.net sudo tailscale up -# TPM2 + PIN 解錠を登録 (※ Secure Boot を ON にした後で。付録 A) +# enrol TPM2 with a PIN, only after Secure Boot is on. See appendix A. sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --tpm2-with-pin=yes /dev/nvme0n1p5 -# restic バックアップの前提: rclone google-drive を再認証 → sops に token を入れる -rclone authorize "drive" # 出力 token を secrets.yaml の rclone_conf へ -# 初回バックアップは systemd timer (日次13:00) を待つか、手動起動: +# restic needs rclone's google-drive re-authorised, with the token in sops +rclone authorize "drive" # put the printed token into rclone_conf in secrets.yaml +# then either wait for the daily systemd timer at 13:00 or run it now: systemctl --user start restic-backup.service -# age 鍵 (sops 復号用) を ~/.config/sops/age/keys.txt に配置 (無いと restic 等が止まる) +# place the age key for sops at ~/.config/sops/age/keys.txt, or restic and friends will stop ``` -Hyprland の主なキーバインド (`home/hyprland.nix`): `SUPER+Return`=ghostty, `SUPER+R`=wofi, -`SUPER+Q`=閉じる, `SUPER+F`=全画面, `SUPER+L`=ロック, `SUPER+E`=yazi, `SUPER+P`=範囲スクショ, -`SUPER+C`=クリップボード履歴, `SUPER+1..0`=ワークスペース。5/15 分でロック/サスペンド (hypridle)。 +The main Hyprland bindings, from `home/hyprland.nix`: `SUPER+Return` for ghostty, `SUPER+R` for +wofi, `SUPER+Q` to close, `SUPER+F` for fullscreen, `SUPER+L` to lock, `SUPER+E` for yazi, +`SUPER+P` for a region screenshot, `SUPER+C` for clipboard history, and `SUPER+1` through `0` +for workspaces. hypridle locks after 5 minutes and suspends after 15. --- -## 付録 A: TPM2 + PIN 解錠 (長いパスフレーズを短い PIN に置き換える) +## Appendix A: TPM2 with a PIN, replacing a long passphrase with a short one -**設定は組込済み**。`nixos-laptop.nix` で `boot.initrd.systemd.enable = true;` と -`boot.initrd.luks.devices.cryptroot.crypttabExtraOpts = [ "tpm2-device=auto" "tpm2-pin=yes" ];` -を有効にしてある。あとは **Secure Boot を有効化した後 (Phase 8 完了後)** に、TPM へ鍵を登録するだけ: +The configuration is already in place: `nixos-laptop.nix` sets +`boot.initrd.systemd.enable = true` and +`boot.initrd.luks.devices.cryptroot.crypttabExtraOpts = [ "tpm2-device=auto" "tpm2-pin=yes" ]`. +All that is left is enrolling the key into the TPM, after Secure Boot is on, meaning after phase +8: ```sh -# 対象 LUKS パーティションに TPM2 を登録 (PCR 7 = Secure Boot 状態に束縛) -# パーティションは lsblk で確認 (例: nvme0n1p5)。現在のパスフレーズと、新しく決める PIN を聞かれる。 +# enrol TPM2 against the LUKS partition, bound to PCR 7, the Secure Boot state. +# Find the partition with lsblk, for example nvme0n1p5. It asks for the current +# passphrase and for a new PIN. sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --tpm2-with-pin=yes /dev/nvme0n1p5 ``` -これで次回起動から、長いパスフレーズの代わりに PIN の入力で解錠できる。ブート列が改ざんされると -PCR 7 が変わり TPM は鍵を出さない。登録前やブート改ざん時はパスフレーズ入力にフォールバックする。 +From the next boot, the PIN unlocks the disk instead of the long passphrase. If the boot chain +is tampered with, PCR 7 changes and the TPM refuses to release the key. Before enrolment, or +after tampering, it falls back to the passphrase. -### なぜ PIN を付けるのか +### Why bother with the PIN -PCR 7 だけの登録は「ブート列が改ざんされていなければ鍵を出す」という条件なので、**人ではなくマシンに -束縛されている**。持ち出すノートでこれをやると、盗んだ相手が電源を入れるだけで復号されたディスクが -手に入る。Secure Boot を固めた意味が、所持だけで抜けてしまう。 +Enrolling against PCR 7 alone means "release the key if the boot chain is untampered", which +binds the disk to the machine rather than to a person. On a laptop that leaves the house, that +means whoever steals it gets a decrypted disk simply by pressing the power button. Everything +Secure Boot was locked down for is undone by possession alone. -PIN は TPM 側でハードウェア的に試行回数制限がかかる (失敗を重ねるとロックアウトする) ので、 -パスフレーズのような長さは要らない。総当たりが成立しないため、数字数桁でも実質的な壁になる。 +A PIN gets hardware-enforced retry limiting from the TPM, which locks out after enough failures, +so it does not need a passphrase's length. Brute force never gets going, which makes even a few +digits a real barrier. -> ⚠️ `--tpm2-with-pin=yes` を付けずに登録したスロットに対して `tpm2-pin=yes` を設定しても -> (逆も同様)、噛み合わずにパスフレーズ入力へフォールバックするだけ。登録済みのスロットを -> 変更する場合は `sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p5` で消してから登録し直す。 +Setting `tpm2-pin=yes` against a slot enrolled without `--tpm2-with-pin=yes`, or the reverse, +does not fail loudly — it simply does not match and falls back to the passphrase. To change an +existing slot, wipe it first with +`sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p5` and enrol again. -> ⚠️ **Secure Boot を有効化する前に登録しないこと**。PCR 7 の値が Secure Boot OFF 状態で固定され、 -> 後で ON にすると解錠できなくなる。必ず Phase 8 (Secure Boot ON) の後で登録する。 +Do not enrol before Secure Boot is on. PCR 7 would be fixed at its Secure-Boot-off value, and +turning Secure Boot on afterwards would leave the disk unopenable. Always enrol after phase 8. -## 付録 B: トラブルシュート +## Appendix B: Troubleshooting -| 症状 | 対処 | +| Symptom | What to do | |---|---| -| Windows 起動時に BitLocker 回復キーを要求される | Phase 0 で控えたキーで解錠。TPM 測定変化による一度きりの想定 | -| `bootctl status` が "Secure Boot: disabled" | Phase 8-2/8-3 の鍵登録 or BIOS の Enable 漏れ。`sbctl verify` で署名確認 | -| Secure Boot ON 後 NixOS が起動しない | BIOS を Setup Mode に戻し `sbctl enroll-keys -m` をやり直す。最悪 Secure Boot を OFF にすれば起動可 | -| Windows が起動しない (Secure Boot ON) | `enroll-keys` で `--microsoft` を付け忘れ。再登録する | -| メニューに Windows が出ない | ESP を `/mnt/boot` に正しくマウントしたか確認。別 ESP に入れていないか | -| ESP 容量不足で switch が失敗 | `boot.lanzaboote.configurationLimit` を下げる / `nix-collect-garbage -d` で古い世代削除 | -| LUKS パスフレーズを毎回聞かれて面倒 | 付録 A の TPM2 自動解錠を設定 | -| Intel GPU で画面が出ない | `nixos-laptop.nix` の `hardware.graphics` を確認。最悪 `nomodeset` で起動して調査 | +| Windows asks for the BitLocker recovery key | Unlock with the key from phase 0. Expected once, from the changed TPM measurements | +| `bootctl status` says "Secure Boot: disabled" | Either the enrolment in 8-2 and 8-3 did not happen or it was not enabled in the BIOS. Check signatures with `sbctl verify` | +| NixOS will not boot with Secure Boot on | Put the BIOS back into Setup Mode and redo `sbctl enroll-keys -m`. Worst case, turning Secure Boot off gets you booting again | +| Windows will not boot with Secure Boot on | `--microsoft` was left off `enroll-keys`. Enrol again | +| Windows is missing from the menu | Check that the ESP really is mounted at `/mnt/boot`, and that it did not end up on a different ESP | +| A switch fails because the ESP is full | Lower `boot.lanzaboote.configurationLimit`, or delete old generations with `nix-collect-garbage -d` | +| Tired of typing the LUKS passphrase | Set up the TPM2 unlock in appendix A | +| Nothing on screen with the Intel GPU | Check `hardware.graphics` in `nixos-laptop.nix`. Worst case, boot with `nomodeset` and investigate | diff --git a/docs/NIXOS_WSL.md b/docs/NIXOS_WSL.md index cef0452f..0e95566b 100644 --- a/docs/NIXOS_WSL.md +++ b/docs/NIXOS_WSL.md @@ -1,42 +1,43 @@ -# Windows の中の NixOS (WSL2) +# NixOS inside Windows (WSL2) -Adobe やテストのために Windows で起動している間も、再起動せずに普段のシェルと -道具を使うための箱。GUI は Windows に任せ、こちらは CLI に徹する。 +A box for keeping the usual shell and tools while Windows is booted for Adobe work or testing, +without rebooting. Windows handles the GUI; this side stays on the command line. -## デュアルブートの NixOS とは何を共有するか +## What it shares with the dual-boot NixOS -**インストールは共有しない。** WSL2 は物理パーティションを起動する仕組みではなく、 -Microsoft のカーネルで VHDX の中の rootfs を動かすので、実機の NixOS パーティションを -そのまま WSL の root にすることはできない。`wsl --mount --partition` でマウントして -中を読むことはできるが、root にはならない。 +**Not the installation.** WSL2 does not boot a physical partition. It runs a rootfs inside a +VHDX under Microsoft's kernel, so the real NixOS partition cannot become the WSL root. +`wsl --mount --partition` can mount it and read its contents, but it will not serve as root. -**共有するのは設定のほう。** `nixosConfigurations.wsl` の home は `roles.wsl` を -読んでいて、これは Lab PC の standalone home-manager (`homeConfigurations.labpc-wsl`) -と同じ実体。つまりどちらから入っても zsh / neovim / tmux / yazi / fzf が同じになる。 +**The configuration, though, is shared.** The home for `nixosConfigurations.wsl` comes from +`roles.wsl`, the same thing the Lab PC's standalone home-manager +(`homeConfigurations.labpc-wsl`) reads. Either way in, zsh, neovim, tmux, yazi and fzf are +identical. -`/nix/store` の共有はしない。デュアルブートなので同時には走らないが、nix の DB と -GC root を 2 つの環境で持ち回ることになって事故りやすい。ビルド済みのものは -Cachix / 自前 attic から降ってくるので、2 回ビルドしても実際の再ビルドはほぼ無い。 +`/nix/store` is not shared. Dual boot means they never run at once, but carrying one nix +database and one set of GC roots across two environments invites accidents. Anything already +built comes down from Cachix or the self-hosted attic, so building twice rarely means +rebuilding twice. -## tarball を作る +## Building the tarball -root と Linux が要る (母艦の mac では作れない)。homeserver で作って持ち帰る: +Needs root and Linux, so the Mac cannot do it. Build it on homeserver and bring it back: ```sh just wsl-tarball ``` -中でやっているのはこれ: +Which runs: ```sh sudo nix run #nixosConfigurations.wsl.config.system.build.tarballBuilder -# → カレントに nixos.wsl ができる +# leaves nixos.wsl in the current directory ``` -### sops の鍵を入れておく +### Include the sops key -`roles.wsl` は sops を読む。age 鍵が無いと初回の home-manager 適用で落ちるので、 -tarball を作るときに一緒に詰めておくのが楽 (あとから手で置いてもよい)。 +`roles.wsl` reads sops. Without an age key the first home-manager activation fails, so it is +easiest to pack the key into the tarball. Placing it by hand afterwards works too. ```sh root=$(mktemp -d) @@ -45,26 +46,28 @@ cp keys.txt "$root/home/gapul/.config/sops/age/keys.txt" sudo nix run #nixosConfigurations.wsl.config.system.build.tarballBuilder -- --extra-files "$root" ``` -## Windows 側に入れる +## Installing it on Windows ```powershell wsl --import nixos $env:LOCALAPPDATA\WSL\nixos nixos.wsl --version 2 wsl -d nixos ``` -以降は中で普通の NixOS として更新する。import し直すのは rootfs を作り直したいときだけ: +After that it updates from the inside like any NixOS. Re-importing is only for rebuilding the +rootfs itself: ```sh sudo nixos-rebuild switch --flake github:gapul/dotfiles?dir=nix#wsl ``` -## 既定の distro にするか +## Making it the default distro -しない。`wsl --import` した distro は既定にしなくても `wsl -d nixos` で入れる。 -Docker Desktop の WSL 統合を使う場合だけ、その distro を統合対象に入れる。 +No. An imported distro is reachable with `wsl -d nixos` without being the default. The only +reason to touch this is Docker Desktop's WSL integration, which needs the distro added to its +integration list. -## Lab PC との使い分け +## How this differs from the Lab PC -Lab PC は OS を入れ替えられないので、Ubuntu の上に standalone home-manager を -載せる形 (`homeConfigurations.labpc-wsl`) のまま。こちらは自分の機械なので -NixOS ごと入れる。どちらも home は `roles.wsl` で同じ。 +The Lab PC's OS cannot be replaced, so it stays on Ubuntu with standalone home-manager on top +(`homeConfigurations.labpc-wsl`). This machine is mine, so it gets NixOS itself. Both share the +same home through `roles.wsl`. diff --git a/docs/homelab-roadmap.md b/docs/homelab-roadmap.md index 42fbccf0..f7704106 100644 --- a/docs/homelab-roadmap.md +++ b/docs/homelab-roadmap.md @@ -1,163 +1,217 @@ -# Homelab ロードマップ & サービス移行調査 +# Homelab roadmap and service migration notes -> **注記(2026-08-09)**: この構成はハイパーバイザ無しの NixOS 1台へ置き換え中。 -> 設定の実体は `nix/hosts/homeserver.nix` と `nix/homelab/` に移っている。 -> 当日の手順は [HOMESERVER_MIGRATION.md](HOMESERVER_MIGRATION.md)。 -> 以下は移行元の記録。 +> **Superseded (2026-08-09).** This setup was replaced by a single NixOS machine with no +> hypervisor. The configuration now lives in `nix/hosts/homeserver.nix` and `nix/homelab/`, and +> the steps taken that day are in [HOMESERVER_MIGRATION.md](HOMESERVER_MIGRATION.md). What +> follows is a record of what was replaced. Where a question has since been answered, the +> answer is noted inline. -最終更新: 2026-07-05 +Last updated 2026-07-05. -pve(Proxmox)+ CT101(dockge/Docker)を中心とした自宅ホームラボの、残タスクとサービス移行の検討メモ。 -タスク番号は Claude Code のタスクリストと対応。 +Remaining work and migration research for a home lab built around pve (Proxmox) and CT101 +(dockge on Docker). Task numbers match the Claude Code task list. --- -## 0. この記録の背景(2026-07-05 に実施済み) +## 0. Background, done on 2026-07-05 -- **Matrix を頂点ドメインで公開**: server_name を `matrix.gapul.net` → `gapul.net` に作り直し、Conduit を federation 有効化。 - 既存 CF Tunnel "homelab-pi" を再利用して `matrix.gapul.net` を公開、`gapul.net` の well-known 委任は Caddy が配信。 - federationtester = OK。アカウント `@gapul:gapul.net` 作成・登録ロック済み。**これが自宅初のインターネット公開 ingress。** - (詳細は memory: matrix-federation-gapul) -- **dash.gapul.net(homepage)をリッチ化**: docker 統計・Proxmox ウィジェット(読み取り専用トークン)・ - siteMonitor・Glances(実ホスト CPU/RAM/温度)・天気・テーマ。表示は英語化。 +- **Matrix published on the apex domain.** server_name was rebuilt from `matrix.gapul.net` to + `gapul.net` and Conduit had federation enabled. The existing "homelab-pi" Cloudflare Tunnel + was reused to publish `matrix.gapul.net`, with Caddy serving the well-known delegation for + `gapul.net`. federationtester passed. The account `@gapul:gapul.net` was created and + registration locked. This was the first ingress from the internet into the house. +- **dash.gapul.net (homepage) was filled out** with docker statistics, a Proxmox widget on a + read-only token, siteMonitor, Glances for host CPU, RAM and temperature, weather and theming. + The display language was switched to English. --- -## 1. 残タスク(ロードマップ) - -### A. Matrix ブリッジ -- **#7 discord/telegram のログイン** — appservice 側(domain 修正・Conduit へ register・再接続)は完了済み。 - 残りはユーザー作業: Element で `@telegrambot:gapul.net` に DM → `login`、 - `@discordbot:gapul.net` に DM → `delete-all-portals`(旧 portal 再生成)or `login-qr`。 -- **#8 signal/slack/twitter/meta の新規構築** — 元から未構築でクラッシュループ(`homeserver.address not configured`)。 - bridgev2 形式で config 生成 → gapul.net 向け設定 → `register-appservice` → 各ネットワークにログイン。1個ずつ。 -- **#9 Google Messages ブリッジ追加** — `mautrix-gmessages` を compose 追加 → config/register(私)→ - `@gmessagesbot` に DM → スマホの Google メッセージで QR ペアリング(ユーザー)。 - ※ #8 と #9 は手順が同一(bridgev2 新規構築)なのでまとめると効率的。 - -### B. 運用ハードニング -- **#10 latest イメージの版固定** — 全スタックが `:latest`。今日ブリッジが壊れたのも latest 追従が一因。 - Matrix スタックのダイジェスト取得済: +## 1. Remaining work + +### A. Matrix bridges + +- **#7, logging discord and telegram in.** The appservice side is done: the domain was + corrected, they are registered with Conduit, and they reconnect. What is left is a user + action — DM `@telegrambot:gapul.net` with `login`, and DM `@discordbot:gapul.net` with + either `delete-all-portals` to regenerate the old portals or `login-qr`. +- **#8, building signal, slack, twitter and meta.** These were never set up and sat in a crash + loop with `homeserver.address not configured`. Generate a bridgev2 config, point it at + gapul.net, `register-appservice`, then log into each network. One at a time. +- **#9, adding a Google Messages bridge.** Add `mautrix-gmessages` to the compose file, + configure and register it, then DM `@gmessagesbot` and pair by QR from Google Messages on the + phone. #8 and #9 are the same procedure, so doing them together is more efficient. + +### B. Operational hardening + +- **#10, pinning `latest` images.** Every stack ran `:latest`. Following latest was part of why + the bridges broke that day. Digests were collected for the Matrix stack: - conduit `sha256:4078e80577ccaaf05290a7bb08badc321a5c44a8c8f5f3dce0fb1ae5a0825e64` - mautrix/discord `sha256:7716389dfb11dc7a44c8363348a48e91c1c463ded012f0fb08cdf266fcb20246` - mautrix/telegram `sha256:17b71cf6d45d7fb4eff3e9ea254613881df6531989031efbfccad12acc1d0782` -- **#11 バックアップの復元テスト** — vzdump+restic→GDrive が「戻せる」か検証。restic check/snapshots、 - 試験復元、Vaultwarden・新 Matrix データのカバレッジ、retention 確認。 -- **#12 平文シークレットを env/sops 化** — Proxmox トークン(homepage services.yaml)、 - Conduit 登録トークン(compose)、CF_API_TOKEN(cf.env)が平文。最低 .env+perms、理想 sops/docker secrets。 -- **#13 Conduit の登録トークン削除** — `ALLOW_REGISTRATION=false` 済みだが compose にトークンが残存(不活性)。掃除。 -- **#14 CF トークンを DNS 専用に分割** — Caddy のトークンが DNS 編集 + Tunnel 編集の両方持ち(過剰)。 - Caddy 用は `Zone:DNS:Edit` のみに。※トークン作成は CF ダッシュボード = ユーザー作業。 -- **#15 公開 Matrix のハードニング** — `matrix.gapul.net` に CF レート制限/WAF、Conduit 更新の徹底。 - ※ Rules はトークン権限外の見込み → CF ダッシュボード作業。 -- **#16 掃除** — `/opt/stacks/matrix` の `cw-data.old-matrixdomain`/`db.old`/`synapse.old`/backup tar、 - homepage の `services.yaml.bak-*` を整理。homepage/glances の docker.sock に socket-proxy を挟むか検討。 -- (通知: コンテナ異常 → ntfy は対応済み) - -依存関係: -- **#17(脱 Conduit)を決めてから #10(conduit ピン留め)/#13(トークン削除)** の順が無駄がない。 -- #14/#15 はどちらも CF ダッシュボードでのユーザー操作が必須。 +- **#11, testing the restore.** Verify that vzdump plus restic to Google Drive can actually be + restored: `restic check`, snapshot listing, a trial restore, coverage of Vaultwarden and the + new Matrix data, and retention. +- **#12, moving plaintext secrets into env or sops.** The Proxmox token in homepage's + services.yaml, Conduit's registration token in the compose file, and `CF_API_TOKEN` in cf.env + were all in the clear. At minimum a `.env` with permissions; ideally sops or docker secrets. +- **#13, deleting Conduit's registration token.** `ALLOW_REGISTRATION=false` was already set, + but the token remained in the compose file, inert. Clean it up. +- **#14, splitting the Cloudflare token so it is DNS-only.** Caddy's token could edit both DNS + and tunnels, which is more than it needs. Caddy's should be `Zone:DNS:Edit` alone. Creating + the token is a dashboard action. +- **#15, hardening the public Matrix.** Rate limiting and WAF for `matrix.gapul.net`, and + keeping Conduit updated. Rules are likely outside what the token can do, so this is dashboard + work. +- **#16, tidying up.** `cw-data.old-matrixdomain`, `db.old`, `synapse.old` and backup tarballs + under `/opt/stacks/matrix`, and homepage's `services.yaml.bak-*`. Consider putting a socket + proxy in front of the docker.sock that homepage and glances use. + +Ordering: decide #17, leaving Conduit, before doing #10 (pinning conduit) and #13 (removing the +token). #14 and #15 both require dashboard work. --- -## 2. サービス移行調査(2026、web 調査ベース) - -### 2.1 やる価値が高い - -#### Conduit 脱却 → Tuwunel(データ保持)or continuwuity(wipe)【#17】 -公開 federation 中なのに Conduit は開発停滞。メンテ性=セキュリティなので脱 Conduit 自体は推奨。ただし**移行先の選択が重要**: - -- **重要事実**: 素の Conduit の RocksDB は **continuwuity と非互換**。continuwuity へ行くとデータ全消し(wipe)になる。 - (conduwuit の Conduit 互換は一度 one-way になり、その後壊れて撤去された。continuwuity もこれを継承) -- **Tuwunel** は 1.8.0 で **Conduit の RocksDB をその場移行**(rooms/media/knocks 等を保持)。データを残したいならこれが技術的に正解。 - ただし企業/スイス政府系・実質1人開発・多少のプロジェクト間ドラマあり。 -- **continuwuity** は非企業コミュニティ後継で活発メンテ(0.5.x)。データ保持は不可だが、 - server_name を `gapul.net` に保てば federation は復活する(=小規模個人サーバなら wipe も許容範囲)。 -- どちらも `CONDUIT_*` env 互換。appservice は admin room の `register-appservice` で登録(bridge 再登録が必要)。 -- **必ず data dir をバックアップ → コピーで試験してから本番**。continuwuity/tuwunel の DB は Conduit に戻せない。 -- 効ort: 低〜中。verdict: **脱 Conduit は推奨。データ保持したいなら Tuwunel、コミュニティ志向で wipe 許容なら continuwuity。** -- Synapse/Dendrite はどちらも Conduit からのクリーン移行不可(同じく wipe)。個人規模には過剰。 - -出典: continuwuity.org/introduction, forgejo.ellis.link/continuwuation/continuwuity, github.com/matrix-construct/tuwunel, -docs.mau.fi(appservice), pistack.xyz(2026 比較) - -#### Uptime Kuma → Gatus【#18】 -- Go 単一バイナリ、YAML 宣言、RAM 約 1/3(~10-40MB vs Kuma ~100MB+)。**config-as-code 思想にドンピシャ。** -- HTTP/TCP/ICMP/DNS/TLS/push、条件式(status/latency/JSONPath body/cert 期限)、ステータスページ、メンテ窓、バッジ/API。 -- **ntfy はネイティブ対応**(既存の通知経路そのまま)。ストレージは sqlite 推奨。 -- 移行: インポータ無し、~20 監視を YAML で再宣言(数時間)。履歴はリセット。 -- 補完(競合ではない): **Beszel**(エージェント型リソース監視)、**Healthchecks**(cron/バックアップの死活)。 -- 効ort: 低。verdict: **明確な勝ち。** Kuma を1週間並走 → パリティ確認して撤去。 - -出典: github.com/TwiN/gatus, gatus.io/docs, homelabstarter.com - -#### WUD → Diun【#19】 -- Go・通知専用・Web UI なし・軽量(~20-40MB、cron で sleep)。**ntfy ネイティブ対応。** -- `watchByDefault=true` で全 ~25 コンテナを一括監視、または `diun.enable=true` ラベルで選択。 -- ピン留め運用(#10)と噛み合う「通知のみ・自動更新しない」に最適(Watchtower は自動更新なので別物)。 -- WUD の Web UI/REST API/Home Assistant・MQTT を使ってないなら死重。 -- 効ort: 低(~20-30分)。verdict: **軽量化の明確な勝ち。** - -出典: crazymax.dev/diun, getwud.github.io/wud - -#### Obsidian: CouchDB → Syncthing【#20】 -- 既に Syncthing 稼働中 → CouchDB(~150-300MB・要バックアップ/更新)を1個丸ごと削減できる。 -- **ただし条件付き**。Syncthing は file-level で、同一ノートを複数端末で同時編集すると `.sync-conflict` を吐く。 - `.obsidian/workspace*.json` は端末ごとに書き換わるので `.stignore` 必須(かつ Syncthing は .stignore 自体を同期しない)。 -- **モバイルが弱点**: iOS に公式アプリ無し → Möbius Sync(有料)or SyncTrain(無料・要 sandbox 連携)+ iOS のバックグラウンド制限。Android は快適。 -- **判断**: デスクトップ中心・常に1端末ずつ・モバイルは Android/読み中心 → **移行して良い**。 - iOS で頻繁に同時編集 → LiveSync 据え置き(リアルタイム + チャンク単位の自動マージ + ネイティブモバイルが強い)。 - 無保守が欲しいなら Obsidian 公式 Sync(有料 E2EE)も選択肢。 -- 効ort: デスクトップ 30-60分 + モバイル毎の調整(iOS が長い)。**sync ≠ backup、必ず先にバックアップ。** - -出典: github.com/vrtmrz/obsidian-livesync, forum.syncthing.net, forum.obsidian.md(Möbius/SyncTrain) - -### 2.2 状況次第【#21】 - -- **RSSHub → rss-bridge**: PHP・Redis 不要・Chromium 不要で軽い。ただし RSSHub は 1000+ ルートに対し - rss-bridge は ~200 bridges + 汎用 CssSelectorBridge。common な数サイトだけなら rss-bridge、ニッチ多数なら RSSHub 据え置き。 - Miniflux はどちらの feed も食える。移行は feed URL 再作成。 -- **ArchiveBox → linkding**: これは**同等ではない**。linkding は軽量ブックマーク管理 + 任意の軽アーカイブ(SingleFile 拡張が本命)。 - ArchiveBox は Chromium で HTML/PDF/screenshot/WARC/動画まで保存する本格アーカイバ。 - 実態が「後で読むリンク管理」なら linkding へ(桁違いに軽い)。本気の link-rot 対策アーカイブなら ArchiveBox 据え置き。 - 移行は Netscape bookmarks HTML で URL は移せるが、アーカイブ済みコンテンツは移らない(旧 data は静的保管で残す)。 - -### 2.3 趣味枠(無理に変えなくていい) - -- **AdGuard Home → Blocky**: Go・単一 YAML・UI 無し・約半分のメモリ。config-as-code 好きなら。 - ただし AGH の UI/クエリログ/クライアント別制御/DHCP を捨てる。二重化を既に組んでるので優先度低。 -- **Homepage → Glance**: Go・YAML・軽量な「朝の briefing」型。ただし homepage の深い per-service ウィジェット(今日作り込んだ)とは用途が違う。両方併用する人も多い。 - -### 2.4 追加の移行候補(残りサービスを精査) - -- **【#22】Proxmox VE → Incus** — MEDIUM-HIGH、**最大の工数**。config-as-code 志向に最も合う唯一の移行。 - Incus(LXD 系)は軽量・完全 OSS・API/CLI first、NixOS+Incus の宣言的ホストと好相性。 - 制約: Linux ゲスト限定(Windows VM 不可)・UI 貧弱・小コミュニティ。vzdump+qemu-img で移行可。 - **まず Proxmox 併存で新規 Linux ワークロードから試すのが安全。** -- **Samba に NFS を併用**(置換ではない)— Linux 間限定の共有は NFS が軽く速い。Samba はクロス OS の既定として残す。低工数の最適化。 -- **dockge → Komodo**(条件付き MEDIUM)— **複数 Docker ホストに増えたら**。Git 駆動のフリート管理。単一ホストには過剰。 - -### 2.5 変えなくていい(既に軽量/ベスト) - -Vaultwarden(Rust・~50MB、公式 Bitwarden より遥かに軽い)、Miniflux(Go、FreshRSS は横移動)、 -ntfy(HTTP first・UnifiedPush)、Navidrome(gonic は軽いが UI を失う・~50MB 差は誤差)、 -Jellyfin(FOSS 動画の勝者)、Forgejo(既に GPL/コミュニティ版・Gitea と互換)、 -Paperless-ngx(OCR で代替が軒並み劣る・重さは category leader の対価)、dockge(単一ホストでは理想)、 -Caddy(config-as-code に最良・自動 TLS)、Home Assistant(2000+ 統合、代替不可)、 -Radicale(最軽量、クライアントが困ったら Baïkal/Davis)、Samba(クロス OS の既定として)。 - -**補助 DB(Redis/Postgres)は per-stack で分離のまま**が正解(共有は SPOF 化・アップグレード結合・バックアップ複雑化を招くだけ、 -節約は数 MB で無意味)。 +## 2. Migration research, 2026 + +### 2.1 Clearly worth doing + +#### Leaving Conduit: Tuwunel (keeps data) or continuwuity (wipes) — #17 + +Conduit's development had stalled while it was federating publicly. Maintenance is security, so +leaving it was the recommendation. The choice of destination mattered: + +- Plain Conduit's RocksDB is **not compatible with continuwuity**. Going there wipes the data. + conduwuit's Conduit compatibility was one-way, then broke, then was removed; continuwuity + inherits that. +- **Tuwunel** migrates a Conduit RocksDB in place as of 1.8.0, keeping rooms, media and knocks. + Technically the right answer if the data matters. It is effectively a one-person project with + corporate and Swiss-government ties, and some inter-project drama. +- **continuwuity** is the non-corporate community successor and is actively maintained (0.5.x). + It cannot keep the data, but keeping server_name as `gapul.net` restores federation, so for a + small personal server a wipe is tolerable. +- Both are `CONDUIT_*` environment compatible. Appservices register through + `register-appservice` in the admin room, so bridges must be re-registered. +- Always back up the data directory and test on a copy first. A continuwuity or tuwunel + database cannot go back to Conduit. +- Effort low to medium. +- Synapse and Dendrite both require a wipe as well, and were judged excessive at this scale. + +> **Outcome (2026-08-31).** Neither was chosen. The server moved to Synapse, because bridges +> were about to go from two to roughly ten and Conduit keeps appservice registrations inside +> RocksDB where only the admin room can reach them. Synapse reads them from config files, which +> makes the whole bridge fleet declarable. See `nix/homelab/matrix.nix`. + +#### Uptime Kuma to Gatus — #18 + +A single Go binary, YAML declarations, roughly a third of the memory (10-40 MB against Kuma's +100 MB and up). It fits config-as-code directly. + +HTTP, TCP, ICMP, DNS, TLS and push checks; conditions on status, latency, JSONPath over the +body and certificate expiry; status pages, maintenance windows, badges and an API. ntfy is +supported natively, so the existing notification path carries over. sqlite is the recommended +store. + +There is no importer; about twenty checks have to be redeclared in YAML, a few hours' work. +History resets. + +Complementary rather than competing: **Beszel** for agent-based resource monitoring, and +**Healthchecks** for whether cron jobs and backups are alive. + +Effort low. Run Kuma alongside for a week, confirm parity, then remove it. + +#### WUD to Diun — #19 + +Go, notification only, no web UI, light (20-40 MB, sleeping between cron runs). ntfy is +supported natively. `watchByDefault=true` watches all twenty-five containers at once, or the +`diun.enable=true` label selects them individually. + +It fits "notify, never update automatically", which pairs with pinning (#10). Watchtower is a +different thing, since it updates. + +If WUD's web UI, REST API, Home Assistant and MQTT integrations are unused, it is dead weight. + +Effort low, twenty to thirty minutes. + +#### Obsidian: CouchDB to Syncthing — #20 + +Syncthing is already running, so this removes one whole service (CouchDB, 150-300 MB, needing +backup and updates). + +It comes with conditions. Syncthing works at file level, so editing the same note on two +devices at once produces `.sync-conflict` files. `.obsidian/workspace*.json` differs per device +and needs a `.stignore`, and Syncthing does not sync `.stignore` itself. + +Mobile is the weak point. There is no official iOS app; the options are Möbius Sync (paid) or +SyncTrain (free, needs sandbox integration), both fighting iOS background limits. Android is +fine. + +The judgement: desktop-centred, one device at a time, mobile mostly Android or read-only, then +migrate. Frequent simultaneous editing on iOS, then stay on LiveSync, which is realtime, merges +per chunk, and has a proper mobile client. Obsidian's own paid Sync is an option if +zero-maintenance matters more. + +Effort thirty to sixty minutes on desktop, plus per-device work on mobile, iOS being the long +one. Sync is not backup — back up first. + +### 2.2 Depends — #21 + +- **RSSHub to rss-bridge.** PHP, no Redis, no Chromium, so much lighter. But RSSHub has over a + thousand routes against rss-bridge's roughly two hundred plus a generic CssSelectorBridge. A + handful of common sites, use rss-bridge; many niche ones, stay. Miniflux consumes either. + Migration means recreating feed URLs. +- **ArchiveBox to linkding.** These are not equivalent. linkding is lightweight bookmark + management with optional light archiving, mainly through the SingleFile extension. ArchiveBox + is a real archiver that drives Chromium to save HTML, PDF, screenshots, WARC and video. If + what is actually happening is "links to read later", linkding is orders of magnitude lighter. + If the point is defending against link rot, stay. URLs migrate through Netscape bookmark HTML + but archived content does not; keep the old data as a static store. + +### 2.3 Hobby-tier, no need to change + +- **AdGuard Home to Blocky.** Go, one YAML file, no UI, about half the memory. Appealing if you + like config-as-code, but it gives up AGH's UI, query log, per-client control and DHCP. The + pair is already redundant, so this is low priority. +- **Homepage to Glance.** Go, YAML, a light "morning briefing". A different purpose from + homepage's deep per-service widgets, which had just been built out. Plenty of people run + both. + +### 2.4 Other candidates found while going through the rest + +- **#22, Proxmox VE to Incus.** Medium to high effort, the largest piece of work here, and the + one migration that genuinely fits config-as-code. Incus (from LXD) is light, fully open + source and API- and CLI-first, and pairs well with a declarative NixOS host. The constraints + are Linux guests only, so no Windows VM, a weak UI and a small community. vzdump plus + qemu-img can migrate. Safest to run it alongside Proxmox and start with new Linux workloads. +- **Add NFS next to Samba**, not instead of it. Linux-to-Linux shares are lighter and faster + over NFS. Samba stays as the cross-OS default. Low effort. +- **dockge to Komodo**, conditionally medium. Worth it once there is more than one Docker host, + for git-driven fleet management. Overkill for a single host. + +### 2.5 Leave alone, already light or already the best + +Vaultwarden (Rust, around 50 MB, far lighter than official Bitwarden), Miniflux (Go; FreshRSS +would be sideways), ntfy (HTTP-first, UnifiedPush), Navidrome (gonic is lighter but loses the +UI, and 50 MB is noise), Jellyfin (the winner among open-source video), Forgejo (already the +GPL community fork, Gitea-compatible), Paperless-ngx (every alternative is worse at OCR; the +weight is what being the category leader costs), dockge (ideal on a single host), Caddy (best +fit for config-as-code, automatic TLS), Home Assistant (2000-plus integrations, no +replacement), Radicale (the lightest; if a client struggles, Baïkal or Davis), Samba (the +cross-OS default). + +Supporting databases (Redis, Postgres) stay separate per stack. Sharing them creates a single +point of failure, couples upgrades and complicates backups, and saves a few megabytes. --- -## 3. 推奨着手順 +## 3. Suggested order -1. **足場固め(B の即効くやつ)**: #11 復元テスト → (#17 の方針決定後に)#10 ピン留め・#13 トークン削除。 -2. **構成を軽く/堅く(C)**: #17 脱 Conduit(Tuwunel or continuwuity)→ #20 Obsidian を Syncthing 化(サービス 1 個減)→ - #18 Gatus → #19 Diun。 -3. **ブリッジ群(A)**: #7 ログイン → #8/#9 新規構築(bridgev2 まとめて)。 -4. **CF ダッシュボード作業(ユーザー)**: #14 トークン分割・#15 WAF/レート制限。 -5. **長期・大物**: #22 Proxmox → Incus は併存で試してから。 +1. Groundwork with immediate effect: #11 restore testing, then, once #17 is decided, #10 + pinning and #13 token removal. +2. Lighter and sturdier: #17 leaving Conduit, #20 moving Obsidian to Syncthing (one service + fewer), #18 Gatus, #19 Diun. +3. Bridges: #7 logins, then #8 and #9 built together as bridgev2. +4. Dashboard work: #14 splitting the token, #15 WAF and rate limiting. +5. Long term: #22 Proxmox to Incus, after running it alongside. -移行は必ず「データ dir バックアップ → コピーで試験 → 本番」。特に Matrix と Vaultwarden。 +Every migration goes: back up the data directory, test on a copy, then do it for real. +Especially Matrix and Vaultwarden. diff --git a/docs/minecraft.md b/docs/minecraft.md index 45c60d95..f8075781 100644 --- a/docs/minecraft.md +++ b/docs/minecraft.md @@ -1,93 +1,102 @@ -# マイクラのサーバー運用 +# Running the Minecraft servers -Realms の代わりに、macmini でマイクラのサーバーを4本動かしている。宣言は -`nix/hosts/macmini.nix` の `minecraftServers`、起動スクリプトは -`configs/macmini/minecraft/`。本書は「触るときにどこを見るか」をまとめたもの。 +Four Minecraft servers run on the mac mini instead of paying for Realms. They are declared in +`minecraftServers` in `nix/hosts/macmini.nix`, and the launch scripts live in +`configs/macmini/minecraft/`. This page is about where to look when you need to touch them. -## 立っているもの +## What is running -| 名前 | 公開ポート | 版 | 何のため | +| Name | Public port | Version | Purpose | |---|---|---|---| -| vanilla | 25565 | Paper 26.2(最新を追う) | 友人と遊ぶ本館。Multiverse で世界を増やせる | -| solo | 25566 | Paper 26.2(最新を追う) | ひとり用。母艦から持ってきた世界2つ | -| fabric | 25568 | Fabric 26.2 | 最新のまま mod で遊ぶ | -| modded | 25567 | NeoForge 1.21.1 | Fabric に来ていない mod(黄昏の森)用 | +| vanilla | 25565 | Paper 26.2, tracking latest | The main server, for playing with friends. Multiverse can add worlds. | +| solo | 25566 | Paper 26.2, tracking latest | Single player. Two worlds carried over from the Mac. | +| fabric | 25568 | Fabric 26.2 | Mods on the newest version | +| modded | 25567 | NeoForge 1.21.1 | Mods that never came to Fabric, such as Twilight Forest | -外からの入口は playit のトンネルで、転送先は macmini の該当ポート。tailnet 内なら -`100.105.135.49:<ポート>` に直接繋がる。 +The way in from outside is a playit tunnel forwarding to the matching port on the mac mini. +From inside the tailnet, connect straight to `100.105.135.49:`. -## 遊んでいない間は動いていない +## Nothing runs while nobody plays -公開ポートを持っているのは lazymc で、サーバー本体は loopback の +100 番で動く。誰も -居なければ本体はプロセスごと落ちていて、待機コストは lazymc 4本ぶん(約 36MB、CPU 0%) -だけ。接続が来ると起こして繋ぎ、その間クライアントには「起動中」と見える。起動は実測で -4〜5秒、10分無人で停止する。 +lazymc holds the public port; the server itself runs on loopback at port + 100. With nobody +connected the server is not running at all, and the idle cost is four lazymc processes, about +36 MB and no measurable CPU. A connection wakes the server and joins it through, which the +client sees as "starting". Startup measures four to five seconds, and it stops again after ten +idle minutes. -`freeze_process` は切ってある。既定の凍結(SIGSTOP)は復帰こそ速いが 1.2GB を握った -ままで、4本ぶんとなると 24GB を AI スタックと分け合う機械には重すぎる。 +`freeze_process` is off. The default freeze, a SIGSTOP, resumes faster but keeps 1.2 GB +resident, and four of those is too much on a machine whose 24 GB is shared with the AI stack. -lazymc が突然死ぬ(SIGKILL される)と、その下のサーバーが止まったまま世界のロックを -掴んで残り、次の起動が `already locked` で落ちる。起動スクリプトがロックの持ち主を -片付けてから上がるので、放っておいても直る。 +If lazymc is killed outright, the server underneath stays stopped while still holding the world +lock, and the next start fails with `already locked`. The launch script clears the stale lock +holder before starting, so this resolves itself. -## 増やす・変える +## Adding and changing things -**インスタンスを足す**: `minecraftServers` に1エントリ足すだけ。lazymc・常駐・優先度・ -ログ・バックアップは表から生える。ポートは他と重ならない値にする(裏では +100 を使う)。 +**Adding an instance** means one more entry in `minecraftServers`. lazymc, the daemon, its +priority, logging and backups all follow from the table. Pick a port that does not collide with +the others; port + 100 is used behind it. -**世界を足す**(Paper 側): ゲーム内で `/mv create <名前> normal`。世界は生成物なので -宣言には出てこない。**名前にハイフンを使わない**——26.2 は世界を `minecraft:<名前>` の -キーで持ち、ハイフンはキーとして通らないため、Multiverse の取り込みが失敗する。 +**Adding a world** on the Paper side is `/mv create normal` in game. Worlds are +generated data and never appear in the declaration. Do not put a hyphen in the name: 26.2 keys +worlds as `minecraft:`, hyphens are not valid in that key, and Multiverse fails to import +the world. -**plugin / mod を足す**: `minecraftServers` の `env.PLUGINS` / `env.MODS` に `fetchurl` -で固定したものを並べる。`plugins/` と `mods/` には store への symlink が置かれ、宣言から -外せば次の起動で消える。試すだけなら手で jar を置いてもよく、そちらは消されない。 +**Adding a plugin or mod** means listing it under `env.PLUGINS` or `env.MODS` in +`minecraftServers`, pinned with `fetchurl`. `plugins/` and `mods/` are filled with symlinks into +the store, and removing an entry from the declaration removes the file on the next start. +Dropping a jar in by hand also works for experiments, and those are left alone. -mod と plugin は自動更新に載せていない。本体に追いつく速度がまちまちで、勝手に上がると -「昨日の世界が開かない」が起きるため。上げるのは人が決める。 +Mods and plugins are not on the automatic update path. They catch up to new server versions at +very different speeds, and letting them move on their own produces "the world I played +yesterday will not open". Bumping them is a decision someone makes. -## 本体の更新 +## Updating the servers -Paper だけは週次で追う。GitHub Actions の `update-custom-packages` が最新の STABLE -ビルドを見つけて `nix/pkgs/paper-server.nix` を書き換え、同時に `paperMcVersion` / -`paperProtocol`(lazymc が寝ている間に返す版)も動かす。protocol 番号は Paper の API に -無いので minecraft-data から引く。引けなければ据え置いて標準エラーに出す。 +Only Paper is tracked, weekly. The `update-custom-packages` GitHub Action finds the newest +STABLE build, rewrites `nix/pkgs/paper-server.nix`, and moves `paperMcVersion` and +`paperProtocol` with it — the version lazymc reports while the server sleeps. The protocol +number is not in Paper's API, so it comes from minecraft-data; if that lookup fails the value +stays as it is and a message goes to standard error. -macmini は毎朝 5:00 に `git pull` し、post-merge フックが `just rebuild` まで走らせる。 +The mac mini pulls at 05:00 every morning, and a post-merge hook carries it through +`just rebuild`. -版が上がると世界の変換が走り、変換は一方向。起動スクリプトが変換前の世界を -`<インスタンス>.pre-<旧版>` へ退避してから上げる(直前の1世代だけ残す)。 +A version bump converts the worlds, and conversion is one-way. The launch script moves the +pre-conversion world aside to `.pre-` first, keeping one generation. -## バックアップ +## Backups -毎晩 4:40 に全インスタンスを1本ずつ止めて固め、`/Users/Shared/minecraft-backups` に -7世代残す。5:00 の restic がそのディレクトリごと offsite へ持っていく。 +Every night at 04:40 each instance is stopped in turn, archived, and kept for seven generations +in `/Users/Shared/minecraft-backups`. The restic run at 05:00 carries that directory offsite. -拾うのは world 系のほか `mods/` `plugins/` `config/` `defaultconfigs/` と -`server.properties` / `whitelist.json` / `ops.json`。宣言した jar は store から戻せるが、 -手で入れた jar と mod ごとの設定はそこにしか無いため。 +Besides the worlds, the archive takes `mods/`, `plugins/`, `config/`, `defaultconfigs/`, +`server.properties`, `whitelist.json` and `ops.json`. Declared jars can be restored from the +store, but hand-installed jars and per-mod configuration exist nowhere else. -戻すときは該当インスタンスを止めて、tar をそのままインスタンスのディレクトリへ展開する。 +To restore, stop the instance and unpack the tar over its directory. ```bash sudo launchctl bootout system/org.nixos.minecraft-solo -sudo -u mcsrv tar xzf /Users/Shared/minecraft-backups/solo-<日付>.tar.gz -C /Users/mcsrv/solo +sudo -u mcsrv tar xzf /Users/Shared/minecraft-backups/solo-.tar.gz -C /Users/mcsrv/solo sudo launchctl bootstrap system /Library/LaunchDaemons/org.nixos.minecraft-solo.plist ``` -## 古い世界を持ち込む +## Bringing in an old world -シングルの世界はそのまま置けば起動時に変換される。ただし DataVersion を持たない世代 -(1.9 以前)は 26.2 が受け取らず、「古い版で開いてから」と言って止まる。間に 1.12.2 の -サーバーを挟んで一度読ませてから渡すと通る(1.12.2 は Java 8 が要る。`zulu8` を使う)。 +A single-player world can be dropped in as it is and will be converted on startup. Worlds old +enough to have no DataVersion, meaning 1.9 and earlier, are refused by 26.2, which asks you to +open them in an older version first. Passing them through a 1.12.2 server once fixes that. +1.12.2 needs Java 8, so use `zulu8`. -持ち物と座標はワールド内の `playerdata`(26.2 では `players/data`)に入っているので、 -フォルダごと移せば引き継がれる。 +Inventory and coordinates live in `playerdata` inside the world — `players/data` in 26.2 — so +moving the folder carries them along. -## 詰まったときに見る場所 +## Where to look when something breaks -- `<インスタンス>/logs/launchd.log` — lazymc の判断(起こした・寝かせた・失敗した) -- `<インスタンス>/logs/latest.log` — サーバー本体 -- 外から繋がらない: Application Firewall は許可をバイナリごとに覚えるので、lazymc の - store path が変わると受信が落ちる。activation で毎回登録し直しているが、疑うならここ。 - loopback からは通るのでサーバー側は正常に見える、という壊れ方をする。 +- `/logs/launchd.log` for what lazymc decided: woken, slept, failed. +- `/logs/latest.log` for the server itself. +- If it will not accept connections from outside, suspect the Application Firewall. It + remembers permission per binary, so a new store path for lazymc silently loses the grant. + Activation re-registers it every time, but this is the thing to check. It fails in a + confusing way: loopback still works, so the server looks healthy. diff --git a/docs/self-build-software.md b/docs/self-build-software.md index 335a982d..e46f2c59 100644 --- a/docs/self-build-software.md +++ b/docs/self-build-software.md @@ -1,43 +1,48 @@ -# 有料/非公開ソフトを「自己ビルドで無料フル」にする — 個人ガイド +# Building paid or unlisted software yourself -FOSS だが「公式バイナリは有料 or App Store 非公開」なソフトを、ソースから自分でビルドして -無料フル版を手に入れ、**このMacの dotfiles で宣言管理**するための実戦ノート。 -2026-07 に Zrythm / ArmorPaint / Fritzing / Ardour / Aseprite / Inochi Creator / (iOS) を -実際にやった手順をパターン化したもの。 +Field notes on taking software that is free in source but paid or App Store-absent as a binary, +building it from source to get the full version at no cost, and then declaring it in this Mac's +dotfiles. Written up from doing it to Zrythm, ArmorPaint, Fritzing, Ardour, Aseprite, Inochi +Creator and a couple of iOS apps in July 2026. -## 0. 要するに +## The short version -- **公式バイナリは有料でも、ソースが自由(GPL/zlib 等)なら自分でビルドすれば無料でフル機能** - (Ardour モデル)。Aseprite のように source-available で「自分でビルドは無料」も同じ。 -- macOS では **「そのソフトの依存の性質」で入れ方が一意に決まる**。iOS は sideload。 -- 入れたら **dotfiles(nix / homebrew)で宣言管理**、または再現スクリプト化する。 +- If the official binary costs money but the source is free (GPL, zlib and so on), building it + yourself gives you the full thing for nothing. This is the Ardour model. Source-available + projects like Aseprite, where building it yourself is explicitly free, work the same way. +- On macOS the nature of the dependencies decides how to install it, with almost no room for + preference. iOS is sideloading. +- Once it is in, declare it through nix or homebrew, or make the build reproducible in a script. -## 1. 判断フレーム(どの手法を選ぶか) +## Choosing an approach -| 状況 | 手法 | 例 | +| Situation | Approach | Examples | |---|---|---| -| nixpkgs にあり **darwin OK**(有料→nixは無料フル) | **型A: 純nix(home.packages に足す)** | Fritzing / Ardour / Aseprite | -| nixpkgs にあるが **darwin broken** | **型B: nix 自作パッケージ移植** | Zrythm | -| **Apple/Metal/Xcode 縛り**のビルド | **型C: Homebrew source-build tap 自作** | ArmorPaint | -| 無料の公式バイナリ有・nixpkgs darwin 不可 | **型D: cask tap 自作** | Inochi Creator | -| サーバ型 | homelab(Proxmox CT に自己ホスト) | Nakama / Weblate / Penpot | -| **iOS で有料 or 非公開** | **型E: Xcode ビルド + AltStore PAL** | Blink Shell / KeePassium | +| In nixpkgs and darwin works; paid upstream, free through nix | **A: plain nix, add to `home.packages`** | Fritzing, Ardour, Aseprite | +| In nixpkgs but darwin is broken | **B: port it into your own nix package** | Zrythm | +| The build is tied to Apple, Metal or Xcode | **C: your own Homebrew source-build tap** | ArmorPaint | +| A free official binary exists but nixpkgs darwin does not work | **D: your own cask tap** | Inochi Creator | +| Server software | Self-host it in the homelab | Nakama, Weblate, Penpot | +| Paid or unlisted on iOS | **E: build in Xcode, distribute through AltStore PAL** | Blink Shell, KeePassium | -**原則**: 依存が自己完結する GTK/Qt 系 → nix が最適(宣言的・再現的)。 -Apple ツールチェーン(xcodebuild / metallib)に縛られる → Homebrew が最適(system Xcode 前提で動く)。 +The rule underneath: GTK and Qt software with self-contained dependencies belongs in nix, which +is declarative and reproducible. Anything bound to the Apple toolchain — `xcodebuild`, +`metallib` — belongs in Homebrew, which assumes the system Xcode and works with it. --- -## 2. 型ごとの再現手順 +## The approaches in detail -### 型A: 純nix で足すだけ(nixpkgs darwin OK) +### A: plain nix, when darwin already works -`nix/home/darwin.nix` の `home.packages` に足すだけ。unfree(Aseprite 等)は `allowUnfree`。 +Add it to `home.packages` in `nix/home/darwin.nix`. Unfree packages such as Aseprite need +`allowUnfree`. ```nix -# 有料だが nixpkgs のソースビルド=無料フルになるものを足す。 -# 26.05-darwin で未提供/broken なら nixos-unstable を使う(zrythm 用に足した input を流用)。 -# aseprite は unfree のため allowUnfree 付きで再インスタンス化。 +# Things that cost money upstream but are a free full build through nixpkgs. +# If 26.05-darwin does not ship it or it is broken, take it from nixos-unstable +# (reusing the input added for zrythm). aseprite is unfree, so the tree is +# re-instantiated with allowUnfree. let unstablePkgs = import nixpkgsUnstable.legacyPackages.${pkgs.stdenv.hostPlatform.system}.path { inherit (pkgs.stdenv.hostPlatform) system; @@ -46,93 +51,111 @@ let in { home.packages = with pkgs; [ - unstablePkgs.fritzing # cache済で即 - unstablePkgs.ardour # cache済で即 - unstablePkgs.aseprite # unfree=source build + unstablePkgs.fritzing # cached, installs immediately + unstablePkgs.ardour # cached, installs immediately + unstablePkgs.aseprite # unfree, built from source ]; } ``` -- `allowUnfree` はこの Mac では `hosts/darwin-common.nix` に `nixpkgs.config.allowUnfree = true` で - 既に有効。ただし `nixpkgsUnstable.legacyPackages` は別インスタンスなので上記の再構成が要る。 -- cache 済みか実ビルドかは `nix build --dry-run nixpkgs#` で確認(fetched=cache / built=実ビルド)。 +`allowUnfree` is already on for this Mac through `nixpkgs.config.allowUnfree = true` in +`hosts/darwin-common.nix`, but `nixpkgsUnstable.legacyPackages` is a separate instance, which is +why it is rebuilt above. -### 型B: nix 自作パッケージ移植(nixpkgs で darwin broken) +To find out whether something is cached or will really be built, run +`nix build --dry-run nixpkgs#`: "fetched" means cache, "built" means a real build. -**例: Zrythm**(→ `nix/pkgs/zrythm-darwin/`)。nixpkgs の派生を `overrideAttrs` で darwin 向けに直す。 +### B: porting it yourself, when nixpkgs marks darwin broken -踏んだ罠と対処(darwin GTK アプリの定番): -1. `--buildtype=plain` の nixpkgs 既定は **最適化なし** → `mesonBuildType = "debugoptimized"`(=-O2)。 -2. `broken = isDarwin` → `meta.broken = false`。 -3. Linux 専用依存(alsa/pulse/jack)を `buildInputs` から除去 + meson で `-Djack=disabled` 等。 -4. **install_name がバラ名の dylib** → 起動時 `dyld: Library not loaded` → `install_name_tool -id `。 - Apple Silicon は改変で **ad-hoc 署名が壊れる** → `codesign --force --sign -`(要 `nativeBuildInputs += pkgs.darwin.sigtool`)。 -5. **SVG アイコンが出ない** → `GDK_PIXBUF_MODULE_FILE`(librsvg 込みの loaders.cache)を wrapper に。 -6. **設定が保存されない**(machine-id 不在) → `GSETTINGS_BACKEND=keyfile` を wrapper に。 -7. GTK4 macos backend は `quartz` でなく `macos` → `GDK_BACKEND=macos` を既定注入。 -8. 土台の nixpkgs は **nixos-unstable pin**(26.05-darwin は gtk4 の ld64 クラッシュ/appstream 破損があった)。 +Zrythm, in `nix/pkgs/zrythm-darwin/`, fixes the nixpkgs derivation for darwin through +`overrideAttrs`. The problems hit along the way are the usual ones for a GTK app on darwin: -検証: `nix build` → `result/bin/xxx --version` が dyld エラー無しで走ること。 +1. The nixpkgs default `--buildtype=plain` means no optimisation. Set + `mesonBuildType = "debugoptimized"`, which is `-O2`. +2. `broken = isDarwin`. Set `meta.broken = false`. +3. Linux-only dependencies (alsa, pulse, jack) have to come out of `buildInputs`, with + `-Djack=disabled` and friends passed to meson. +4. dylibs whose `install_name` is a bare filename produce `dyld: Library not loaded` at + startup. Fix with `install_name_tool -id `. On Apple Silicon that edit breaks the + ad-hoc signature, so follow it with `codesign --force --sign -`, which needs + `pkgs.darwin.sigtool` in `nativeBuildInputs`. +5. Missing SVG icons mean the wrapper needs `GDK_PIXBUF_MODULE_FILE`, pointing at a + loaders.cache that includes librsvg. +6. Settings that never persist means there is no machine-id. Put `GSETTINGS_BACKEND=keyfile` in + the wrapper. +7. The GTK4 macOS backend is called `macos`, not `quartz`. Inject `GDK_BACKEND=macos`. +8. Build against nixos-unstable rather than 26.05-darwin, which had a gtk4 ld64 crash and a + broken appstream. -### 型C: Homebrew source-build tap 自作(Apple/Metal 縛り) +To check the result: `nix build`, then `result/bin/xxx --version` should run without dyld +errors. -**例: ArmorPaint**(xcodebuild + Metal 依存 → nix と相性×、Homebrew と◎)。 +### C: your own Homebrew source-build tap, for Apple-bound builds + +ArmorPaint needs xcodebuild and Metal, which fits Homebrew and fights nix. ```bash -# 1) tap repo を作る +# 1. create the tap mkdir -p ~/tmp/homebrew-armorpaint/Formula && cd ~/tmp/homebrew-armorpaint -# Formula/armorpaint.rb を書く(下記テンプレ) +# write Formula/armorpaint.rb, see below git init && git add -A && git commit -m init gh repo create gapul/homebrew-armorpaint --public --source=. --push -# 2) ローカルテスト(母艦の brew は tap 信頼で止まるので env で回避) +# 2. test locally. The Mac's brew stops on tap trust, so work around it here. HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --build-from-source gapul/armorpaint/armorpaint ``` -Formula テンプレ(ソースビルド): +The formula: + ```ruby class Armorpaint < Formula desc "..."; homepage "https://armorpaint.org" url "https://github.com/armory3d/armorpaint.git", revision: "" version "23.09-2477"; license "Zlib" - depends_on xcode: :build # Metal shader コンパイルに Xcode 必須 + depends_on xcode: :build # Xcode is required to compile the Metal shaders def install cd "paint" do - system "../base/make", "--compile" # 内蔵 amake が xcodebuild をヘッドレス起動 + system "../base/make", "--compile" # the bundled amake drives xcodebuild headlessly prefix.install "build/build/Release/ArmorPaint.app" end end end ``` -- `darwin.nix` の `taps` に `"gapul/armorpaint"`、`brews` に `"gapul/armorpaint/armorpaint"` を追加。 -- 成果物は cask と違い `$(brew --prefix)/opt/armorpaint/ArmorPaint.app`(/Applications ではない)。 -### 型D: cask tap 自作(無料の公式バイナリ有) +Add `"gapul/armorpaint"` to `taps` and `"gapul/armorpaint/armorpaint"` to `brews` in +`darwin.nix`. Unlike a cask, the result lands in +`$(brew --prefix)/opt/armorpaint/ArmorPaint.app`, not `/Applications`. + +### D: your own cask tap, when a free official binary exists -**例: Inochi Creator**(無料だが homebrew/cask 未収録・nixpkgs darwin broken → 公式 .dmg を cask 化)。 +Inochi Creator is free but is not in homebrew/cask and is broken in nixpkgs on darwin, so the +official .dmg gets wrapped in a cask. ```ruby cask "inochi-creator" do version "0.8.6" - sha256 "" + sha256 "" url "https://github.com/Inochi2D/inochi-creator/releases/download/v#{version}/Install_Inochi_Creator.dmg" name "Inochi Creator"; homepage "https://inochi2d.com/" - app "Inochi Creator.app" # dmg 内の .app 名。違えば brew install がエラーで教える + app "Inochi Creator.app" # the .app name inside the dmg. Get it wrong and brew install says so. end ``` -- sha256 は `curl -L -o x.dmg && shasum -a 256 x.dmg`。 -- `gh repo create gapul/homebrew-inochi --public --source=. --push` → `darwin.nix` の taps + casks に追加。 -- cask なので `/Applications` に入る。 -### 型E: iOS(有料 or 非公開) +Get the sha256 with `curl -L -o x.dmg && shasum -a 256 x.dmg`, then +`gh repo create gapul/homebrew-inochi --public --source=. --push` and add it to `taps` and +`casks` in `darwin.nix`. Being a cask, it installs into `/Applications`. + +### E: iOS, for paid or unlisted apps + +Blink Shell costs $20 and is GPL; KeePassium's premium features are free if you build it +yourself. -**例: Blink Shell($20 GPL)/ KeePassium(premium は自己ビルドで無料)**。 +The setup here is AltStore PAL, the alternative marketplace available in Japan, plus an Apple +Developer Program membership. That gives year-long signing instead of the seven-day re-signing +a free Apple ID forces, and automatic updates through the marketplace. -前提(この環境): **AltStore PAL(日本の代替マーケット)+ Apple Developer Program**。 -→ **1年署名**(無料 Apple ID の7日再署名が不要)、マーケット経由で**自動更新**可。 +1. **Check it builds**, without signing, by running it in the Simulator. -段取り: -1. **ビルド検証**(署名不要): Simulator で動くか。 ```bash git clone https://github.com/keepassium/KeePassium && cd KeePassium xcodebuild -workspace KeePassium.xcworkspace -scheme KeePassium \ @@ -140,51 +163,60 @@ end -destination 'generic/platform=iOS Simulator' -derivedDataPath build \ CODE_SIGNING_ALLOWED=NO build ``` - KeePassium は依存同梱・Xcode で開くだけ。README に「build your personal premium version for free」明記。 -2. **署名して .ipa 化**: ローカル証明書が失効していたら再生成(Xcode > Settings > Accounts、or - App Store Connect API キー `~/.appstoreconnect/private_keys` で自動署名)。Team = KQZ7J45NTN。 -3. **AltStore ソース化(自動更新の肝)**: .ipa を CF Pages/R2 か homelab にホストし、 - AltSource の JSON(版・URL・sha)を作る → iPhone の **AltStore PAL に「ソース追加」**。 - 以降 AltStore PAL がそこから自動更新+署名管理。 -4. **上流追従の完全自動化**(stretch): 上流リリース→CI(GitHub Actions の macOS runner)で再ビルド→ - AltSource JSON を更新、まで組むと手放しで最新化。 - -注意: AltStore PAL のマーケット配布アプリは Apple の**公証(notarization)**が要る場合がある。 -無料 Apple ID + AltStore(非PAL)なら7日再署名・3アプリ制限(LiveContainer で回避)。 ---- + KeePassium vendors its dependencies and opens in Xcode as-is. Its README says outright that + you may build your personal premium version for free. + +2. **Sign it and produce an .ipa.** If the local certificate has expired, regenerate it through + Xcode > Settings > Accounts, or automate signing with an App Store Connect API key in + `~/.appstoreconnect/private_keys`. Team is KQZ7J45NTN. -## 3. 共通の罠と対処 +3. **Turn it into an AltStore source**, which is what makes updates automatic. Host the .ipa on + Cloudflare Pages, R2 or the homelab, write an AltSource JSON with version, URL and hash, and + add that source in AltStore PAL on the phone. From then on AltStore PAL handles updates and + signing. -- **CI lint は push 前にローカルで**: `cd nix && nix fmt -- ` と - `nix run nixpkgs#statix -- check -c ../.statix.toml .`(rc=0)を必ず通す。 - 自分/agent が書いた nix は未整形・inherit 指摘で CI が落ちがち(実際 zrythm で2回落ちた)。 -- **Homebrew tap trust**(Homebrew 6.0 で既定 true 化): - - 宣言 rebuild: `homebrew.onActivation.extraEnv.HOMEBREW_NO_REQUIRE_TAP_TRUST = "1"`(darwin.nix に設定済)。 - - 手動 install: `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install ...`(qmk 等の未信頼 tap で止まるのを回避)。 -- **taps conflict**: 複数 PR が同じ taps 位置に足すと merge 時 conflict → 両方残して解消 - (`git merge origin/main` → 手で両行残す → commit)。 -- **重いビルドは macmini 自走**: `CLAUDE_CODE_OAUTH_TOKEN`(`claude setup-token` → sops)を env で渡し - headless claude をループさせて反復ビルドを回せる(ssh 越し keychain は不可、env 必須)。詳細は - memory `macmini-headless-claude-auth`。 -- **nixpkgs は 26.05 より nixos-unstable**: darwin の GTK/appstream スタックは unstable の方が成熟。 +4. **Follow upstream automatically**, if you want to go further: an upstream release triggers a + rebuild on a GitHub Actions macOS runner, which updates the AltSource JSON. -## 4. 成果物の場所(このリポ/tap/PR) +One caveat: apps distributed through AltStore PAL may need Apple notarization. With a free +Apple ID and non-PAL AltStore you get seven-day re-signing and a three-app limit, which +LiveContainer can work around. + +--- -| ソフト | 手法 | 場所 | +## Problems that come up regardless + +- **Run the CI lint locally before pushing.** `cd nix && nix fmt -- ` and + `nix run nixpkgs#statix -- check -c ../.statix.toml .` should both return 0. Hand-written nix + tends to fail CI on formatting or an `inherit` suggestion — zrythm failed twice this way. +- **Homebrew tap trust**, which became the default in Homebrew 6.0. For declarative rebuilds, + `homebrew.onActivation.extraEnv.HOMEBREW_NO_REQUIRE_TAP_TRUST = "1"` is already set in + darwin.nix. For manual installs, `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install ...` + gets past untrusted taps such as qmk's. +- **Conflicting taps.** Two pull requests adding to the same place in `taps` conflict on merge. + Resolve by keeping both lines: `git merge origin/main`, edit, commit. +- **Heavy builds go to the mac mini.** Pass `CLAUDE_CODE_OAUTH_TOKEN` (from + `claude setup-token`, stored in sops) through the environment and loop a headless claude for + iterative builds. Keychain access over ssh does not work, so the environment variable is + required. +- **Prefer nixos-unstable over 26.05.** The GTK and appstream stack on darwin is more mature + there. + +## Where the results live + +| Software | Approach | Location | |---|---|---| -| Zrythm | 型B 純nix移植 | `nix/pkgs/zrythm-darwin/`(PR #66) | -| ArmorPaint | 型C Homebrew source-build | tap `gapul/homebrew-armorpaint`(PR #74) | -| Fritzing / Ardour / Aseprite | 型A 純nix | `nix/home/darwin.nix`(PR #77) | -| Inochi Creator | 型D cask | tap `gapul/homebrew-inochi`(PR #77) | -| Blink / KeePassium (iOS) | 型E Xcode + AltStore | `~/tmp/ios-selfbuild/`(作業中) | -| keebmouse (自作) | 型D cask | tap `gapul/homebrew-tap`(ソース repo は private なので成果物だけ tap に置く) | - -keebmouse は TCC(アクセシビリティ許可)が**パスとコード署名に紐づく**ので、nix store ではなく -`/Applications` に置く形にしてある。`scripts/dist.sh` が HEAD から作業ツリー外でビルド → -Developer ID 署名 → 公証 → tap の Release に zip → cask の version/sha256 を書き換えて push、 -までやる。署名 ID を変えなければ許可は取り直しにならない。常駐エージェントは -`nix/modules/home/darwin-chrome.nix` で宣言済み。 - -関連 memory: `zrythm-darwin-nix-port`, `armorpaint-selfbuild`, `macmini-headless-claude-auth`, -`nix-darwin-homebrew-declarative`。 +| Zrythm | B, ported into nix | `nix/pkgs/zrythm-darwin/`, PR #66 | +| ArmorPaint | C, Homebrew source build | tap `gapul/homebrew-armorpaint`, PR #74 | +| Fritzing, Ardour, Aseprite | A, plain nix | `nix/home/darwin.nix`, PR #77 | +| Inochi Creator | D, cask | tap `gapul/homebrew-inochi`, PR #77 | +| Blink, KeePassium (iOS) | E, Xcode and AltStore | `~/tmp/ios-selfbuild/`, in progress | +| keebmouse (mine) | D, cask | tap `gapul/homebrew-tap`; the source repo is private, so only the build output goes in the tap | + +keebmouse goes to `/Applications` rather than the nix store because its TCC grant, for +accessibility, is tied to the path and the code signature. `scripts/dist.sh` builds from HEAD +outside the working tree, signs with a Developer ID, notarizes, attaches a zip to a release in +the tap, rewrites the cask's version and sha256, and pushes. As long as the signing identity +does not change, the permission survives. The resident agent is declared in +`nix/modules/home/darwin-chrome.nix`. diff --git a/docs/terminal-browser-vimkeys.md b/docs/terminal-browser-vimkeys.md index 6ab73373..a2df9cce 100644 --- a/docs/terminal-browser-vimkeys.md +++ b/docs/terminal-browser-vimkeys.md @@ -1,82 +1,87 @@ -# terminal-browser の Vim 風キー操作 (Surfingkeys 代替) +# Vim-style keys in terminal-browser (a stand-in for Surfingkeys) -terminal-browser は拡張機能を読み込めない。Electron の `session.loadExtension` を露出していないので、 -Surfingkeys を入れる経路そのものが存在しない。代わりに同等のものを二層で書いた。 +terminal-browser cannot load extensions. It does not expose Electron's +`session.loadExtension`, so there is no path by which Surfingkeys could be installed at all. +What follows is an equivalent written in two layers. -| | 置き場所 | 対応するもの | +| | Where it goes | Equivalent to | | --- | --- | --- | -| `configs/cli/terminal-browser/vimkeys.js` | `--preload` (ページの隔離ワールド) | content script | -| `configs/cli/terminal-browser/main.js` | `--main-script` (Electron メインプロセス) | background script | +| `configs/cli/terminal-browser/vimkeys.js` | `--preload`, the page's isolated world | content script | +| `configs/cli/terminal-browser/main.js` | `--main-script`, the Electron main process | background script | -`--main-script` は `createRequire(file)(file)` として読まれるので素の CommonJS で、`require("electron")` -がそのまま使える。ページ側は `ipcRenderer` を持っているので、ページから手の届かない操作だけを -メイン側へ投げている。 +`--main-script` is loaded as `createRequire(file)(file)`, so it is plain CommonJS and +`require("electron")` works directly. The page side has `ipcRenderer`, so only the operations a +page cannot reach are forwarded to the main process. -配線は `nix/pkgs/terminal-browser.nix` の bin ラッパーが持つ。`open` と `new-tab` のときだけ 2 つの -フラグを足す (`--preload` はこの 2 つしか受け付けず、`shutdown` や `ls` に渡すと落ちる)。 -**どちらのファイルも、無ければ何も足さない。**挙動が邪魔になったら消せば素の terminal-browser に戻る。 +The wiring lives in the bin wrapper in `nix/pkgs/terminal-browser.nix`. It adds the two flags +only for `open` and `new-tab`; `--preload` is accepted by those two alone and crashes +`shutdown` or `ls`. Neither file is required — if either is missing nothing is added, so +deleting them returns you to stock terminal-browser. -## 実装したもの +## What is implemented -Surfingkeys の既定マッピング (`src/content_scripts/common/default.js`, 145 件) と、`normal.js` の -スクロール系を突き合わせて実装した。 +Built by working through Surfingkeys' default mappings +(`src/content_scripts/common/default.js`, 145 of them) alongside the scrolling code in +`normal.js`. -スクロール: `j` `k` `h` `l` `d` `u` `gg` `G` `0` `$` `;fs` -ヒント: `f` `af` `gf` `cf` `q` `ya` `yma` `yv` `ymv` `yc` `yq` `yi` `i` `gi` `O` -履歴と URL: `S` `D` `r` `gu` `gU` `g?` `g#` `[[` `]]` -yank: `yy` `yh` `yl` `ys` `yf` `yp` `gs` -visual と検索: `v` `V` `zv` `n` `N` `*` `/`、visual 中の `h` `j` `k` `l` `w` `b` `0` `$` `y` -マーク: `m<文字>` `'<文字>` -zoom: `zi` `zo` `zr` -タブ (main.js 経由): `x` `X` `on` `t` `go` `yt` `yT` `gxx` `gxt` `gxT` -その他: `.` `?` `Esc` +Scrolling: `j` `k` `h` `l` `d` `u` `gg` `G` `0` `$` `;fs` +Hints: `f` `af` `gf` `cf` `q` `ya` `yma` `yv` `ymv` `yc` `yq` `yi` `i` `gi` `O` +History and URLs: `S` `D` `r` `gu` `gU` `g?` `g#` `[[` `]]` +Yank: `yy` `yh` `yl` `ys` `yf` `yp` `gs` +Visual and search: `v` `V` `zv` `n` `N` `*` `/`, and inside visual mode `h` `j` `k` `l` `w` `b` `0` `$` `y` +Marks: `m` `'` +Zoom: `zi` `zo` `zr` +Tabs, through main.js: `x` `X` `on` `t` `go` `yt` `yT` `gxx` `gxt` `gxT` +Other: `.` `?` `Esc` -`?` でこの一覧が画面に出る。 +`?` shows this list on screen. -## 再現していないもの +## What is not reproduced -### 対応物が存在しない (構造的に不可能) +### Nothing to bind to -Chrome という製品固有の面を叩くもの。terminal-browser にはブックマークも履歴 DB もダウンロード -シェルフも拡張機構もセッション復元もプロキシ設定もないので、繋ぐ先が無い。 +These reach into Chrome-specific surfaces. terminal-browser has no bookmarks, no history +database, no download shelf, no extension mechanism, no session restore and no proxy settings, +so there is nothing on the other end. -- ブックマーク: `b` `ab` `;db` `gb` -- 履歴: `oh` `ox` `;dh` `;yh` `;ph` `gh` -- ダウンロード: `yd` `;di` `;j` `gd` -- `chrome://` を開くもの: `ga` `gc` `gk` `ge` `gn` `;i` -- 拡張一覧: `ge` -- プロキシ: `cp` `;cp` `;ap` -- セッション: `ZZ` `ZR` -- コンテナタブ: `;cl`、シークレット: `oi` -- ウィンドウ操作: `W` `;gt` `;gw` (terminal-browser のウィンドウはペイン) +- Bookmarks: `b` `ab` `;db` `gb` +- History: `oh` `ox` `;dh` `;yh` `;ph` `gh` +- Downloads: `yd` `;di` `;j` `gd` +- Anything opening `chrome://`: `ga` `gc` `gk` `ge` `gn` `;i` +- Extension list: `ge` +- Proxy: `cp` `;cp` `;ap` +- Sessions: `ZZ` `ZR` +- Container tabs `;cl` and incognito `oi` +- Window management: `W` `;gt` `;gw`. A terminal-browser window is a pane. -### 公開経路が無い +### No public entry point -terminal-browser のタブ模型は内部クラスが持っていて外から順序を触れない。新規タブは CLI -(`terminal-browser new-tab`) が唯一の公開経路なので、増やす・閉じる・複製はそこを通している。 -選択と並べ替えは terminal-browser 自身のタブキーとパレットが持っているので、嘘の実装を置かずに -その旨を返すだけにした。 +terminal-browser's tab model lives in an internal class and its ordering cannot be touched from +outside. The CLI (`terminal-browser new-tab`) is the only public way in, so adding, closing and +duplicating go through it. Selection and reordering belong to terminal-browser's own tab keys +and palette, so rather than fake an implementation these report that instead. - `gt` `gT` `<<` `>>` `T` `` `gp` `B` `F` `;x` `gx0` `gx$` `gxp` -### 外部サービス / 別プロセスが要る +### Needs an external service or a separate process -- 翻訳と LLM: `Q` `;t` `A` `t` `cq` `gr` -- vim/neovim 連携: `` `` `;v` `;u` `;U` `I` -- PDF ビューア: `;s` -- markdown プレビュー: `;pm` -- 設定編集と設定の yank/put: `;e` `yj` `;pj` `yQ` +- Translation and LLMs: `Q` `;t` `A` `t` `cq` `gr` +- vim and neovim integration: `` `` `;v` `;u` `;U` `I` +- PDF viewer: `;s` +- Markdown preview: `;pm` +- Editing settings, and yanking or putting them: `;e` `yj` `;pj` `yQ` -### 意図的に省いた +### Left out on purpose -- `w` (フレーム切り替え) と `;w` (最上位フレームへ): 端末の中の 1 ペインという使い方で iframe を - 跨ぐ場面が想定しにくい -- `` / `` (mouseover/mouseout の発火): 修飾キー付きは terminal-browser 自身の - ショートカットと食い合うので単独キーだけを見る方針にした -- `;pp` / `cc` / `;ap` などクリップボードから読む系: `--allow-clipboard-read` を渡さない限り読めない。 - 既定で無効なのは意図した設定なので、そちらを優先した +- `w` (switch frame) and `;w` (go to the top frame). Crossing iframes is hard to picture in a + single pane inside a terminal. +- `` and ``, which fire mouseover and mouseout. Modified keys collide with + terminal-browser's own shortcuts, so the rule here is to watch unmodified keys only. +- Anything that reads the clipboard, such as `;pp`, `cc` and `;ap`. Reading requires + `--allow-clipboard-read`, and leaving that off is a deliberate choice that wins here. -## 拡張したいとき +## Adding your own -`~/.config/terminal-browser/vimkeys.js` の `CMD` に 1 行足すだけで増える。rebuild は要らない -(次に開いたときから効く)。定着したら `configs/cli/terminal-browser/vimkeys.js` に持っていく。 +One line in `CMD` in `~/.config/terminal-browser/vimkeys.js` is enough. No rebuild — it applies +the next time you open a page. Once a binding sticks, move it into +`configs/cli/terminal-browser/vimkeys.js`. diff --git a/docs/windows-roadmap.md b/docs/windows-roadmap.md index 41887e47..e5fa4f4a 100644 --- a/docs/windows-roadmap.md +++ b/docs/windows-roadmap.md @@ -1,257 +1,260 @@ -# Windows 対応 ロードマップ +# Windows roadmap -macOS / WSL を主軸にしてきた本 dotfiles を **Windows ネイティブ + WSL2 ハイブリッド**で -完全運用するための残作業リスト。`windows/SETUP-CHECKLIST.md` の "実機で詰める論点" を -優先度付き TODO に展開したもの。 +These dotfiles grew up on macOS and WSL. This is the list of work needed to run them properly on +Windows as a native plus WSL2 hybrid, expanded from the "questions to settle on real hardware" +section of `windows/SETUP-CHECKLIST.md` into prioritised tasks. -## 実機の現状(2026-06-26 採取) +## The machine as found, 2026-06-26 -| 項目 | 状態 | +| Item | State | |---|---| -| pwsh | Windows PowerShell **5.1** のみ。PowerShell 7 (`Microsoft.PowerShell`) は未導入 | +| pwsh | Windows PowerShell 5.1 only. PowerShell 7 (`Microsoft.PowerShell`) not installed | | winget | v1.28.240 | -| WSL | `Ubuntu-24.04` / `Ubuntu` の 2 distro が Stopped で導入済み | -| 導入済 CLI | `git`(scoop), `nvim`, `yazi`, `gh`, `scoop`, `oh-my-posh` | -| 未導入 CLI | `starship`, `zoxide`, `fzf`, `rg`, `bat`, `fd`, `jq`, `lazygit`, `sops`, `age` | -| Nerd Font | M+ 系は導入済み。`JetBrainsMono Nerd Font` (Terminal `fontFace` の前提) は未確認 | +| WSL | Two distros installed and stopped: `Ubuntu-24.04` and `Ubuntu` | +| CLI present | `git` via scoop, `nvim`, `yazi`, `gh`, `scoop`, `oh-my-posh` | +| CLI missing | `starship`, `zoxide`, `fzf`, `rg`, `bat`, `fd`, `jq`, `lazygit`, `sops`, `age` | +| Nerd Font | M+ family installed. `JetBrainsMono Nerd Font`, which the Terminal `fontFace` assumes, unconfirmed | -## configs/ のネイティブ pwsh 再利用可否 +## How much of configs/ native pwsh can reuse -| ツール | symlink で動くか | 備考 | +| Tool | Works as a symlink | Notes | |---|---|---| -| `configs/shell/starship.toml` | ✅ そのまま | `$env:STARSHIP_CONFIG` で参照 (`$PROFILE` 既設) | -| `configs/cli/gh/config.yml` | ✅ | `$env:GH_CONFIG_DIR` を向けるだけ | -| `configs/cli/bat/` | ✅ (themes のみ) | `$env:BAT_CONFIG_DIR` | -| `configs/editors/zed/` | ✅ | Zed Windows は preview だが telemetry 設定のみで無害 | -| `configs/cli/yazi/yazi.toml` | ❌ | `[opener]` が `for="macos"/"unix"` のみ。Windows 用セクション要追加 | -| `configs/editors/nvim/` | ❌ (3 か所修正) | `obsidian.lua:36` の `"open"`、`skkeleton.lua:10-16` の XDG パス直書き、`lazy.lua:39` の `~/Developer/...` | -| `configs/terminals/tmux/` | N/A | Windows ネイティブ未対応 (WSL 内のみ) | -| `configs/terminals/ghostty/` | N/A | macOS/Linux 専用 | - -## WSL ↔ Windows 橋渡しの穴 - -| 方向 | 状態 | +| `configs/shell/starship.toml` | Yes, unchanged | Referenced through `$env:STARSHIP_CONFIG`, already in `$PROFILE` | +| `configs/cli/gh/config.yml` | Yes | Just point `$env:GH_CONFIG_DIR` at it | +| `configs/cli/bat/` | Yes, themes only | `$env:BAT_CONFIG_DIR` | +| `configs/editors/zed/` | Yes | Zed on Windows is preview, but the config is telemetry settings only, so it is harmless | +| `configs/cli/yazi/yazi.toml` | No | `[opener]` only covers `for="macos"` and `"unix"`. Needs a Windows section | +| `configs/editors/nvim/` | No, three places | `"open"` at `obsidian.lua:36`, hardcoded XDG paths at `skkeleton.lua:10-16`, `~/Developer/...` at `lazy.lua:39` | +| `configs/terminals/tmux/` | N/A | No native Windows support; WSL only | +| `configs/terminals/ghostty/` | N/A | macOS and Linux only | + +## Gaps in the WSL-Windows bridge + +| Direction | State | |---|---| -| WSL → Windows | ✅ `pbcopy/pbpaste`(clip.exe / powershell.exe), `explorer`, `wslview`(自作), `code` ラッパー | -| Windows → WSL | ⚠️ `wsl-here` 関数のみ。SSH agent / 1Password / GPG / Git credential helper 共有は未対応 | +| WSL to Windows | Working: `pbcopy`/`pbpaste` through clip.exe and powershell.exe, `explorer`, a hand-written `wslview`, a `code` wrapper | +| Windows to WSL | Only the `wsl-here` function. Sharing the SSH agent, 1Password, GPG and the git credential helper is unhandled | --- -## 優先度付き TODO +## Tasks by priority -### 🔴 P0(動かすため必須) +### P0, needed to work at all -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P0-1 | `bootstrap.ps1` の冒頭で `Microsoft.PowerShell`(=7) を winget でインストール。PSReadLine `PredictionSource` は 5.1 同梱版で失敗する。profile も能力検出で 5.1/7 両対応に | ✅ 38efee9, (PSReadLine 検出は P1-8 と同コミット) | -| P0-2 | scoop ↔ winget 重複の診断ヘルパー `Find-DotfilesToolOverlap` を profile.ps1 に追加 + 「winget 一次 / scoop 補助」運用方針を README に明文化 | ✅ 4cdc178 | -| P0-3 | `apps.json` の全 ID 実在を機械的に検証する `verify.ps1` を追加 | ✅ 38efee9 | -| P0-4 | Nerd Font 自動化 — `DEVCOM.JetBrainsMonoNerdFont` を `apps.json` に追加 | ✅ 38efee9 | +| P0-1 | Install `Microsoft.PowerShell` (7) through winget at the top of `bootstrap.ps1`. PSReadLine's `PredictionSource` fails on the version bundled with 5.1, so the profile detects capabilities and supports both | Done, 38efee9; the PSReadLine detection landed with P1-8 | +| P0-2 | Add a `Find-DotfilesToolOverlap` helper to profile.ps1 to diagnose scoop/winget overlap, and write the "winget first, scoop as backup" policy into the README | Done, 4cdc178 | +| P0-3 | Add `verify.ps1` to check mechanically that every ID in `apps.json` exists | Done, 38efee9 | +| P0-4 | Automate the Nerd Font: add `DEVCOM.JetBrainsMonoNerdFont` to `apps.json` | Done, 38efee9 | -実機検証で確定した不確実 ID の正しい名前: +IDs that were uncertain, resolved against the real machine: -| 候補名 | 正しい winget ID | +| Guess | Correct winget ID | |---|---| -| sops | `SecretsOPerationS.SOPS` (3.12.2) ← Mozilla.SOPS は旧版 | +| sops | `SecretsOPerationS.SOPS` (3.12.2). Mozilla.SOPS is the old one | | gitleaks | `Gitleaks.Gitleaks` (8.30.1) | | typst | `Typst.Typst` | | bottom | `Clement.bottom` (0.14.1) | | mpv | `shinchiro.mpv` (0.41.0) | | jetbrains nerd font | `DEVCOM.JetBrainsMonoNerdFont` (3.3.0) | -### 🟠 P1(ネイティブ体験の完成) +### P1, a complete native experience -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P1-5 | `bootstrap.ps1` に `New-DotfilesLink` 関数追加 + `gh`/`bat`/`yazi`/`nvim`/`zed` symlink + `$PROFILE` を pwsh7/5.1 両方へ + `-DryRun` 対応 | ✅ 5d1f218 | -| P1-6 | `configs/cli/yazi/yazi.toml` に `[opener]` の `for = "windows"` セクション追加(`start "" "$@"`, `tar -xf`, `nvim`, `mpv`) | ✅ d9485d4 | -| P1-7 | nvim 3 か所を `vim.fn.has("win32")` 分岐: obsidian.lua follow_url、skkeleton.lua 辞書パス、lazy.lua dev.fallback | ✅ 26e0aa7 | -| P1-8 | SOPS ネイティブ復号導線 — `$env:SOPS_AGE_KEY_FILE` 設定 + `Get-DotfilesSecret` / `Copy-DotfilesSecret` 関数 | ✅ (this commit) | +| P1-5 | Add `New-DotfilesLink` to `bootstrap.ps1`, symlink `gh`, `bat`, `yazi`, `nvim` and `zed`, place `$PROFILE` for both pwsh 7 and 5.1, and support `-DryRun` | Done, 5d1f218 | +| P1-6 | Add a `for = "windows"` `[opener]` section to `configs/cli/yazi/yazi.toml`: `start "" "$@"`, `tar -xf`, `nvim`, `mpv` | Done, d9485d4 | +| P1-7 | Branch the three nvim spots on `vim.fn.has("win32")`: obsidian.lua's follow_url, skkeleton.lua's dictionary path, lazy.lua's dev.fallback | Done, 26e0aa7 | +| P1-8 | Native SOPS decryption: set `$env:SOPS_AGE_KEY_FILE` and add `Get-DotfilesSecret` and `Copy-DotfilesSecret` | Done | -### 🟡 P2(運用品質) +### P2, operational quality -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P2-9 | `Justfile` に `win-bootstrap` / `win-verify` / `win-fmt`(PSScriptAnalyzer)を追加 + `Casey.Just` を apps.json に追加 | ✅ d7156c1 | -| P2-10 | `check.yml` に windows-latest ジョブ追加(parse / `-DryRun` / PSScriptAnalyzer) + `.gitattributes` で BOM 維持 | ✅ 34640ca (実 CI で SUCCESS) | -| P2-11 | Windows ssh-agent サービスを bootstrap で auto-start、WSL から `npiperelay + socat` で共有 | ✅ 15de024 | -| P2-12 | scoop ↔ winget 運用方針を README に明文化(P0-2 と統合) | ✅ 4cdc178 | +| P2-9 | Add `win-bootstrap`, `win-verify` and `win-fmt` (PSScriptAnalyzer) to the Justfile, and `Casey.Just` to apps.json | Done, d7156c1 | +| P2-10 | Add a windows-latest job to `check.yml` doing parse, `-DryRun` and PSScriptAnalyzer, and keep the BOM through `.gitattributes` | Done, 34640ca, green in real CI | +| P2-11 | Auto-start the Windows ssh-agent service from bootstrap and share it into WSL over `npiperelay` and `socat` | Done, 15de024 | +| P2-12 | Document the scoop/winget policy in the README, folded into P0-2 | Done, 4cdc178 | -### 🔵 P3(ハイブリッド体験の磨き込み) +### P3, polishing the hybrid -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P3-13 | WT `settings.json`: defaultProfile を pwsh7 に、Windows PowerShell 5.1 を出す、Ubuntu (WSL) は維持 | ✅ (this commit) | -| P3-14 | espanso は `Espanso.Espanso` を apps.json + match/base.yml symlink で共有。SKK は Windows 専用 `nathancorvussolis.corvusskk` を apps.json に追加(macOS plist は移植不可) | ✅ (this commit) | -| P3-15 | `docs/CHEATSHEET.md` に Windows セクション追加(macOS コマンドとの対応表、ネイティブ pwsh 関数一覧、SKK の OS 別実装メモ) | ✅ 0c3197b | +| P3-13 | Windows Terminal `settings.json`: default profile becomes pwsh 7, Windows PowerShell 5.1 stays visible, Ubuntu (WSL) is kept | Done | +| P3-14 | Share espanso through `Espanso.Espanso` in apps.json plus a match/base.yml symlink. SKK needs the Windows-only `nathancorvussolis.corvusskk`, since the macOS plist cannot be ported | Done | +| P3-15 | Add a Windows section to `docs/CHEATSHEET.md`: the mapping from macOS commands, the native pwsh functions, and a note on the per-OS SKK implementations | Done, 0c3197b | -### 🟣 P4(磨き込みの続き) +### P4, more polish -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P4-16 | profile.ps1 に `Open-Wsl` / `ConvertTo-WslPath` / `ConvertFrom-WslPath` 追加 | ✅ (this commit) | -| P4-17 | `$env:GHQ_ROOT` を `%USERPROFILE%\Developer` に設定し nvim lazy.lua の dev.path と整合。apps.json に `x-motemen.ghq` 追加 | ✅ (this commit) | -| P4-18 | Justfile に `win-upgrade` / `win-status`、`status.ps1` で宣言 ↔ 実 install の差分検出 | ✅ (this commit) | -| P4-19 | verify.ps1 / status.ps1 の `$PSScriptRoot` を param デフォルト式から本体へ移動 (5.1 で空になる挙動の回避) | ✅ 5b00bf7 | +| P4-16 | Add `Open-Wsl`, `ConvertTo-WslPath` and `ConvertFrom-WslPath` to profile.ps1 | Done | +| P4-17 | Set `$env:GHQ_ROOT` to `%USERPROFILE%\Developer` so it agrees with nvim's lazy.lua dev.path, and add `x-motemen.ghq` to apps.json | Done | +| P4-18 | Add `win-upgrade` and `win-status` to the Justfile, with `status.ps1` diffing the declaration against what is installed | Done | +| P4-19 | Move `$PSScriptRoot` in verify.ps1 and status.ps1 out of the param default expression and into the body, where it is not empty under 5.1 | Done, 5b00bf7 | -### 🟤 P5(セルフ診断) +### P5, self-diagnosis -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P5-20 | `Test-DotfilesSetup` を profile.ps1 に追加 (macOS `just doctor` 相当)。configs symlink / 環境変数 / 主要ツール / 鍵 ACL / ssh-agent サービスを 5 セクションで集計 | ✅ (this commit) | -| P5-21 | `STARSHIP_CONFIG` の設定を `Get-Command starship` 条件から外し、ツール未導入でも env だけは先に設定 | ✅ 01449f3 | +| P5-20 | Add `Test-DotfilesSetup` to profile.ps1, the equivalent of `just doctor` on macOS. Five sections: config symlinks, environment variables, the main tools, key ACLs, and the ssh-agent service | Done | +| P5-21 | Set `STARSHIP_CONFIG` unconditionally rather than behind `Get-Command starship`, so the variable is right even before the tool is installed | Done, 01449f3 | -### 🟫 P6(実機本番セットアップ) +### P6, the real setup run -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P6-22 | 実機で `bootstrap.ps1` 本番実行(管理者 UAC 経由)。20:30:57 開始 → 21:12:08 完了(41 分)。winget import 36 アプリ install / 全 6 symlink 作成 / ssh-agent サービス Auto+Running / git config 上書き | ✅ 完了 | -| P6-23 | `Test-DotfilesSetup` で **17/17 passed**(symlink 6 + env 3 + tools 5 + key 1 + service 1 + STARSHIP_CONFIG/SOPS/GHQ ack 2)。starship/zoxide/sops/age は現プロセス PATH 未更新で "未導入" 表示だが winget list には存在(新ターミナルで解決) | ✅ 完了 | -| P6-24 | 実行中に発覚した不要 GUI を整理: `Microsoft.VisualStudioCode` と `Brave.Brave` を apps.json から削除 + 実機 winget uninstall。代わりに `Anthropic.ClaudeCode` (既導入) と `KeePassXCTeam.KeePassXC` を最低限の追加 | ✅ (this commit) | -| P6-25 | KeePassXC は単独 install を試みたが MSI 1618 (`別のインストールが進行中`) で失敗 — bootstrap 中に動いた msiexec が管理者権限保持のままセッション残存していた。apps.json には残してあるので次回 bootstrap か再起動後の `winget install KeePassXCTeam.KeePassXC --exact` で導入 | ⏳ pending(再起動後) | -| P6-26 | WSL Ubuntu-24.04 で `~/.dotfiles` clone + `bootstrap-wsl.sh` 実行。Step 1 (apt: 既存)/ Step 2 (Nix 2.34.7 確認) スキップ、Step 4 で age 鍵不在停止。`nix shell nixpkgs#socat` 経由で SSH agent 共有を一時実証: socat → npiperelay → `//./pipe/openssh-ssh-agent`、`ssh-add -L` が "The agent has no identities" を返す = **通信成功**(鍵未登録のためエラー) | ✅ (this commit) | -| P6-27 | wsl.nix の zsh init で WIN_USER から WinGet Packages 経由 npiperelay PATH を組み立てるロジック追加 + 実機検証(`WIN_USER=ispc_5CG54406V7`、`npr_dir=...albertony.npiperelay_...`、`npiperelay.exe exists` 全 ok) | ✅ fb0b11e | +| P6-22 | Run `bootstrap.ps1` for real on the machine, through the UAC prompt. Started 20:30:57, finished 21:12:08, 41 minutes. 36 apps installed through winget import, all six symlinks created, ssh-agent service Auto and Running, git config overwritten | Done | +| P6-23 | `Test-DotfilesSetup` reports 17 of 17 passing: six symlinks, three environment variables, five tools, one key, one service, and the STARSHIP_CONFIG/SOPS/GHQ acknowledgements. starship, zoxide, sops and age show as missing only because the current process PATH is stale; winget list has them, and a new terminal resolves it | Done | +| P6-24 | Clean up GUI apps that turned out to be unwanted: removed `Microsoft.VisualStudioCode` and `Brave.Brave` from apps.json and uninstalled them. Added `Anthropic.ClaudeCode`, already present, and `KeePassXCTeam.KeePassXC` as the minimum replacements | Done | +| P6-25 | KeePassXC failed to install on its own with MSI 1618, another installation in progress: the msiexec started during bootstrap was still holding the session with administrator rights. It stays in apps.json, so the next bootstrap or `winget install KeePassXCTeam.KeePassXC --exact` after a reboot will get it | Pending, after a reboot | +| P6-26 | Clone `~/.dotfiles` in WSL Ubuntu-24.04 and run `bootstrap-wsl.sh`. Step 1 (apt) and step 2 (Nix 2.34.7) skipped as already satisfied; step 4 stopped on the missing age key. SSH agent sharing proven temporarily through `nix shell nixpkgs#socat`: socat to npiperelay to `//./pipe/openssh-ssh-agent`, with `ssh-add -L` answering "The agent has no identities", which is the connection working and simply having no keys | Done | +| P6-27 | Add logic to the wsl.nix zsh init that builds the npiperelay PATH from `WIN_USER` through WinGet Packages, verified on the machine: `WIN_USER=ispc_5CG54406V7`, `npr_dir=...albertony.npiperelay_...`, npiperelay.exe present | Done, fb0b11e | -### ⚫ P7(Ghostty 代替: WezTerm 採用) +### P7, WezTerm in place of Ghostty -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P7-28 | Ghostty は macOS/Linux 専用のため Windows native では起動不可。クロスプラットフォーム動作する WezTerm を ghostty 代替として採用。`configs/terminals/wezterm/wezterm.lua` を新規追加 — Ghostty config (`HackGen Console NF` / `Rose Pine` / opacity 0.88 / blur 30 / 閉じる確認なし / Ctrl+C は SIGINT) を Lua に翻訳、OS 分岐で Mac は `macos_window_background_blur`、Windows は `win32_system_backdrop = 'Acrylic'` | ✅ (this commit) | -| P7-29 | `apps.json` に `wez.wezterm` 追加、`bootstrap.ps1` の ConfigLinks に wezterm を追加(`%USERPROFILE%\.wezterm.lua` へファイル単独 symlink)、`Test-DotfilesSetup` の symlink チェックにも追加(7 件目) | ✅ 1b4314e | -| P7-30 | `status.ps1 -ShowExtra` で実機の EXTRA 30 個を確認 → 意図的 install 16 個を apps.json に追加(WSL/Terminal/WM/Launcher/Browser/Sync/Util/Game 系)、システム依存・OEM・バージョン断片は宣言外として除外。INSTALLED 42/45 → 58/61、EXTRA 30 → 14 | ✅ ac85464 | +| P7-28 | Ghostty is macOS and Linux only and will not start on native Windows, so WezTerm takes its place as the cross-platform terminal. Added `configs/terminals/wezterm/wezterm.lua`, translating the Ghostty config into Lua: HackGen Console NF, Rose Pine, opacity 0.88, blur 30, no close confirmation, Ctrl+C as SIGINT. The OS branch gives Mac `macos_window_background_blur` and Windows `win32_system_backdrop = 'Acrylic'` | Done | +| P7-29 | Add `wez.wezterm` to apps.json, add wezterm to `bootstrap.ps1`'s ConfigLinks as a single-file symlink to `%USERPROFILE%\.wezterm.lua`, and add it as the seventh entry in `Test-DotfilesSetup`'s symlink check | Done, 1b4314e | +| P7-30 | Review the 30 EXTRA entries `status.ps1 -ShowExtra` found on the machine. Sixteen were installed deliberately and went into apps.json (WSL, Terminal, WM, launcher, browser, sync, utilities, games); system-dependent, OEM and version-fragment entries stay undeclared. INSTALLED went from 42/45 to 58/61 and EXTRA from 30 to 14 | Done, ac85464 | -### ⚪ P8(テレメトリ/標準機能 declarative 化) +### P8, declaring telemetry and built-in features -Mac の `nix/hosts/darwin.nix` の `system.defaults` 相当を Windows で再現。 +The Windows equivalent of `system.defaults` in the Mac's `nix/hosts/darwin.nix`. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P8-31 | `windows/privacy/` 新設 — `win11debloat-args.txt`(`Raphire/Win11Debloat` の CLI 引数 13 個: Silent / RemoveApps / DisableTelemetry / DisableBing / DisableCopilot / DisableRecall / DisableLockscreenTips / DisableSuggestions / DisableSticky / ShowHiddenFolders(`AppleShowAllFiles` 相当) / ShowKnownFileExt(`AppleShowAllExtensions` 相当) / HideHome / HideGallery)、`winutil-config.json`(`ChrisTitusTech/winutil` の export 形式、13 tweaks: Telemetry/AH/Loc/BingSearch/Wifi/RemoveCopilot/RemoveHomeGallery/EndTaskOnTaskbar/RightClickMenu/RP/Services/Storage/DisableLMS1) | ✅ (this commit) | -| P8-32 | `windows/privacy/apply.ps1` orchestrator: `Win11Debloat` は CLI 自動、`WinUtil` は GUI 起動 + Config 自動 import。`-DryRun` / `-SkipWinUtil` / `-SkipWin11Debloat` 対応。PowerShell 5.1 の `Get-Content` ANSI 既定対策で `-Encoding UTF8` を明示 | ✅ (this commit) | -| P8-33 | `Justfile` に `win-privacy *flags` 追加。`bootstrap.ps1` Step 7 として自動実行(`-SkipPrivacy` で省略可)。`apply.ps1` 側でも `-SkipWinUtil` / `-SkipWin11Debloat` で個別 skip 可能 — 細粒度の選択性を担保 | ✅ (this commit) | +| P8-31 | Add `windows/privacy/`: `win11debloat-args.txt` with 13 CLI arguments for `Raphire/Win11Debloat` (Silent, RemoveApps, DisableTelemetry, DisableBing, DisableCopilot, DisableRecall, DisableLockscreenTips, DisableSuggestions, DisableSticky, ShowHiddenFolders as the `AppleShowAllFiles` equivalent, ShowKnownFileExt as `AppleShowAllExtensions`, HideHome, HideGallery), and `winutil-config.json` in `ChrisTitusTech/winutil`'s export format with 13 tweaks | Done | +| P8-32 | Add the `windows/privacy/apply.ps1` orchestrator: Win11Debloat runs from the CLI automatically, WinUtil starts its GUI and imports the config. Supports `-DryRun`, `-SkipWinUtil` and `-SkipWin11Debloat`. `-Encoding UTF8` is passed explicitly because PowerShell 5.1's `Get-Content` defaults to ANSI | Done | +| P8-33 | Add `win-privacy *flags` to the Justfile and run it as step 7 of `bootstrap.ps1`, skippable with `-SkipPrivacy`. `apply.ps1` keeps its own `-SkipWinUtil` and `-SkipWin11Debloat` so the choice stays fine-grained | Done | -### ⬜ P9(Microsoft 純正からの代替拡張 + scoop 統合) +### P9, replacing the Microsoft defaults, and scoop -純正アプリ(File Explorer / OneDrive / Mail / Defender / Photos / ペイント / 録音 等)を Mac 環境(`brew cask`)と整合する形で代替。 +Replacing the built-in apps (File Explorer, OneDrive, Mail, Defender, Photos, Paint, Voice +Recorder and so on) with things that line up with the Mac's `brew cask` set. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P9-34 | `apps.json` に GUI app 12 個追加 — `Syncthing.Syncthing`(OneDrive 代替) / `Mozilla.Thunderbird`(Mail) / `WinSCP.WinSCP`(Cyberduck 相当) / `ente-io.auth-desktop`(2FA) / `WiresharkFoundation.Wireshark` / `KDE.Krita`(Paint) / `Ditto.Ditto`(Maccy 相当のクリップボード履歴) / `Captura.Captura`(動画録画) / `Bitdefender.Bitdefender`(Defender 代替 AV) / `Henry++.simplewall`(LuLu 相当の Outbound Firewall) / `Microsoft.Sysinternals.Autoruns`(BlockBlock/KnockKnock 相当の永続化監視) / `Malwarebytes.Malwarebytes`(セカンドオピニオン スキャナ) | ✅ (this commit) | -| P9-35 | scoop 統合 — MS Store 専用 app(Files 等)の sideload 用に `windows/scoop/` 新設。`scoop.json`(buckets + apps の declarative)+ `apply.ps1` orchestrator(scoop 自動 install、bucket 追加、app install、`-DryRun` / `-SkipBuckets` / `-SkipApps` 対応)。`bootstrap.ps1` Step 2.5 として自動実行(`-SkipScoop` で省略可)、`Justfile` に `win-scoop *flags` 追加。File Explorer 代替は `nonportable/files-np` で sideload(Store 経由を回避) | ✅ (this commit) | -| P9-36 | `win11debloat-args.txt` に 6 引数追加 — `-RemoveCommApps`(Mail/Calendar/People、Thunderbird 代替前提)/ `-RemoveDevApps`(3D Builder / MR Portal)/ `-RemoveW11Outlook`(新 Outlook 強制プッシュ削除)/ `-RemoveGamingApps`(Xbox 全部、Steam 使用)/ `-DisableOnedrive`(Syncthing 代替)/ `-DisableWidgets`(タスクバー お天気/ニュース) | ✅ (this commit) | -| P9-37 | 録音 / フォトプレビュー — 用途未定のため**保留**。録音は会議録音用途で適切な OSS 探索中、フォトプレビューも候補絞り込み中 | ⏳ pending | -| P9-38 | カスタム UWP 削除 — `win11debloat-customapps.txt` 新設(Win11Debloat 標準セット外の 8 個: `Microsoft.GetHelp` / `Microsoft.Windows.DevHome` / `Microsoft.YourPhone` / `Microsoft.OfficePushNotificationUtility` / `Microsoft.CommandPalette` / `AppUp.IntelGraphicsExperience`(Intel グラフィックス・コマンド・センター) / `aimgr`(Local AI Manager for Microsoft 365) / `Microsoft.MicrosoftStickyNotes`)。`apply.ps1` に Step 1.5 として `Get-AppxPackage \| Remove-AppxPackage` + Provisioned package 削除 step 追加(`-SkipCustomApps` で省略可)。中リスク(`Microsoft.BingSearch` / `Microsoft.Windows.ContentDeliveryManager` / `Microsoft.StartExperiencesApp`)は除外、機能 OFF のみで対応 | ✅ (this commit) | -| P9-39 | Windows Backup 機能 OFF — UWP 本体は `MicrosoftWindows.Client.CBS` で削除不可なため、`HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsBackup\DisableWindowsBackupUI = 1` を `apply.ps1` Step 1.7 で declarative に設定 | ✅ (this commit) | -| P9-40 | apps.json から `Typst.Typst` 削除(現状不使用、再追加可能) | ✅ (this commit) | -| P9-41 | 実機 cleanup(別 commit、apps.json には影響なし) — HP プリインの Bitdefender Total Security 三点セット(VPN は uninstall 成功 / Total Security 本体は既に未 install 状態 / BDContextualMenu は Remove-AppxPackage 済) / Poly.PolyLens 削除 + Poly Lens Control Service MSI uninstall / Start メニュー残骸ショートカット(OneNote.lnk / Office 言語設定.lnk)削除 / Office 2013 デスクトップ版は実は最初から未 install と判明(プリイン UI 残骸のみ)。HP Wolf Security 系(Wolf / Sure Run / Sure Recover / One Agent / Security Update) と Vivado は保護 | ✅ 実機 cleanup 完了 | -| P9-42 | Win11Debloat の呼び出しを 4 段階修正 — (a) 短縮 URL `win11debloat.raphi.re` が GitHub user page にリダイレクトする破綻状態を発見 → release asset の `Get.ps1` launcher を直叩き、(b) PS 7 の `(Invoke-WebRequest).Content` が byte[] を返す問題 → `-OutFile` で binary 直書き、(c) 同一プロセス splatting で `[CmdletBinding(SupportsShouldProcess)]` と switch 引数の bind が衝突 → 別 pwsh プロセスで `-File` 呼出、(d) 単体 `Win11Debloat.ps1` 取得では multi-file 構造で「unable to find required files」 → Get.ps1 launcher が内部でフル repo を取得する仕様を利用 | ✅ | -| P9-43 | `apply.ps1` 系に自動 UAC elevation — `windows/privacy/apply.ps1` / `windows/sharpkeys/apply.ps1` を非管理者で起動すると `Start-Process pwsh -Verb RunAs` で UAC prompt を起こし、元の switch を子プロセスに引き継いで再実行。`-DryRun` は elevation 不要なのでスキップ | ✅ | - -#### 「Microsoft 純正からの代替」現状マッピング - -| 分類 | Microsoft 純正 | 代替 | +| P9-34 | Add 12 GUI apps to `apps.json`: `Syncthing.Syncthing` for OneDrive, `Mozilla.Thunderbird` for Mail, `WinSCP.WinSCP` for Cyberduck, `ente-io.auth-desktop` for 2FA, `WiresharkFoundation.Wireshark`, `KDE.Krita` for Paint, `Ditto.Ditto` for Maccy-style clipboard history, `Captura.Captura` for screen recording, `Bitdefender.Bitdefender` in place of Defender, `Henry++.simplewall` as the LuLu-equivalent outbound firewall, `Microsoft.Sysinternals.Autoruns` for persistence monitoring like BlockBlock and KnockKnock, and `Malwarebytes.Malwarebytes` as a second opinion scanner | Done | +| P9-35 | Integrate scoop for sideloading Store-only apps such as Files. Add `windows/scoop/` with a declarative `scoop.json` of buckets and apps plus an `apply.ps1` orchestrator that installs scoop, adds buckets and installs apps, supporting `-DryRun`, `-SkipBuckets` and `-SkipApps`. Runs as step 2.5 of `bootstrap.ps1`, skippable with `-SkipScoop`, with `win-scoop *flags` in the Justfile. The File Explorer replacement sideloads through `nonportable/files-np`, avoiding the Store | Done | +| P9-36 | Add six arguments to `win11debloat-args.txt`: `-RemoveCommApps` (Mail, Calendar, People, now that Thunderbird is in), `-RemoveDevApps` (3D Builder, MR Portal), `-RemoveW11Outlook` (the forced new Outlook), `-RemoveGamingApps` (all Xbox, since Steam is what gets used), `-DisableOnedrive` (replaced by Syncthing) and `-DisableWidgets` (taskbar weather and news) | Done | +| P9-37 | Voice recording and photo preview: on hold, since neither use is settled. The recorder is for meetings and a suitable open-source option is still being looked for; the photo viewer candidates are still being narrowed | Pending | +| P9-38 | Remove custom UWP apps: add `win11debloat-customapps.txt` with the eight outside Win11Debloat's standard set (`Microsoft.GetHelp`, `Microsoft.Windows.DevHome`, `Microsoft.YourPhone`, `Microsoft.OfficePushNotificationUtility`, `Microsoft.CommandPalette`, `AppUp.IntelGraphicsExperience`, `aimgr`, `Microsoft.MicrosoftStickyNotes`), and add step 1.5 to `apply.ps1` doing `Get-AppxPackage \| Remove-AppxPackage` plus provisioned package removal, skippable with `-SkipCustomApps`. The riskier three (`Microsoft.BingSearch`, `Microsoft.Windows.ContentDeliveryManager`, `Microsoft.StartExperiencesApp`) are excluded and handled by turning the feature off instead | Done | +| P9-39 | Turn off Windows Backup. The UWP part cannot be removed because it belongs to `MicrosoftWindows.Client.CBS`, so step 1.7 of `apply.ps1` sets `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsBackup\DisableWindowsBackupUI = 1` declaratively | Done | +| P9-40 | Remove `Typst.Typst` from apps.json, unused for now and easy to add back | Done | +| P9-41 | Machine cleanup, in a separate commit and not affecting apps.json: HP's preinstalled Bitdefender Total Security trio (VPN uninstalled, Total Security itself already absent, BDContextualMenu removed through Remove-AppxPackage); Poly.PolyLens removed along with the Poly Lens Control Service MSI; leftover Start menu shortcuts (OneNote.lnk, the Office language settings link) deleted; the desktop Office 2013 turned out never to have been installed, leaving only preinstall UI remnants. The HP Wolf Security family (Wolf, Sure Run, Sure Recover, One Agent, Security Update) and Vivado are protected | Done | +| P9-42 | Four fixes to how Win11Debloat is invoked: (a) the short URL `win11debloat.raphi.re` redirects to a GitHub user page, so the release asset's `Get.ps1` launcher is fetched directly; (b) `(Invoke-WebRequest).Content` returns a byte[] under PowerShell 7, so `-OutFile` writes the binary directly; (c) in-process splatting collides with `[CmdletBinding(SupportsShouldProcess)]` when binding switch arguments, so it runs `-File` in a separate pwsh process; (d) fetching `Win11Debloat.ps1` alone fails with "unable to find required files" because of the multi-file layout, so the Get.ps1 launcher, which pulls the whole repo, is used instead | Done | +| P9-43 | Automatic UAC elevation for the apply scripts: `windows/privacy/apply.ps1` and `windows/sharpkeys/apply.ps1` started without administrator rights raise a UAC prompt through `Start-Process pwsh -Verb RunAs` and re-run in the child process with the original switches. `-DryRun` needs no elevation and skips it | Done | + +#### Where each Microsoft default ended up + +| Category | Microsoft | Replacement | |---|---|---| | Terminal | Windows Terminal | WezTerm | | Shell | PowerShell 5.1 | PowerShell 7 | | Editor | Notepad | Neovim | -| Browser | Edge | Chrome / Zen-Browser | -| Launcher | Start 検索 | Flow Launcher + Everything | -| Snipping | Snipping Tool | ShareX | -| WindowManager | Snap | GlazeWM | -| Keymap | (なし) | SharpKeys + AutoHotkey + PowerToys | -| Phone Link | Phone Link | KDE Connect + LocalSend | -| Photo Viewer | フォト | (保留) | -| Mail / Calendar | Mail / Outlook | Thunderbird | -| Sticky Notes | Sticky Notes | Obsidian | -| OneDrive | OneDrive | Syncthing | -| クリップボード履歴 | Win+V(揮発) | Ditto(SQLite 永続化) | -| ペイント | Paint | Krita | -| 動画録画 | Game Bar | Captura(+ OBS / ShareX) | -| 2FA | (なし) | Ente Auth | -| AV / リアルタイム保護 | Defender | Bitdefender Free(Defender は待機モード自動切替) | -| Outbound Firewall | Defender Firewall | simplewall(LuLu 相当) | -| 永続化監視 | (なし) | Sysinternals Autoruns(BlockBlock / KnockKnock 相当) | -| セカンド AV | (なし) | Malwarebytes Free | -| File Explorer | エクスプローラー | Files(scoop nonportable bucket、Store 回避) | -| プライバシー / Telemetry | (標準有効) | Win11Debloat + WinUtil で declarative 無効化 | - -### ⬛ P10(キーマップ宣言化 — Karabiner 相当) - -Mac の Karabiner-Elements に相当するキー remap を Windows で再現。 -**物理キー単体は SharpKeys 機構(Scancode Map レジストリ)、組合せは AutoHotkey** の 2 層構成で -Karabiner と同じ表現力を達成する。 - -| # | 内容 | 状態 | +| Browser | Edge | Chrome, Zen Browser | +| Launcher | Start search | Flow Launcher and Everything | +| Screenshots | Snipping Tool | ShareX | +| Window manager | Snap | GlazeWM | +| Key mapping | none | SharpKeys, AutoHotkey, PowerToys | +| Phone | Phone Link | KDE Connect and LocalSend | +| Photo viewer | Photos | undecided | +| Mail and calendar | Mail, Outlook | Thunderbird | +| Notes | Sticky Notes | Obsidian | +| File sync | OneDrive | Syncthing | +| Clipboard history | Win+V, volatile | Ditto, persisted in SQLite | +| Painting | Paint | Krita | +| Screen recording | Game Bar | Captura, with OBS and ShareX | +| 2FA | none | Ente Auth | +| Antivirus | Defender | Bitdefender Free; Defender switches itself to passive | +| Outbound firewall | Defender Firewall | simplewall, the LuLu equivalent | +| Persistence monitoring | none | Sysinternals Autoruns, standing in for BlockBlock and KnockKnock | +| Second-opinion scanner | none | Malwarebytes Free | +| File manager | File Explorer | Files, through the scoop nonportable bucket, avoiding the Store | +| Privacy and telemetry | on by default | disabled declaratively through Win11Debloat and WinUtil | + +### P10, declaring the keymap, the Karabiner equivalent + +Reproducing the Mac's Karabiner-Elements remapping on Windows. Two layers reach the same +expressiveness: single physical keys go through the SharpKeys mechanism, a Scancode Map registry +value, and combinations go through AutoHotkey. + +| # | Task | State | |---|---|---| -| P10-42 | `windows/sharpkeys/` 新設 — `keymap.skl`(人間可読の SharpKeys 形式)+ `apply.ps1`(SharpKeys GUI に依存しない PowerShell 実装、`HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map` をバイナリ直書き)+ README。CapsLock(0x3A) → Left Ctrl(0x1D)を declarative に管理。`-DryRun` / `-Clear`(全削除して standard に戻す)対応 | ✅ | -| P10-43 | `windows/autohotkey/` 新設 — `keymap.ahk`(AHK v2)で 2 機能を実装。(a) Copilot キー → 右 Ctrl(SharpKeys で scancode 単独 remap 不可な OEM 機種向け保険、`F23` / `+#F23` / `vk89` / `sc15D` の候補をコメント残し、実機 KeyHistory で確認)、(b) Emacs ショートカット復活(`Ctrl+A→Home` / `Ctrl+E→End` / `Ctrl+B→Left` / `Ctrl+F→Right` / `Ctrl+P→Up` / `Ctrl+N→Down` / `Ctrl+H→Backspace` / `Ctrl+D→Delete` / `Ctrl+K→Shift+End,Del`)。ターミナル(`ConsoleWindowClass` / `CASCADIA_HOSTING_WINDOW_CLASS` / WezTerm / mintty)とエディタ(Vim / VS Code / Cursor / nvim)は `#HotIf !IsEmacsExcluded()` で除外 | ✅ | -| P10-44 | `bootstrap.ps1` に Step 8 追加 — SharpKeys apply.ps1 呼び出し + AHK を `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\dotfiles-keymap.ahk` へ symlink(ログイン時自動起動)。`-SkipKeymap` で省略可能。`Justfile` に `win-keymap *flags`(SharpKeys 適用 + AHK reload)追加 | ✅ | +| P10-42 | Add `windows/sharpkeys/`: `keymap.skl` in SharpKeys' human-readable format, an `apply.ps1` that writes `HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map` as binary directly in PowerShell without needing the SharpKeys GUI, and a README. CapsLock (0x3A) becomes Left Ctrl (0x1D), declaratively. Supports `-DryRun` and `-Clear`, which removes everything and returns to standard | Done | +| P10-43 | Add `windows/autohotkey/` with `keymap.ahk` in AHK v2, doing two things. First, the Copilot key becomes Right Ctrl, as insurance for OEM machines where SharpKeys cannot remap that scancode alone; the candidates `F23`, `+#F23`, `vk89` and `sc15D` are left in comments and were checked with KeyHistory on the machine. Second, the Emacs shortcuts come back: `Ctrl+A` to Home, `Ctrl+E` to End, `Ctrl+B` to Left, `Ctrl+F` to Right, `Ctrl+P` to Up, `Ctrl+N` to Down, `Ctrl+H` to Backspace, `Ctrl+D` to Delete, `Ctrl+K` to Shift+End then Delete. Terminals (`ConsoleWindowClass`, `CASCADIA_HOSTING_WINDOW_CLASS`, WezTerm, mintty) and editors (Vim, VS Code, Cursor, nvim) are excluded through `#HotIf !IsEmacsExcluded()` | Done | +| P10-44 | Add step 8 to `bootstrap.ps1`: call the SharpKeys apply.ps1 and symlink the AHK script to `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\dotfiles-keymap.ahk` so it starts at login. Skippable with `-SkipKeymap`. Add `win-keymap *flags` to the Justfile, which applies SharpKeys and reloads AHK | Done | -### ⬛ P11(ロケール宣言化 — SKK 専有 / UTF-8 / US Region) +### P11, declaring the locale: SKK alone, UTF-8, US region -英語 UI で運用しつつ SJIS 由来 mojibake (`\` → `¥`) と冗長な IME を解消。 +Running an English UI while getting rid of the Shift-JIS mojibake, where `\` shows as `¥`, and +the redundant IMEs. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P11-45 | `windows/locale/apply.ps1` — 3 段階の declarative 適用。(A) User Language List = `ja-JP` 1 個 + `InputMethodTips` を CorvusSKK の固定 CLSID (`{EAEA0E29-AA1E-48EF-B2DF-46F4E24C6265}{956F14B3-5310-4CEF-9651-26710EB72F3A}`) 1 つに、`Set-WinUILanguageOverride en-US` で UI 英語固定 — 英語キーボードレイアウト (`0409:00000409`) と MS-IME が消え Win+Space 切替も無くなる、(B) `Set-WinSystemLocale en-US` + `HKLM\...\Nls\CodePage` の `ACP`/`OEMCP`/`MACCP` を 932 → 65001 化(Beta UTF-8 機能、`\` → `¥` の根本解消)、(C) `Set-WinHomeLocation -GeoId 244`(US)。自動 UAC elevation 付き、`-DryRun` / `-Skip{LanguageList,SystemLocale,HomeLocation}` で部分適用 | ✅ | -| P11-46 | `bootstrap.ps1` Step 9 として統合(`-SkipLocale` で省略可)、`Justfile` に `win-locale *flags` 追加 | ✅ | +| P11-45 | `windows/locale/apply.ps1` applies three things declaratively. (A) The user language list becomes `ja-JP` alone with `InputMethodTips` set to CorvusSKK's fixed CLSID (`{EAEA0E29-AA1E-48EF-B2DF-46F4E24C6265}{956F14B3-5310-4CEF-9651-26710EB72F3A}`), and `Set-WinUILanguageOverride en-US` fixes the UI to English. This removes the English keyboard layout (`0409:00000409`) and MS-IME, and with them the Win+Space switch. (B) `Set-WinSystemLocale en-US` plus setting `ACP`, `OEMCP` and `MACCP` under `HKLM\...\Nls\CodePage` from 932 to 65001, the Beta UTF-8 feature, which is what actually fixes `\` showing as `¥`. (C) `Set-WinHomeLocation -GeoId 244` for the US. It elevates itself through UAC and supports `-DryRun` and `-Skip{LanguageList,SystemLocale,HomeLocation}` | Done | +| P11-46 | Wire it in as step 9 of `bootstrap.ps1`, skippable with `-SkipLocale`, and add `win-locale *flags` to the Justfile | Done | -### ⬛ P12(WM 宣言化 — AeroSpace → GlazeWM + Karabiner → AHK 補強) +### P12, declaring the window manager: AeroSpace to GlazeWM, Karabiner to AHK -Mac の AeroSpace タイリング WM と Karabiner の app shortcut を Windows に移植。 +Porting the Mac's AeroSpace tiling window manager and Karabiner's app shortcuts to Windows. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P12-47 | `configs/wm/glazewm/config.yaml` — AeroSpace の Hyper key (`cmd+ctrl+alt`) を Win+Ctrl+Alt に 1:1 翻訳。focus/move/resize/workspace 切替(1-9,0)/move-to-workspace/floating/fullscreen/binding mode (resize)/monitor 切替/workspace back-and-forth/launcher(Flow Launcher)/terminal(WezTerm)を網羅。同じ筋肉記憶で Mac/Win 両方の WM を操作可能 | ✅ | -| P12-48 | `windows/autohotkey/keymap.ahk` に Karabiner の `cmd-ctrl-alt-o → Obsidian` を `#^!o::Run "obsidian://"` で移植 + 保険として `#^!Enter::Run "wezterm-gui.exe"` | ✅ | -| P12-49 | `configs/wm/zebar/{styles.css, settings.json}` — Mac SketchyBar との見た目統一を狙う。Tokyo Night palette → Rose Pine palette(`atuin`/`bat`/`man`/`wezterm`/`sketchybar` 等と同色)、SketchyBar 風 widget pill レイアウト、`box-shadow` で薄影、workspace focus invert、cpu 高負荷 = Love、充電中 = Foam。`` icon は HackGen Console NF 1st(Nerd 版で glyph 含む)+ M+Code/JetBrainsMono Nerd Font の fallback chain | ✅ | -| P12-50 | `bootstrap.ps1` ConfigLinks に `glazewm` / `zebar-css` / `zebar-settings` 追加 → `~/.glzr/glazewm/config.yaml` / `~/.glzr/zebar/{styles.css,settings.json}` へ symlink | ✅ | -| P12-51 | Zebar `bar.html` の fork + `sketchybar-app-font` + `icon_map.sh` JSON 化 で focused app icon を SketchyBar 並み精密化 | ⏳ pending | +| P12-47 | `configs/wm/glazewm/config.yaml` translates AeroSpace's Hyper key (`cmd+ctrl+alt`) one-to-one to Win+Ctrl+Alt, covering focus, move, resize, workspace switching 1-9 and 0, move-to-workspace, floating, fullscreen, the resize binding mode, monitor switching, workspace back-and-forth, the launcher (Flow Launcher) and the terminal (WezTerm). The same muscle memory drives both window managers | Done | +| P12-48 | Port Karabiner's `cmd-ctrl-alt-o` to Obsidian as `#^!o::Run "obsidian://"` in `windows/autohotkey/keymap.ahk`, with `#^!Enter::Run "wezterm-gui.exe"` as a fallback | Done | +| P12-49 | `configs/wm/zebar/{styles.css, settings.json}` aims to match SketchyBar on the Mac: the Tokyo Night palette becomes Rose Pine, matching `atuin`, `bat`, `man`, `wezterm` and sketchybar; SketchyBar-style widget pills; a faint `box-shadow`; inverted colours on the focused workspace; Love for high CPU and Foam while charging. The `` icons use HackGen Console NF first, whose Nerd build has the glyphs, falling back to M+Code and JetBrainsMono Nerd Font | Done | +| P12-50 | Add `glazewm`, `zebar-css` and `zebar-settings` to `bootstrap.ps1`'s ConfigLinks, symlinked to `~/.glzr/glazewm/config.yaml` and `~/.glzr/zebar/{styles.css,settings.json}` | Done | +| P12-51 | Fork Zebar's `bar.html` and add `sketchybar-app-font` with `icon_map.sh` converted to JSON, to make the focused app icon as precise as SketchyBar's | Pending | -### ⬛ P13(フォント宣言化 — sketchybar-app-font 等 user-scope install) +### P13, declaring fonts, user-scope installs for sketchybar-app-font and others -Mac の home-manager で `font-*` cask を入れるのと同じ精神で declarative 化。 +The same idea as installing `font-*` casks through home-manager on the Mac. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P13-52 | `windows/fonts/apply.ps1` — `configs/fonts/*.ttf|.otf` を `%LOCALAPPDATA%\Microsoft\Windows\Fonts\` に copy + `HKCU\Software\Microsoft\Windows NT\CurrentVersion\Fonts` にレジストリ登録。Win10 1809 以降の user-scope font install を活用(管理者不要)。冪等(既存 + reg 同値で skip)、`-DryRun` / `-Force` 対応 | ✅ | -| P13-53 | `bootstrap.ps1` Step 8.5 として統合(`-SkipFonts` で省略可)、`Justfile` に `win-fonts *flags` 追加 | ✅ | +| P13-52 | `windows/fonts/apply.ps1` copies `configs/fonts/*.ttf` and `*.otf` into `%LOCALAPPDATA%\Microsoft\Windows\Fonts\` and registers them under `HKCU\Software\Microsoft\Windows NT\CurrentVersion\Fonts`, using the user-scope font install available since Windows 10 1809, which needs no administrator rights. Idempotent, skipping when the file and registry value already match, with `-DryRun` and `-Force` | Done | +| P13-53 | Wire it in as step 8.5 of `bootstrap.ps1`, skippable with `-SkipFonts`, and add `win-fonts *flags` to the Justfile | Done | -### ⬛ P14(Bitwarden Desktop SSH Agent + WSL forward 統合) +### P14, Bitwarden Desktop's SSH agent plus WSL forwarding -Bitwarden Desktop 2025.1.2+ の SSH Agent 機能を Lab PC で使う。Windows OpenSSH -ssh-agent サービスを Disable して Bitwarden が `\\.\pipe\openssh-ssh-agent` を -独占、WSL は socat + npiperelay で透過 forward。 +Using the SSH agent feature of Bitwarden Desktop 2025.1.2 and later on the lab PC. The Windows +OpenSSH ssh-agent service is disabled so Bitwarden owns `\\.\pipe\openssh-ssh-agent`, and WSL +forwards transparently through socat and npiperelay. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P14-54 | `apps.json` に `Bitwarden.Bitwarden`(Desktop GUI)を追加(Bitwarden.CLI は既存)。SSH Key item type で private/public を vault に保管、Settings → SSH Agent で Enable + Bitwarden 再起動 | ✅ | -| P14-55 | `bootstrap.ps1` に `-UseBitwardenSSH` フラグ追加 — 指定時 `Stop-Service ssh-agent` + `Set-Service ssh-agent -StartupType Disabled`(Windows OpenSSH と Bitwarden が同じパイプ名で競合するため)。未指定時は従来通り Auto + Running | ✅ | -| P14-56 | WSL 側は `nix/home/wsl.nix` の既存 socat+npiperelay forward が Bitwarden 起動パイプにそのまま接続(socket 名同じ `\\.\pipe\openssh-ssh-agent`)。`ssh-add -l` で Bitwarden の鍵が見える | ✅(実機検証 OK) | -| P14-57 | 初回 SSH 認証時の Bitwarden Approval prompt — Settings → SSH Agent → Auto-approve / Whitelist に `npiperelay.exe` を入れると以後 prompt なし | ⏳ user が GUI で設定 | +| P14-54 | Add `Bitwarden.Bitwarden`, the desktop GUI, to apps.json; Bitwarden.CLI was already there. The private and public keys go into the vault as an SSH Key item, and the agent is enabled under Settings, SSH Agent, followed by a Bitwarden restart | Done | +| P14-55 | Add a `-UseBitwardenSSH` flag to `bootstrap.ps1`. When given it runs `Stop-Service ssh-agent` and `Set-Service ssh-agent -StartupType Disabled`, because Windows OpenSSH and Bitwarden compete for the same pipe name. Without it, the service stays Auto and Running as before | Done | +| P14-56 | On the WSL side the existing socat and npiperelay forwarding in `nix/home/wsl.nix` connects to Bitwarden's pipe unchanged, since the socket name is the same `\\.\pipe\openssh-ssh-agent`. `ssh-add -l` shows the Bitwarden keys | Done, verified on the machine | +| P14-57 | The Bitwarden approval prompt on first SSH use goes away once `npiperelay.exe` is added under Settings, SSH Agent, Auto-approve or the whitelist | Pending, a GUI action for the user | -### ⬛ P15(Lab PC username 非対称 — flake 拡張 + 個人情報の gitignore) +### P15, the asymmetric username on the lab PC, and keeping personal details out of git -Mac は `yuki`、Lab PC は `ispc_5cg54406v7`(社用、変更不可)。 -公開 repo に個人情報を残さず、Mac の `yuki` も壊さない form で対応。 +The Mac is `yuki`; the lab PC is `ispc_5cg54406v7`, which is company-assigned and cannot be +changed. This handles that without leaving personal details in a public repository and without +breaking `yuki` on the Mac. -| # | 内容 | 状態 | +| # | Task | State | |---|---|---| -| P15-58 | `nix/flake.nix` に generic 名の `homeConfigurations.labpc-wsl` 追加。`./user.local.nix` が存在すれば `user // import ./user.local.nix` で override、無ければ `user` のまま fallback | ✅ | -| P15-59 | `nix/user.local.nix.example` を template として commit。`.gitignore` に `nix/user.local.nix` 追加(個人情報を local に閉じる) | ✅ | -| P15-60 | `nix/home/common.nix` の `xcodegen` を `nix/home/darwin.nix` へ移動 — Linux/WSL の home-manager apply で `Refusing to evaluate package 'xcodegen' ... meta.platforms = [ darwin ]` 回避 | ✅ | -| P15-61 | WSL home-manager apply: `nix run github:nix-community/home-manager -- switch --flake ~/.dotfiles/nix#labpc-wsl`。`nix flake update --flake ./nix`(bare 名は registry 衝突)| ⏳ 実機で進行中 | +| P15-58 | Add a generically named `homeConfigurations.labpc-wsl` to `nix/flake.nix`. If `./user.local.nix` exists it overrides through `user // import ./user.local.nix`; otherwise `user` is used unchanged | Done | +| P15-59 | Commit `nix/user.local.nix.example` as a template and add `nix/user.local.nix` to `.gitignore`, keeping personal details local | Done | +| P15-60 | Move `xcodegen` from `nix/home/common.nix` to `nix/home/darwin.nix`, so a home-manager apply on Linux or WSL does not hit `Refusing to evaluate package 'xcodegen' ... meta.platforms = [ darwin ]` | Done | +| P15-61 | The WSL home-manager apply: `nix run github:nix-community/home-manager -- switch --flake ~/.dotfiles/nix#labpc-wsl`, and `nix flake update --flake ./nix`, since the bare name collides with the registry | In progress on the machine | --- -## 実装順 +## Order ``` -P0-1 → P0-3 → P0-4 → P0-2 (bootstrap が安定して走る) +P0-1 → P0-3 → P0-4 → P0-2 bootstrap runs reliably ↓ -P1-5 → P1-6 → P1-7 → P1-8 (ネイティブで configs が読まれる) +P1-5 → P1-6 → P1-7 → P1-8 configs are actually read natively ↓ -P2-9 → P2-10 → P2-11 → P2-12 (運用ルール化) +P2-9 → P2-10 → P2-11 → P2-12 turn the practices into rules ↓ -P3-13 → P3-14 → P3-15 (磨き込み) +P3-13 → P3-14 → P3-15 polish ``` diff --git a/esphome/README.md b/esphome/README.md index f1c8c2cd..22065fd2 100644 --- a/esphome/README.md +++ b/esphome/README.md @@ -1,51 +1,57 @@ # ESPHome -ESP チップに載る設定。ESPHome サーバ自体は `nix/hosts/homeserver.nix` の -`sites` 表 (`esphome.gapul.net`) で宣言しているが、デバイスの YAML は -そのコンテナの中にしか無く、repo の外に取り残されていた。ESPHome は -YAML そのものが本体なので、置かない理由が無い。 +The configuration that runs on the ESP chips. The ESPHome server itself is declared through the +`sites` table in `nix/hosts/homeserver.nix`, as `esphome.gapul.net`, but the device YAML existed +only inside that container and was left outside the repository. With ESPHome the YAML is the +thing itself, so there is no reason not to keep it here. ``` esphome/ -├── plant-watering.yaml # 鉢植え 4 つの自動水やり機 (ESP32 DevKitC) -├── common/pot.yaml # 鉢 1 つ分。GPIO と閾値だけ変えて 4 鉢に複製する -├── secrets.example.yaml # secrets.yaml の雛形 (実物は gitignore) -└── validate.sh # 実機なしで構文検証する +├── plant-watering.yaml # automatic watering for four pots, on an ESP32 DevKitC +├── common/pot.yaml # one pot; copied four times with different GPIOs and thresholds +├── secrets.example.yaml # a template for secrets.yaml, which is gitignored +└── validate.sh # checks the syntax without any hardware ``` -## 検証 +## Validating ```sh just esphome # nix shell nixpkgs#esphome -c ./validate.sh ``` -実物の `secrets.yaml` が無ければ雛形を使って一時ディレクトリで検証するので、 -鍵を持っていない環境でも通る。CI もこれを回している。 +If there is no real `secrets.yaml`, it validates in a temporary directory using the template, so +it passes on a machine without the keys. CI runs the same thing. -`esphome config` は検証エラーを stdout に出す。`>/dev/null` すると黙って -失敗するので、`validate.sh` は一度受けてから失敗時だけ見せている。 +`esphome config` writes validation errors to stdout, so redirecting to `/dev/null` makes it fail +silently. `validate.sh` captures the output and shows it only on failure. -## 書き込み +## Flashing ```sh nix shell nixpkgs#esphome -c esphome run esphome/plant-watering.yaml ``` -初回は USB、以降は OTA。`secrets.yaml` を先に作る (雛形をコピーして埋める)。 - -## 水やり機について - -設計は [gapul/esp32-plant-watering](https://github.com/gapul/esp32-plant-watering) -の `docs/design.md`。ここはその設計をそのまま YAML にしたもので、**部品が届く前に -書いてある**。閾値と給水秒数は実測で詰める前提の仮置きなので、キャリブレーション後に -直す。 - -設計から持ってきている制約: - -- ADC は必ず ADC1 系 (GPIO32-35)。ADC2 系は Wi-Fi 使用中に読めない -- ポンプは strapping pin (0, 2, 12, 15) を避ける。起動時に HIGH になる -- センサーは常時通電しない。GPIO25 から給電し、測るときだけ ON にする (電蝕対策) -- ポンプは必ず script 経由で回す。switch を直接 on にすると止める人がいなくなる -- 再起動でポンプが回り出さないよう `restore_mode: ALWAYS_OFF` -- 4 鉢は順番に回す。同時だと突入電流が USB 5V 2A を超える -- センサーは給水するかしないかの判定にのみ使う。水量は秒数 × 実測流量の決め打ち +Over USB the first time and OTA afterwards. Create `secrets.yaml` first, by copying the +template and filling it in. + +## About the watering machine + +The design is in `docs/design.md` in +[gapul/esp32-plant-watering](https://github.com/gapul/esp32-plant-watering). What is here is +that design turned straight into YAML, written before the parts arrived. The thresholds and +watering durations are placeholders meant to be pinned down by measurement, so revise them after +calibration. + +Constraints carried over from the design: + +- The ADC must be on ADC1, GPIO32-35. ADC2 cannot be read while Wi-Fi is in use. +- Keep the pumps off the strapping pins, 0, 2, 12 and 15, which go high at boot. +- The sensors are not permanently powered. They are fed from GPIO25 and switched on only while + measuring, to avoid electrolytic corrosion. +- Pumps are always driven through a script. Turning the switch on directly leaves nobody to turn + it off. +- `restore_mode: ALWAYS_OFF`, so a reboot does not start a pump. +- The four pots are watered in turn. Simultaneously, the inrush current exceeds what 5 V 2 A + over USB can supply. +- The sensors decide only whether to water, not how much. The volume is a fixed duration + multiplied by the measured flow rate. diff --git a/mobile/README.md b/mobile/README.md index 4d5ee4b3..36a953b6 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -1,95 +1,102 @@ -# モバイル (iOS / Android) +# Mobile: iOS and Android -スマホは nix が動かない (Android の Termux だけ例外) ので、`windows/` と同じ方針で扱う。 -**宣言ファイルを repo に置き、実機との差分を機械に判定させる。** -適用まで自動化できるかはプラットフォーム次第で、できない層は差分を出すところで止める。 +Nix does not run on a phone, with Termux on Android the only exception, so phones are handled +the way `windows/` is: the declaration lives in the repository, and a machine works out how the +real device differs from it. How much of the applying can be automated depends on the platform, +and where it cannot, the tooling stops at reporting the difference. ``` mobile/ ├── android/ -│ ├── apps.tsv + apps.sh # 宣言 vs 実機、F-Droid 系は install まで -│ ├── os-settings.conf # adb で流すグローバル設定 -│ ├── os-apps.tsv + os.sh # アプリ単位の設定 (権限 / 電池 / 既定ランチャー) -│ ├── launcher-theme.py # Kvaesitso のテーマを palettes.json から生成 -│ └── test.sh # 偽 adb での自己チェック +│ ├── apps.tsv + apps.sh # declaration against the device; F-Droid apps install too +│ ├── os-settings.conf # global settings pushed with adb +│ ├── os-apps.tsv + os.sh # per-app settings: permissions, battery, default launcher +│ ├── launcher-theme.py # generates Kvaesitso's theme from palettes.json +│ └── test.sh # self-check against a fake adb └── ios/ - ├── apps.tsv + sources.tsv # App Store / AltStore Classic / AltStore PAL - ├── apps.sh # 宣言 vs 実機 (ideviceinstaller)、install は不可 - ├── profiles/serve.sh # nix が生成した .mobileconfig を配る + ├── apps.tsv + sources.tsv # App Store, AltStore Classic, AltStore PAL + ├── apps.sh # declaration against the device through ideviceinstaller; cannot install + ├── profiles/serve.sh # serves the .mobileconfig files nix generated └── test.sh -nix/mobile/ios-profiles.nix # .mobileconfig の中身 (pkgs.formats.plist) -nix/hosts/droid.nix # Termux の中の CLI 環境 (nix-on-droid) +nix/mobile/ios-profiles.nix # the contents of the .mobileconfig, through pkgs.formats.plist +nix/hosts/droid.nix # the CLI environment inside Termux, through nix-on-droid ``` -## どこまで機械にやらせるか +## How far the machine gets -| 層 | Android | iOS | +| Layer | Android | iOS | |---|---|---| -| アプリ: 宣言 | `android/apps.tsv` | `ios/apps.tsv` | -| アプリ: 実在確認 | `apps.sh verify` (F-Droid 索引 / GitHub API / Play) | `apps.sh verify` (iTunes API / AltStore source) | -| アプリ: 実機との差分 | `apps.sh status` (adb) | `apps.sh status` (ideviceinstaller / USB) | -| アプリ: インストール | `apps.sh install` (F-Droid 系のみ。他は Obtainium / Aurora Store) | **不可**。署名済み ipa が要る | -| OS 設定 (全体) | `os-settings.conf` → `os.sh` | **不可**。`.mobileconfig` で届く範囲だけ | -| OS 設定 (アプリ単位) | `os-apps.tsv` → `os.sh` (権限 / 電池 / 既定ランチャー) | **不可** | -| ランチャー | 既定の指定 + テーマ生成 (レイアウトは不可) | ホーム画面は一切触れない | -| プロファイル生成 | — | `nix build .#ios-profiles` | -| CLI 環境 | `nix/hosts/droid.nix` | 作らない。Blink から母艦へ ssh | +| Apps: declared in | `android/apps.tsv` | `ios/apps.tsv` | +| Apps: confirming they exist | `apps.sh verify`, against the F-Droid index, the GitHub API and Play | `apps.sh verify`, against the iTunes API and the AltStore source | +| Apps: comparing with the device | `apps.sh status`, over adb | `apps.sh status`, through ideviceinstaller over USB | +| Apps: installing | `apps.sh install`, F-Droid only. The rest go through Obtainium or Aurora Store | Not possible; it needs a signed ipa | +| OS settings, global | `os-settings.conf` through `os.sh` | Not possible, beyond what a `.mobileconfig` can reach | +| OS settings, per app | `os-apps.tsv` through `os.sh` | Not possible | +| Launcher | Setting the default, and generating the theme. Layout is not possible | The home screen is untouched | +| Generating profiles | — | `nix build .#ios-profiles` | +| CLI environment | `nix/hosts/droid.nix` | Not attempted. ssh to the Mac from Blink | + +Everything marked as not possible has no API behind it. Standing up an MDM would let iOS be +pushed around too, but running a server and putting two devices into supervised mode is not +worth it. + +`status` on both sides exits 1 if anything is MISSING, the same as +`windows/winget/status.ps1`. EXTRA, meaning something on the device that is not declared, does +not fail. + +## What runs automatically and what does not + +| | When it runs | +|---|---| +| Checking a declared app has not vanished from its source, `verify` | Automatically, weekly in CI, through `.github/workflows/mobile-drift.yml` | +| The scripts' own sanity, `test.sh` | Automatically, in the same CI and through `just mobile-test` | +| Comparing with the device, `status` | By hand, only when a device is plugged in | +| The `settings` part of the OS settings | Can be applied from the device itself. Once Termux has the permission, it can run from cron | +| Debloating and per-app OS settings | By hand. They need signature-level permissions, so adb only | +| Installing apps | Hand the device a file and it does the rest: Obtainium takes a URL list, Aurora Store takes Favourites | -「不可」と書いた欄は API が無い。MDM を建てれば iOS も押し込めるが、端末 2 台に -サーバを建てて監視モードを掛ける値打ちは無いと判断した。 +Installing and `settings` have reached the point where handing the device a file is enough — +Obtainium's URL list, Aurora Store's favourites, and `os.sh` run from Termux. What still needs +the Mac and a cable is debloating and the per-app settings, which need signature-level +permissions, plus `status`, which counts the difference against the device. -`status` はどちらも MISSING があれば exit 1 で、`windows/winget/status.ps1` と -同じ扱い。EXTRA (実機に在るが宣言に無い) では落とさない。 +Leaving wireless debugging permanently available over the tailnet would automate the rest, but +that would create a state where the declaration moves on while nothing is connected and it +looks applied when it is not. So those stay explicit. -## 何が自動で、何が手動か +What was automated instead is the opposite: the things you cannot notice at the moment they +happen. A source being renamed or deleted goes by silently, so it gets checked weekly — which +is how the rename of `Catfriend1/syncthing-android` was caught. -| | いつ走るか | -|---|---| -| 宣言が配布元から消えていないか (`verify`) | **自動**。週次 CI (`.github/workflows/mobile-drift.yml`) | -| スクリプト自身の健全性 (`test.sh`) | **自動**。同 CI + `just mobile-test` | -| 実機との差分 (`status`) | 手動。端末を繋いだときだけ | -| OS 設定のうち `settings` | 端末内から当てられる。Termux に権限を一度与えれば cron 可 | -| OS 設定のうち debloat / アプリ個別 | 手動。署名レベルの権限が要るので adb 経由のみ | -| アプリのインストール | ファイルを渡して端末側で一括。Obtainium (URL リスト) / Aurora Store (Favourites) | +## Syncing app settings -インストールと `settings` は、**ファイルを渡せば端末側が自分でやる**ところまで来ている -(Obtainium の URL リスト / Aurora Store の Favourites / Termux から走る `os.sh`)。 -母艦とケーブルが要るのは、署名レベルの権限が要る debloat とアプリ個別、 -それに実機との差分を数える `status` だけ。 +Only settings that can be synced as files are synced through the home server, and the repository +holds nothing more than which app syncs through which route: the sync column in `apps.tsv` and +the table below. No keys and no databases. -無線デバッグを tailnet 越しに常設すれば残りも自動にできるが、繋がっていない間に -宣言だけ進んで「適用したつもり」になる状態を作りたくないので、そこは明示的に叩く形のまま。 +| App | Route | Declared on the server in | +|---|---|---| +| Obsidian | Self-hosted LiveSync, over CouchDB | `nix/homelab/obsidian-couchdb.nix` | +| KeePassium, self-built, and KeePassDX | The kdbx through Syncthing's SyncHub | `nix/homelab/syncthing.nix` | +| Bitwarden | Vaultwarden | `nix/homelab/vaultwarden.nix` | +| ntfy | Subscribing to topics on push.gapul.net | `nix/homelab/ntfy.nix` | +| OwnTracks | Posting location to Dawarich | `nix/homelab/dawarich.nix` | +| Calendar and contacts | CalDAV and CardDAV | `nix/homelab/radicale.nix` | +| RSS | Miniflux, through the Fever API | `nix/homelab/miniflux.nix` | +| Music, video and documents | Navidrome, Jellyfin and Paperless | The respective `nix/homelab/*.nix` | +| Matrix | The homeserver at `@gapul:gapul.net` | `nix/homelab/matrix.nix` | -自動にしたのは逆に**押した瞬間には気付けないもの**だけ。配布元の改名や削除は -黙って進むので週次で見る (実際 `Catfriend1/syncthing-android` の改名はこれで見つかった)。 +To add a device to Syncthing, put its device ID into `settings.devices` in +`nix/homelab/syncthing.nix` and rebuild. Committing it, rather than approving it in the web UI, +is how this repository works. The ID is a public key, so committing it is fine. -## アプリ設定の同期 +## What is deliberately not attempted -スマホ側の設定は「ファイルとして同期できるもの」だけ自宅サーバで同期し、 -repo は**どのアプリが何の経路で同期されているか**だけを持つ (`apps.tsv` の -同期列と下表)。鍵や DB そのものは入れない。 +**Home screen and widget layout.** iOS offers no way at all, and Android's Kvaesitso stores it +in a binary that is not compatible between versions, so putting it in the repository would show +no meaningful diff. Only the launcher theme is declared, in `android/launcher-theme.py`. -| アプリ | 経路 | サーバ側の宣言 | -|---|---|---| -| Obsidian | Self-hosted LiveSync (CouchDB) | `nix/homelab/obsidian-couchdb.nix` | -| KeePassium (自ビルド) / KeePassDX | kdbx を Syncthing の SyncHub 経由 | `nix/homelab/syncthing.nix` | -| Bitwarden | Vaultwarden | `nix/homelab/vaultwarden.nix` | -| ntfy | push.gapul.net の topic 購読 | `nix/homelab/ntfy.nix` | -| OwnTracks | 位置ログを Dawarich へ POST | `nix/homelab/dawarich.nix` | -| カレンダー / 連絡先 | CalDAV / CardDAV | `nix/homelab/radicale.nix` | -| RSS | Miniflux (Fever API) | `nix/homelab/miniflux.nix` | -| 音楽 / 動画 / 書類 | Navidrome / Jellyfin / Paperless | 各 `nix/homelab/*.nix` | -| Matrix | Conduit (@gapul:gapul.net) | `nix/homelab/matrix.nix` | - -新しい端末を Syncthing に加えるときは、端末の Device ID を -`nix/homelab/syncthing.nix` の `settings.devices` に足して rebuild する。 -Web UI で承認するのではなく commit するのがこの repo の作法 -(ID は公開鍵なので commit してよい)。 - -## やらないこと - -- **ホーム画面 / ウィジェット配置** — iOS は手段が無く、Android の Kvaesitso は - バージョン間で互換の無いバイナリなので、repo に置いても差分が見えない。 - ランチャーはテーマだけ宣言する (`android/launcher-theme.py`)。 -- **アプリ本体のバックアップ** — 端末のフルバックアップは iCloud / Seedvault の仕事。 -- **iOS の設定トグル** — 監視モードを掛けない限り触れない。手で設定する。 +**Backing up the apps themselves.** A full device backup is iCloud's or Seedvault's job. + +**iOS settings toggles.** Nothing can touch them without supervised mode, so they are set by +hand. diff --git a/mobile/android/README.md b/mobile/android/README.md index 7972c6dd..a034dc42 100644 --- a/mobile/android/README.md +++ b/mobile/android/README.md @@ -2,149 +2,155 @@ ``` android/ -├── apps.tsv # 入れるアプリの宣言 (packageId + 経路) +├── apps.tsv # which apps to install: packageId plus the route ├── apps.sh # status | install | verify | obtainium -├── os-settings.conf # settings put するグローバル設定 -├── os-apps.tsv # アプリ単位の OS 側設定 (既定ランチャー / 権限 / 電池) -├── os-debloat.txt # ユーザー 0 から外すプリイン -├── os.sh # 上 3 つを adb で適用 (差分表示 → 適用) -├── launcher-theme.py # Kvaesitso のテーマを palettes.json から生成 -└── test.sh # 上のスクリプトの自己チェック (偽 adb、実機不要) +├── os-settings.conf # global settings applied through settings put +├── os-apps.tsv # per-app OS settings: default launcher, permissions, battery +├── os-debloat.txt # preinstalled apps to remove from user 0 +├── os.sh # applies the three above over adb, showing the difference first +├── launcher-theme.py # generates Kvaesitso's theme from palettes.json +└── test.sh # self-check for the scripts above, against a fake adb, no device needed ``` -## アプリ +## Apps -3 つのストアを使い分けているので、`apps.tsv` の source 列でどれ担当かを宣言する。 +Three stores are in use, so the `source` column in `apps.tsv` says which one handles each app. -| source | 配布元 | 母艦から入れる | 端末側で更新 | +| source | Distributed by | Installable from the Mac | Updated on the device by | |---|---|---|---| -| `fdroid` | F-Droid 公式 | ○ `apps.sh install` | Obtainium / F-Droid | -| `izzy` | IzzyOnDroid | ○ 同上 | Obtainium | -| `github` | GitHub Releases | × | Obtainium | -| `play` | Play ストア | × | Aurora Store (Favourites を import して一括インストール) | +| `fdroid` | F-Droid | Yes, `apps.sh install` | Obtainium or F-Droid | +| `izzy` | IzzyOnDroid | Yes, the same | Obtainium | +| `github` | GitHub releases | No | Obtainium | +| `play` | The Play Store | No | Aurora Store, importing Favourites and installing them in one go | ```sh -./apps.sh status # 宣言 vs 実機。MISSING があれば exit 1 -./apps.sh install # F-Droid 系を fdroidcl 経由で入れる。残りは経路を報告 -./apps.sh verify # 4 経路すべてに実在するか確かめる -./apps.sh obtainium # 端末の Obtainium に貼る URL リストを出す -./apps.sh adopt # 端末に在って宣言に無いものを、経路を判定して tsv 行で出す -./apps.sh aurora # play 行を Aurora Store の Favourites に import できる JSON にする +./apps.sh status # declaration against the device. Exits 1 if anything is MISSING +./apps.sh install # installs the F-Droid ones through fdroidcl, and reports the route for the rest +./apps.sh verify # checks all four routes still carry what is declared +./apps.sh obtainium # prints the URL list to paste into Obtainium on the device +./apps.sh adopt # prints tsv lines for what is on the device but not declared, working out the route +./apps.sh aurora # turns the play rows into JSON that Aurora Store can import as Favourites ``` -Aurora Store で入れたものなど、端末側で先に増やしたアプリは `adopt` で回収する。 -packageId を目で追って書き写す作業になるので、経路の判定ごと機械にやらせる: +Apps added on the device first, through Aurora Store or otherwise, are collected with `adopt`. +Doing it by hand means reading package IDs off the screen and copying them, so the route +detection is left to the machine: ```sh -./apps.sh adopt >>apps.tsv # 追記してから中身を見て整える +./apps.sh adopt >>apps.tsv # append, then read through and tidy up ``` -APK を取って `adb install` する部分は [fdroidcl](https://github.com/mvdan/fdroidcl) -に任せている。ここが持つのは「何を入れるか」の宣言と差分の判定だけ。 +Fetching the APK and running `adb install` is [fdroidcl](https://github.com/mvdan/fdroidcl)'s +job. What lives here is the declaration of what should be installed and the comparison against +what is. -`status` が `windows/winget/status.ps1` と同じ役割で、`just android-apps` から呼ぶ。 -EXTRA (端末に在るが宣言に無い) では落とさない。試しに入れたものは必ず在るし、 -消すかどうかは人が決めること。宣言を満たしていないことだけを失敗として扱う。 +`status` plays the same role as `windows/winget/status.ps1` and is called from +`just android-apps`. EXTRA, meaning something present but undeclared, does not fail: anything +tried out is going to be there, and whether to remove it is a person's decision. Only failing +to satisfy the declaration counts as a failure. -`verify` は F-Droid の索引 / GitHub API / Play のストアページを引いて、 -綴り間違いと配布元の移転を捕まえる。実際これで `Catfriend1/syncthing-android` が -`researchxxl/` に改名済みなのが見つかった。GitHub 照会は `gh` があればそちらを使う -(未認証の API は 60 回/時で、宣言が増えると rate limit で落ちる)。 +`verify` queries the F-Droid index, the GitHub API and the Play store page, catching typos and +sources that have moved. That is how `Catfriend1/syncthing-android` was found to have been +renamed to `researchxxl/`. GitHub is queried through `gh` if it is available, because the +unauthenticated API allows 60 requests an hour and hits the rate limit as the declaration grows. -**端末側の自動更新は Obtainium が担う。** `apps.sh obtainium` が出す URL リストを -アプリ内の Import/Export → Import from URL List に貼る。URL は packageId / ref から -機械的に決まるので、URL 一覧を別ファイルで持つことはしない。 +**Updates on the device are Obtainium's job.** Paste the URL list from `apps.sh obtainium` into +the app under Import/Export, Import from URL List. The URLs follow mechanically from the package +ID and ref, so there is no separate file listing them. -Play しか配布元が無いものは Aurora Store に渡す。4.6 以降は Favourites の -import/export と一括インストールがあるので、こちらもファイルを渡す形で済む: +Anything distributed only through Play goes to Aurora Store, which since 4.6 can import and +export Favourites and install them in bulk, so it is also a matter of handing over a file: ```sh ./apps.sh aurora >/tmp/aurora-favourites.json adb push /tmp/aurora-favourites.json /sdcard/Download/ -# 端末で Aurora Store → Favourites → Import → まとめて install +# on the device: Aurora Store, Favourites, Import, then install them all ``` -形式は AuroraStore の `data/room/favourite/{ImportExport,Favourite}.kt` に合わせてある。 -`displayName` は packageId をそのまま置く (表示用のラベルでしかなく、正しい名前を -取るには Play を 1 件ずつ引く必要があって割に合わない)。 +The format matches Aurora Store's `data/room/favourite/{ImportExport,Favourite}.kt`. +`displayName` is just the package ID, since it is only a label and getting the real name would +mean one Play lookup per app, which is not worth it. -いまは `play` 行がゼロなので、**宣言した全アプリが Obtainium への 1 回の import で端末側に載る**。 -母艦から `install` できない `github` 行も、Obtainium に入れば追跡と更新は同じように効く。 -Play にしか無いものが出てきたときだけ手が要るので、GitHub 配布があるなら -`play` ではなく `github` に寄せる (Bitwarden は GitHub Releases に APK があったので移した)。 +There are currently no `play` rows, which means every declared app arrives on the device through +a single Obtainium import. Even the `github` rows, which cannot be installed from the Mac, are +tracked and updated the same way once Obtainium has them. Only something available exclusively +on Play needs manual work, so when a GitHub distribution exists, prefer `github` over `play` — +Bitwarden was moved for exactly that reason, since its APK is on GitHub releases. -## OS 設定 +## OS settings ```sh -./os.sh --dry-run # 何が変わるか見る -./os.sh # 適用 +./os.sh --dry-run # see what would change +./os.sh # apply ``` -母艦から adb 越しにも、**端末の中 (Termux) からも同じファイルで走る**。 -USB デバッグを有効にして 1 台だけ繋いだ状態で実行する。 +The same file runs both over adb from the Mac and from inside the device, in Termux. Run it with +USB debugging on and exactly one device connected. -### ケーブル無しで settings を当てる +### Applying settings without a cable -Termux に `WRITE_SECURE_SETTINGS` を一度だけ与えておくと、以降は端末の中から -`settings` を書けるようになる。母艦を出さずに済むので、ここだけは端末側で -自動化できる (Termux:Boot や cron から呼ぶ)。 +Grant Termux `WRITE_SECURE_SETTINGS` once and it can write `settings` from the device +afterwards. That removes the Mac from the loop, so this part can be automated on the device, +from Termux:Boot or cron. ```sh -# 母艦から一度だけ (この 1 回はケーブルが要る) +# once from the Mac. This is the only time a cable is needed. adb shell pm grant com.termux.nix android.permission.WRITE_SECURE_SETTINGS -# 以降は端末の Termux で +# afterwards, in Termux on the device cd ~/.dotfiles/mobile/android && ./os.sh ``` -`uname -o` が `Android` を返すことで端末内と判定し、`adb` を挟まずに実行する。 +It detects that it is on the device by `uname -o` returning `Android`, and runs without adb. -**debloat とアプリ個別は端末内からは通らない。** `pm uninstall` は -`DELETE_PACKAGES`、`pm grant` は `GRANT_RUNTIME_PERMISSIONS` で、どちらも -署名レベル。adb の shell uid だから通るのであって、アプリの uid には与えられない。 -黙って飛ばさず理由を出すので、その 2 つだけ母艦から流す。現在値と一致する行は -飛ばすので、何度流しても同じ結果になる。 +**Debloating and the per-app settings cannot work from the device.** `pm uninstall` needs +`DELETE_PACKAGES` and `pm grant` needs `GRANT_RUNTIME_PERMISSIONS`, both signature-level. They +work over adb because of the shell uid, and cannot be granted to an app's uid. Rather than +skipping them silently, the script says why, and those two are run from the Mac. Rows that +already match the current value are skipped, so running it repeatedly gives the same result. -- `os-settings.conf` — `settings` テーブルに載っているグローバル設定。 - トグルの多くはここに無く、adb からは届かないので手で設定する。 -- `os-apps.tsv` — アプリ単位。既定ランチャー / 権限の付け外し / AppOps / - 電池最適化の除外。アプリ「内部」の設定 (アカウントや同期先) はどうやっても - 外から触れないので、アプリ自身の同期機能に任せる (`../README.md` の表)。 -- `os-debloat.txt` — `pm uninstall -k --user 0` なのでシステムパーティションは - 無傷で、`adb shell cmd package install-existing ` で戻せる。 +- `os-settings.conf` — the global settings that live in the `settings` tables. Many toggles are + not there and cannot be reached over adb at all, so those are set by hand. +- `os-apps.tsv` — per app: the default launcher, granting and revoking permissions, AppOps, and + battery optimisation exemptions. Settings *inside* an app, such as accounts and sync targets, + cannot be touched from outside by any means, so they are left to the app's own sync; see the + table in `../README.md`. +- `os-debloat.txt` — this is `pm uninstall -k --user 0`, so the system partition is untouched + and `adb shell cmd package install-existing ` brings anything back. -## ランチャー (Kvaesitso) +## The launcher, Kvaesitso -ホーム画面のレイアウトやウィジェット配置は宣言しない。Kvaesitso のバックアップは -バージョン間で互換が保証されないバイナリで、repo に置いても差分が見えないため。 +Home screen layout and widget placement are not declared. Kvaesitso's backup is a binary with no +compatibility guarantee between versions, so putting it in the repository would show no +meaningful diff. -宣言しているのは 2 つ: +Two things are declared: -- **既定ランチャーであること** — `os-apps.tsv` の `home` 行が - `cmd package set-home-activity` で設定する。 -- **テーマ** — `launcher-theme.py` が `configs/theme/palettes.json` から - ThemeBundle v2 (JSON) を生成する。母艦・tmux・Windows と同じ SSOT に乗るので、 - rose-pine を差し替えればランチャーも一緒に変わる。light/dark 両方を 1 つの - テーマに入れてあるので、端末の外観設定に追従する。 +- **That it is the default launcher**, through the `home` row in `os-apps.tsv`, which runs + `cmd package set-home-activity`. +- **The theme.** `launcher-theme.py` generates a ThemeBundle v2 JSON from + `configs/theme/palettes.json`, which is the same single source the Mac, tmux and Windows use, + so replacing rose-pine changes the launcher along with everything else. Light and dark are + both in the one theme, so it follows the device's appearance setting. ```sh -just android-launcher-theme # 生成して /sdcard/Download/ に push -# 端末で Kvaesitso → 設定 → 外観 → テーマ → インポート +just android-launcher-theme # generates it and pushes it to /sdcard/Download/ +# on the device: Kvaesitso, Settings, Appearance, Theme, Import ``` -## 端末内の CLI (nix-on-droid) +## The CLI on the device, through nix-on-droid -母艦と同じ zsh / git / tmux / CLI ツールが Termux の上に載る。設定の実体は -`nix/hosts/droid.nix` で、`nix/modules/home/` の git / cli / shell / terminal を -そのまま共有している。GUI 前提の component と、flake input のモジュールに依存する -component (nix-index, agent-skills) は読み込まない。 +The same zsh, git, tmux and CLI tools as the Mac, running on top of Termux. The configuration is +`nix/hosts/droid.nix`, sharing the git, cli, shell and terminal components in +`nix/modules/home/` unchanged. Components that assume a GUI, and components that depend on +modules from flake inputs such as nix-index and agent-skills, are not loaded. -初回は [Termux:Nix](https://f-droid.org/packages/com.termux.nix/) を入れて -(通常の Termux ではなく nix 対応版)、アプリ内で: +The first time, install [Termux:Nix](https://f-droid.org/packages/com.termux.nix/) — the +nix-capable build, not ordinary Termux — and inside it run: ```sh nix-on-droid switch --flake github:gapul/dotfiles?dir=nix#default ``` -以降の更新も同じコマンド。CI では `nix run .#ci-nixondroid` (aarch64-linux) -が activation package のビルドだけ通している。 +Updates use the same command. In CI, `nix run .#ci-nixondroid` on aarch64-linux builds the +activation package and no more. diff --git a/mobile/ios/README.md b/mobile/ios/README.md index 2d0f2714..9be1f0d7 100644 --- a/mobile/ios/README.md +++ b/mobile/ios/README.md @@ -1,118 +1,120 @@ # iOS -iOS には adb に当たるものが無く、監視モード (Apple Configurator で supervise) を -掛けない限り外から押し込めるものが何も無い。**入れる仕事は自動化できないが、 -入っているかの確認と、入れる物の生成はできる**ので、その 2 つを持っている。 +iOS has no equivalent of adb, and without supervised mode, meaning supervising the device with +Apple Configurator, nothing can be pushed in from outside. Installing cannot be automated, but +checking what is installed and generating what gets installed can be, so those are what lives +here. ``` ios/ -├── apps.tsv # 入れるアプリの宣言 (bundleId + 入手経路 + 同期経路) -├── sources.tsv # AltStore 系 source の URL (Classic / PAL) +├── apps.tsv # which apps to install: bundleId, how to get it, how it syncs +├── sources.tsv # the URLs of the AltStore sources, Classic and PAL ├── apps.sh # status | verify -├── test.sh # apps.sh の自己チェック (偽 ideviceinstaller、実機不要) -└── profiles/serve.sh # nix が生成した .mobileconfig を LAN 配信 +├── test.sh # self-check for apps.sh, against a fake ideviceinstaller, no device needed +└── profiles/serve.sh # serves the .mobileconfig files nix generated over the LAN ``` -プロファイルの中身は `nix/mobile/ios-profiles.nix`。 +The contents of the profiles are in `nix/mobile/ios-profiles.nix`. -## アプリ +## Apps ```sh -./apps.sh status # USB 接続した iPhone と宣言の差分。MISSING があれば exit 1 -./apps.sh verify # 宣言した bundleId が経路上に実在するか (3 経路すべて) +./apps.sh status # the declaration against a USB-connected iPhone. Exits 1 if anything is MISSING +./apps.sh verify # checks each declared bundleId still exists on its route, across all three ``` -3 つの経路を使い分けているので、`apps.tsv` の source 列でどれ担当かを宣言する。 +Three routes are in use, so the `source` column in `apps.tsv` says which one handles each app. -| source | 入手 | verify の照会先 | +| source | Where it comes from | What verify queries | |---|---|---| -| `appstore` | App Store | iTunes Search API | -| `altstore-classic` | 母艦で再署名して入れる自ビルド | `sources.tsv` の classic な source の JSON | -| `altstore-pal` | AltStore PAL (代替マーケットプレイス) | 同 pal | +| `appstore` | The App Store | The iTunes Search API | +| `altstore-classic` | Self-built, re-signed on the Mac | The JSON of the classic source in `sources.tsv` | +| `altstore-pal` | AltStore PAL, the alternative marketplace | The pal source in the same file | -`status` は `ideviceinstaller` で実機を照会するので、USB 接続と端末側の -「このコンピュータを信頼」が要る。ネットワーク越しには照会できない。 +`status` queries the device through `ideviceinstaller`, so it needs USB and "Trust this +Computer" on the phone. There is no way to query it over the network. -インストールは自動化できない。App Store も PAL も署名済み ipa を要求するので、 -手で入れる。だから `status` は「入れ直しの残りを数える」道具として使う。 +Installing cannot be automated. Both the App Store and PAL want a signed ipa, so it is done by +hand, which makes `status` a way of counting what is left to reinstall. -**自ビルドは再署名で bundleId が変わる。** App Store 版の `com.keepassium.ios` と -自ビルドの `net.gapul.keepassium` は端末から見て別物なので、宣言する側も -実機に入っている方を書く。`verify` が source の JSON と突き合わせるので、 -ここを取り違えると落ちる。 +**Re-signing a self-built app changes its bundleId.** The App Store's `com.keepassium.ios` and +the self-built `net.gapul.keepassium` are different apps as far as the device is concerned, so +declare whichever is actually installed. `verify` compares against the source's JSON, so getting +this wrong makes it fail. -自ビルドの署名配信は [gapul/altstore-source](https://github.com/gapul/altstore-source) -(`sources.tsv` の `gapul-selfbuild`)。ビルド手順は `docs/self-build-software.md`。 +The signed self-builds are distributed through +[gapul/altstore-source](https://github.com/gapul/altstore-source), which is `gapul-selfbuild` in +`sources.tsv`. How they are built is in `docs/self-build-software.md`. -## 構成プロファイル +## Configuration profiles -`.mobileconfig` は XML plist でしかないので、payload を nix の attrset で書いて -`pkgs.formats.plist` に流している (`nix/mobile/ios-profiles.nix`)。 +A `.mobileconfig` is only an XML plist, so the payloads are written as a nix attrset and run +through `pkgs.formats.plist`, in `nix/mobile/ios-profiles.nix`. ```sh -nix build ./nix#ios-profiles # 生成 -./profiles/serve.sh # LAN に出す (中で nix build もする) +nix build ./nix#ios-profiles # generate them +./profiles/serve.sh # serve them on the LAN; this builds them too ``` -同じ LAN の iPhone の Safari から表示された URL を開くと、ダウンロード後に -設定アプリの「プロファイルがダウンロードされました」から入る。Safari 以外の -ブラウザではこの導線に乗らない。 +Open the printed URL in Safari on an iPhone on the same LAN, and after it downloads it appears +in Settings under "Profile Downloaded". No other browser leads anywhere. -PayloadUUID は名前のハッシュから決定的に導いている。ここが毎回変わると、 -更新のたびに別物として端末にプロファイルが積み上がる。 +The PayloadUUID is derived deterministically from a hash of the name. If it changed each time, +every update would pile up on the device as a separate profile. -ベンダーが署名済みで配っているものは書かない — NextDNS の DNS プロファイルも -Tailscale の VPN プロファイルも本家が配っていて、そちらの方が信頼済みとして入る。 -配布元が無いものだけを宣言する。 +Anything a vendor distributes ready-signed is not written here. NextDNS's DNS profile and +Tailscale's VPN profile both come from upstream and install as trusted, which is better. Only +the things nobody distributes get declared. -| 用途 | どこから | +| Purpose | Where it comes from | |---|---| -| 自宅 Radicale の CalDAV/CardDAV | `nix/mobile/ios-profiles.nix` (配布元が無いので自前)。宛先は `hosts/homeserver.nix` の `sites` 表が立てる `dav` の vhost | -| DNS (NextDNS) | `https://apple.nextdns.io/` を Safari で開く | -| 自宅 tailnet | Tailscale アプリ本体が VPN プロファイルを入れる | +| CalDAV and CardDAV for the Radicale at home | `nix/mobile/ios-profiles.nix`, because nobody distributes one. The target is the `dav` vhost created by the `sites` table in `hosts/homeserver.nix` | +| DNS, through NextDNS | Open `https://apple.nextdns.io/` in Safari | +| The tailnet at home | The Tailscale app installs its own VPN profile | -## ショートカット +## Shortcuts -iCloud で iPhone と母艦を往復しているので、母艦の -`~/Library/Shortcuts/Shortcuts.sqlite` を読めば端末を繋がずに中身が取れる -(素の bplist で暗号化されていない)。 +Shortcuts move between the phone and the Mac through iCloud, so reading the Mac's +`~/Library/Shortcuts/Shortcuts.sqlite` gets at their contents without connecting a device. They +are plain bplist and are not encrypted. ```sh -./shortcuts.sh export # 母艦の Shortcuts から shortcuts/*.plist に書き出す -./shortcuts.sh status # 書き出し済みと母艦の一覧の差分 -./shortcuts.sh build # shortcuts/*.cherri を署名済み .shortcut にする +./shortcuts.sh export # writes the Mac's Shortcuts out into shortcuts/*.plist +./shortcuts.sh status # what has been exported against what the Mac has +./shortcuts.sh build # turns shortcuts/*.cherri into signed .shortcut files ``` -書き出しは**バックアップとレビューのため**にやっている。ショートカットの実体は -iCloud にしか無く、消えたら戻せない。XML plist に開いてあるので差分も読める。 +Exporting is for backup and review. A shortcut exists only in iCloud, and if it goes there is +nothing to restore from. Written out as XML plist, the diffs are readable too. -新規に書くときは [Cherri](https://github.com/electrikmilk/cherri) を使う。 -テキストから署名済みの `.shortcut` (AEA1 コンテナ) を直接吐く Go 製の -コンパイラで、flake があるので `nix run` で足りる。 +New ones are written with [Cherri](https://github.com/electrikmilk/cherri), a compiler in Go +that turns text straight into a signed `.shortcut`, an AEA1 container. It has a flake, so +`nix run` is enough. -**手で組んだ plist は `shortcuts sign` に通らない。** XML でも binary でも、 -最小構成でも現行の必須キーを揃えても弾かれた。だから mobileconfig のように -nix で組み立てる形は取れず、Cherri に任せている。 +**A hand-built plist will not pass `shortcuts sign`.** XML or binary, minimal or with every +currently required key present, it was rejected either way. So unlike the mobileconfig files +these cannot be assembled in nix, and Cherri does it instead. -`build` が出した `.shortcut` は母艦の Shortcuts に `open` で入れれば -iCloud が iPhone に運ぶ。端末を繋ぐ必要は無い。 +A `.shortcut` from `build` goes into the Mac's Shortcuts with `open`, and iCloud carries it to +the phone. No cable involved. -## 端末内の CLI +## The CLI on the device -**iOS 上に環境を作らない。** iSH は i386 エミュレーションで遅く、a-Shell は -サンドボックスの都合で普通の Unix にならない。どちらも母艦の設定を持ち込むには -別系統の config を維持する羽目になる。 +**No environment is built on iOS.** iSH emulates i386 and is slow, and a-Shell's sandbox means +it never quite becomes a normal Unix. Either would mean maintaining a second set of +configuration to carry the Mac's setup across. -代わりに [Blink Shell](https://blink.sh) から tailnet 越しに母艦へ入る: +Instead, [Blink Shell](https://blink.sh) connects to the Mac over the tailnet: ```sh -ssh macmini # あるいは homeserver -nssh # rootless nix で nvim/yazi/tmux を自分の設定のまま +ssh macmini # or homeserver +nssh # nvim, yazi and tmux through rootless nix, with the usual configuration ``` -`nssh` が置いていく設定は `configs/shell/zshrc.remote` で、母艦と共通の部分は -`configs/shell/zshrc.common` を直接読む。つまり iPhone から入っても同じ shell になる。 +The configuration `nssh` leaves behind is `configs/shell/zshrc.remote`, and the parts shared +with the Mac come from reading `configs/shell/zshrc.common` directly. So the shell is the same +one whether you arrive from the phone or not. -Blink 自体の設定 (キーマップ、ホスト定義) はアプリ内に閉じていて外に出せないので、 -これは宣言管理の外。SSH 鍵は Blink で端末ごとに生成して公開鍵だけ配る -(母艦の鍵を持ち出さない)。 +Blink's own settings — key mappings, host definitions — are locked inside the app and cannot be +extracted, so they are outside declarative management. SSH keys are generated per device in +Blink and only the public key is distributed; the Mac's key never leaves the Mac. diff --git a/nix/homelab/README.md b/nix/homelab/README.md index 221e77f8..08e2448c 100644 --- a/nix/homelab/README.md +++ b/nix/homelab/README.md @@ -26,22 +26,22 @@ name that matters now. | file | keys | | --- | --- | | `archivebox.env` | `ADMIN_PASSWORD` | -| `authelia/jwt`, `authelia/session`, `authelia/storage-encryption` | それぞれ `openssl rand -hex 32` の生の文字列 1 行 (`.env` ではない)。`storage-encryption` を失うと DB 内の TOTP 秘密が復号できなくなり、登録し直しになる | -| `authelia/users.yml` | Authelia のユーザーファイル。`users: { : { disabled: false, displayname: ..., password: "", email: ..., groups: [admins] } }`。ハッシュは `nix run nixpkgs#authelia -- crypto hash generate argon2` で作る。平文のパスワードは入れない | +| `authelia/jwt`, `authelia/session`, `authelia/storage-encryption` | one raw line from `openssl rand -hex 32` each, not an `.env`. Lose `storage-encryption` and the TOTP secrets in the database cannot be decrypted, so everyone re-enrols | +| `authelia/users.yml` | Authelia's user file: `users: { : { disabled: false, displayname: ..., password: "", email: ..., groups: [admins] } }`. Generate the hash with `nix run nixpkgs#authelia -- crypto hash generate argon2`. Never a plaintext password | | `attic.env` | `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64`, `POSTGRES_PASSWORD` | -| `calnode.env` | `CALNODE_ENCRYPTION_KEY`, `CALNODE_RECOVERY_SECRET` — どちらも `openssl rand -hex 32`。前者を失うと DB 内の暗号化データは復号できなくなり、後者はそのときの唯一の逃げ道なので、同じ場所に置かない | +| `calnode.env` | `CALNODE_ENCRYPTION_KEY` and `CALNODE_RECOVERY_SECRET`, both `openssl rand -hex 32`. Losing the first makes the encrypted data in the database unreadable and the second is the only way out of that, so do not keep them in the same place | | `dawarich.env` | `POSTGRES_PASSWORD`, `DATABASE_PASSWORD`, `SECRET_KEY_BASE`, `OTP_ENCRYPTION_PRIMARY_KEY`, `OTP_ENCRYPTION_KEY_DERIVATION_SALT`, `OTP_ENCRYPTION_DETERMINISTIC_KEY`, `APPLICATION_HOSTS` | | `matrix.env` | `CONDUIT_REGISTRATION_TOKEN` | | `miniflux.env` | `DATABASE_URL` (the whole `postgres://miniflux:@db/miniflux?sslmode=disable` string), `POSTGRES_PASSWORD`, `ADMIN_PASSWORD` | | `obsidian-couchdb.env` | `COUCHDB_USER`, `COUCHDB_PASSWORD` | -| `searx.env` | `SEARXNG_SECRET` (`openssl rand -hex 32`)。SearXNG の `server.secret_key` に envsubst で入る | -| `rallly.env` | `DATABASE_URL` (`postgres://rallly:@db/rallly` の全体), `POSTGRES_PASSWORD`, `SECRET_PASSWORD` (**32文字以上**。短いと zod の検証で起動時に落ちる), `SUPPORT_EMAIL` (必須。ログイン用のメールを出さなくても値自体は要る) | -| `spliit.env` | `POSTGRES_PRISMA_URL`, `POSTGRES_URL_NON_POOLING` (どちらも `postgresql://spliit:@db/spliit` の全体), `POSTGRES_PASSWORD` | +| `searx.env` | `SEARXNG_SECRET`, from `openssl rand -hex 32`, substituted into SearXNG's `server.secret_key` with envsubst | +| `rallly.env` | `DATABASE_URL`, the whole `postgres://rallly:@db/rallly`, plus `POSTGRES_PASSWORD` and `SECRET_PASSWORD`, which must be at least 32 characters or zod rejects it at startup, and `SUPPORT_EMAIL`, which is required even if no login mail is ever sent | +| `spliit.env` | `POSTGRES_PRISMA_URL` and `POSTGRES_URL_NON_POOLING`, both the whole `postgresql://spliit:@db/spliit`, plus `POSTGRES_PASSWORD` | | `paperless.env` | `PAPERLESS_SECRET_KEY`, `PAPERLESS_ADMIN_PASSWORD` | | `vaultwarden.env` | `ADMIN_TOKEN` | -| `romm.env` | `MARIADB_ROOT_PASSWORD`, `MARIADB_PASSWORD`, `DB_PASSWD` (後ろ2つは同じ値), `ROMM_AUTH_SECRET_KEY` (`openssl rand -hex 32`), `IGDB_CLIENT_ID`, `IGDB_CLIENT_SECRET` | -| `gameyfin.env` | `APP_KEY` (**`head -c 32 /dev/urandom \| base64`**。AES 鍵として読まれるので 128/192/256 bit ちょうどでないと `Invalid AES key length` で起動ループする。hex 64 文字は 64 byte 扱いになって通らない), `IGDB_CLIENT_ID`, `IGDB_CLIENT_SECRET` (romm と同じ値で良い) | -| `hauk/config.php` | `.env` ではなく PHP の設定ファイル。`/var/lib/homelab/hauk/config.php` に置く。イメージ内の `/etc/hauk/config-sample.php` を写して `password` をハッシュに差し替える (`podman exec hauk php -r 'echo password_hash("...", PASSWORD_DEFAULT);'`)。`public_url` は `https://where.gapul.net/` | +| `romm.env` | `MARIADB_ROOT_PASSWORD`, `MARIADB_PASSWORD` and `DB_PASSWD`, the last two being the same value, plus `ROMM_AUTH_SECRET_KEY` from `openssl rand -hex 32`, `IGDB_CLIENT_ID` and `IGDB_CLIENT_SECRET` | +| `gameyfin.env` | `APP_KEY`, from `head -c 32 /dev/urandom \| base64`. It is read as an AES key, so anything other than exactly 128, 192 or 256 bits loops on `Invalid AES key length`; 64 hex characters are treated as 64 bytes and fail. Plus `IGDB_CLIENT_ID` and `IGDB_CLIENT_SECRET`, which can be the same values romm uses | +| `hauk/config.php` | A PHP configuration file rather than an `.env`, placed at `/var/lib/homelab/hauk/config.php`. Copy `/etc/hauk/config-sample.php` out of the image and replace `password` with a hash, from `podman exec hauk php -r 'echo password_hash("...", PASSWORD_DEFAULT);'`. `public_url` is `https://where.gapul.net/` | | `gatus.env` | `NTFY_TOPIC`, `NTFY_TOKEN` (the `tk_...` bearer token; gatus substitutes them into its own config) | | `mosquitto-ha.password` | a `mosquitto_passwd` hash, the part after `ha:` — not a plaintext password | | `free-games-claimer.env` | `NOTIFY` (an ntfy publish URL, so it embeds a credential) and `PANEL_PASSWORD`, which is also the VNC password | diff --git a/scripts/gen-docs.py b/scripts/gen-docs.py index 37081c67..c8602353 100755 --- a/scripts/gen-docs.py +++ b/scripts/gen-docs.py @@ -64,16 +64,16 @@ def gen_just_list() -> str: # 唯一の真実。説明だけはここで一元管理し、新フック追加時は `just docs` が # "—" を出すので追記に気付ける。 HOOK_DESCRIPTIONS = { - "nixfmt": "整形チェック (未整形なら fail)", - "deadnix": "未使用コード検出 (モジュール引数 `{ lib, ... }` は許容)", - "shellcheck": "shell lint (.shellcheckrc に従う)", - "gitleaks": "機密 leak 検出", + "nixfmt": "Formatting check. Fails on anything unformatted.", + "deadnix": "Finds unused code. Module arguments like `{ lib, ... }` are allowed.", + "shellcheck": "Shell lint, following .shellcheckrc", + "gitleaks": "Secret detection", } def _files_to_target(files: str) -> str: mapping = { - "": "全 staged", + "": "all staged", r"\.nix$": "`*.nix`", } if files in mapping: @@ -94,12 +94,12 @@ def gen_hooks() -> str: "}; }) (builtins.attrNames hs)))" ), ) - lines = ["| フック | 対象 | 除外 | 内容 |", "|---|---|---|---|"] + lines = ["| Hook | Target | Excluded | What it does |", "|---|---|---|---|"] for name in sorted(hooks): h = hooks[name] target = _files_to_target(h.get("files", "")) excludes = h.get("excludes", []) - excl = "、".join(f"`{e}`" for e in excludes) if excludes else "—" + excl = ", ".join(f"`{e}`" for e in excludes) if excludes else "—" desc = HOOK_DESCRIPTIONS.get(name, "—") lines.append(f"| `{name}` | {target} | {excl} | {desc} |") return "\n".join(lines) @@ -127,7 +127,7 @@ def gen_aliases() -> str: aliases.setdefault(m.group(1), m.group(2)) # ホームディレクトリの絶対パスは fork 先で変わるので伏せる。 home = f"/Users/{user}" - lines = ["| alias | 展開先 |", "|---|---|"] + lines = ["| alias | Expands to |", "|---|---|"] for name in sorted(aliases): expansion = aliases[name].replace(home, "~") lines.append(f"| `{name}` | `{expansion}` |") @@ -146,7 +146,7 @@ def gen_aliases() -> str: def _inject(text: str, name: str, body: str) -> str: begin, end = f"", f"" if begin not in text or end not in text: - sys.exit(f"マーカー {begin} / {end} が見つからない") + sys.exit(f"marker {begin} / {end} not found") pre, rest = text.split(begin, 1) _, post = rest.split(end, 1) return f"{pre}{begin}\n{body}\n{end}{post}" @@ -183,13 +183,13 @@ def main() -> int: if check and drifted: print( - f"\nドキュメントが設定と乖離している: {', '.join(drifted)}\n" - "`just docs` を実行して commit してください。", + f"\ndocs have drifted from the configuration: {', '.join(drifted)}\n" + "Run `just docs` and commit the result.", file=sys.stderr, ) return 1 if not check: - print("ドキュメント生成ブロックを再生成した (git diff で確認)") + print("regenerated the generated doc blocks; check with git diff") return 0 diff --git a/templates/README.md b/templates/README.md index 35772a55..889a3a49 100644 --- a/templates/README.md +++ b/templates/README.md @@ -1,34 +1,36 @@ -# direnv テンプレ +# direnv templates -プロジェクト毎に言語別の dev shell を持つためのテンプレ。 +Templates for giving a project its own per-language dev shell. -## 使い方 +## Using them ```bash cp -r ~/.dotfiles/templates/node/{.envrc,flake.nix} ~/Dev/my-project/ cd ~/Dev/my-project -direnv allow # 初回のみ承認 +direnv allow # approve once ``` -これで `cd` した瞬間に Nix dev shell に入る(`node`/`pnpm`/`typescript` が PATH に)。 +After that, entering the directory drops you into the Nix dev shell, with `node`, `pnpm` and +`typescript` on PATH. -## 中身 +## What each one contains -| stack | 入るもの | +| Stack | Packages | |---|---| | `node/` | nodejs_22, pnpm, typescript | | `python/` | python3, uv, ruff | | `rust/` | rustc, cargo, rust-analyzer, rustfmt, clippy | -## 拡張 +## Extending one + +Add to `packages` in `flake.nix`: -`flake.nix` の `packages` に追加するだけ: ```nix packages = with pkgs; [ nodejs_22 pnpm - postgresql # ← 追加 + postgresql # added ]; ``` -`cd` で reload される(`direnv reload` でも明示)。 +Changing directory reloads it, and `direnv reload` does so explicitly. diff --git a/windows/README.md b/windows/README.md index 47ec59b9..d9cfe5f3 100644 --- a/windows/README.md +++ b/windows/README.md @@ -1,63 +1,67 @@ -# Windows ネイティブ環境(WSL 外) +# The native Windows environment, outside WSL -Windows 上で動く部分(PowerShell, winget, WezTerm 等)の dotfiles。 -WSL2 側の Linux 環境は `~/.dotfiles/nix/home/wsl.nix` で別管理。 +The dotfiles for what runs on Windows itself: PowerShell, winget, WezTerm and so on. The Linux +side inside WSL2 is managed separately, through `~/.dotfiles/nix/home/wsl.nix`. -## 構成 +## Layout ``` windows/ ├── README.md -├── bootstrap.ps1 # 0→1 セットアップ +├── bootstrap.ps1 # setup from nothing ├── ssh/ -│ └── config # Windows OpenSSH 接続先 +│ └── config # hosts for Windows OpenSSH ├── profile/ │ └── Microsoft.PowerShell_profile.ps1 # $PROFILE └── winget/ - └── apps.json # winget 宣言的 import 形式 + └── apps.json # declarative, in winget import format ``` -## 初回セットアップ +## First-time setup -PowerShell 7 (`pwsh.exe`) を管理者で開いて: +Open PowerShell 7 (`pwsh.exe`) as administrator: ```powershell -# 実行ポリシーを ローカルスクリプト許可に +# allow local scripts Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -# dotfiles を clone (git は winget で別途入れるか手動) +# clone dotfiles. git comes from winget separately, or by hand git clone https://github.com/gapul/dotfiles.git $env:USERPROFILE\dotfiles -# bootstrap 実行 +# run bootstrap & $env:USERPROFILE\dotfiles\windows\bootstrap.ps1 ``` -## bootstrap.ps1 が何をするか +## What bootstrap.ps1 does -1. **winget** が無ければ Microsoft Store 経由で install を促す -2. `winget/apps.json` を `winget import` で一括 install -3. PowerShell `$PROFILE` を symlink (`profile/Microsoft.PowerShell_profile.ps1`) -4. Windows OpenSSHのconfigを `%USERPROFILE%\.ssh\config` へsymlink -5. age 鍵 / SSH 鍵が在れば ACL を本人のみに制限 (icacls)。無ければ警告 -6. git の global config 設定 +1. If winget is missing, points you at the Microsoft Store to install it. +2. Installs everything in `winget/apps.json` with `winget import`. +3. Symlinks PowerShell's `$PROFILE` to `profile/Microsoft.PowerShell_profile.ps1`. +4. Symlinks the Windows OpenSSH config to `%USERPROFILE%\.ssh\config`. +5. Restricts the ACL on the age and SSH keys to you alone with icacls, if they exist, and warns + if they do not. +6. Sets the global git configuration. -## 何が含まれない +## What it does not do -- 具体的にどの app を入れるか — `winget/apps.json` に追記して決める -- WSL の install — Windows 機能を有効化するのは Windows 側手動 - - PowerShell: `wsl --install -d Ubuntu` - - WSL 側で `~/.dotfiles/scripts/bootstrap-wsl.sh` を走らせる +- Decide which apps to install; that is what adding to `winget/apps.json` is for. +- Install WSL. Turning on the Windows feature is a manual step: + - In PowerShell, `wsl --install -d Ubuntu` + - Then, inside WSL, run `~/.dotfiles/scripts/bootstrap-wsl.sh` -## パッケージマネージャの使い分け +## winget and scoop -- **winget が一次**。CLI / GUI とも基本は `apps.json` に追記して宣言的に管理 -- **scoop は補助**。`winget` リポジトリに無いもの限定 (legacy / portable 配布のみのツール) -- 同名ツールが両方から入ると PATH 順で先勝ちになるので、`Find-DotfilesToolOverlap` - 関数 (profile.ps1 で定義) を起動後 `Find-DotfilesToolOverlap` と叩いて重複を可視化。 - 重複があれば原則 scoop 側を `scoop uninstall ` で外し winget に揃える +winget comes first. Both CLIs and GUI apps are normally declared by adding them to `apps.json`. -## 設定変更後の反映 +scoop is the fallback, only for things winget's repository does not carry: legacy tools and +things distributed only as portables. -PowerShell プロファイル: ファイル編集後 `. $PROFILE` で再読込 +If the same tool arrives from both, whichever comes first in PATH wins. `Find-DotfilesToolOverlap`, +defined in profile.ps1, lists the duplicates. When there is one, the rule is to remove the scoop +copy with `scoop uninstall ` and standardise on winget. -WezTerm: 設定ファイル変更後 WezTerm を再起動 +## Applying changes + +For the PowerShell profile, edit the file and reload with `. $PROFILE`. + +For WezTerm, edit the configuration and restart it. diff --git a/windows/SETUP-CHECKLIST.md b/windows/SETUP-CHECKLIST.md index c5e8fa3a..0916e47d 100644 --- a/windows/SETUP-CHECKLIST.md +++ b/windows/SETUP-CHECKLIST.md @@ -1,118 +1,136 @@ -# Windows 実機セットアップ チェックリスト +# Setting up a Windows machine -macOS 上で前段階(設定ファイル整備)は済んでいる。実機ではこの順で進める。 -各ステップの「確認」を満たしてから次へ。 +The preparation, meaning the configuration files, is already done on macOS. On the machine +itself, work through this in order, satisfying each step's check before moving on. --- -## 0. 前提・準備 +## 0. Prerequisites -- [ ] Windows 11 (PowerShell 7 = `pwsh` が使えること。無ければ後述の winget で入る) -- [ ] **ローカルアカウント**で入る (Microsoft アカウントは使わない)。OOBE で - 「インターネットに接続していません」経由か、既に MSA なら 設定 → アカウント → - 「代わりにローカル アカウントでサインインする」で切り替える。宣言では表現 - できないのでここに書く -- [ ] BitLocker が有効か確認 (`manage-bde -status C:`)。秘密鍵を置くので**ディスク暗号化は必須** -- [ ] Bitwarden 等に age 秘密鍵 / SSH 秘密鍵を準備しておく +- [ ] Windows 11, with PowerShell 7 (`pwsh`) available. If it is not, winget installs it below. +- [ ] Sign in with a **local account**, not a Microsoft account. Either take the "I don't have + internet" route during OOBE, or, if you already have an MSA, switch through Settings, + Accounts, "Sign in with a local account instead". This cannot be expressed + declaratively, which is why it is written down here. +- [ ] Confirm BitLocker is on, with `manage-bde -status C:`. Private keys are going on this + disk, so encryption is not optional. +- [ ] Have the age private key and the SSH private key ready, from Bitwarden or wherever they + live. -## 1. clone +## 1. Clone ```powershell -Set-ExecutionPolicy -Scope CurrentUser RemoteSigned # ローカルスクリプト許可 -# git は winget 宣言から外した (開発は WSL 側)。clone 用に一度だけ手で入れる: +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned # allow local scripts +# git is not in the winget declaration, since development happens on the WSL side. +# Install it once by hand, just to clone: # winget install --exact --id Git.Git git clone https://github.com/gapul/dotfiles.git $env:USERPROFILE\dotfiles ``` -- 確認: `%USERPROFILE%\dotfiles\windows\bootstrap.ps1` が存在 -- 注意: Windows ネイティブの clone 先は **`%USERPROFILE%\dotfiles`(ドット無し)**。 - macOS/WSL の `~/.dotfiles` とは非対称(プラットフォーム慣習による意図的なもの) +Check that `%USERPROFILE%\dotfiles\windows\bootstrap.ps1` exists. -## 2. winget パッケージ ID の実在確認 ★最重要 +Note that the native Windows clone goes to `%USERPROFILE%\dotfiles`, without the leading dot, +unlike `~/.dotfiles` on macOS and WSL. The asymmetry is deliberate and follows each platform's +convention. -`windows/winget/apps.json` の ID は実機で実在検証済 (2026-06-26)。 -編集後は **必ず `verify.ps1`** を回す: +## 2. Check the winget package IDs exist + +This is the step that matters most. The IDs in `windows/winget/apps.json` were verified against +a real machine on 2026-06-26. After editing them, always run `verify.ps1`: ```powershell pwsh -NoProfile -File $env:USERPROFILE\dotfiles\windows\winget\verify.ps1 -# CI 用 (MISS で exit 1) +# for CI, exiting 1 on a miss pwsh -NoProfile -File $env:USERPROFILE\dotfiles\windows\winget\verify.ps1 -Strict ``` -- [ ] `verify.ps1` で MISS / ERR が 0 件 -- 解消済みの実在 ID(SETUP-CHECKLIST 旧版で「不確実」と書かれていたもの): - - sops → `SecretsOPerationS.SOPS` (Mozilla.SOPS は旧版) - - gitleaks → `Gitleaks.Gitleaks` - - typst → `Typst.Typst` - - bottom → `Clement.bottom` - - mpv → `shinchiro.mpv` - - **JetBrainsMono Nerd Font** → `DEVCOM.JetBrainsMonoNerdFont` - Terminal の `fontFace` 前提なので apps.json で自動 install される -- Tor Browser / Zen / Beeper / Affinity 等 GUI は実機で要否を判断して追加 +- [ ] `verify.ps1` reports zero MISS and zero ERR. + +IDs that used to be listed here as uncertain and have since been confirmed: + +- sops is `SecretsOPerationS.SOPS`; Mozilla.SOPS is the old one +- gitleaks is `Gitleaks.Gitleaks` +- typst is `Typst.Typst` +- bottom is `Clement.bottom` +- mpv is `shinchiro.mpv` +- JetBrainsMono Nerd Font is `DEVCOM.JetBrainsMonoNerdFont`, installed automatically from + apps.json because Terminal's `fontFace` assumes it + +GUI applications such as Tor Browser, Zen, Beeper and Affinity are added case by case, once you +decide on the machine whether you want them. -## 3. bootstrap 実行 (管理者 PowerShell 推奨) +## 3. Run bootstrap, ideally from an administrator PowerShell ```powershell -# 既定 (WSL=Ubuntu / ユーザー=Windows の $env:USERNAME) +# defaults: WSL is Ubuntu, the user is Windows's $env:USERNAME & $env:USERPROFILE\dotfiles\windows\bootstrap.ps1 -# 別ユーザー/distro の場合 +# for a different user or distro & $env:USERPROFILE\dotfiles\windows\bootstrap.ps1 -WslUser alice -WslDistro Debian ``` -bootstrap がやること: -1. winget 確認 → `apps.json` を `winget import` -2. `$PROFILE` を symlink -3. Windows Terminal `settings.json` を**生成**(`__WSL_USER__`/`__WSL_DISTRO__` 置換) -4. age/SSH 鍵が在れば `icacls` で本人のみに ACL 制限 -5. git global config +What it does: -- [ ] symlink 作成に失敗する場合 → 管理者で実行 or 開発者モードを有効化 - (設定 → プライバシーとセキュリティ → 開発者向け) -- [ ] 既存の `$PROFILE` / WT settings は `.bak-<日時>` に退避される +1. Checks winget, then installs everything in `apps.json` with `winget import`. +2. Symlinks `$PROFILE`. +3. Generates Windows Terminal's `settings.json`, substituting `__WSL_USER__` and + `__WSL_DISTRO__`. +4. Restricts the ACL on the age and SSH keys to you alone with `icacls`, if they are present. +5. Sets the global git configuration. -## 4. 鍵配置 + 権限確認 +- [ ] If creating symlinks fails, run as administrator or turn on developer mode, under + Settings, Privacy and security, For developers. +- [ ] An existing `$PROFILE` or Terminal settings file is moved aside to `.bak-`. -- [ ] age 秘密鍵を `%USERPROFILE%\.config\sops\age\keys.txt` に配置 -- [ ] SSH 秘密鍵を `%USERPROFILE%\.ssh\id_ed25519` に配置 -- [ ] bootstrap を**再実行** → `icacls` で ACL が本人のみに絞られる -- 確認: `icacls $env:USERPROFILE\.ssh\id_ed25519` が現在ユーザーのみ - (これをやらないと OpenSSH が "bad permissions" で鍵を拒否) -- [ ] 署名/push する場合は GitHub に公開鍵を登録 (`gh ssh-key add` 等) +## 4. Place the keys and check the permissions -## 5. WSL2 (Linux 環境) +- [ ] Put the age private key at `%USERPROFILE%\.config\sops\age\keys.txt`. +- [ ] Put the SSH private key at `%USERPROFILE%\.ssh\id_ed25519`. +- [ ] Run bootstrap again, so `icacls` narrows the ACLs to you alone. +- Check with `icacls $env:USERPROFILE\.ssh\id_ed25519` that only the current user is listed. + Without this, OpenSSH refuses the key with "bad permissions". +- [ ] If you are going to sign or push, register the public key with GitHub, through + `gh ssh-key add` or similar. + +## 5. WSL2 ```powershell -wsl --install -d Ubuntu # 管理者。再起動が要る場合あり +wsl --install -d Ubuntu # as administrator. May need a reboot ``` -WSL に入ってから: +Then, inside WSL: + ```bash git clone https://github.com/gapul/dotfiles.git ~/.dotfiles ~/.dotfiles/scripts/bootstrap-wsl.sh ``` -- [ ] `bootstrap-wsl.sh` が home-manager(`#homeConfigurations.-wsl`)を switch -- 確認: `wslview` / `pbcopy`(clip.exe) / `explorer` 関数が動く -- 注意: Terminal の WSL プロファイルの `startingDirectory` は bootstrap で - `//wsl$//home/` に展開済み。distro/user が違うと開けないので - `-WslDistro`/`-WslUser` を合わせること +- [ ] `bootstrap-wsl.sh` switches home-manager to `#homeConfigurations.-wsl`. +- Check that `wslview`, `pbcopy` through clip.exe, and the `explorer` function all work. +- Note that the WSL profile's `startingDirectory` in Terminal was already expanded by bootstrap + to `//wsl$//home/`. If the distro or user differs it will not open, so match + them with `-WslDistro` and `-WslUser`. -## 6. 動作確認 +## 6. Checking it works -- [ ] 新しい PowerShell: `starship` プロンプトが macOS と同じ見た目か - (`$env:STARSHIP_CONFIG` が `...\dotfiles\configs\shell\starship.toml` を指す) -- [ ] `v` / `vim` で nvim が開く -- [ ] `zoxide` (`z`) が動く -- [ ] Windows Terminal: 既定が WSL(Ubuntu) プロファイル、フォントが Nerd Font -- [ ] `g`/`gs`/`ga` 等 git エイリアス +- [ ] In a new PowerShell, the starship prompt looks the same as on macOS, meaning + `$env:STARSHIP_CONFIG` points at `...\dotfiles\configs\shell\starship.toml`. +- [ ] `v` and `vim` open nvim. +- [ ] zoxide's `z` works. +- [ ] Windows Terminal defaults to the WSL (Ubuntu) profile, with a Nerd Font. +- [ ] The git aliases `g`, `gs`, `ga` and friends work. --- -## まだ未対応 / 実機で詰める論点 +## Still unresolved, to settle on the machine + +**There is no native path for decrypting with SOPS.** On the native Windows side the age key is +merely placed; nothing uses it to decrypt secrets. For now the assumption is that decryption +happens on the WSL side, through sops-nix. If it becomes necessary natively, add a wrapper +around `sops -d` to the profile. + +**winget can only go so far declaratively.** Import installs things but has no way to remove +what is not declared — there is no equivalent of Nix's `cleanup="uninstall"`. Unwanted apps are +uninstalled by hand. -- **SOPS 復号のネイティブ導線が無い**: Windows ネイティブ側は age 鍵を置くだけで、 - secrets を復号して使う仕組みは未定義。当面は **WSL 側(sops-nix)で復号**して使う想定。 - ネイティブで必要になったら `sops -d` を叩くラッパーを profile に足す -- **winget の宣言的運用の限界**: import は入れるだけで「宣言外を消す」機能が無い - (Nix の `cleanup="uninstall"` 相当が無い)。不要 app は手動 uninstall -- **profile の高度化** (oh-my-posh / PSFzf / Terminal-Icons) は実機で描画確認しながら +**A more capable profile** — oh-my-posh, PSFzf, Terminal-Icons — is worth doing on the machine, +where the rendering can actually be checked. diff --git a/windows/autohotkey/README.md b/windows/autohotkey/README.md index 4d0112e2..955b1e4c 100644 --- a/windows/autohotkey/README.md +++ b/windows/autohotkey/README.md @@ -1,94 +1,95 @@ -# Windows 組合せキー remap (AutoHotkey) +# Remapping key combinations on Windows, with AutoHotkey -SharpKeys / Scancode Map では実現できない **組合せ remap** を担当する。 +Handles the combinations that SharpKeys and the Scancode Map cannot express. -| レイヤー | ファイル | できること | +| Layer | File | What it can do | |---|---|---| -| 物理キー単体 (永続) | `windows/sharpkeys/` | CapsLock → Ctrl 等の 1 対 1 | -| **組合せキー** (常駐) | `windows/autohotkey/keymap.ahk` | Ctrl+A → Home 等の Emacs ショートカット、コンテキスト依存 | +| Single physical keys, persistent | `windows/sharpkeys/` | One-to-one, such as CapsLock to Ctrl | +| Combinations, resident | `windows/autohotkey/keymap.ahk` | Emacs shortcuts such as Ctrl+A to Home, and anything context-dependent | -## 構成 +## Layout ``` windows/autohotkey/ ├── README.md -└── keymap.ahk # AHK v2 スクリプト +└── keymap.ahk # an AHK v2 script ``` -## 起動 +## Starting it -`bootstrap.ps1` が `keymap.ahk` を `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\` -に symlink するため、ログイン時に自動起動する。 +`bootstrap.ps1` symlinks `keymap.ahk` into +`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\`, so it starts at login. -リロード: +To reload: ```powershell -just win-keymap # sharpkeys 適用 + AHK 再起動 +just win-keymap # applies sharpkeys and restarts AHK ``` -手動: -- AHK が常駐していれば、`.ahk` ファイル右クリック → `Run Script` で再ロード -- またはタスクトレイ AHK アイコン右クリック → `Reload Script` +By hand, if AHK is already resident, right-click the `.ahk` file and choose Run Script, or +right-click AHK in the tray and choose Reload Script. -## 実装内容 +## What it does -### 1. Copilot キー → 右 Ctrl (scancode 単独 remap 不可な機種向け保険) +### 1. The Copilot key to Right Ctrl, as insurance for machines where a scancode remap cannot -Win11 OEM の Copilot 専用キーは機種によって挙動が違う: +The dedicated Copilot key behaves differently by vendor: -| 機種 | 送信内容 | 対処 | +| Machine | What it sends | Handled by | |---|---|---| -| HP 一部 | scancode `0xE0 0x5C`(extended) | SharpKeys (Scancode Map) で完結 | -| Lenovo / 一部 24H2 | `LShift + LWin + F23`(キーシーケンス) | **scancode remap 不可 → AHK 担当** | +| Some HP models | The extended scancode `0xE0 0x5C` | SharpKeys, through the Scancode Map | +| Lenovo, and some 24H2 machines | The key sequence `LShift + LWin + F23` | Not remappable by scancode, so AHK | -実機の挙動確認: -1. AHK 起動中にタスクトレイ右クリック → `Open` → `View` → `Key history` -2. Copilot キーを押す → ログから scancode を確認 -3. `keymap.ahk` の Copilot 行を実機に合うものに切り替え +To find out which one you have: with AHK running, right-click the tray icon, then Open, View, +Key history; press the Copilot key; read the scancode from the log; and switch the Copilot line +in `keymap.ahk` to whichever matches. -### 2. Emacs ショートカット復活 +### 2. Bringing back the Emacs shortcuts -macOS は Cocoa text field 全般で Emacs キーバインドが標準で効く。Windows はデフォルトでは効かない。 -AHK で emulate する。 +macOS gives you Emacs bindings in every Cocoa text field as standard. Windows does not, so AHK +emulates them. -| key | 動作 | +| Key | What it does | |---|---| -| `Ctrl+A` | 行頭(Home) | -| `Ctrl+E` | 行末(End) | -| `Ctrl+B` | 1 文字左 | -| `Ctrl+F` | 1 文字右 | -| `Ctrl+P` | 1 行上 | -| `Ctrl+N` | 1 行下 | -| `Ctrl+H` | 1 文字削除(前) | -| `Ctrl+D` | 1 文字削除(後) | -| `Ctrl+K` | 行末まで kill | +| `Ctrl+A` | Start of line, Home | +| `Ctrl+E` | End of line, End | +| `Ctrl+B` | One character left | +| `Ctrl+F` | One character right | +| `Ctrl+P` | One line up | +| `Ctrl+N` | One line down | +| `Ctrl+H` | Delete the character before | +| `Ctrl+D` | Delete the character after | +| `Ctrl+K` | Kill to end of line | -#### 除外コンテキスト +#### Where they are switched off -- ターミナル系: `ConsoleWindowClass` / `CASCADIA_HOSTING_WINDOW_CLASS` / `WezTermWindow` / `mintty` -- エディタ系: `Vim` / VS Code / Cursor / nvim / Hyper +- Terminals: `ConsoleWindowClass`, `CASCADIA_HOSTING_WINDOW_CLASS`, `WezTermWindow`, `mintty` +- Editors: Vim, VS Code, Cursor, nvim, Hyper -これらでは Ctrl+A=全選択 等の元の意味を尊重。WinTitle Class または Process 名で判別。 +In those, the original meanings — Ctrl+A as select all, and so on — are respected. They are +matched on window class or process name. -### カスタマイズ +### Customising it -`EmacsExcludeClasses` / `EmacsExcludeProcesses` に追加してその app では Emacs binding を無効化: +Add to `EmacsExcludeClasses` or `EmacsExcludeProcesses` to disable the Emacs bindings in another +app: ```ahk EmacsExcludeProcesses := "i)^(WezTerm|wt|alacritty|Code|MyApp)\.exe$" ``` -## Bitdefender 除外設定 +## Excluding it from Bitdefender -AHK は低レベルキーフック (`SetWindowsHookEx WH_KEYBOARD_LL`) を使うため、Bitdefender の -Advanced Threat Defense でキーロガー扱いされてブロックされる場合がある (起動 → 数秒で消える、 -タスクトレイにアイコンが出ない等)。 -**Bitdefender Security Center → Protection → Antivirus → Settings → Manage Exceptions** で: +AHK uses a low-level keyboard hook, `SetWindowsHookEx WH_KEYBOARD_LL`, so Bitdefender's Advanced +Threat Defense sometimes treats it as a keylogger and blocks it: it starts, disappears a few +seconds later, and never shows a tray icon. + +Under Bitdefender Security Center, Protection, Antivirus, Settings, Manage Exceptions, add both: ``` C:\Users\\dotfiles\windows\autohotkey\ C:\Program Files\AutoHotkey\ ``` -の両方を Add。Advanced Threat Defense でも別途 Application Exception 追加が必要なことがある -(Protection → Advanced Threat Defense → Settings → Manage Exceptions → AutoHotkey64.exe)。 +Advanced Threat Defense sometimes needs its own application exception as well, under Protection, +Advanced Threat Defense, Settings, Manage Exceptions, for AutoHotkey64.exe. diff --git a/windows/fonts/README.md b/windows/fonts/README.md index 6974a6d8..a905a687 100644 --- a/windows/fonts/README.md +++ b/windows/fonts/README.md @@ -1,45 +1,47 @@ -# Windows フォント declarative install +# Declarative font installation on Windows -`configs/fonts/` 配下の `.ttf` / `.otf` を Windows に user-scope install する。 -Mac の home-manager で `font-*` cask を入れるのと同じ精神で declarative 化。 +Installs every `.ttf` and `.otf` under `configs/fonts/` into Windows at user scope — the same +idea as installing `font-*` casks through home-manager on the Mac. -## 構成 +## Layout ``` windows/fonts/ ├── README.md -└── apply.ps1 # configs/fonts/*.ttf|.otf を user-scope install +└── apply.ps1 # installs configs/fonts/*.ttf and *.otf at user scope ``` -## 実行 +## Running it ```powershell -just win-fonts # 本番 -just win-fonts -DryRun # 副作用確認 -just win-fonts -Force # 既存も強制上書き +just win-fonts # for real +just win-fonts -DryRun # see what it would do +just win-fonts -Force # overwrite what is already there ``` -bootstrap.ps1 でも自動実行(`-SkipFonts` で省略可)。 +bootstrap.ps1 runs it too, and `-SkipFonts` skips it. -## install 場所 +## Where things land -- ファイル: `%LOCALAPPDATA%\Microsoft\Windows\Fonts\` -- レジストリ: `HKCU\Software\Microsoft\Windows NT\CurrentVersion\Fonts` -- 管理者権限不要(Windows 10 1809 以降 user-scope サポート) +- Files: `%LOCALAPPDATA%\Microsoft\Windows\Fonts\` +- Registry: `HKCU\Software\Microsoft\Windows NT\CurrentVersion\Fonts` +- No administrator rights required; user-scope installation has been supported since Windows 10 + 1809. -## 主な用途 +## What it is mainly for -- **sketchybar-app-font.ttf**: Zebar bar.html で focused app icon を - process 名 → `:app_name:` ligature 変換するための専用フォント - (Mac SketchyBar と同じ icon mapping を再現) +`sketchybar-app-font.ttf`, which Zebar's bar.html uses to turn a process name into an +`:app_name:` ligature for the focused app icon, reproducing the icon mapping SketchyBar uses on +the Mac. -## HackGen Console NF について +## HackGen Console NF -apps.json / scoop どちらにも未収録のため、yuru7/HackGen の GitHub Release -から `.zip` を DL → 解凍 → `configs/fonts/` に置けば apply.ps1 で install。 +It is in neither apps.json nor scoop, so download the `.zip` from yuru7/HackGen's GitHub +releases, extract it, and drop the fonts into `configs/fonts/`; apply.ps1 installs them from +there. ```powershell -# 例: HackGen_NF release を取得 +# find the HackGen_NF release asset $url = (gh release view --repo yuru7/HackGen --json assets --jq '.assets[] | select(.name | contains("NF")) | .url' | Select-Object -First 1) -# 手動 DL → 解凍 → configs/fonts/ にコピー +# then download, extract and copy into configs/fonts/ by hand ``` diff --git a/windows/locale/README.md b/windows/locale/README.md index f9ca9291..e027abf4 100644 --- a/windows/locale/README.md +++ b/windows/locale/README.md @@ -1,67 +1,71 @@ -# Windows ロケール / 言語設定 +# Windows locale and language -英語 UI で動かしつつ、SJIS 由来の文字化け (`\` が `¥` で表示される等) を解消する。 +Running an English UI while getting rid of the Shift-JIS mojibake, such as `\` displaying as +`¥`. -## 構成 +## Layout ``` windows/locale/ ├── README.md -└── apply.ps1 # 3 段階で declarative に適用 +└── apply.ps1 # applies three things declaratively ``` -## 適用内容(3 段階) +## What it applies -### A. User Language List = `ja-JP` 1 個 / IME = CorvusSKK のみ / UI = en-US Override +### A. One language, `ja-JP`; CorvusSKK as the only IME; the UI overridden to en-US -- `en-US` 言語を削除 → **英語キーボードレイアウト (0409:00000409) が消える** -- `ja-JP` の `InputMethodTips` を CorvusSKK 1 個に → MS-IME が消える -- 結果: タスクバーの言語インジケーターは SKK 1 個のみ、`Win+Space` 切替表示なし -- 英語入力は **CorvusSKK の直接入力モード** (`l` キーで切替) で行う -- UI Display は `Set-WinUILanguageOverride en-US` で英語に固定 -- **再ログイン**で完全反映 +- Removing the `en-US` language removes the English keyboard layout, `0409:00000409`. +- Setting `ja-JP`'s `InputMethodTips` to CorvusSKK alone removes MS-IME. +- The result is a single SKK entry in the taskbar's language indicator, with no `Win+Space` + switching. +- English is typed through CorvusSKK's direct input mode, toggled with `l`. +- `Set-WinUILanguageOverride en-US` fixes the display language to English. +- Log out and back in for all of it to take effect. -CorvusSKK の TIP は install 時に固定 CLSID で登録される(ユーザー間で共通): -- ProfileGUID: `{956F14B3-5310-4CEF-9651-26710EB72F3A}` -- CLSID: `{EAEA0E29-AA1E-48EF-B2DF-46F4E24C6265}` +CorvusSKK's TIP is registered at install time under fixed CLSIDs, the same for every user: -### B. System Locale = `en-US` + CodePage 65001 (UTF-8) +- ProfileGUID `{956F14B3-5310-4CEF-9651-26710EB72F3A}` +- CLSID `{EAEA0E29-AA1E-48EF-B2DF-46F4E24C6265}` -- 非 Unicode プログラムを SJIS (CP932) から UTF-8 (CP65001) に切替 -- **`\` が `¥` で表示される根本原因の SJIS 解消** -- cmd / PowerShell の console code page も 65001 化 -- Win10 の "Use Unicode UTF-8 for worldwide language support" Beta 機能と同等 -- **再起動必須**(CodePage は OS 起動時にしか効かない) +### B. System locale `en-US` with code page 65001, UTF-8 -### C. Home Location = United States (GeoId 244) +- Switches non-Unicode programs from Shift-JIS (CP932) to UTF-8 (CP65001). +- This is what actually fixes `\` showing as `¥`. +- The console code page for cmd and PowerShell becomes 65001 as well. +- Equivalent to Windows 10's beta "Use Unicode UTF-8 for worldwide language support". +- Requires a reboot; the code page only takes effect at boot. -- Region を US 化 -- 時計 / 通貨 / 天気 app 等が英語表記 -- 日本在住で時刻 / 通貨は日本のままがいい場合は `-SkipHomeLocation` +### C. Home location, United States, GeoId 244 -## 実行 +- Sets the region to the US. +- The clock, currency and the weather app all read in English. +- If you live in Japan and want the time and currency to stay Japanese, use + `-SkipHomeLocation`. + +## Running it ```powershell -# DryRun (副作用なし) +# no side effects just win-locale -DryRun -# 本番 (自動 UAC、B 適用後は要再起動) +# for real. Elevates itself; a reboot is needed after B just win-locale -# 部分適用 -just win-locale -SkipSystemLocale # 言語順序と Home だけ (再起動不要) -just win-locale -SkipHomeLocation # 言語順序と System Locale (日本地域は残す) +# partially +just win-locale -SkipSystemLocale # only the language order and home location; no reboot +just win-locale -SkipHomeLocation # language order and system locale, keeping the Japanese region ``` -bootstrap.ps1 の Step 9 として自動実行される。skip したい時: +bootstrap.ps1 runs it as step 9. To skip it: ```powershell just win-bootstrap -SkipLocale ``` -## 元に戻したい時 +## Undoing it -Settings → Time & Language → Language で個別変更、または: +Either change things individually under Settings, Time and Language, Language, or: ```powershell Set-WinUserLanguageList -LanguageList 'ja', 'en-US' -Force diff --git a/windows/privacy/README.md b/windows/privacy/README.md index 9305fa7e..9057ef19 100644 --- a/windows/privacy/README.md +++ b/windows/privacy/README.md @@ -1,74 +1,80 @@ -# Windows プライバシー / 標準機能 declarative 化 +# Declaring Windows privacy and built-in features -Mac の `nix/hosts/darwin.nix` の `system.defaults` 相当を Windows でも宣言的に管理する。 -**WinUtil**(プライバシー + Service / Performance / Features)と -**Win11Debloat**(UWP プリイン削除)の併用。 +The Windows equivalent of `system.defaults` in the Mac's `nix/hosts/darwin.nix`, managed +declaratively. It uses WinUtil for privacy, services, performance and features, alongside +Win11Debloat for removing the preinstalled UWP apps. -## 構成 +## Layout ``` windows/privacy/ ├── README.md -├── winutil-config.json # ChrisTitusTech/winutil の export 設定 -├── win11debloat-args.txt # Raphire/Win11Debloat の CLI 引数(空行/`#` コメント可) -├── win11debloat-customapps.txt # Win11Debloat 標準セット外で追加削除する UWP -└── apply.ps1 # 上 3 つを順に呼ぶ orchestrator + 追加 registry tweak +├── winutil-config.json # an exported ChrisTitusTech/winutil configuration +├── win11debloat-args.txt # CLI arguments for Raphire/Win11Debloat; blank lines and `#` comments allowed +├── win11debloat-customapps.txt # extra UWP apps to remove, beyond Win11Debloat's standard set +└── apply.ps1 # the orchestrator that runs the three in order, plus extra registry tweaks ``` -## 実行 +## Running it ```powershell -# DryRun(副作用なしで何が走るか確認) +# see what would run, with no side effects just win-privacy -DryRun pwsh -File windows/privacy/apply.ps1 -DryRun -# 本番(管理者要、UAC promp あり) +# for real. Needs administrator rights and raises a UAC prompt just win-privacy ``` -実行内容: -1. `Win11Debloat`(`irm https://win11debloat.raphi.re/ | iex`)を一時取得 → - `win11debloat-args.txt` の引数で **自動実行**(`-Silent -RunDefaults` 等) -2. `WinUtil`(`irm https://christitus.com/win | iex`)を一時取得 → - `winutil-config.json` を引数で渡して **GUI 起動**。GUI で **Import → Apply** +What happens: -WinUtil の CLI 自動 apply は version によって挙動が変わるため、本リポは GUI 経由の -半自動運用(設定は declarative、最終 Apply はユーザー操作)を採用。 +1. Win11Debloat is fetched temporarily, through `irm https://win11debloat.raphi.re/ | iex`, and + run automatically with the arguments in `win11debloat-args.txt`, such as `-Silent` and + `-RunDefaults`. +2. WinUtil is fetched temporarily, through `irm https://christitus.com/win | iex`, and started + with `winutil-config.json` passed to it. Its GUI opens, and you choose Import then Apply. -## 設定の変更 +WinUtil's automatic CLI apply changes behaviour between versions, so this repository settles for +a half-automatic arrangement: the configuration is declarative, and the final Apply is a user +action. + +## Changing the configuration ### Win11Debloat -`win11debloat-args.txt` を編集。1 行 1 引数(空行と `#` コメント可)。 -利用可能な引数は を参照。 -現状の引数(15 個): +Edit `win11debloat-args.txt`, one argument per line, with blank lines and `#` comments allowed. +The available arguments are at . + +The current fifteen: -- 必須: `-Silent` -- UWP 削除: `-RemoveApps` / `-RemoveGamingApps` -- テレメトリ/AI: `-DisableTelemetry` / `-DisableBing` / `-DisableCopilot` / `-DisableRecall` -- 通知/誘導: `-DisableLockscreenTips` / `-DisableSuggestions` / `-DisableStickyKeys` / `-DisableWidgets` -- エクスプローラ: `-ShowHiddenFolders` / `-ShowKnownFileExt` / `-HideHome` / `-HideGallery` +- Required: `-Silent` +- Removing UWP apps: `-RemoveApps`, `-RemoveGamingApps` +- Telemetry and AI: `-DisableTelemetry`, `-DisableBing`, `-DisableCopilot`, `-DisableRecall` +- Notifications and nudges: `-DisableLockscreenTips`, `-DisableSuggestions`, + `-DisableStickyKeys`, `-DisableWidgets` +- Explorer: `-ShowHiddenFolders`, `-ShowKnownFileExt`, `-HideHome`, `-HideGallery` -### Win11Debloat に存在しない引数の代替 +### Arguments that turned out not to exist -以下は Win11Debloat の公式 wiki に**存在しない引数**だったため、別経路で実装: +These are not in Win11Debloat's wiki, so they are implemented another way: -| 元 | 代替 | +| Intended | Instead | |---|---| -| `-RemoveCommApps` | `win11debloat-customapps.txt` に `microsoft.windowscommunicationsapps` | -| `-RemoveDevApps` | `win11debloat-customapps.txt` に `Microsoft.Microsoft3DViewer` / `Microsoft.MixedReality.Portal` | -| `-RemoveW11Outlook` | `win11debloat-customapps.txt` に `Microsoft.OutlookForWindows` | -| `-DisableOnedrive`(Win10 only) | `apply.ps1` の OneDrive uninstall step(`OneDriveSetup.exe /uninstall`) | +| `-RemoveCommApps` | `microsoft.windowscommunicationsapps` in `win11debloat-customapps.txt` | +| `-RemoveDevApps` | `Microsoft.Microsoft3DViewer` and `Microsoft.MixedReality.Portal` in `win11debloat-customapps.txt` | +| `-RemoveW11Outlook` | `Microsoft.OutlookForWindows` in `win11debloat-customapps.txt` | +| `-DisableOnedrive`, which is Windows 10 only | The OneDrive uninstall step in `apply.ps1`, running `OneDriveSetup.exe /uninstall` | ### WinUtil -1. 実機で WinUtil を起動: `irm https://christitus.com/win | iex` -2. GUI でチェックを入れて選択 -3. `Settings → Export Config` で JSON 保存 -4. 保存先を `windows/privacy/winutil-config.json` に置き換え → commit -## bootstrap への組み込み +1. Start WinUtil on the machine: `irm https://christitus.com/win | iex` +2. Tick what you want in the GUI. +3. Save the JSON through Settings, Export Config. +4. Replace `windows/privacy/winutil-config.json` with it and commit. -`bootstrap.ps1` のステップ 7 として自動実行される。skip したい時は: +## How it fits into bootstrap + +`bootstrap.ps1` runs it as step 7. To skip it: ```powershell pwsh -File windows/bootstrap.ps1 -SkipPrivacy @@ -76,32 +82,36 @@ pwsh -File windows/bootstrap.ps1 -SkipPrivacy just win-bootstrap -SkipPrivacy ``` -後から個別に適用: +To apply it separately afterwards: ```powershell -just win-privacy # 本番 -just win-privacy -DryRun # 副作用確認 +just win-privacy # for real +just win-privacy -DryRun # check the side effects ``` -`apply.ps1` は `-SkipWinUtil` / `-SkipWin11Debloat` / `-SkipCustomApps` で部分実行も可能。 +`apply.ps1` also takes `-SkipWinUtil`, `-SkipWin11Debloat` and `-SkipCustomApps` for partial +runs. + +### Removing custom UWP apps, through win11debloat-customapps.txt -### カスタム UWP 削除 (win11debloat-customapps.txt) +UWP apps outside Win11Debloat's standard set are removed directly with +`Get-AppxPackage | Remove-AppxPackage`. The provisioned package is removed at the same time so +they do not come back when a new user is created, which needs administrator rights. -Win11Debloat 標準セットに含まれない UWP を `Get-AppxPackage | Remove-AppxPackage` で直接削除する。 -Provisioned package も同時に削除して新規ユーザー作成時に戻らないようにする(管理者要)。 +One PackageName per line, with blank lines and `#` comments allowed. -1 行 1 PackageName、空行 / `#` コメント可。 +### Extra registry tweaks -### 追加 registry tweak +Declarative registry settings written directly in `apply.ps1`: -`apply.ps1` に直書きされている declarative な registry 設定: +- `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsBackup\DisableWindowsBackupUI = 1`, which + disables the Windows Backup UI in Windows 11 24H2. The UWP part itself belongs to CBS and + cannot be removed, so turning the feature off is all there is. -- `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsBackup\DisableWindowsBackupUI = 1` - Windows 11 24H2 の Windows Backup UI を無効化(UWP 本体は CBS で削除不可なため機能 OFF のみ) +## What this cannot cover -## 補完できないもの +Registry tweaks neither tool includes go into the `$ExtraRegistry` array in `apply.ps1`, which +is the declarative extension point. -- 個別レジストリ書き換えで両ツールに含まれない tweak は別途 `apply.ps1` の - `$ExtraRegistry` 配列に追加(declarative 拡張ポイント) -- macOS の `CustomUserPreferences` 相当の細かい設定は、各アプリ専用ポリシー - (Chrome の Enterprise Policy 等)で対応 +The fine-grained settings that would be `CustomUserPreferences` on macOS are handled through +each application's own policy mechanism, such as Chrome's enterprise policy. diff --git a/windows/scoop/README.md b/windows/scoop/README.md index 1908c2e5..db2acb01 100644 --- a/windows/scoop/README.md +++ b/windows/scoop/README.md @@ -1,43 +1,44 @@ -# Scoop (winget サブセット) +# Scoop, alongside winget -`winget` で取得できない MS Store 専用 app を sideload する目的で Scoop を併用する。 -GUI / 公式 CLI は `windows/winget/apps.json`、それ以外で必要なものを `scoop.json` に置く。 +Scoop is used to sideload apps that are only available from the Microsoft Store and cannot be +had through `winget`. GUI apps and official CLIs go in `windows/winget/apps.json`; anything +else goes in `scoop.json`. -## 方針 +## Which goes where -| 入れる先 | 対象 | +| Installed through | For | |---|---| -| `winget` (apps.json) | GUI app / 公式 CLI / system 統合 / 認証重視 | -| `scoop` (scoop.json) | MS Store 専用 (Files 等) / portable / nonportable bucket | +| winget, apps.json | GUI apps, official CLIs, anything integrated with the system, anything where provenance matters | +| scoop, scoop.json | Store-only apps such as Files, portable apps, and the nonportable bucket | -## 構成 +## Layout ``` windows/scoop/ ├── README.md -├── scoop.json # bucket + app の declarative 定義 -└── apply.ps1 # scoop 未導入なら install → bucket add → app install +├── scoop.json # buckets and apps, declaratively +└── apply.ps1 # installs scoop if missing, adds buckets, installs apps ``` -## 実行 +## Running it ```powershell -# DryRun (副作用なし) +# no side effects just win-scoop -DryRun -# 本番 +# for real just win-scoop ``` -bootstrap.ps1 の Step 2.5 として自動実行される。skip したい時: +bootstrap.ps1 runs it as step 2.5. To skip it: ```powershell pwsh -File windows/bootstrap.ps1 -SkipScoop ``` -`apply.ps1` には `-SkipBuckets` / `-SkipApps` もある。 +`apply.ps1` also takes `-SkipBuckets` and `-SkipApps`. -## scoop.json の書式 +## The format of scoop.json ```json { @@ -46,14 +47,18 @@ pwsh -File windows/bootstrap.ps1 -SkipScoop } ``` -- `buckets`: `scoop bucket add` で登録する追加 bucket -- `apps`: `scoop install` で入れる app。bucket 名のプレフィクスは scoop が自動解決 +`buckets` are registered with `scoop bucket add`, and `apps` are installed with +`scoop install`. Scoop resolves the bucket prefix itself. -## 何故 MS Store を避けるか +## Why avoid the Microsoft Store -- MSIX は `C:\Program Files\WindowsApps\` の TrustedInstaller 領域に入って設定 symlink できない -- インストール履歴が MS アカウントに紐付く (テレメトリ方針と矛盾) -- オフラインで再現できない (Store ログイン必須) -- 配布チャネル次第で有料化や撤去のリスクあり (Files は Store だと有料、GitHub MSIX は無料、という非対称) +- MSIX packages land in `C:\Program Files\WindowsApps\`, which belongs to TrustedInstaller, so + configuration cannot be symlinked into them. +- Installation history is tied to a Microsoft account, which contradicts the telemetry policy + here. +- It cannot be reproduced offline, since it requires being signed in. +- Depending on the distribution channel, an app can become paid or disappear. Files is a good + example of the asymmetry: paid on the Store, free as an MSIX from GitHub. -`nonportable` bucket は GitHub Release から MSIX を取得して sideload するため、これらの問題を回避できる。 +The `nonportable` bucket fetches the MSIX from a GitHub release and sideloads it, which avoids +all of this. diff --git a/windows/sharpkeys/README.md b/windows/sharpkeys/README.md index 9c0075ad..51fb5434 100644 --- a/windows/sharpkeys/README.md +++ b/windows/sharpkeys/README.md @@ -1,82 +1,82 @@ -# Windows 物理キー remap +# Remapping physical keys on Windows -Mac の Karabiner-Elements のキー remap 部分を Windows で再現。 -SharpKeys と同じ仕組み(`HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map`)を、 -SharpKeys GUI に依存せず PowerShell で declarative に書き換える。 +The key-remapping half of Karabiner-Elements, reproduced on Windows. It uses the same mechanism +SharpKeys does — `HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map` — but +writes it declaratively from PowerShell, without needing the SharpKeys GUI. -## 構成 +## Layout ``` windows/sharpkeys/ ├── README.md -├── keymap.skl # 人間可読の SharpKeys 形式 (参照用 — 実体は apply.ps1 内の $Mappings 配列) -└── apply.ps1 # Scancode Map を組み立ててレジストリに書き込む +├── keymap.skl # human-readable SharpKeys format, for reference; the real source is the $Mappings array in apply.ps1 +└── apply.ps1 # builds the Scancode Map and writes it to the registry ``` -## 実行 +## Running it ```powershell -# 適用 (管理者要、再起動で反映) +# apply. Needs administrator rights, and takes effect after a reboot just win-keymap -# 副作用なし確認 +# check without side effects just win-keymap -DryRun -# remap を全削除して standard layout に戻す +# remove every remap and return to the standard layout just win-keymap -Clear ``` -bootstrap.ps1 の Step 8 として自動実行される。skip したい時: +bootstrap.ps1 runs it as step 8. To skip it: ```powershell pwsh -File windows/bootstrap.ps1 -SkipKeymap ``` -## 現状の remap +## What is remapped -| 機能 | 状態 | +| Mapping | State | |---|---| -| CapsLock → Left Ctrl | ✅ 適用済 (scancode 0x3A → 0x1D) | -| Copilot key → Right Ctrl | ⏳ 実機 scancode 検証待ち | +| CapsLock to Left Ctrl | Applied, scancode 0x3A to 0x1D | +| The Copilot key to Right Ctrl | Waiting on a scancode from the real machine | -## Copilot key の scancode 調査 +## Finding the Copilot key's scancode -Win11 OEM の Copilot 専用キーは機種により異なる: +The dedicated Copilot key on Windows 11 OEM machines differs by vendor: -- **HP の一部機種**: scancode `0xE0 0x5C`(extended) -- **Lenovo**: Win + Shift + F23 のシーケンス(scancode レベル remap 不可) -- **その他**: `vk89` / `F23` 等 +- Some HP machines send the extended scancode `0xE0 0x5C`. +- Lenovo sends a Win+Shift+F23 sequence, which cannot be remapped at the scancode level. +- Others send `vk89`, `F23` and similar. -調査方法(AutoHotkey の Key History を使う): +To find out, use AutoHotkey's Key History: -1. AHK スクリプトを起動した状態で Copilot キーを押す -2. AHK のタスクトレイ右クリック → `Open` → `View` → `Key history and script info` -3. ログから scancode を確認 +1. With the AHK script running, press the Copilot key. +2. Right-click AHK in the tray, then Open, View, "Key history and script info". +3. Read the scancode out of the log. -scancode が `E0 5C` のようなら `apply.ps1` の `$Mappings` に有効化: +If it turns out to be something like `E0 5C`, enable it in `$Mappings` in `apply.ps1`: ```powershell @{ Source = 'E05C'; Dest = 'E01D'; Comment = 'Copilot -> Right Ctrl' } ``` -`LSHIFT+LWIN+F23` のような **combination** だった場合は scancode 単独では remap 不可。 -その場合は **AHK 側** で `+#F23::RControl` のように remap する。 +If it is a combination such as `LSHIFT+LWIN+F23`, no scancode remap can catch it, and it has to +be handled on the AHK side, as `+#F23::RControl`. -## 組合せ remap (Ctrl+A → Home 等) は別レイヤー +## Combinations are a different layer -SharpKeys / Scancode Map は **キー単体の 1 対 1 remap** のみ。 -Emacs ショートカット(Ctrl+A → Home 等)は `windows/autohotkey/keymap.ahk` で実装。 +SharpKeys and the Scancode Map only do one-to-one remapping of single keys. The Emacs +shortcuts, such as Ctrl+A to Home, are implemented in `windows/autohotkey/keymap.ahk`. -## Bitdefender 除外設定 +## Excluding it from Bitdefender -`apply.ps1` は `HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map` を -直書きするため、Bitdefender のヒューリスティック検知で隔離される場合がある。 -**Bitdefender Security Center → Protection → Antivirus → Settings → Manage Exceptions** -で以下を Add: +Because `apply.ps1` writes +`HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map` directly, Bitdefender's +heuristics sometimes quarantine it. Add the directory under Bitdefender Security Center, +Protection, Antivirus, Settings, Manage Exceptions: ``` C:\Users\\dotfiles\windows\sharpkeys\ ``` -隔離された後は Bitdefender → Notifications → Quarantine から Restore して上記除外を追加。 -`just win-keymap` を再実行すれば再適用される。 +If it has already been quarantined, restore it from Bitdefender, Notifications, Quarantine, add +the exception above, and run `just win-keymap` again.