diff --git a/.github/workflows/nix-check.yml b/.github/workflows/nix-check.yml new file mode 100644 index 0000000..6f950b8 --- /dev/null +++ b/.github/workflows/nix-check.yml @@ -0,0 +1,72 @@ +# Nix flake / nix-darwin 構成の CI 検証 +# +# 目的: +# PR で nix/ が変更されるたびに `nix flake check` と system closure +# ビルドを走らせ、構文・型・依存解決の壊れを検知する。これにより main 更新 +# による drift (Brewfile ↔ homebrew.nix 等) を継続検知できる。 +# +# 設計判断: +# - `darwin-rebuild switch` 自体は CI で実行しない: +# ci 用に専用 host を作って switch する案を試したが、 +# 実機と CI 環境 (runner / TCC / GUI / hardware) の差分を埋める +# 作業が膨らみ、本番投入の安全性向上に対して費用対効果が悪かった。 +# activation の検証は実機での `darwin-rebuild build` → `switch` で行う方針。 +# - runs-on: macos-latest を採用 (最新 macOS runner に自動追従) +# - DeterminateSystems/nix-installer-action: ローカル開発と同じ Determinate Nix +# - `USER=ciuser` を env で渡す: flake は username を $USER から動的解決する +# (`builtins.getEnv "USER"`)。CI runner の $USER は環境依存なため、 +# リポジトリに個人情報を残さない generic 値を明示する。 +# +# セキュリティ: +# - run: で github.event.* の自由文 (PR title / body / commit message 等) を +# 一切評価しない (injection 対策) +name: nix-check + +on: + pull_request: + paths: + - 'nix/**' + - '.github/workflows/nix-check.yml' + push: + # feature branch への push は pull_request イベントだけで検証する + # (push と pull_request の重複起動を避ける)。main の直接 push (hotfix 等) + # のみここで拾う。 + branches: + - main + paths: + - 'nix/**' + - '.github/workflows/nix-check.yml' + +# 同一ブランチで複数 push があった場合、進行中のジョブをキャンセルして最新だけ走らせる +concurrency: + group: nix-check-${{ github.ref }} + cancel-in-progress: true + +jobs: + flake-check: + name: nix flake check + build closure + runs-on: macos-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix (Determinate Systems) + uses: DeterminateSystems/nix-installer-action@main + with: + # diagnostic 送信を無効化 (Determinate 公式推奨の opt-out) + diagnostic-endpoint: '' + + - name: nix flake check + working-directory: nix + env: + USER: ciuser + run: nix flake check --impure --print-build-logs + + - name: Build system closure + working-directory: nix + env: + USER: ciuser + run: | + nix build .#darwinConfigurations.default.system \ + --no-link --impure --print-build-logs diff --git a/CLAUDE.md b/CLAUDE.md index 8182df5..d6d233b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ - `aliases` — シェルエイリアス定義(`~/.aliases` にシンボリックリンク) - `aliase/` — 外部シェルスクリプト(エイリアスから呼び出される) -- `Brewfile` — Homebrew パッケージ定義 +- `Brewfile` — Homebrew パッケージ定義(Phase A 期間中は読み取り専用バックアップ、Phase B で削除予定) - `claude/` — Claude Code 設定(`~/.claude/` にシンボリックリンク) - `claude/hooks/` — Claude Code フックスクリプト群(PreCompact / SessionStart / UserPromptSubmit) - `claude/agents/` — マルチエージェント開発用サブエージェント定義(developer × 10 / reviewer × 3 / pr-publisher × 1 = 14 体。`~/.claude/agents/` にシンボリックリンク) @@ -15,7 +15,8 @@ - `gitconfig` — Git 設定(`~/.gitconfig` にシンボリックリンク) - `gitignore_global` — グローバル gitignore(`~/.gitignore_global` にシンボリックリンク) - `grip/` — grip 設定(`~/.grip/` にシンボリックリンク) -- `setup/` — セットアップスクリプト群(シンボリックリンク対象外) +- `nix/` — nix-darwin + home-manager + flakes による環境構築定義(Phase A の主管理対象。`darwin-rebuild` から参照される。詳細は `nix/README.md`) +- `setup/` — セットアップスクリプト群(シンボリックリンク対象外。Phase B で home-manager へ完全移行後に削除予定) - `ssh/` — SSH 設定(`~/.ssh/` にシンボリックリンク) - `zsh/` — zsh 補完ファイル(`~/.zsh/` にシンボリックリンク) - `zshrc` — zsh 設定(`~/.zshrc` にシンボリックリンク) @@ -23,13 +24,15 @@ # シンボリックリンク管理 +- **Phase A 期間中の方針**: 新規 dotfiles は `nix/modules/home/` 以下で home-manager 管理に倒す。`setup/setup.zsh` の symlink ループはレガシー扱い (Phase B で削除予定) - `setup/setup.zsh` がリポジトリルートのファイル/ディレクトリを `~/.${name}` にシンボリックリンクする -- 以下はシンボリックリンク対象外として除外されている: `setup`, `README.md`, `ssh`, `claude`, `CLAUDE.md`, `docs` +- 以下はシンボリックリンク対象外として除外されている: `setup`, `README.md`, `ssh`, `claude`, `CLAUDE.md`, `docs`, `nix` - `ssh/` と `claude/` は専用ループで個別にシンボリックリンクされる - ルートにファイルやディレクトリを追加する場合、シンボリックリンクが不要なものは `setup.zsh` の除外条件に追加すること # Brewfile +- **Phase A 注記**: `Brewfile` は読み取り専用バックアップとして残し、cask / mas / 例外 brew は `nix/modules/darwin/homebrew.nix` で管理する。**Brewfile を変更したら同 PR で `homebrew.nix` も更新すること**(CI の `nix-check` で drift 検知される)。Phase B で `Brewfile` 自体を削除予定 - パッケージの追加・削除は Brewfile のみで管理する。手動の `brew install` は禁止 - 既存のパッケージのみを対象とする。ユーザーが明示的に依頼していないパッケージを追加しない - セクションコメント(`# Utilities`, `# Shell & Terminal` 等)に従って適切な位置に追記する @@ -56,6 +59,65 @@ - `claude/hooks/` 配下のフックスクリプトは PreCompact で未 handover 時のコンパクトをブロックし、SessionStart / UserPromptSubmit で未消費メモを Claude に通知する - `claude/RTK.md` は rtk (Rust Token Killer) のガイドライン。`claude/CLAUDE.md` 末尾の `@RTK.md` で取り込まれ、`claude/settings.json` の `PreToolUse: Bash` matcher に追加した `rtk hook claude` と連動して Bash 出力を圧縮する。各 PC への展開は `brew bundle` + `setup.zsh` で完結し、PC ローカルな `~/Library/Application Support/rtk/filters.toml` は初回フック実行時に自動生成される。フック順序は「破壊的コマンドブロック → rtk hook」で、`rm -rf` / `git push --force` 等が rtk のリライトを通過する前に exit 2 で止まる +# Nix 環境 (Phase A) + +`~/.dotfiles/nix/` 配下で nix-darwin + home-manager + flakes による宣言的環境構築を行う。詳細手順は `nix/README.md` を参照。 + +## 主要コマンド + +```sh +cd ~/.dotfiles/nix + +# 副作用なしビルド確認 (CI と同じ検証を手元で) +USER=ciuser nix build .#darwinConfigurations.default.system --no-link --impure + +# 適用 (sudo 必須、USER=$USER は sudo の env_reset で USER=root になるのを回避、--impure は username 動的解決のため必須) +sudo USER=$USER darwin-rebuild switch --flake .#default --impure + +# 直前世代に戻す +sudo darwin-rebuild switch --rollback + +# 世代一覧 +darwin-rebuild --list-generations +``` + +## 重要な設計判断 + +- **`nix.enable = false`**: ローカル PC に Determinate Nix がインストールされている前提。nix-darwin の native Nix 管理は Determinate daemon と競合するため、`nix/darwin.nix` で明示的に無効化している。実験的機能 (nix-command / flakes) は Determinate がデフォルト有効化しているため別途宣言不要 +- **PC 名・ユーザー名のリポジトリ非格納**: `darwinConfigurations.default` で output 名を hostname フリーに固定し、`username = builtins.getEnv "USER"` で macOS ローカルアカウント名を実行時解決する。公開リポジトリに PC 名や個人アカウント名を晒さないための設計。`--impure` フラグが必須になる代償と引き換え (S15) +- **`homebrew.onActivation.cleanup = "zap"`**: 宣言外パッケージは Cellar ごと削除する強い管理。Brewfile 由来の旧パッケージが残らないよう破壊的に同期する。Phase A 移行期はリスクを認識した上で運用する (`nix/modules/darwin/homebrew.nix` のコメント参照) +- **`rtk` overlay**: `flake.nix` の `rtk-src` input から `rustPlatform.buildRustPackage` でビルド。`nix/modules/overlays/rtk.nix` で `pkgs.rtk` として供給され、`home/packages.nix` から参照される + +## 棚卸 → triage → 翻訳ワークフロー (S10) + +macOS の `defaults` 値を `defaults.nix` に翻訳するための人間 in-the-loop プロセス: + +1. `zsh nix/scripts/inventory.zsh` を実行 → `docs/inventory/-.md` 生成 (READ-ONLY) +2. 生成された Markdown を開き、各項目に `nix化 / 無視 / 検討` をマーク +3. triage 結果を `nix/modules/darwin/defaults.nix` に翻訳 (`nix/darwin.nix` から import) +4. `nix build` で検証 → `darwin-rebuild switch` で適用 + +triage で「無視」マークした項目は OS デフォルト値が PC 間で異なる可能性があるため、複数 PC で運用する場合は PC 別に再評価する必要がある。 + +## CI 検証 (`nix-check` workflow) + +`.github/workflows/nix-check.yml` で PR ごとに以下を検証する: + +- `nix flake check` (構文・型・依存解決) +- `USER=ciuser nix build .#darwinConfigurations.default.system --no-link --impure` (closure ビルド) + +`darwin-rebuild switch` の activation 自体は CI 範囲外 (環境差で消耗するため)。実機での `darwin-rebuild build` → `switch` で検証する方針。 + +## Phase A → Phase B + +Phase A は **並存期間** (Brewfile / setup.zsh / nix の三重管理)。Phase B で: + +- `Brewfile` 削除 +- `setup/` 削除 (一部は `nix/scripts/` に残す) +- `aliases` / `functions` / `zshrc` 等の symlink を home-manager 経由に統一 + +Phase B の作業は別 plan で扱う。Phase A 完了 + 運用安定確認後に着手する。 + # CLAUDE.md の自己更新 - リポジトリに新しいディレクトリやファイルを追加した場合、「リポジトリ構造」セクションを更新すること diff --git a/README.md b/README.md index 83fd9d7..93d727a 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,19 @@ ## Initialize Setup -1. Install XCode +1. Grant Full Disk Access to your terminal app + (System Settings → Privacy & Security → Full Disk Access) + +2. Install Xcode CLT ```terminal xcode-select --install ``` -2. Install Homebrew +3. Install Nix and dotfiles ```terminal -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +git clone https://github.com/gotomts/dotfiles.git ~/.dotfiles +zsh ~/.dotfiles/nix/scripts/install-nix.zsh +cd ~/.dotfiles/nix && sudo USER=$USER nix run nix-darwin -- switch --flake .#default --impure ``` -3. Install my dot files: -```terminal -zsh -c "$(curl -s https://raw.githubusercontent.com/gotomts/dotfiles/master/setup/setup.zsh)" -``` \ No newline at end of file +See [`nix/README.md`](nix/README.md) for details. diff --git a/docs/superpowers/plans/2026-05-02-nix-migration.md b/docs/superpowers/plans/2026-05-02-nix-migration.md new file mode 100644 index 0000000..888554b --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-nix-migration.md @@ -0,0 +1,422 @@ +# Implementation Plan: 環境構築の Nix 一元化 (Phase A) + +- 作成日: 2026-05-02 +- 関連 spec: `docs/superpowers/specs/2026-05-02-nix-migration-design.md` +- 対象スコープ: **Phase A のみ**(並存期間。Phase B の Brewfile/setup 削除は別 plan) + +## 前提 + +- spec で確定済の決定事項 11 項目を実装に落とす +- 作業ディレクトリ: `~/.dotfiles/nix/`(新規)と `~/.dotfiles/docs/`(既存) +- 各ステップは独立 PR / sub-issue として並列実装可能(依存関係を守る限り) +- 検証は手元 macOS マシンで実施。CI は対象外(Phase A 範囲では) +- すべての Nix ファイルに対して `nix flake check` が通ることを最低限の品質保証とする + +## ステップ一覧(依存関係付き) + +``` +S1: 棚卸スクリプト ────┐ + ├─→ S10: nix-darwin defaults.nix +S2: nix/ 雛形 + flake ─┤ + ├─→ S3..S8 (home-manager 全モジュール) + ├─→ S9: nix-darwin homebrew + ├─→ S11: nix-darwin sudoers/fonts/pam + └─→ S12: 検証 + README + 別 PC 手順 + │ + └─→ S13: CLAUDE.md 更新 +``` + +S2 が他の全てを blocks する。S1 は S10 のみを blocks(独立並列実装可)。S12 は S3..S11 全てに blocked-by。S13 は S12 に blocked-by。 + +## ステップ詳細 + +### S1: 棚卸スクリプト + 初回 triage 文書生成 + +**目的**: 現マシンの macOS 設定を自動ダンプし、人間 triage 用のチェックリストを `docs/inventory/-2026-05-02.md` として生成する。 + +**変更対象** +- `nix/scripts/inventory.zsh`(新規) +- `docs/inventory/-2026-05-02.md`(生成物。コミット対象) + +**実装内容** +- `defaults domains` から既知優先ドメイン(`com.apple.dock`, `com.apple.finder`, `com.apple.menuextra.clock`, `NSGlobalDomain`, `com.apple.controlcenter`, `com.apple.universalaccess`, `com.apple.HIToolbox`, `com.apple.screencapture`, `com.apple.trackpad`, `com.apple.AppleMultitouchTrackpad`)を抜粋して `defaults read ` でダンプ +- `mas list` の出力をチェックリスト化 +- `launchctl list | grep ` の出力を抽出 +- `ls /etc/sudoers.d/` の中身ダンプ +- `brew bundle dump --file=/tmp/Brewfile.dump --no-restart` を実行し、現 Brewfile と diff +- `fc-list :family` と `~/Library/Fonts` の比較(自前フォント検出) +- 上記をすべて `docs/inventory/-.md` に Markdown チェックリスト形式で出力(`` プレースホルダ付き) +- スクリプトのシバンと util.zsh ロードは既存 `setup/util.zsh` の規約に従う(`#!/bin/zsh`、`util::info` など) + +**受入条件** +- [ ] `zsh nix/scripts/inventory.zsh` を引数なしで実行すると `docs/inventory/-.md` が生成される +- [ ] 生成物に `defaults` の各ドメインのダンプが含まれている +- [ ] 生成物に `mas list` の結果が含まれている +- [ ] 生成物に `brew bundle dump` 差分が含まれている +- [ ] チェックリスト形式で各項目に `` が付いている +- [ ] スクリプトが `set -e` 相当の挙動でエラー時に止まる +- [ ] スクリプトに最低限のテスト(`bats-core` で `--help` と空ディレクトリ実行) + +**依存**: なし(並列実装可能) +**blocks**: S10 + +### S2: `nix/` 雛形 + `flake.nix` + `lib/mkHost.nix` + +**目的**: nix の基盤ディレクトリ構造を作り、最小限の flake で `darwin-rebuild build` がエラーなしで通る状態にする。 + +**変更対象** +- `nix/flake.nix`(新規) +- `nix/flake.lock`(生成) +- `nix/README.md`(新規) +- `nix/lib/mkHost.nix`(新規) +- `nix/hosts//default.nix`(新規。ホスト名は実装時に確定) +- `nix/hosts//darwin.nix`(新規。空モジュール) +- `nix/hosts//home.nix`(新規。空モジュール) + +**実装内容** +- `flake.nix`: `nixpkgs`, `nix-darwin`, `home-manager`, `rtk-src` の 4 input を宣言(rtk-src は実装時にリポジトリ URL を確定) +- `lib/mkHost.nix`: `{ hostname, system, username }` を受け取り `darwin.lib.darwinSystem` を返すヘルパー +- `hosts//default.nix`: `darwin.nix` と `home.nix` を import するだけのスタブ +- `nix/README.md`: 「`darwin-rebuild build --flake .#` の手順」「`flake.lock` の更新コマンド」「ホスト追加手順」を記述 + +**受入条件** +- [ ] `cd nix && nix flake check` がエラーなしで通る +- [ ] `darwin-rebuild build --flake .#` がエラーなしで完了する(空モジュールでも構成は成立する) +- [ ] `flake.lock` がコミットされている +- [ ] README に最低限の運用手順が記載されている + +**依存**: なし +**blocks**: S3, S4, S5, S6, S7, S8, S9, S10, S11 + +### S3: home-manager `packages.nix`(CLI ツール群移植) + +**目的**: Brewfile の brew セクション(CLI ツール)を `home.packages` に翻訳する。 + +**変更対象** +- `nix/modules/home/packages.nix`(新規) +- `nix/hosts//home.nix`(import 追加) + +**実装内容** +- 既存 Brewfile の以下セクションを `home.packages = with pkgs; [ ... ]` で宣言: + - `# Utilities`: jq, bats-core, pwgen, qpdf 等 + - `# Shell & Terminal`: fzf + - `# Git & Version Control`: gh, ghq, lazygit, lazydocker, worktrunk + - `# Cloud & DevOps`: kubectl, kubectx, stern, sops + - `# Languages & Runtimes`: bun, fvm, pipx + - `# Network & API`: grpcurl, tailscale + - `# Task Management`: linear (schpet/tap) +- nixpkgs に存在しないパッケージ(`schpet/tap/linear`, `leoafarias/fvm/fvm`, `manaflow-ai/cmux/cmux`)は **コメントで「Brewfile 残置」と明示** し、`darwin/homebrew.nix` 側で `brews = [...]` として管理 +- ビルド系(autoconf, automake, pkg-config 等)は home.packages にも入れず、必要時に `nix shell` で対応する旨をコメント + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `darwin-rebuild build --flake .#` 成功 +- [ ] 生成された profile に `which jq`, `which gh` 等が `~/.nix-profile/bin/` 配下を返す +- [ ] nixpkgs 不在パッケージは `darwin/homebrew.nix` 側に残置されコメントで根拠が記述されている + +**依存**: S2 +**blocks**: なし + +### S4: home-manager `zsh.nix`(programs.zsh + oh-my-zsh 宣言化) + +**目的**: 既存 `zshrc`, `zshenv`, `aliases`, `functions/` を `programs.zsh` モジュールに分解移植する。 + +**変更対象** +- `nix/modules/home/zsh.nix`(新規) +- `nix/hosts//home.nix`(import 追加) + +**実装内容** +- `programs.zsh.enable = true` +- `programs.zsh.oh-my-zsh.enable = true` + 必要プラグインの宣言(zsh-autosuggestions 等を `plugins = [ ... ]` で) +- 既存 `aliases` ファイルの内容を `programs.zsh.shellAliases` 属性セットに移植 +- 既存 `zshrc` の独自設定(PATH 操作、関数 source、completion 設定等)を `programs.zsh.initExtra` に移植 +- `functions/fzf-history` は `home.file."./.functions/fzf-history".source` で symlink、`initExtra` 内で `source` 呼び出し +- `aliase/get-gke-credentials.sh` も同様 symlink +- 既存 `zshenv` の内容を `programs.zsh.envExtra` に移植 + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `darwin-rebuild switch` 後に新しい zsh セッションを開いて、`alias` コマンドで既存 alias が全て出る +- [ ] `compinit` が機能する +- [ ] `fzf-history` 関数が実行できる +- [ ] 既存 zshrc/zshenv/aliases ファイルは Phase A 期間中は残す(home-manager が生成する `~/.zshrc` を優先するため、 dotfiles の zshrc symlink は競合しないよう home-manager 側でハンドリング) +- [ ] 生成された `~/.zshrc` を新規シェルで `source` した際にエラーゼロ + +**依存**: S2 +**blocks**: なし + +### S5: home-manager `git.nix` / `starship.nix` / `yazi.nix` / `ssh.nix` + +**目的**: 設定ファイル系の宣言化をまとめて行う(量が小さい複数モジュールの集約)。 + +**変更対象** +- `nix/modules/home/git.nix`(新規) +- `nix/modules/home/starship.nix`(新規) +- `nix/modules/home/yazi.nix`(新規) +- `nix/modules/home/ssh.nix`(新規) +- `nix/hosts//home.nix`(import 追加) + +**実装内容** +- **git.nix**: `programs.git.{enable, userName, userEmail, extraConfig, ignores}` で `gitconfig` と `gitignore_global` を移植。`gitmessage` は `programs.git.extraConfig.commit.template` +- **starship.nix**: `programs.starship.enable = true` + `settings = (builtins.fromTOML (builtins.readFile ../../../config/starship/starship.toml))` で既存設定をそのまま読み込む +- **yazi.nix**: `programs.yazi.enable = true` + `settings`/`keymap` を同様に TOML 読み込み +- **ssh.nix**: `home.file.".ssh/config".source = ../../../ssh/config`(鍵ファイルは対象外) + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `git config --global --get user.email` が `mh.goto.web@gmail.com` を返す +- [ ] `starship` プロンプトが期待通り表示される +- [ ] `yazi` 起動時にカスタム keymap が効く +- [ ] `ssh -G ` で `~/.ssh/config` の設定が反映されている + +**依存**: S2 +**blocks**: なし + +### S6: home-manager `claude.nix`(plugin sync activation) + +**目的**: `~/.claude/{agents,skills,settings.json,...}` を home-manager 経由で symlink し、`enabledPlugins` を読んで `claude plugin install/update` を activation script で実行する。 + +**変更対象** +- `nix/modules/home/claude.nix`(新規) +- `nix/hosts//home.nix`(import 追加) + +**実装内容** +- `home.file.".claude/agents".source = ../../../claude/agents` を再帰的に +- `home.file.".claude/skills".source = ../../../claude/skills` を再帰的に +- `home.file.".claude/settings.json".source = ../../../claude/settings.json` +- `home.file.".claude/RTK.md".source = ../../../claude/RTK.md` +- `home.activation.claudePlugins = lib.hm.dag.entryAfter ["writeBoundary"] ''...''` で: + - `claude plugin marketplace update`(失敗しても続行) + - `jq -r '.enabledPlugins // {} | keys[]' ~/.claude/settings.json` をループ + - 各 plugin に対し `claude plugin install` または `claude plugin update` + - 失敗しても他のプラグインは続行(warning ログ) + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `darwin-rebuild switch` 後 `~/.claude/agents/.md` が dotfiles リポジトリのファイルへの symlink になっている +- [ ] `claude plugin list` で `enabledPlugins` 全てがインストール済み +- [ ] activation 実行ログに plugin 同期の進捗が出る +- [ ] 既存 `setup/install/10_claude.zsh` の sudoers 編集 (pmset NOPASSWD) は **このモジュールの対象外**(S11 で扱う) + +**依存**: S2 +**blocks**: なし + +### S7: home-manager `languages.nix`(言語ツールチェーン) + +**目的**: mise 経由の言語ランタイムと cargo install / pipx で入れていたツールを `home.packages` 経由に置換。 + +**変更対象** +- `nix/modules/home/languages.nix`(新規) +- `nix/hosts//home.nix`(import 追加) + +**実装内容** +- 言語ランタイム: + - Node.js: `nodejs_20`, `nodejs_22` 等を `home.packages`(複数バージョン併存はラッパーで切替) + - Go: `go` + - Ruby: `ruby_3_3` 等 + - Rust: `fenix` overlay 採用 or `rustc` + `cargo` + `rust-analyzer` + `rustfmt` + `clippy` を home.packages + - Python: `python311` または `python312` + - Dart: `dart`(fvm は Brewfile 残置) +- cargo / pip ツール: + - `cargo-nextest`, `cargo-watch` を nixpkgs から + - `poetry` を nixpkgs から + - `grip` を nixpkgs から(pipx 不要に) +- mise は **`darwin/homebrew.nix` から外す**(このステップで変更を予告コメント) + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `which node`, `which go`, `which rustc`, `which python3` が `~/.nix-profile/bin/` 配下を返す +- [ ] `cargo nextest --version`, `cargo watch --version` 動作 +- [ ] `poetry --version`, `grip --version` 動作 +- [ ] `which mise` が解決しない or `homebrew.nix` 側で「移行中残置」コメント付きの場合のみ残る + +**依存**: S2 +**blocks**: なし + +### S8: rtk overlay(flake input + buildRustPackage) + +**目的**: `rtk` を flake input から `rustPlatform.buildRustPackage` でビルドし、`pkgs.rtk` として供給。Brewfile の `# AI Tooling` セクションから `brew 'rtk'` を外す前提を作る。 + +**変更対象** +- `nix/modules/overlays/rtk.nix`(新規) +- `nix/flake.nix`(`rtk-src` input 確定 + overlay 適用) +- `nix/modules/home/packages.nix`(`rtk` を追加) + +**実装内容** +- `rtk-src` の URL を実装時に確定(GitHub の rtk リポジトリ) +- overlay で `final.rtk = prev.rustPlatform.buildRustPackage { ... }` を定義 +- `cargoLock.lockFile` で再現性確保 +- `pname = "rtk"`, `version = inputs.rtk-src.shortRev` で flake 評価時のリビジョンを反映 +- meta 情報(`description`, `homepage`, `license`, `mainProgram = "rtk"`)も定義 + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `darwin-rebuild switch` 後 `which rtk` が `~/.nix-profile/bin/rtk` を返す +- [ ] `rtk --version` が動作 +- [ ] `rtk gain` 等のメタコマンドが動作 +- [ ] flake.lock に `rtk-src` がロックされ、再現性確保 + +**依存**: S2 +**blocks**: なし + +### S9: nix-darwin `homebrew.nix`(cask + mas + 例外 brew) + +**目的**: Brewfile の cask / mas / 例外 brew(nixpkgs 未収録)を `homebrew.{casks, masApps, brews, taps}` で宣言する。 + +**変更対象** +- `nix/modules/darwin/homebrew.nix`(新規) +- `nix/hosts//darwin.nix`(import 追加) + +**実装内容** +- `homebrew.enable = true` +- `homebrew.onActivation.{autoUpdate, cleanup} = "zap"` 等の挙動を選択 +- `homebrew.taps = [ "leoafarias/fvm", "manaflow-ai/cmux", "oven-sh/bun", "schpet/tap" ]`(rtk が flake input になったので不要なら外す) +- `homebrew.casks`: 現 Brewfile の cask 全 25 個 +- `homebrew.masApps = { LINE = 539883307; Magnet = 441258766; ... }` +- `homebrew.brews`: nixpkgs に無いもの(`leoafarias/fvm/fvm`, `manaflow-ai/cmux/cmux` 等) + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `darwin-rebuild switch` で全 cask がインストール済みになる +- [ ] `mas list` で 5 アプリすべてがリストされる +- [ ] `homebrew.brews` に残したパッケージは正常に動作 +- [ ] 既存 `Brewfile` から `mise` と `rtk` が外れることが S7 / S8 完了後に成立 + +**依存**: S2 +**blocks**: なし + +### S10: nix-darwin `defaults.nix`(棚卸 triage 翻訳) + +**目的**: S1 で生成 + ユーザー triage 済みの `docs/inventory/-.md` を `system.defaults.*` に翻訳する。 + +**変更対象** +- `nix/modules/darwin/defaults.nix`(新規) +- `nix/hosts//darwin.nix`(import 追加) +- `docs/inventory/-.md`(triage マーク済みであること。**人間レビューが先行している前提**) + +**実装内容** +- triage で「Nix 化」マークされた項目を `system.defaults.{NSGlobalDomain,dock,finder,...}` に翻訳 +- nix-darwin が直接サポートしない key は `system.defaults.NSGlobalDomain.` の generic スロット、それでも書けないものは `system.activationScripts.postUserActivation.text` で `defaults write` を直接呼ぶ +- 翻訳できなかった項目(triage で「無視」「検討」マーク)は `defaults.nix` の **コメントで列挙** し、なぜ無視したかを記録 + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `darwin-rebuild switch` 後に `defaults read com.apple.dock` 等で triage で「Nix 化」した値が反映されている +- [ ] 翻訳しなかった項目の根拠が `defaults.nix` のコメントに残っている +- [ ] `system.activationScripts` を使った場合、その内容が冪等(複数回実行しても問題なし) + +**依存**: S1, S2 +**blocks**: なし + +### S11: nix-darwin `sudoers.nix` / `fonts.nix` / `pam.nix` + +**目的**: 小粒の nix-darwin モジュールをまとめて。 + +**変更対象** +- `nix/modules/darwin/sudoers.nix`(新規) +- `nix/modules/darwin/fonts.nix`(新規) +- `nix/modules/darwin/pam.nix`(新規) +- `nix/hosts//darwin.nix`(import 追加) + +**実装内容** +- **sudoers.nix**: `security.sudo.extraRules = [{ users = ["goto"]; commands = [{ command = "/usr/bin/pmset"; options = ["NOPASSWD"]; }]; }]` で `setup/install/10_claude.zsh` の pmset NOPASSWD を再現 +- **fonts.nix**: `fonts.packages = with pkgs; [ sf-mono ]`(nixpkgs に sf-mono が無ければ Brewfile 残置)。または cask 経由のままで `homebrew.casks` に残す判断 +- **pam.nix**: `security.pam.services.sudo_local.touchIdAuth = true`(新規宣言。棚卸で見つかった場合のみ) + +**受入条件** +- [ ] `nix flake check` 成功 +- [ ] `sudo pmset` がパスワードなしで実行できる +- [ ] `fc-list :family | grep "SF Mono"` が結果を返す +- [ ] Touch ID for sudo が機能する(棚卸で有効化が望ましいと判明した場合) + +**依存**: S2 +**blocks**: なし + +### S12: 検証 + README + 別 PC 手順 + +**目的**: 全モジュール統合後の最終検証を行い、`nix/README.md` を別 PC セットアップ手順を含めて充実させる。 + +**変更対象** +- `nix/README.md`(更新) +- `docs/inventory/-2026-05-02.md`(baseline diff の結果を追記) + +**実装内容** +- `darwin-rebuild build --flake .#` 実行(switch せず) +- `nvd diff /run/current-system result` で差分確認 +- `darwin-rebuild switch --flake .#` 適用 +- 適用後、棚卸 baseline `docs/inventory/baseline-com.apple.dock.txt` 等と `defaults read` 出力を diff +- `which jq`, `which rtk`, `which node` 等の確認 +- `claude plugin list` の確認 +- README に以下を追記: + - 別 PC への展開手順(Command Line Tools → Nix インストール → flake clone → `darwin-rebuild switch`) + - トラブルシューティング(flake.lock が壊れた場合の復旧、rollback コマンド) + - `flake.lock` 更新運用(`nix flake update` の頻度方針) + +**受入条件** +- [ ] `darwin-rebuild build --flake .#` がエラーなしで完了 +- [ ] `nvd diff` 出力が PR コメントに記録されている +- [ ] `darwin-rebuild switch` 適用後、shellと CLI ツールが期待通り動作 +- [ ] 別 PC 手順が README に記載されている +- [ ] baseline diff の結果が triage で「無視」とマークされた項目以外で完全一致している(一致しない場合はその根拠を inventory に追記) + +**依存**: S3, S4, S5, S6, S7, S8, S9, S10, S11 +**blocks**: S13 + +### S13: CLAUDE.md 更新 + +**目的**: dotfiles のグローバルドキュメントを Phase A の現実に合わせる。 + +**変更対象** +- `CLAUDE.md`(既存。リポジトリ root) + +**実装内容** +- 「リポジトリ構造」セクションに `nix/` を追加 +- 「Brewfile」セクションに「Phase B で削除予定。Phase A 期間中は読み取り専用バックアップとして残す」注記 +- 「シンボリックリンク管理」セクションに「home-manager がメインで管理。`setup.zsh` は Phase B で削除予定」注記 +- 新規セクション「Nix 環境」を追加: + - flake 構造の概要 + - `darwin-rebuild` 運用コマンド + - 棚卸 → triage → 翻訳ワークフロー + - 別 PC への展開ポインタ(`nix/README.md` 参照) + +**受入条件** +- [ ] `CLAUDE.md` の差分が Phase A の構造変化を全てカバー +- [ ] `nix/README.md` への参照が貼られている +- [ ] 既存セクション間の整合性(`Brewfile` セクションが「現在の事実」と齟齬しない) + +**依存**: S12 +**blocks**: なし + +## 横断的な作業ルール + +- 各ステップは **独立 worktree** で実装し、独立 PR を出す(`feature-team` の Phase 4-A 並列開発に対応) +- 各 PR は `nix flake check` と `darwin-rebuild build`(必要に応じ)が通ることを最低ライン +- ブランチ命名: `feature/nix-migration-s-` 例: `feature/nix-migration-s2-flake-skeleton` +- レビュー観点: quality 必須、`security`(sudoers / pam を扱う S11)、`performance`(該当なし) + +## リスクと検証ステップ + +| リスク | 検証 | +|---|---| +| 棚卸で漏れがある | S12 で baseline diff を取って漏れを検出 | +| `home.activation` で claude plugin が壊れる | `lib.mkAfter` で末尾置き、失敗しても他は通す(S6 受入条件に明記) | +| `darwin-rebuild switch` が macOS 本体に副作用 | switch 前に `build` で停止確認、ロールバック手順を README に記載 | +| 並列 PR の merge 順序ミス | S2 を最初にマージ、他は S2 に rebase してから | +| `defaults` の key 名間違い | `darwin-rebuild build` が catch するキーは可視、catch しないキーは S12 baseline diff で検出 | + +## Phase B(参考) + +このプランの範囲外。Phase A 完了後、運用安定を確認してから別 plan として: +- `Brewfile` 削除 +- `setup/install/` 削除 +- `setup/setup.zsh` 削除 +- `CLAUDE.md` の関連セクション削除 +- `nix/` の root 昇格検討 + +## 完了の定義(Phase A) + +- [ ] S1〜S13 全ステップが完了 +- [ ] `darwin-rebuild switch --flake .#` が新規セッションで成功 +- [ ] 棚卸 baseline と switch 後の `defaults read` の diff がゼロ(triage 「無視」項目を除く) +- [ ] PR がマージされ main の `CLAUDE.md` に Nix 環境セクションが含まれる +- [ ] 別 PC で `darwin-rebuild` 初回実行のドライランが README 通りに進む(実機なくても手順 trace で検証) diff --git a/docs/superpowers/specs/2026-05-02-nix-migration-design.md b/docs/superpowers/specs/2026-05-02-nix-migration-design.md new file mode 100644 index 0000000..6b87232 --- /dev/null +++ b/docs/superpowers/specs/2026-05-02-nix-migration-design.md @@ -0,0 +1,305 @@ +# Design: 環境構築の Nix 一元化 + +- 作成日: 2026-05-02 +- 対象リポジトリ: `~/.dotfiles` (github.com/gotomts/dotfiles) +- 関連 plan: `docs/superpowers/plans/2026-05-02-nix-migration.md` + +## 1. 目的・成功条件 + +### 目的 +`~/.dotfiles` の環境構築を **Nix(nix-darwin + home-manager + flakes)** に最大限寄せて、宣言的・再現可能・マルチホスト対応にする。CLI / GUI / macOS 設定 / シェル / Claude Code プラグインまでを単一の `darwin-rebuild switch` で復元できる状態を作る。 + +### 成功条件(受入条件) +- [ ] 現マシン上で `darwin-rebuild switch --flake ~/.dotfiles/nix#` 一発で「現在の環境と同等」が再現できる +- [ ] CLI / GUI(cask)/ App Store(mas)/ macOS defaults / sudoers / launchd / フォント / シェル設定 / claude plugins が flake から復元できる +- [ ] mise / pipx / cargo install / rustup の global 状態に依存しない(Brewfile からも mise が外れている) +- [ ] `~/.dotfiles/Brewfile` と `~/.dotfiles/setup/install/` が削除されている(Phase B 完了後) +- [ ] `setup.zsh` 由来の symlink 群が home-manager の `home.file` で再現されている +- [ ] 別 PC で flake をクローンして `darwin-rebuild` を初回実行する README 手順がある +- [ ] 棚卸 → triage → 翻訳の人間 in-the-loop ワークフローが文書化されている +- [ ] `rtk` が flake input 経由でビルドされ、バージョン pin できている + +## 2. 背景と現状 + +### 現状の管理範囲 +- **Brewfile**: tap 4 / brew 25 / cask 25 / mas 5 / fonts 1(合計約 60 entry) +- **setup.zsh**: dotfiles ルートと `claude/`, `config/`, `ssh/` を `~/` 配下に symlink +- **install/ 11 スクリプト**: oh-my-zsh、mise(node/go/ruby/rust/python/dart)、claude plugins、starship/yazi/grip、linear 認証、pmset NOPASSWD sudoers +- **複数の補助マネージャ**: mise(言語ランタイム)、pipx(poetry, grip)、cargo install(cargo-nextest, cargo-watch)、npm -g(npm-fzf)、rustup component +- **macOS 固有副作用**: sudoers 編集、mas、cask、Application Support パス +- **明文化されていない設定**: `defaults write` 系の Mac 設定は現状 dotfiles に宣言が一つも存在しない + +### 課題 +1. **再現性の欠如**: install スクリプトが手続き的で、現在のマシン状態を厳密に再現する保証がない +2. **マルチマシン対応不能**: ホスト間で「同じ状態」を維持する仕組みがない +3. **macOS 設定が暗黙**: `defaults write` 系がドキュメント化されておらず、PC 移行時に欠落しがち +4. **依存マネージャの分散**: brew / mise / pipx / cargo / npm / rustup の 6 系統が並立している + +## 3. 主要な意思決定(決定事項一覧) + +| # | 軸 | 決定 | +|---|---|---| +| 1 | スコープ | 最大化(mise/pipx/cargo install も置換) | +| 2 | 移行戦略 | 段階移行(並存 → 逆転 → 削除) | +| 3 | shell config | `programs.zsh` で宣言化(initExtra / shellAliases / oh-my-zsh モジュール) | +| 4 | macOS 設定 | フル(defaults / sudoers / services / cask / mas / fonts) | +| 5 | flake 構造 | マルチホスト(`hosts//` + `modules/`) | +| 6 | mise | 完全削除(Brewfile からも外す) | +| 7 | rtk | flake input として GitHub から fetch、`rustPlatform.buildRustPackage` でビルド | +| 8 | claude plugins | 宣言化(`enabledPlugins` を読んで `home.activation` で同期) | +| 9 | oh-my-zsh | 宣言化(`programs.zsh.oh-my-zsh` モジュール) | +| 10 | macOS 棚卸 | 現マシン = source of truth、自動 inventory → 人間 triage → 翻訳 | +| 11 | リポジトリ構造 | Approach A(`nix/` サブディレクトリ)。Phase B で root 昇格を別検討 | + +## 4. 全体アーキテクチャ + +``` +~/.dotfiles/ +├── nix/ ← 新規追加。Phase A の作業対象 +│ ├── flake.nix ← inputs / outputs ルート +│ ├── flake.lock ← 全 input のロック +│ ├── README.md ← nix/ 配下の運用手順 +│ ├── hosts/ +│ │ └── / +│ │ ├── default.nix ← ホスト固有の合成 +│ │ ├── darwin.nix ← nix-darwin module 集約 +│ │ └── home.nix ← home-manager module 集約 +│ ├── modules/ +│ │ ├── darwin/ +│ │ │ ├── defaults.nix ← system.defaults.* (棚卸結果から翻訳) +│ │ │ ├── homebrew.nix ← cask / mas / 例外 brew を宣言 +│ │ │ ├── sudoers.nix ← pmset NOPASSWD など +│ │ │ ├── fonts.nix ← font-sf-mono 等 +│ │ │ ├── launchd.nix ← (棚卸で発見されたもの) +│ │ │ └── pam.nix ← Touch ID for sudo +│ │ ├── home/ +│ │ │ ├── packages.nix ← CLI ツール (jq, fzf, gh, … nixpkgs から) +│ │ │ ├── zsh.nix ← programs.zsh + oh-my-zsh + initExtra +│ │ │ ├── git.nix ← gitconfig / gitignore_global を宣言化 +│ │ │ ├── starship.nix ← starship 設定 +│ │ │ ├── yazi.nix ← yazi.toml / keymap.toml +│ │ │ ├── claude.nix ← claude plugin 同期 + symlink +│ │ │ ├── ssh.nix ← ssh/ symlink (鍵以外) +│ │ │ └── languages.nix ← Node/Go/Ruby/Rust/Python/Dart toolchain +│ │ └── overlays/ +│ │ └── rtk.nix ← rtk を flake input としてビルド +│ ├── lib/ +│ │ └── mkHost.nix ← ホスト合成ヘルパー +│ └── scripts/ +│ └── inventory.zsh ← 棚卸スクリプト +├── docs/ +│ ├── superpowers/specs/2026-05-02-nix-migration-design.md ← この spec +│ ├── superpowers/plans/2026-05-02-nix-migration.md ← writing-plans が作る +│ └── inventory/ +│ └── -2026-05-02.md ← 棚卸結果と triage チェックリスト +├── Brewfile ← Phase A 期間中は読み取り専用バックアップ +├── setup/ ← Phase A 期間中は使わない +├── claude/ ← 維持(home.file で symlink 化) +├── config/ ← 維持(home.file または各 module) +├── functions/ ← 維持(programs.zsh.initExtra から source) +├── ssh/ ← 維持(home.file で symlink) +├── zshrc / zshenv / aliases ← 中身は programs.zsh モジュールに移植 +└── CLAUDE.md ← nix/ セクションを追加 +``` + +## 5. コンポーネント設計 + +### 5.1 `nix/flake.nix` (inputs / outputs) + +```nix +{ + description = "gotomts macOS dotfiles via nix-darwin + home-manager"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + nix-darwin.url = "github:LnL7/nix-darwin"; + nix-darwin.inputs.nixpkgs.follows = "nixpkgs"; + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + rtk-src = { url = "github:"; flake = false; }; + }; + + outputs = { self, nixpkgs, nix-darwin, home-manager, rtk-src, ... }@inputs: + let + mkHost = import ./lib/mkHost.nix { inherit inputs; }; + in { + darwinConfigurations. = mkHost { + hostname = ""; + system = "aarch64-darwin"; + username = "goto"; + }; + }; +} +``` + +### 5.2 nix-darwin モジュール + +| モジュール | 役割 | 翻訳元 | +|---|---|---| +| `darwin/defaults.nix` | `system.defaults.{NSGlobalDomain,dock,finder,trackpad,…}` | 棚卸 triage 結果 | +| `darwin/homebrew.nix` | `homebrew.{enable, casks, masApps, brews}` | 現 Brewfile の cask + mas + 例外 brew | +| `darwin/sudoers.nix` | `security.sudo.extraRules` | `setup/install/10_claude.zsh` の pmset NOPASSWD | +| `darwin/fonts.nix` | `fonts.packages` | `cask 'font-sf-mono'` | +| `darwin/launchd.nix` | `launchd.user.agents.*` | 棚卸で見つかったエージェント | +| `darwin/pam.nix` | `security.pam.services.sudo_local.touchIdAuth = true` | 新規宣言 | + +> nix-darwin の `homebrew` モジュールは **brew 自体を `services.nix-darwin` 的に管理しない**。brew は引き続き手動で(または `homebrew.onActivation.autoUpdate` 経由で)動作するが、Brewfile.nix 等価が flake で表現される。 + +### 5.3 home-manager モジュール + +| モジュール | 役割 | +|---|---| +| `home/packages.nix` | CLI ツール群を `home.packages = with pkgs; [ ... ]` で宣言。Brewfile の `# Utilities` 〜 `# Network & API` セクションを移植 | +| `home/zsh.nix` | `programs.zsh.{enable, oh-my-zsh, shellAliases, initExtra}`。現 zshrc/aliases/functions の中身を分解して移植 | +| `home/git.nix` | `programs.git.{enable, userName, userEmail, extraConfig, ignores}` | +| `home/starship.nix` | `programs.starship.{enable, settings}` | +| `home/yazi.nix` | `programs.yazi.{enable, settings, keymap}` | +| `home/claude.nix` | `~/.claude/{agents,skills,settings.json,...}` を `home.file.*.source` で symlink。`enabledPlugins` を読んで `home.activation.claudePlugins` で `claude plugin install/update` | +| `home/ssh.nix` | `programs.ssh` または `home.file.".ssh/".source`(鍵は対象外) | +| `home/languages.nix` | Node/Go/Ruby/Rust/Python/Dart の各ツールチェーン。`fenix` overlay 等を必要に応じて使う。`cargo-nextest`, `cargo-watch`, `poetry`, `grip` も `home.packages` | + +### 5.4 rtk overlay + +```nix +# nix/modules/overlays/rtk.nix +final: prev: { + rtk = prev.rustPlatform.buildRustPackage { + pname = "rtk"; + version = inputs.rtk-src.shortRev; + src = inputs.rtk-src; + cargoLock = { lockFile = "${inputs.rtk-src}/Cargo.lock"; }; + }; +} +``` + +flake input でバージョンをロックし、`pkgs.rtk` として供給。`home/packages.nix` から参照。 + +## 6. データフロー / アクティベーション + +``` +ユーザー操作 Nix システムの動き +────────── ────────────────── +$ darwin-rebuild switch \ + --flake ~/.dotfiles/nix# + │ + ├─ flake 評価 + │ └─ inputs (nixpkgs, home-manager, nix-darwin, rtk-src) を fetch + │ + ├─ darwinConfigurations. ビルド + │ ├─ system.defaults.* を生成 (defaults write 等価) + │ ├─ homebrew モジュールが brew bundle 実行 (cask/mas) + │ ├─ security.sudo.extraRules を /etc/sudoers.d/* に書く + │ ├─ fonts.packages を /Library/Fonts に配置 + │ └─ launchd エージェント生成 + │ + └─ home-manager.users. 適用 + ├─ home.packages を ~/.nix-profile に + ├─ programs.zsh が ~/.zshrc を生成 + ├─ home.file が ~/.claude/skills/* など symlink 群を生成 + └─ home.activation スクリプト (claude plugin sync) 実行 +``` + +ロールバック: `darwin-rebuild --rollback` または `darwin-rebuild switch --flake .# --switch-generation `。 + +## 7. 段階移行戦略 + +### Phase A — 並存(nix が主、Brewfile が読み取り専用バックアップ) + +1. **A0: 棚卸 & triage** + - `nix/scripts/inventory.zsh` を新設し、`defaults`/`mas list`/`launchctl list`/sudoers/brew/font の現状を `docs/inventory/-.md` に出力 + - ユーザーが triage 文書を 1 件ずつ「Nix 化 / 無視 / 別途検討」とマーク + - triage 結果を `nix/modules/darwin/defaults.nix` 等に翻訳 +2. **A1: nix/ ディレクトリと flake 雛形作成** +3. **A2: home-manager モジュール構築**(packages → git → zsh → claude → 言語) +4. **A3: nix-darwin モジュール構築**(homebrew → defaults → sudoers → fonts → pam) +5. **A4: rtk overlay**(flake input + buildRustPackage) +6. **A5: 検証**: 現マシンで `darwin-rebuild build`(switch せず)→ diff 確認 → `switch` 適用 +7. **A6: Brewfile / setup/ をリポジトリ内で「使わない」状態に**(README に「Phase B で削除予定」記載) + +### Phase B — 削除と整理 + +1. Phase A の運用が安定(後述ゲート充足)したのち +2. `Brewfile` / `setup/install/` / `setup/setup.zsh` を削除 +3. `CLAUDE.md` の「リポジトリ構造」「Brewfile」「setup スクリプト」関連セクションを更新 +4. 任意: `nix/` を repo root に昇格させるかは別 issue で検討 + +### Phase A → B のゲート +- `darwin-rebuild switch` がエラー無しで複数回連続して完了(具体回数は plan で確定) +- 棚卸 triage で「Nix 化」マーク済み項目が全て nix-darwin 宣言に翻訳されている +- ユーザーが日常使う CLI/GUI/設定が全て nix 経由で再現できる +- 別 PC への展開シミュレーション(ドライラン)が完了 + +## 8. 棚卸 & 人間 in-the-loop + +### 8.1 自動部分 +`nix/scripts/inventory.zsh` が以下を出力: +- `defaults domains` の全ドメインから既知優先ドメイン(`com.apple.dock`, `com.apple.finder`, `com.apple.menuextra.clock`, `NSGlobalDomain` 等)を抜粋し `defaults read` でダンプ +- `mas list` +- `launchctl list | grep ` +- `ls /etc/sudoers.d/` の中身 +- `brew bundle dump --no-restart` を `/tmp` に出して現 Brewfile と diff(手動追加の検出) +- `fc-list :family` から自前フォント検出 + +出力形式: チェックリスト Markdown +```markdown +## defaults: com.apple.dock +- [ ] orientation = "bottom" +- [ ] tilesize = 48 +``` + +### 8.2 手動部分 +- ユーザーが triage(チェック / 削除 / コメント) +- triage 完了後、人間が `nix/modules/darwin/defaults.nix` に翻訳 +- 翻訳結果は `darwin-rebuild build` で構文・型チェック +- `darwin-rebuild switch` で適用 + +### 8.3 マルチホスト時の注意 +nix-darwin は「明示宣言した key だけを書く」設計のため、triage で「無視」とマークした項目は **未宣言のまま** となる。現 PC では現状値が事実上維持されるが、別 PC では OS デフォルト値が露出する。triage で「無視」を選ぶ条件は **「OS デフォルトと同じであること」を確認した上で** とし、文書化する。 + +## 9. エラー処理 / ロールバック + +| シナリオ | 対応 | +|---|---| +| `darwin-rebuild switch` が中途半端に失敗 | 自動で前世代に戻る(nix-darwin 標準動作)。手動なら `darwin-rebuild --rollback` | +| flake.lock が壊れる / inputs の hash 不整合 | `git restore nix/flake.lock` | +| rtk のビルド失敗(input rev が壊れた) | `flake.lock` で前 rev に固定、追って修正 | +| home-manager activation で claude plugin が壊れる | `home.activation.claudePlugins` を `lib.mkAfter` で末尾に置き、失敗しても他は通す。エラーは warning ログ | +| Brewfile.nix の cask が brew に存在しない | `darwin-rebuild` がエラー停止。修正は cask 名 typo 確認 | + +Phase A 中は **Brewfile 自体は残っている** ので、最悪 `brew bundle --file ~/.dotfiles/Brewfile` で旧来手順に戻れる。これが phased 戦略の安全弁。 + +## 10. テスト / 検証戦略 + +| レベル | 検証方法 | +|---|---| +| 構文 | `nix flake check` がエラーなしで通る | +| ビルド | `darwin-rebuild build --flake .#` が成功 | +| dry-run diff | `nvd diff /run/current-system result` で適用差分を表示 | +| 副作用 | switch 後に `defaults read com.apple.dock` 等で実値確認 | +| 別 PC シミュレーション | clean な VM or 別ユーザーで `darwin-rebuild switch` 初回実行 | +| 回帰 | `nix profile history` で前世代を残し、次回 switch 後の diff を PR コメントに貼る | + +ゴールデンテスト的に: 棚卸時点の `defaults read com.apple.dock` 出力を `docs/inventory/baseline-com.apple.dock.txt` で凍結し、A6 の switch 後に同コマンドの出力と diff を取る。差分が triage で「無視」したもの以外なら failed とする。 + +## 11. CLAUDE.md / 文書更新 + +- **「リポジトリ構造」セクション**: `nix/` を追加、Phase B 完了時に `Brewfile`, `setup/` 行を削除 +- **「Brewfile」セクション**: Phase A 期間中は「Phase B で削除予定」注記、Phase B 完了時に削除 +- **「シンボリックリンク管理」セクション**: home-manager が管理することを明記、`setup.zsh` の記述を削除 +- **新規セクション「Nix 環境」**: flake 構造、`darwin-rebuild` 運用、棚卸 → triage → 翻訳ワークフローの説明 + +## 12. スコープ外 / 将来作業 + +- VM や別 PC でのフルセットアップシミュレーション(Phase A の最終ステップで部分的に検証するが、追加 PC 展開は別 issue) +- `nix/` を repo root に昇格(Phase B 完了後の別検討) +- nix flakes の自動更新 CI(dependabot 的な) +- 各種 GUI アプリの App settings(`~/Library/Application Support//*`)の Nix 化(多くは TCC/sandbox 制約で困難) +- iCloud / Spotlight / Time Machine の宣言化(system.defaults サポート範囲外) + +## 13. 既知のリスクと前提 + +- nix-darwin の `homebrew` モジュールは **brew CLI 自体は管理しない**。brew インストール手順は別途必要 +- macOS のバージョンアップで `defaults` の key が変わる可能性。アップグレード後に `nix flake check` が通っても挙動が変わるリスクあり +- `rtk` のリポジトリは公開リポジトリかつ `flake = false` で source のみ取り込む前提。private repo の場合は flake input の認証設定が必要 +- App Store アプリのライセンス・サインイン状態は宣言化不可。初回手動ログインが必要 +- TCC(プライバシー許可)/ Full Disk Access はユーザー操作必須で nix-darwin の管理外 diff --git a/nix/.gitignore b/nix/.gitignore new file mode 100644 index 0000000..750baeb --- /dev/null +++ b/nix/.gitignore @@ -0,0 +1,2 @@ +result +result-* diff --git a/nix/README.md b/nix/README.md new file mode 100644 index 0000000..d1e67ea --- /dev/null +++ b/nix/README.md @@ -0,0 +1,149 @@ +# nix/ + +`darwin-rebuild` 適用後の事故対応・運用ポリシー集。クイックスタートはルート [`README.md`](../README.md) を参照。 + +関連ドキュメント: + +- spec: `docs/superpowers/specs/2026-05-02-nix-migration-design.md` +- plan: `docs/superpowers/plans/2026-05-02-nix-migration.md` + +## ロールバック + +直前世代に戻す: + +```sh +sudo darwin-rebuild switch --rollback +``` + +世代一覧の確認と特定世代への切替: + +```sh +darwin-rebuild --list-generations +sudo darwin-rebuild switch -G +``` + +home-manager 個別のロールバック: + +```sh +home-manager generations +home-manager switch --switch-generation +``` + +## flake.lock の更新運用 + +- `nix flake update` で全 input を最新に更新できる +- 特定 input だけ更新する場合: `nix flake lock --update-input nixpkgs` +- 更新後は必ず `darwin-rebuild build --flake .#default --impure` で検証してからコミット +- `flake.lock` は必ずコミットする(再現性確保のため) +- 更新頻度の方針: **必要時のみ**(依存ライブラリの脆弱性 / nixpkgs に必要なパッケージが入ったタイミング等) + +## アプリ・パッケージの追加 + +`brew install` を直接打つことは事実上禁止 (`homebrew.onActivation.cleanup = "zap"` により次回 `darwin-rebuild switch` で削除される)。**宣言してから入れる** 順序を強制する設計。 + +### 種別ごとの配置先 + +| 種別 | 配置先 | 例 | +|---|---|---| +| CLI (nixpkgs 収録あり) | `nix/modules/home/packages.nix` の `home.packages` | `ripgrep`, `fzf`, `jq` | +| 言語ランタイム | `nix/modules/home/languages.nix` | `nodejs_22`, `python3` | +| CLI (nixpkgs 未収録 / 最新版が必要) | `nix/modules/darwin/homebrew.nix` の `brews` (例外扱い) | `mas` | +| GUI アプリ (.app) | `nix/modules/darwin/homebrew.nix` の `casks` | `visual-studio-code`, `slack` | +| Mac App Store アプリ | `nix/modules/darwin/homebrew.nix` の `masApps` | `{ "Xcode" = 497799835; }` | +| 独自ビルド (nixpkgs 外のソース) | `nix/modules/overlays/` に overlay 定義 + `home.packages` から参照 | `rtk` | + +### 追加 → 適用の流れ + +```sh +# 1. 該当の .nix に 1 行追加 (例: packages.nix の home.packages に pkgs.ripgrep) +# 2. ビルド確認 (副作用なし) +darwin-rebuild build --flake ~/.dotfiles/nix#default --impure +# 3. 適用 (sudo の env_reset で USER=root になるのを USER=$USER で回避) +sudo USER=$USER darwin-rebuild switch --flake ~/.dotfiles/nix#default --impure +``` + +削除も同じ流れ (`.nix` から行を消して switch すると `zap` で消える)。 + +### 「お試し」のための逃げ道 + +`brew install` 即試用の代替手段: + +| やりたいこと | コマンド | +|---|---| +| nixpkgs にある CLI を一時的に試す | `nix shell nixpkgs#ripgrep`(その shell セッション限定 / `exit` で消える) | +| nixpkgs 最新で試す | `nix run nixpkgs/master#foo` | +| 1 回だけ実行 | `nix run nixpkgs#foo -- --args` | +| nixpkgs に無い GUI を試す | 現実的には手動 `brew install` → 気に入ったら `casks` に追加 → switch / 気に入らなければ `brew uninstall` | + +`nix shell` / `nix run` は永続インストールしないので、`zap` の影響を受けない。お試しは基本これに倒すこと。 + +## トラブルシューティング + +### Full Disk Access (FDA) 未付与で `install-nix.zsh` が停止する + +スクリプトが以下のエラーで停止した場合: + +``` +Full Disk Access is NOT granted to the current terminal. +``` + +1. **System Settings → Privacy & Security → Full Disk Access** を開く +2. 自分が使うターミナルアプリ(Terminal.app, iTerm2, etc.)を追加して有効化する +3. ターミナルを**完全に終了**して起動し直す(プロセス再起動で TCC が反映される) +4. `zsh ~/.dotfiles/nix/scripts/install-nix.zsh` を再実行 + +macOS 15 では FDA なしでは root でも `/etc` への書き込みが TCC で拒否されるため、`sudo` をつけても回避できない。Claude Code 経由 (osascript with administrator privileges) でも同様に回避不可。 + +### Determinate Nix と nix-darwin が競合する + +`darwin-rebuild switch` 実行時に以下のエラーで失敗した場合: + +``` +error: Determinate detected, aborting activation +Determinate uses its own daemon to manage the Nix installation that +conflicts with nix-darwin's native Nix management. +``` + +`nix/darwin.nix` で `nix.enable = false;` が宣言されているか確認する。S14 (KISSA-46) で対処済みの本番ブロッカー。詳細は `nix/darwin.nix` のコメントを参照。 + +### `USER env var is empty` で `darwin-rebuild` が落ちる + +`--impure` フラグなしで実行している、または `sudo` 経由で `USER` が `root` に置き換わっている。`nix/flake.nix` は `builtins.getEnv "USER"` で実行ユーザー名を動的解決するため、`--impure` と `USER=$USER` の両方が必須: + +```sh +sudo USER=$USER darwin-rebuild switch --flake .#default --impure +``` + +### flake.lock が壊れた / hash 不整合 + +```sh +git restore nix/flake.lock +nix flake update +``` + +### `darwin-rebuild` がビルドエラーで失敗する + +ビルドエラーのログを確認: + +```sh +darwin-rebuild build --flake .#default --impure 2>&1 | less +``` + +nix-darwin のロールバック: + +```sh +sudo darwin-rebuild switch --rollback +``` + +### nix-darwin の初回ブートストラップ + +nix-darwin が未インストールの状態で初めて適用する場合: + +```sh +cd ~/.dotfiles/nix +nix run nix-darwin -- switch --flake .#default --impure +``` + +### Homebrew パッケージが消えた + +`homebrew.onActivation.cleanup = "zap"` 設定により、`nix/modules/darwin/homebrew.nix` に宣言されていない Homebrew パッケージは初回 switch で削除される。残したいパッケージは `nix/modules/darwin/homebrew.nix` に追加してから switch すること。 diff --git a/nix/darwin.nix b/nix/darwin.nix new file mode 100644 index 0000000..3c0df1c --- /dev/null +++ b/nix/darwin.nix @@ -0,0 +1,53 @@ +# nix-darwin モジュール集約点。 +# flake.nix から直接 import される。 +# specialArgs 由来: inputs / username (flake.nix から注入) +# 自動注入: pkgs / lib / config (... で受け取る) +{ inputs, username, ... }: + +{ + imports = [ + # cask + mas + 例外 brew (S9 / KISSA-29) + ./modules/darwin/homebrew.nix + # pmset NOPASSWD (S11 / KISSA-31) + ./modules/darwin/sudoers.nix + # SF Mono 等 (S11)。空リストで雛形のみ + ./modules/darwin/fonts.nix + # Touch ID for sudo (S11) + ./modules/darwin/pam.nix + # S10 (defaults.nix) は棚卸 triage 完了後に追加 + # ./modules/darwin/defaults.nix + ]; + + # nix-darwin が要求する最低限の宣言: + # stateVersion: 1〜maxStateVersion(6) の整数を指定する (2026-05 時点) + # 初回インストール時のバージョンを設定し、以後変更しないこと + system.stateVersion = 6; + + # nix-darwin の multi-user 移行に伴い、homebrew.enable 等の + # ユーザースコープオプションは system.primaryUser で対象を明示する必要がある。 + system.primaryUser = username; + + # Determinate Nix (公式インストーラの最新版) と nix-darwin の native Nix 管理は + # 同時稼働できないため、nix-darwin 側の管理を無効化する。 + # Determinate Nix は nix-command / flakes をデフォルトで有効化済みなので、 + # 旧来の `nix.settings.experimental-features` 宣言は不要。 + # + # 本修正は CI 検証 (S14) で `darwin-rebuild switch` が以下のエラーで失敗した + # ことから発見した本番ブロッカー: + # error: Determinate detected, aborting activation + # Determinate uses its own daemon to manage the Nix installation that + # conflicts with nix-darwin's native Nix management. + nix.enable = false; + + # ユーザー宣言(home-manager から参照される) + users.users.${username} = { + name = username; + home = "/Users/${username}"; + }; + + # rtk overlay 適用 (S8 / KISSA-28) + # inputs.rtk-src を取得して pkgs.rtk として供給。home/packages.nix から参照可。 + nixpkgs.overlays = [ + (import ./modules/overlays/rtk.nix { inherit inputs; }) + ]; +} diff --git a/nix/flake.lock b/nix/flake.lock new file mode 100644 index 0000000..9ba33f8 --- /dev/null +++ b/nix/flake.lock @@ -0,0 +1,86 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777679572, + "narHash": "sha256-egYNbRrkn+6SwTHinhdb6WUfzzdC3nXfCRqS321VylY=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "9cb587ade2aa1b4a7257f0238d41072690b0ca4f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nix-darwin": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775037210, + "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", + "owner": "LnL7", + "repo": "nix-darwin", + "rev": "06648f4902343228ce2de79f291dd5a58ee12146", + "type": "github" + }, + "original": { + "owner": "LnL7", + "repo": "nix-darwin", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777548390, + "narHash": "sha256-WacE23EbHTsBKvr8cu+1DFNbP6Rh1brHUH5SDUI0NQI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7aaa00e7cc9be6c316cb5f6617bd740dd435c59d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nix-darwin": "nix-darwin", + "nixpkgs": "nixpkgs", + "rtk-src": "rtk-src" + } + }, + "rtk-src": { + "flake": false, + "locked": { + "lastModified": 1777488499, + "narHash": "sha256-eINYlatbjpsqe46LNZIXvIrZEBf+QC3+2EjY7Ei7VZI=", + "owner": "rtk-ai", + "repo": "rtk", + "rev": "4338f029ec43b69eb959748ec02cd7885200c264", + "type": "github" + }, + "original": { + "owner": "rtk-ai", + "repo": "rtk", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/flake.nix b/nix/flake.nix new file mode 100644 index 0000000..7bdd2fa --- /dev/null +++ b/nix/flake.nix @@ -0,0 +1,68 @@ +{ + description = "gotomts macOS dotfiles via nix-darwin + home-manager"; + + inputs = { + # Phase A は unstable を使用 (home-manager との整合性優先)。 + # stable に切り替える場合は nixpkgs-YY.MM 形式に変更し、 + # home.stateVersion も対応バージョンに更新すること。 + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + nix-darwin.url = "github:LnL7/nix-darwin"; + nix-darwin.inputs.nixpkgs.follows = "nixpkgs"; + + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + + # rtk-src: rtk (Rust Token Killer) のソース。 + # nix/modules/overlays/rtk.nix が rustPlatform.buildRustPackage でビルドする。 + # flake = false で nix flake input としてはソースのみ取得(rtk 自身は flake ではない)。 + rtk-src = { + url = "github:rtk-ai/rtk"; + flake = false; + }; + }; + + outputs = + { + self, + nixpkgs, + nix-darwin, + home-manager, + ... + }@inputs: + let + # username は実行環境の $USER から動的に解決する。 + # darwin-rebuild は --impure を必要とする (alias で吸収しないので明示的に付ける)。 + # 未設定時は throw で明示エラーにして、silent な誤動作を防ぐ。 + username = + let + u = builtins.getEnv "USER"; + in + if u != "" then + u + else + throw "USER env var is empty. Run darwin-rebuild with --impure, or set USER explicitly."; + in + { + # output 名は固定値 default。PC の hostname には影響しない (flake 内部のアドレス名)。 + # darwin-rebuild 自動選択 ($HOSTNAME ベース) は活かさず、--flake .#default --impure を明示する運用。 + darwinConfigurations.default = nix-darwin.lib.darwinSystem { + system = "aarch64-darwin"; + specialArgs = { inherit inputs username; }; + modules = [ + ./darwin.nix + home-manager.darwinModules.home-manager + { + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + # setup.zsh が以前作った既存 symlink (~/.zshrc, ~/.claude/agents 等) を + # home-manager が clobber エラーで弾かないよう、退避拡張子を指定する。 + # 初回 activation で既存ファイルは .before-nix にリネームされる。 + home-manager.backupFileExtension = "before-nix"; + home-manager.users.${username} = import ./home.nix; + home-manager.extraSpecialArgs = { inherit inputs username; }; + } + ]; + }; + }; +} diff --git a/nix/home.nix b/nix/home.nix new file mode 100644 index 0000000..b3e2799 --- /dev/null +++ b/nix/home.nix @@ -0,0 +1,28 @@ +# home-manager モジュール集約点。 +# flake.nix から直接 import される。 +# extraSpecialArgs 由来: inputs / username (flake.nix から注入) +# 自動注入: pkgs / lib / config (... で受け取る) +{ inputs, username, ... }: + +{ + imports = [ + # CLI ツール群 (S3 / KISSA-23) + ./modules/home/packages.nix + # zsh + oh-my-zsh + initExtra/envExtra (S4 / KISSA-24) + ./modules/home/zsh.nix + # 設定ファイル系 (S5 / KISSA-25) + ./modules/home/git.nix + ./modules/home/starship.nix + ./modules/home/yazi.nix + ./modules/home/ssh.nix + # claude plugin sync activation (S6 / KISSA-26) + ./modules/home/claude.nix + # 言語ツールチェーン: mise 完全置換 (S7 / KISSA-27) + ./modules/home/languages.nix + ]; + + home.username = username; + home.homeDirectory = "/Users/${username}"; + # home-manager 25.11 (nixpkgs-unstable との組み合わせ) の stateVersion + home.stateVersion = "25.11"; +} diff --git a/nix/modules/darwin/fonts.nix b/nix/modules/darwin/fonts.nix new file mode 100644 index 0000000..f8d8389 --- /dev/null +++ b/nix/modules/darwin/fonts.nix @@ -0,0 +1,22 @@ +# nix-darwin フォント設定モジュール +# specialArgs 由来: inputs / username (flake.nix から注入) +# 自動注入: pkgs / lib / config (... で受け取る) +# +# fonts.packages は nixpkgs のフォントパッケージを /Library/Fonts/Nix Fonts/ に配置する。 +# 型: list of absolute path (pkgs. の評価結果) +# 現状は SF Mono のみ管理しているが nixpkgs 未収録のため空リスト。 +# pkgs を追加する場合は引数を { pkgs, ... }: に変更すること。 +{ ... }: + +{ + # nixpkgs に収録されているオープンソースフォントをここで管理する。 + # + # font-sf-mono (Apple 独占フォント) は nixpkgs に未収録のため、 + # Brewfile の `cask 'font-sf-mono'` で引き続き管理する。 + # 親の integration commit で Brewfile (homebrew.nix S9) との整合を確認すること。 + fonts.packages = [ + # SF Mono: nixpkgs 未収録(Apple プロプライエタリライセンス)→ Brewfile 残置 + # 他のフォントが必要になった場合はここに追加する。例: + # pkgs.nerd-fonts.jetbrains-mono + ]; +} diff --git a/nix/modules/darwin/homebrew.nix b/nix/modules/darwin/homebrew.nix new file mode 100644 index 0000000..18c5afb --- /dev/null +++ b/nix/modules/darwin/homebrew.nix @@ -0,0 +1,101 @@ +# nix-darwin homebrew モジュール +# Brewfile の内容を nix-darwin の homebrew オプションに移植する。 +# +# specialArgs 由来: inputs / username (flake.nix から注入) +# 自動注入: pkgs / lib / config (... で受け取る) +# 本モジュールでは上記引数を使用しないため { ... } で受け取る +# +# darwin.nix への配線: +# imports = [ ./modules/darwin/homebrew.nix ]; +{ ... }: + +{ + homebrew = { + enable = true; + + onActivation = { + # 手動制御。CI 化する場合は true 検討 + autoUpdate = false; + # 既存パッケージは upgrade する + upgrade = true; + # "zap": 宣言外パッケージを Cellar ごと削除する ("uninstall" より破壊的)。 + # Phase A 移行期は darwin-rebuild switch のたびに実行されるため、 + # homebrew.nix に載っていない手動インストール済みパッケージは即削除される。 + # 本ファイルが Brewfile の全量を網羅している前提なので実害は最小だが、 + # Phase A → Phase B (Brewfile 廃止) まで "uninstall" に下げる選択肢も検討する。 + cleanup = "zap"; + }; + + taps = [ + "leoafarias/fvm" + "manaflow-ai/cmux" + "oven-sh/bun" + "schpet/tap" + ]; + + brews = [ + # ============================================================ + # nixpkgs 未収録または macOS 特殊事情で Homebrew 経由が継続必要なもの + # + # 除外済 (親 integration commit で削除): + # - jq / bats-core / pwgen / qpdf / fzf / gh / ghq / lazygit / + # lazydocker / kubectl / kubectx / stern / sops / grpcurl + # → S3 (packages.nix) で nixpkgs から提供 + # - autoconf / automake / bison / freetype / gd / gettext / + # gmp / libyaml / openssl@3 / pkg-config / re2c / zlib + # → ビルド系。必要時は `nix shell nixpkgs#` で一時利用 (S3 ポリシー) + # - mise: S7 完全削除方針 + # - rtk: S8 flake input 化済み + # - mas (CLI): nixpkgs 収録済みだが現状 nix 側にも S3 不在。必要なら後付け + # ============================================================ + + # nixpkgs 未収録 (homebrew 専用 tap or macOS でのみ実用) + "worktrunk" + "oven-sh/bun/bun" # tap: oven-sh/bun。S3 で「S7 で確認」とした保守的残置 + "leoafarias/fvm/fvm" # tap: leoafarias/fvm + "pipx" # nixpkgs にもあるが、Python venv 周りの ergonomics で homebrew 版を選好 + "schpet/tap/linear" # tap: schpet/tap + + # macOS 特殊事情 (システム拡張・cask 連携) + "tailscale" # nixpkgs にもあるが macOS は cask + system extension が公式推奨 + ]; + + casks = [ + # Brewfile の cask 全 25 件から font-sf-mono を除いた 24 件 + # font-sf-mono は darwin/fonts.nix (S11) で管理するため除外 + "1password" + "amazon-photos" + "android-studio" + "claude-code" + "cmux" # tap: manaflow-ai/cmux + "contexts" + "cursor" + "docker-desktop" + "dropbox" + "figma" + "flutter" + "gcloud-cli" + "google-chrome" + "google-japanese-ime" + "linear-linear" + "medis" + "notion" + "orbstack" + "postman" + "raycast" + "slack" + "tableplus" + "visual-studio-code" + "zed" + "zoom" + ]; + + masApps = { + "LINE" = 539883307; + "Magnet" = 441258766; + "TestFlight" = 899247664; + "Apple Developer" = 640199958; + "Transporter" = 1450874784; + }; + }; +} diff --git a/nix/modules/darwin/pam.nix b/nix/modules/darwin/pam.nix new file mode 100644 index 0000000..168d07b --- /dev/null +++ b/nix/modules/darwin/pam.nix @@ -0,0 +1,24 @@ +# nix-darwin PAM 設定モジュール +# specialArgs 由来: inputs / username (flake.nix から注入) +# 自動注入: pkgs / lib / config (... で受け取る) +# +# security.pam.services.sudo_local.touchIdAuth は nix-darwin 固有のオプション。 +# macOS 14 (Sonoma) 以降は /etc/pam.d/sudo_local が OS アップデートで上書きされないため +# この設定が有効。旧来の security.pam.enableSudoTouchIdAuth から改名された。 +# 参照: https://github.com/nix-darwin/nix-darwin/blob/master/modules/security/pam.nix +{ ... }: + +{ + # Touch ID for sudo 有効化 (macOS 14+ の sudo_local PAM)。 + # 旧 enableSudoTouchIdAuth から改名されたオプション。 + security.pam.services.sudo_local.touchIdAuth = true; + + # reattach: tmux/cmux 経由で sudo を実行する場合、Touch ID プロンプトを TTY 経由で + # 受け取るために pam-reattach パッケージが必要になる。本リポジトリは tmux/cmux 運用を + # 含むが、Phase A では reattach = false (デフォルト) として、tmux 内 sudo は + # パスワードフォールバックを許容する判断。 + # + # tmux 内 Touch ID を求める場合は以下を有効化 (pam-reattach への依存追加): + # security.pam.services.sudo_local.reattach = true; + # security.pam.services.sudo_local.reattach = true; # 採用時のみコメント解除 +} diff --git a/nix/modules/darwin/sudoers.nix b/nix/modules/darwin/sudoers.nix new file mode 100644 index 0000000..9f11570 --- /dev/null +++ b/nix/modules/darwin/sudoers.nix @@ -0,0 +1,33 @@ +# nix-darwin sudo 設定モジュール +# specialArgs 由来: inputs / username (flake.nix から注入) +# 自動注入: pkgs / lib / config (... で受け取る) +# +# 注意: nix-darwin の security.sudo は extraConfig (raw sudoers テキスト) のみを持つ。 +# NixOS の security.sudo.extraRules (構造化 attr) は nix-darwin 非対応。 +# https://github.com/nix-darwin/nix-darwin/blob/master/modules/security/sudo.nix +{ username, ... }: + +{ + # 現 setup/install/10_claude.zsh が /etc/sudoers.d/pmset に追加していたエントリを宣言化: + # ALL=(ALL) NOPASSWD: /usr/bin/pmset + # sleep-guard スキル (claude/skills/sleep-guard) で pmset をパスワードなし実行するために必要。 + # nix-darwin はこの内容を /etc/sudoers.d/10-nix-darwin-extra-config として書き出す。 + # + # TODO(Phase A 完了時): setup/install/10_claude.zsh の sudoers ブロック (行 7-15) を + # 削除すること。並存期間中は二重エントリ (/etc/sudoers.d/pmset と + # /etc/sudoers.d/10-nix-darwin-extra-config の両方) になるが、sudo 動作上は無害。 + # 削除を忘れると Phase B 後も古いエントリが残り、sudoers 監査時の誤解を招く。 + # + # 注意: security.sudo.extraConfig は types.lines (複数モジュール連結)。 + # 他モジュールから extraConfig を追加する場合は事前に visudo -c で構文検証すること。 + # 適用前検証: echo "..." | visudo -c -f - + security.sudo.extraConfig = '' + ${username} ALL=(ALL) NOPASSWD: /usr/bin/pmset + ''; + + # 検証ポイント (実機 darwin-rebuild switch 後): + # - /etc/sudoers.d/10-nix-darwin-extra-config の所有者が root:wheel か + # - パーミッションが 0440 か + # - sudo がエラーなく動作するか (sudo: ... is owned by uid X エラーが出ないこと) + # 問題が出た場合は environment.etc."sudoers.d/...".{mode,user,group} を明示指定する。 +} diff --git a/nix/modules/home/claude.nix b/nix/modules/home/claude.nix new file mode 100644 index 0000000..35d861e --- /dev/null +++ b/nix/modules/home/claude.nix @@ -0,0 +1,57 @@ +{ pkgs, lib, ... }: + +# このモジュールは setup.zsh の claude/ symlink ループ + setup/install/10_claude.zsh の +# symlink セクション(ステップ 2, 3)を home-manager で置換する。 +# +# 注意: 既存 10_claude.zsh はスキルごとに個別 symlink を作るが、本モジュールは +# ~/.claude/skills ディレクトリ全体を Nix store 経由の単一 symlink にする。新スキル追加時は +# `home-manager switch` または `darwin-rebuild switch` で反映する必要がある。 +{ + # ~/.claude/{agents,skills,hooks,settings.json,CLAUDE.md,RTK.md} を + # dotfiles から symlink する。 + # 従来の setup/install/10_claude.zsh の symlink ループを home-manager で置換。 + home.file = { + ".claude/agents".source = ../../../claude/agents; + ".claude/skills".source = ../../../claude/skills; + ".claude/hooks".source = ../../../claude/hooks; + ".claude/settings.json".source = ../../../claude/settings.json; + ".claude/CLAUDE.md".source = ../../../claude/CLAUDE.md; + ".claude/RTK.md".source = ../../../claude/RTK.md; + }; + + # claude plugin の宣言的同期。 + # enabledPlugins キーを settings.json から読んで CLI で install/update を実行する。 + # writeBoundary 後に走らせることで symlink が確立された状態で実行される。 + # 関数化により set +e / return 0 を局所化し、後続 activation への漏出を防ぐ。 + home.activation.claudePlugins = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + _run_claude_plugin_sync() { + set +e # 個々のプラグイン失敗で関数内処理を止めない + + if ! command -v claude &>/dev/null; then + echo "[claude.nix] claude CLI 未インストール、plugin 同期をスキップ" + return 0 + fi + + local SETTINGS="''${HOME}/.claude/settings.json" + if [ ! -f "$SETTINGS" ]; then + echo "[claude.nix] settings.json 不在、plugin 同期をスキップ" + return 0 + fi + + $DRY_RUN_CMD claude plugin marketplace update 2>/dev/null || true + + ${pkgs.jq}/bin/jq -r '.enabledPlugins // {} | keys[]' "$SETTINGS" 2>/dev/null | while IFS= read -r plugin; do + if claude plugin list --json 2>/dev/null | ${pkgs.jq}/bin/jq -e --arg p "$plugin" '.[] | select(.id == $p)' &>/dev/null; then + $DRY_RUN_CMD claude plugin update "$plugin" 2>/dev/null || \ + echo "[claude.nix] plugin $plugin: update failed" + else + $DRY_RUN_CMD claude plugin install "$plugin" 2>/dev/null && \ + echo "[claude.nix] plugin $plugin: installed" || \ + echo "[claude.nix] plugin $plugin: install failed" + fi + done + } + + _run_claude_plugin_sync + ''; +} diff --git a/nix/modules/home/git.nix b/nix/modules/home/git.nix new file mode 100644 index 0000000..72d508d --- /dev/null +++ b/nix/modules/home/git.nix @@ -0,0 +1,63 @@ +{ inputs, pkgs, ... }: + +{ + # gitmessage を ~/.gitmessage として固定パスに配置する。 + # inputs.self を commit.template に直接使うと darwin-rebuild のたびに + # store hash が変わり git config の表示が毎回変化するため、 + # home.file 経由で固定パスに symlink してから参照する。 + home.file.".gitmessage".source = ../../../gitmessage; + + programs.git = { + enable = true; + + userName = "gotomts"; + userEmail = "mh.goto.web@gmail.com"; + + extraConfig = { + core = { + # excludesFile は programs.git.ignores で管理するため不要 + ignorecase = false; + }; + + ghq = { + # toGitINI がリスト値を同一キーの複数行として展開するため、 + # gitconfig の "[ghq] root = ~/.dotfiles / root = ~/ghq" が再現される。 + root = [ + "~/.dotfiles" + "~/ghq" + ]; + }; + + "filter \"lfs\"" = { + clean = "git-lfs clean -- %f"; + smudge = "git-lfs smudge -- %f"; + process = "git-lfs filter-process"; + required = true; + }; + + rerere = { + enabled = true; + }; + + pull = { + autostash = true; + }; + + rebase = { + autoStash = true; + }; + + commit = { + template = "~/.gitmessage"; + }; + + alias = { + graph = "log --graph --date-order -C -M --pretty=format:\"<%h> %ad [%an] %Cgreen%d%Creset %s\" --all --date=short"; + }; + }; + + ignores = [ + ".DS_Store" + ]; + }; +} diff --git a/nix/modules/home/languages.nix b/nix/modules/home/languages.nix new file mode 100644 index 0000000..f65d036 --- /dev/null +++ b/nix/modules/home/languages.nix @@ -0,0 +1,119 @@ +# home-manager モジュール: 言語ランタイム + 開発ツールチェーン +# +# 移行対象: setup/install/{04_node,05_go,06_ruby,07_rust,08_python,09_dart}.zsh で +# mise 経由でインストールしていたグローバルランタイムを nix に完全移管する。 +# +# mise は Brewfile から外す方針 (本 sub-issue で削除)。 +# プロジェクトローカルの mise 設定 (.mise.toml) は引き続き利用可能だが、 +# グローバルランタイムの管理はこのモジュールで完結させる。 +# +# pkgs は flake.nix で宣言済みの nixpkgs-unstable を参照 (mkHost.nix 経由で注入)。 +# fenix overlay (nightly/beta Rust toolchain) は flake input 追加が必要なため、 +# 本 sub-issue のスコープ外 (flake.nix 変更禁止制約)。 +# Phase B または別 sub-issue で fenix overlay 移行を検討すること。 +{ pkgs, ... }: + +{ + home.packages = with pkgs; [ + + # =========================================================================== + # Node.js + # =========================================================================== + # 現状: mise install node@16.14.2 / 18.12.1 / 24.2.0 / latest + # mise use --global node@latest + # npm i -g npm-fzf + # + # nix では同一 PATH に複数 Node.js バージョンを同居させることが困難。 + # Node.js: nixpkgs-unstable の `nodejs` デフォルトは 2025 年現在 24 LTS に更新済み。 + # 22 LTS は 2025-10-28 まで Active LTS のため意図的に 22 を固定。 + # プロジェクト互換性確認後に nodejs_24 への移行を検討すること。 + # 旧バージョンが必要なプロジェクトでは `nix shell nixpkgs#nodejs_18` 等で対応。 + nodejs_22 + + # npm-fzf: nixpkgs 未収録のため Brewfile (S9 homebrew.nix) 残置 or `npm i -g npm-fzf` で対応 + + # =========================================================================== + # Go + # =========================================================================== + # 現状: mise install go@1.18.1 / 1.19.4 / 1.19.13 / latest + # mise use --global go@latest + # + # nixpkgs-unstable の go は最新安定版を追従する。 + # 旧バージョンが必要な場合は go_1_21 / go_1_22 等のバージョン固定パッケージを参照。 + go + + # =========================================================================== + # Ruby + # =========================================================================== + # 現状: mise install ruby@3.2.2 / latest + # mise use --global ruby@latest + # gem install bundler cocoapods fastlane + # + # ruby_3_4 は nixpkgs-unstable に収録済み (2025 年 5 月現在の最新安定版)。 + # ruby_3_3 との選定理由: 3.4 は 2024 年 12 月リリースで安定し、 + # nixpkgs-unstable の default ruby も 3.3 → 3.4 に移行中。 + # + # NOTE: bundler / cocoapods / fastlane は gem install で管理していたが、 + # nix では ruby.gems.bundler 等を使うか、bundler を PATH に入れる方法がある。 + # cocoapods / fastlane は macOS 依存が強いため S9 homebrew.nix または + # gem install で引き続き管理することを推奨。 + ruby_3_4 + + # =========================================================================== + # Rust (nixpkgs の rustc + cargo) + # =========================================================================== + # 現状: mise install rust@stable + # rustup component add rust-analyzer rustfmt clippy + # cargo install --locked cargo-nextest cargo-watch + # + # fenix overlay を使うと nightly / beta / stable channel を柔軟に切り替えられるが、 + # flake.nix に input 追加が必要 (本 sub-issue スコープ外)。 + # nixpkgs の rustc + cargo は stable channel に相当し、日常開発では十分。 + # Phase B / 別 sub-issue で fenix overlay 移行を検討すること。 + rustc + cargo + rust-analyzer + rustfmt + clippy + + # cargo install 相当 (nixpkgs に収録されているためビルド不要) + cargo-nextest # 高速テストランナー + cargo-watch # ファイル変更時の自動再実行 + + # =========================================================================== + # Python + # =========================================================================== + # 現状: mise install python@3.12.1 / latest + # mise use --global python@latest + # pipx install poetry==1.2.0 + # + # python313 は nixpkgs-unstable に収録済み (2025 年 5 月現在の最新安定版)。 + # python312 との選定理由: 3.13 は 2024 年 10 月リリースで本番採用が進んでいる。 + # 既存スクリプトが python@3.12.1 を入れており、3.13 との互換性リスクがある場合は + # python312 に戻すこと。 + # + # NOTE: pipx は nixpkgs に収録されているが、poetry が nixpkgs に直接入るため不要。 + python313 + + # pipx install poetry==1.2.0 の代替 (nixpkgs の poetry は最新安定版) + poetry + + # pipx install grip (GitHub Readme Instant Preview) の代替。 + # nixpkgs では python3Packages.grip として収録 (joeyespo/grip v4.6.1)。 + # pkgs.grip トップレベルは GTK CD プレイヤーで別物のため注意。 + python3Packages.grip + + # =========================================================================== + # Dart + # =========================================================================== + # 現状: 09_dart.zsh: dart pub global activate flutterfire_cli + # Brewfile: leoafarias/fvm/fvm (tap 経由), cask 'flutter' + # + # nixpkgs-unstable v3.11.4 時点で aarch64-darwin 対応済み (meta.platforms に明記)。 + # Flutter SDK は引き続き Brewfile (cask 'flutter') / S9 homebrew.nix で管理。 + # fvm (Flutter Version Manager) は nixpkgs 未収録のため Brewfile (S9 homebrew.nix) 残置。 + dart + # fvm: nixpkgs 未収録のため Brewfile (S9 homebrew.nix) 残置 + + ]; +} diff --git a/nix/modules/home/packages.nix b/nix/modules/home/packages.nix new file mode 100644 index 0000000..223e5d0 --- /dev/null +++ b/nix/modules/home/packages.nix @@ -0,0 +1,72 @@ +# nix/modules/home/packages.nix +# +# CLI ツール群を home.packages として宣言する home-manager モジュール。 +# home.nix の imports に追加することで有効化される(S3 integration commit で配線)。 +# +# 自動注入: pkgs / lib / config (... で受け取る) +{ pkgs, ... }: + +{ + home.packages = with pkgs; [ + # ------------------------------------------------------------------------- + # Utilities + # ------------------------------------------------------------------------- + # autoconf / automake / bison / freetype / gd / gettext / gmp / libyaml / + # openssl@3 / pkg-config / re2c / zlib はビルド系ツールのため home.packages + # には含めない。必要な場合は `nix shell nixpkgs#` で一時利用する。 + jq + bats # Brewfile: bats-core(nixpkgs では bats) + pwgen + qpdf + + # ------------------------------------------------------------------------- + # Shell & Terminal + # ------------------------------------------------------------------------- + # mise は S7 で削除予定のため S3 では含めない(Brewfile 残置) + fzf + + # ------------------------------------------------------------------------- + # Git & Version Control + # ------------------------------------------------------------------------- + gh + ghq + lazygit + lazydocker + # worktrunk: nixpkgs 未収録のため darwin/homebrew.nix (S9) 残置 + + # ------------------------------------------------------------------------- + # Cloud & DevOps + # ------------------------------------------------------------------------- + kubectl + kubectx + stern + sops + + # ------------------------------------------------------------------------- + # Languages & Runtimes + # ------------------------------------------------------------------------- + # bun: nixpkgs にも bun は存在する。oven-sh/bun tap 版とのバージョン追跡方針差を + # S7 で確認するまで Brewfile 残置(保守的判断)。 + # fvm (leoafarias/fvm): nixpkgs 未収録のため darwin/homebrew.nix (S9) 残置 + # pipx: S7 / homebrew 残置検討のため S3 では含めない(Brewfile 残置) + + # ------------------------------------------------------------------------- + # Network & API + # ------------------------------------------------------------------------- + grpcurl + # tailscale: macOS ではシステム拡張 + Cask 経由が推奨のため + # darwin/homebrew.nix (S9) 残置。nixpkgs に存在はするが + # tailscaled デーモンの管理方式が homebrew cask と異なるため除外 + + # ------------------------------------------------------------------------- + # Task Management + # ------------------------------------------------------------------------- + # linear (schpet/tap): nixpkgs 未収録のため darwin/homebrew.nix (S9) 残置 + + # ------------------------------------------------------------------------- + # AI Tooling + # ------------------------------------------------------------------------- + # rtk: S8 で nix/modules/overlays/rtk.nix から pkgs.rtk として供給される + rtk + ]; +} diff --git a/nix/modules/home/ssh.nix b/nix/modules/home/ssh.nix new file mode 100644 index 0000000..1c42bd2 --- /dev/null +++ b/nix/modules/home/ssh.nix @@ -0,0 +1,7 @@ +{ inputs, pkgs, ... }: + +{ + # SSH 鍵管理はスコープ外(Phase A 終了後に programs.ssh への移行を別検討)。 + # config ファイルのみを symlink で配置する。 + home.file.".ssh/config".source = ../../../ssh/config; +} diff --git a/nix/modules/home/starship.nix b/nix/modules/home/starship.nix new file mode 100644 index 0000000..a960389 --- /dev/null +++ b/nix/modules/home/starship.nix @@ -0,0 +1,10 @@ +{ inputs, pkgs, ... }: + +{ + programs.starship = { + enable = true; + # 既存の starship.toml を builtins.fromTOML で直接読み込む。 + # TOML の構造をそのまま Nix 属性に変換するため手動変換は不要。 + settings = builtins.fromTOML (builtins.readFile ../../../config/starship/starship.toml); + }; +} diff --git a/nix/modules/home/yazi.nix b/nix/modules/home/yazi.nix new file mode 100644 index 0000000..bb9a04d --- /dev/null +++ b/nix/modules/home/yazi.nix @@ -0,0 +1,14 @@ +{ inputs, pkgs, ... }: + +{ + programs.yazi = { + enable = true; + # 既存の yazi.toml / keymap.toml を builtins.fromTOML で読み込む。 + # keymap の prepend_keymap エントリは TOML 配列としてそのまま注入される。 + # TODO(Phase B): config/yazi/keymap.toml の grip-preview 呼び出しに含まれる + # `$HOME/.dotfiles/config/yazi/grip-preview.sh` を pkgs パッケージ化または + # Nix store path 参照に置換する。Phase A では Brewfile + 既存 dotfiles パスのまま運用。 + settings = builtins.fromTOML (builtins.readFile ../../../config/yazi/yazi.toml); + keymap = builtins.fromTOML (builtins.readFile ../../../config/yazi/keymap.toml); + }; +} diff --git a/nix/modules/home/zsh.nix b/nix/modules/home/zsh.nix new file mode 100644 index 0000000..98e3155 --- /dev/null +++ b/nix/modules/home/zsh.nix @@ -0,0 +1,185 @@ +# nix/modules/home/zsh.nix +# +# home-manager programs.zsh モジュール。 +# 現環境 (zshrc / zshenv / aliases) を宣言的に再現する。 +# +# 自動注入: pkgs / lib / config 等は ... で受け取る(本モジュールでは現在未使用) +{ ... }: + +{ + programs.zsh = { + enable = true; + + # ----------------------------------------------------------------------- + # oh-my-zsh + # ----------------------------------------------------------------------- + oh-my-zsh = { + enable = true; + # 現 zshrc plugins から移植。 + # zsh-autosuggestions は programs.zsh.autosuggestion.enable で管理するため除外。 + plugins = [ + "git" + "kubectl" + "terraform" + "gcloud" + ]; + # 現 zshrc では ZSH_THEME="" (starship が代替)。 + # oh-my-zsh.theme を空にすると "robbyrussell" にフォールバックするため、 + # theme 設定は行わず starship は initExtra で有効化する。 + # (starship モジュールは別 sub-issue で追加予定) + }; + + # zsh-autosuggestions を home-manager 組み込み機能で有効化 + autosuggestion.enable = true; + + # ----------------------------------------------------------------------- + # shellAliases — aliases ファイルから移植 + # ----------------------------------------------------------------------- + shellAliases = { + # general + history = "history 1"; + reload = "exec $SHELL -l"; + datetime = "date '+%Y%m%d%T' | tr -d ':'"; + + # git + gp = "git push origin HEAD"; + gch = "git branch --all | tr -d '* ' | grep -v -e '->' | fzf | sed -e 's+remotes/[^/]*/++g' | xargs git checkout"; + gchb = "git checkout -b $1"; + grsh = "git reset --soft HEAD^"; + gbclear = "git branch --merged|egrep -v '\\*|develop|main|master'|xargs git branch -d; git fetch -p"; + + # fzf + repo = "ghq list -p | fzf"; + repoc = "cd \"$(repo)\""; + + # gcloud + gcal = "gcloud auth login"; + gcadl = "gcloud auth application-default login"; + gcpa = "gcloud config configurations activate $(gcloud config configurations list | fzf | awk \"{print \\$1}\")"; + gcps = "gcloud config set project $(gcloud projects list | fzf | awk \"{print \\$1}\")"; + gcgc = "bash $HOME/.aliase/get-gke-credentials.sh"; + + # vscode + codeo = "code $(repo)"; + + # docker + dcu = "docker compose up -d $@"; + dcn = "docker compose down $@"; + }; + + # ----------------------------------------------------------------------- + # initExtra — zshrc の独自設定 + # ----------------------------------------------------------------------- + # oh-my-zsh の source・shellAliases・envExtra は home-manager が自動挿入するため除外。 + # programs.zsh に対応属性がないものをここに移植。 + initExtra = '' + # Homebrew (Apple Silicon) を PATH と env に注入 + # nix-darwin の /etc/zprofile は path_helper を呼ばないため、 + # /etc/paths.d/homebrew が読み込まれず /opt/homebrew/bin が PATH に入らない。 + # brew shellenv で明示的に注入する (副作用なし、brew 未インストール環境では skip)。 + if [ -x /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + fi + + # mise (interactive hook) — shims は envExtra で有効化済み + if type mise &>/dev/null; then + eval "$(mise activate zsh)" + fi + + # gcloud path — gcloud-cli (新名) と google-cloud-sdk (旧名) 両対応 + for _gcloud_inc in \ + '/opt/homebrew/Caskroom/gcloud-cli/latest/google-cloud-sdk/path.zsh.inc' \ + '/opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.zsh.inc'; do + if [[ -f "''${_gcloud_inc}" ]]; then source "''${_gcloud_inc}"; break; fi + done + unset _gcloud_inc + + # worktrunk shell integration + if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi + + # fzf — カスタム履歴ウィジェット (functions/fzf-history を使用) + autoload fzf-history + zle -N fzf-history + bindkey '^r' fzf-history + + # stern completion + if [ ''${commands[stern]} ]; then + source <(stern --completion=zsh) + fi + + # bison + export PATH="/opt/homebrew/opt/bison/bin:$PATH" + + # pipx local bin + export PATH="$PATH:$HOME/.local/bin" + + # dart-cli completion + [[ -f "$HOME/.dart-cli-completion/zsh-config.zsh" ]] && . "$HOME/.dart-cli-completion/zsh-config.zsh" || true + + # TODO(S5): programs.starship モジュール追加後にこの行を削除すること。 + # starship を有効化したまま残すと二重初期化が発生する。 + eval "$(starship init zsh)" + + # firebase / pub-cache + export PATH="$PATH:$HOME/.pub-cache/bin" + + # kubectl グローバルエイリアス (shellAliases は -g 非対応のため initExtra に配置) + alias -g KP='$(kubectl get pods | fzf | awk "{print \$1}")' + alias -g KD='$(kubectl get deploy | fzf | awk "{print \$1}")' + alias -g KS='$(kubectl get svc | fzf | awk "{print \$1}")' + alias -g KI='$(kubectl get ing | fzf | awk "{print \$1}")' + alias -g KJ='$(kubectl get job | fzf | awk "{print \$1}")' + alias -g KA='$(kubectl get all | awk "! /NAME/" | fzf | awk "{print \$1}")' + # kubectle 系は KP/KA グローバルエイリアスを参照するため、展開順を保証するため直後に定義 + alias kubectle='kubectl exec -it KP $@' + alias kubectll='kubectl stern $(kubectl get deploy | fzf | awk "{print \$1}")' + alias kubectlo='kubectl get KA -o yaml' + ''; + + # ----------------------------------------------------------------------- + # envExtra — zshenv の内容を移植 + # ----------------------------------------------------------------------- + # FPATH への .functions 追加は home-manager が管理する ~/.zshenv に挿入される。 + # ただし home-manager 自身が FPATH を管理するケースと競合しないよう、 + # .functions を fpath に加える記述をここに置く。 + envExtra = '' + # general settings — .functions を fpath に追加 + export FPATH=''${HOME}/.functions:''${FPATH} + + # pipx + export PIPX_HOME="''${HOME}/.local/pipx" + export PIPX_BIN_DIR="''${HOME}/.local/bin" + + # fzf + export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!.git"' + export FZF_DEFAULT_OPTS='--height 40% --reverse --border' + + # gcloud + export USE_GKE_GCLOUD_AUTH_PLUGIN=True + + # golang + export GOPATH=''${HOME}/go + export PATH=''${GOPATH}/bin:''${PATH} + + # mise (shims — non-interactive シェル向け) + if type mise &>/dev/null; then + eval "$(mise activate --shims)" + fi + + # custom local override + if [[ -f ''${HOME}/.zshenv.local ]]; then + source ''${HOME}/.zshenv.local + fi + ''; + }; + + # ----------------------------------------------------------------------- + # home.file — 関数・スクリプトの symlink 配置 + # ----------------------------------------------------------------------- + home.file = { + # fzf カスタム履歴ウィジェット (initExtra の `autoload fzf-history` が参照) + ".functions/fzf-history".source = ../../../functions/fzf-history; + # GKE 認証情報取得スクリプト (shellAliases.gcgc が参照) + ".aliase/get-gke-credentials.sh".source = ../../../aliase/get-gke-credentials.sh; + }; +} diff --git a/nix/modules/overlays/rtk.nix b/nix/modules/overlays/rtk.nix new file mode 100644 index 0000000..ba4d8d9 --- /dev/null +++ b/nix/modules/overlays/rtk.nix @@ -0,0 +1,64 @@ +# rtk overlay +# Phase A の S8 として、flake input から取得した rtk のソースを +# rustPlatform.buildRustPackage でビルドして pkgs.rtk として供給する。 +# +# Source repository: https://github.com/rtk-ai/rtk (確認方法: brew info rtk) +# License: Apache-2.0 (homebrew formula および upstream リポジトリ LICENSE ファイルで確認) +# Current stable version: 0.38.0 +# +# このファイル単独では機能しない。flake.nix の outputs で: +# +# 1. inputs に追加: +# rtk-src = { +# url = "github:rtk-ai/rtk"; +# flake = false; +# }; +# +# 2. mkHost.nix またはホスト設定で nixpkgs に overlay として適用: +# pkgs = import nixpkgs { +# inherit system; +# overlays = [ (import ./modules/overlays/rtk.nix { inherit inputs; }) ]; +# }; +# または nixpkgs.overlays を使う場合: +# nixpkgs.overlays = [ (import ./modules/overlays/rtk.nix { inherit inputs; }) ]; +# +# 上記 flake.nix 編集は親の integration commit で実施。 +# +# Native deps: libc (Unix のみ、nixpkgs が自動提供)、 +# rusqlite は "bundled" feature で SQLite を内包するため追加 buildInputs 不要。 +{ inputs }: + +final: prev: { + rtk = prev.rustPlatform.buildRustPackage { + pname = "rtk"; + # version: 通常は flake.lock で固定された commit の shortRev (8 桁) が入る。 + # "unknown" フォールバックは flake.lock 未生成または評価失敗時のみ使用される。 + # rtk のメジャーバージョンを示したい場合は inputs.rtk-src.rev からの動的取得を検討。 + version = inputs.rtk-src.shortRev or "unknown"; + src = inputs.rtk-src; + + cargoLock = { + lockFile = inputs.rtk-src + /Cargo.lock; + }; + + # libc は nixpkgs が stdenv 経由で自動提供されるため明示不要。 + # rusqlite は "bundled" feature で SQLite をソースからコンパイルするため + # システムの libsqlite3 に依存しない。追加の buildInputs は不要。 + # nativeBuildInputs = with prev; [ pkg-config ]; + # buildInputs = with prev; [ openssl ]; + + # rtk の cargo test は HOME 書き込みや外部 binary (git 等) に依存するテストを + # 含む。nix sandbox は HOME が read-only (/homeless-shelter) で外部 binary も + # 制限されるため、これらのテストが失敗する。CI バイナリ品質は上流 rtk + # リポジトリの CI に委ね、本ビルドではテストフェーズを skip する。 + doCheck = false; + + meta = with prev.lib; { + description = "CLI proxy to minimize LLM token consumption"; + homepage = "https://github.com/rtk-ai/rtk"; + license = licenses.asl20; # Apache-2.0 + mainProgram = "rtk"; + platforms = platforms.unix; + }; + }; +} diff --git a/nix/scripts/install-nix.zsh b/nix/scripts/install-nix.zsh new file mode 100755 index 0000000..ae65a5d --- /dev/null +++ b/nix/scripts/install-nix.zsh @@ -0,0 +1,89 @@ +#!/bin/zsh +# Determinate Nix を macOS にインストールするための薄いラッパー。 +# +# 役割: +# 1. 既に Nix がインストール済みなら skip して exit 0 (idempotent) +# 2. Full Disk Access (FDA) を事前検証し、未付与なら明確なエラーで停止 +# 3. Determinate Nix の公式インストーラを非対話モードで起動 +# +# 前提: +# - macOS (Apple Silicon / Intel) +# - Xcode Command Line Tools がインストール済み +# - 実行元ターミナル (Terminal.app / iTerm2 等) に Full Disk Access が付与されている +# +# 使い方: +# zsh ${HOME}/.dotfiles/nix/scripts/install-nix.zsh +# +# 終了コード: +# 0 成功 or 既存 Nix を検出して skip +# 1 Full Disk Access 未付与 +# 2 Determinate Nix インストーラ失敗 +# +# 関連: +# - Linear: KISSA-32 (S12) +# - 公式: https://github.com/DeterminateSystems/nix-installer +# - nix-darwin との競合解消: nix/darwin.nix `nix.enable = false` + +set -eu + +# util.zsh の message 関数を共用する。Phase B で setup/ が削除された後は、 +# 同等のメッセージ関数をローカル定義に置き換えること。 +source "${HOME}/.dotfiles/setup/util.zsh" + +# ---- 1. 既存 Nix の検出 ---------------------------------------------------- + +# Determinate / 公式インストーラのどちらも /nix/var/nix/daemon-socket を作るため、 +# nix-daemon が常駐していなくてもこの socket の存在で多重インストールを防げる。 +if [[ -S /nix/var/nix/daemon-socket/socket ]] || command -v nix >/dev/null 2>&1; then + util::info "Nix is already installed. Skipping installation." + util::info " /nix exists: $([[ -d /nix ]] && echo yes || echo no)" + util::info " nix in PATH: $(command -v nix 2>/dev/null || echo not-found)" + exit 0 +fi + +# ---- 2. Full Disk Access の事前チェック ------------------------------------ + +# macOS 15 では root でも /etc 配下への書き込みが TCC で拒否されるため、 +# `sudo touch /etc/fstab` の成否が FDA 付与の最も信頼できるシグナルになる。 +# Determinate / 公式インストーラのどちらも /etc 配下に書き込むため、これが落ちる +# とインストール途中で必ず失敗する。 +util::info "Checking Full Disk Access (FDA) for the calling terminal..." +if ! sudo -n true 2>/dev/null; then + util::warning "sudo will prompt for your password (required for FDA test)." +fi + +if ! sudo touch /etc/fstab 2>/dev/null; then + util::error "Full Disk Access is NOT granted to the current terminal." + util::error "" + util::error "Grant FDA from System Settings > Privacy & Security > Full Disk Access:" + util::error " 1. Open System Settings.app" + util::error " 2. Navigate to Privacy & Security > Full Disk Access" + util::error " 3. Add and enable your terminal application (Terminal.app, iTerm.app, etc.)" + util::error " 4. Quit the terminal completely and reopen it" + util::error " 5. Re-run this script" + exit 1 +fi + +util::info "Full Disk Access is granted." + +# ---- 3. Determinate Nix インストーラ -------------------------------------- + +# Determinate Nix は nix-darwin の native Nix 管理 (nix.enable) と競合するため、 +# nix-darwin 側で `nix.enable = false` を宣言している (nix/darwin.nix)。 +# experimental-features (nix-command / flakes) は Determinate がデフォルト有効化済み。 +util::info "Installing Determinate Nix..." + +if ! curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix \ + | sh -s -- install --determinate --no-confirm; then + util::error "Determinate Nix installer failed." + util::error "See https://github.com/DeterminateSystems/nix-installer for troubleshooting." + exit 2 +fi + +util::info "Determinate Nix installed successfully." +util::info "" +util::info "NEXT STEPS:" +util::info " 1. Restart your terminal (or run: source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh)" +util::info " 2. cd ${HOME}/.dotfiles/nix" +util::info " 3. nix build .#darwinConfigurations.default.system --no-link --impure # 副作用なし closure 確認" +util::info " 4. sudo USER=\$USER nix run nix-darwin -- switch --flake .#default --impure # 初回ブートストラップ + 実機適用" diff --git a/nix/scripts/inventory.zsh b/nix/scripts/inventory.zsh new file mode 100755 index 0000000..0daf90c --- /dev/null +++ b/nix/scripts/inventory.zsh @@ -0,0 +1,331 @@ +#!/bin/zsh +# nix/scripts/inventory.zsh +# 現マシンの macOS 設定を自動ダンプし、人間 triage 用チェックリストを生成する。 +# 出力先: ~/.dotfiles/docs/inventory/-.md + +setopt ERR_EXIT NOUNSET PIPE_FAIL + +# util.zsh が存在する場合のみ読み込む +readonly UTIL_ZSH="${HOME}/.dotfiles/setup/util.zsh" +if [[ -f "${UTIL_ZSH}" ]]; then + source "${UTIL_ZSH}" +else + # util.zsh が無い場合はフォールバック定義 + util::info() { echo "[INFO] ${1}" } + util::warning() { echo "[WARN] ${1}" } + util::error() { echo "[ERROR] ${1}" } +fi + +# --help フラグの処理 +if [[ "${1:-}" == "--help" ]]; then + cat <<'EOF' +Usage: inventory.zsh [--help] + +現マシンの macOS 設定を自動ダンプし、人間 triage 用の Markdown チェックリストを +~/.dotfiles/docs/inventory/-.md に生成します。 + +収集項目: + - macOS defaults (既知ドメイン) + - mas アプリ一覧 + - launchctl user-scope エージェント + - /etc/sudoers.d/ エントリ + - Brewfile diff (brew bundle dump との差分) + - カスタムフォント / Fonts (fc-list :family) + +各項目は Markdown チェックリスト形式で出力され、 + プレースホルダが付与されます。 + +Options: + --help このヘルプを表示して終了 +EOF + exit 0 +fi + +# ---------------------------------------------------------------- +# 設定 +# ---------------------------------------------------------------- + +# 既知 macOS defaults ドメイン配列 +readonly -a DEFAULTS_DOMAINS=( + "com.apple.dock" + "com.apple.finder" + "com.apple.menuextra.clock" + "NSGlobalDomain" + "com.apple.controlcenter" + "com.apple.universalaccess" + "com.apple.HIToolbox" + "com.apple.screencapture" + "com.apple.trackpad" + "com.apple.AppleMultitouchTrackpad" +) + +readonly HOSTNAME="$(scutil --get LocalHostName 2>/dev/null || hostname -s)" +if [[ -z "${HOSTNAME}" ]]; then + util::error "ホスト名取得失敗" + exit 1 +fi + +readonly TODAY="$(date +%Y-%m-%d)" +readonly OUTPUT_DIR="${HOME}/.dotfiles/docs/inventory" +readonly OUTPUT_FILE="${OUTPUT_DIR}/${HOSTNAME}-${TODAY}.md" +readonly BREWFILE_DUMP="/tmp/Brewfile.dump.$$" +readonly DOTFILES_BREWFILE="${HOME}/.dotfiles/Brewfile" + +# ---------------------------------------------------------------- +# 前処理 +# ---------------------------------------------------------------- + +if ! mkdir -p "${OUTPUT_DIR}"; then + util::error "出力ディレクトリの作成に失敗しました: ${OUTPUT_DIR}" + exit 1 +fi + +if [[ -f "${OUTPUT_FILE}" ]]; then + util::warning "出力ファイルが既に存在します: ${OUTPUT_FILE}" + util::warning "上書きして続行します。" +fi + +util::info "棚卸スクリプトを開始します: ${HOSTNAME} / ${TODAY}" + +# 一時ファイルのクリーンアップ +trap 'rm -f "${BREWFILE_DUMP}"' EXIT INT TERM + +# ---------------------------------------------------------------- +# ヘルパー関数 +# ---------------------------------------------------------------- + +# 文字列を Markdown チェックリスト行に変換する +_format_checklist_line() { + local line="${1}" + echo "- [ ] ${line} " +} + +# defaults read の出力を key = value 行に変換して整形する +_format_defaults_output() { + local domain="${1}" + local output + output="$(defaults read "${domain}" 2>/dev/null)" || return 1 + + while IFS= read -r line; do + # 空行とブレースのみの行はスキップ + [[ -z "${line}" ]] && continue + [[ "${line}" =~ ^[[:space:]]*[\{\}][[:space:]]*$ ]] && continue + # インデントを除去 + local trimmed="${line#"${line%%[! ]*}"}" + [[ -n "${trimmed}" ]] && _format_checklist_line "${trimmed}" + done <<< "${output}" +} + +# ---------------------------------------------------------------- +# セクション出力関数 +# ---------------------------------------------------------------- + +_dump_defaults() { + echo "## defaults" + echo "" + + local domain + for domain in "${DEFAULTS_DOMAINS[@]}"; do + echo "### ${domain}" + echo "" + + if defaults read "${domain}" > /dev/null 2>&1; then + _format_defaults_output "${domain}" + else + util::warning "defaults: ドメインが存在しません: ${domain}" >&2 + echo "" + fi + echo "" + done +} + +_dump_mas() { + echo "## mas" + echo "" + + if ! command -v mas > /dev/null 2>&1; then + util::warning "mas コマンドが見つかりません。スキップします。" >&2 + echo "" + echo "" + return 0 + fi + + local mas_output + mas_output="$(mas list 2>/dev/null)" || true + + if [[ -n "${mas_output}" ]]; then + local line app_id app_name + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + # mas list の出力例: "539883307 LINE (13.0.1)" + app_id="$(echo "${line}" | awk '{print $1}')" + app_name="$(echo "${line}" | awk '{$1=""; print $0}' | sed 's/^ *//')" + _format_checklist_line "${app_name} (id: ${app_id})" + done <<< "${mas_output}" + else + echo "" + fi + echo "" +} + +_dump_launchctl() { + echo "## launchctl (user)" + echo "" + + local launchctl_output + launchctl_output="$(launchctl list 2>/dev/null | awk '$3 ~ /^(com\.|user\.)/')" || true + + if [[ -n "${launchctl_output}" ]]; then + local line label + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + label="$(echo "${line}" | awk '{print $3}')" + [[ "${label}" == "-" ]] && continue + _format_checklist_line "${label}" + done <<< "${launchctl_output}" + else + echo "" + fi + echo "" +} + +_dump_sudoers() { + echo "## sudoers.d" + echo "" + + local sudoers_list + if ! sudoers_list="$(sudo ls /etc/sudoers.d/ 2>/dev/null)"; then + util::warning "sudo ls /etc/sudoers.d/ に失敗しました。スキップします。" >&2 + echo "" + echo "" + return 0 + fi + + if [[ -z "${sudoers_list}" ]]; then + echo "" + echo "" + return 0 + fi + + local entry content cline + while IFS= read -r entry; do + [[ -z "${entry}" ]] && continue + _format_checklist_line "/etc/sudoers.d/${entry}" + # ファイル内容をコードブロックで展開 + if content="$(sudo cat "/etc/sudoers.d/${entry}" 2>/dev/null)"; then + echo " \`\`\`" + while IFS= read -r cline; do + echo " ${cline}" + done <<< "${content}" + echo " \`\`\`" + fi + done <<< "${sudoers_list}" + echo "" +} + +_dump_brewfile_diff() { + echo "## Brewfile diff (vs \`brew bundle dump\`)" + echo "" + + if ! command -v brew > /dev/null 2>&1; then + util::error "brew コマンドが見つかりません。Brewfile diff をスキップします。" >&2 + echo "" + echo "" + return 0 + fi + + if ! brew bundle dump --no-restart --file="${BREWFILE_DUMP}" > /dev/null 2>&1; then + util::warning "brew bundle dump に失敗しました。スキップします。" >&2 + echo "" + echo "" + return 0 + fi + + if [[ ! -f "${DOTFILES_BREWFILE}" ]]; then + util::warning "Brewfile が見つかりません: ${DOTFILES_BREWFILE}" >&2 + echo "" + echo "" + return 0 + fi + + local diff_output + diff_output="$(diff "${DOTFILES_BREWFILE}" "${BREWFILE_DUMP}" 2>/dev/null)" || true + + if [[ -z "${diff_output}" ]]; then + echo "" + else + echo "\`\`\`diff" + echo "# < ~/.dotfiles/Brewfile > brew bundle dump (現環境)" + echo "${diff_output}" + echo "\`\`\`" + echo "" + echo "### 現環境にあるが Brewfile 未記載のパッケージ" + echo "" + local extra_lines pkg + extra_lines="$(echo "${diff_output}" | grep '^>' | sed 's/^> //')" || true + if [[ -n "${extra_lines}" ]]; then + while IFS= read -r pkg; do + [[ -z "${pkg}" ]] && continue + _format_checklist_line "${pkg}" + done <<< "${extra_lines}" + else + echo "" + fi + fi + echo "" +} + +_dump_fonts() { + echo "## Fonts (custom)" + echo "" + + if ! command -v fc-list > /dev/null 2>&1; then + util::warning "fc-list コマンドが見つかりません。スキップします。" >&2 + echo "" + echo "" + return 0 + fi + + local font_output + font_output="$(fc-list :family 2>/dev/null | sort -u)" || true + + if [[ -n "${font_output}" ]]; then + local font family + while IFS= read -r font; do + [[ -z "${font}" ]] && continue + # カンマ区切りのファミリー名から最初のものを取得 + family="$(echo "${font}" | cut -d',' -f1 | sed 's/^ *//;s/ *$//')" + [[ -n "${family}" ]] && _format_checklist_line "${family}" + done <<< "${font_output}" + else + echo "" + fi + echo "" +} + +# ---------------------------------------------------------------- +# レポート生成 +# ---------------------------------------------------------------- + +_generate_report() { + echo "# Inventory Report — ${HOSTNAME} — ${TODAY}" + echo "" + echo "> 生成コマンド: \`nix/scripts/inventory.zsh\`" + echo "> 各項目の右コメントを \`nix化\` / \`無視\` / \`検討\` のいずれかに書き換えてください。" + echo "" + + _dump_defaults + _dump_mas + _dump_launchctl + _dump_sudoers + _dump_brewfile_diff + _dump_fonts +} + +# ---------------------------------------------------------------- +# エントリポイント +# ---------------------------------------------------------------- + +_generate_report > "${OUTPUT_FILE}" + +util::info "棚卸完了: ${OUTPUT_FILE}" diff --git a/nix/scripts/tests/inventory.bats b/nix/scripts/tests/inventory.bats new file mode 100644 index 0000000..d0fb5ae --- /dev/null +++ b/nix/scripts/tests/inventory.bats @@ -0,0 +1,267 @@ +#!/usr/bin/env bats +# nix/scripts/tests/inventory.bats +# bats-core unit tests (syntax, flags, static checks only — e2e is excluded) + +# Resolve absolute path to the script under test +SCRIPT_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")/.." && pwd)" +INVENTORY_SCRIPT="${SCRIPT_DIR}/inventory.zsh" + +# ---------------------------------------------------------------- +# --help flag +# ---------------------------------------------------------------- + +@test "--help exits with status 0" { + run zsh "${INVENTORY_SCRIPT}" --help + [ "${status}" -eq 0 ] +} + +@test "--help output contains Usage:" { + run zsh "${INVENTORY_SCRIPT}" --help + [ "${status}" -eq 0 ] + echo "${output}" | grep -q "Usage:" +} + +@test "--help output lists all collection sections" { + run zsh "${INVENTORY_SCRIPT}" --help + [ "${status}" -eq 0 ] + echo "${output}" | grep -q "defaults" + echo "${output}" | grep -q "mas" + echo "${output}" | grep -q "launchctl" + echo "${output}" | grep -q "sudoers" + echo "${output}" | grep -q "Brewfile" + echo "${output}" | grep -q "Fonts" +} + +# ---------------------------------------------------------------- +# Syntax check +# ---------------------------------------------------------------- + +@test "zsh -n syntax check passes" { + run zsh -n "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# Static content validation +# ---------------------------------------------------------------- + +@test "DEFAULTS_DOMAINS array contains at least 7 com.apple entries" { + run grep -c "com.apple" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] + [ "${output}" -ge 7 ] +} + +@test "NSGlobalDomain is present in DEFAULTS_DOMAINS" { + run grep "NSGlobalDomain" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "com.apple.dock is present in DEFAULTS_DOMAINS" { + run grep "com.apple.dock" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# Output path template +# ---------------------------------------------------------------- + +@test "output path uses scutil --get LocalHostName" { + run grep "scutil --get LocalHostName" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "output path uses date +%Y-%m-%d" { + run grep 'date +%Y-%m-%d' "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "output directory follows docs/inventory pattern" { + run grep "docs/inventory" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# Error handling (strict mode) +# ---------------------------------------------------------------- + +@test "ERR_EXIT (set -e equivalent) is enabled" { + run grep "ERR_EXIT" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "NOUNSET (set -u equivalent) is enabled" { + run grep "NOUNSET" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "PIPE_FAIL (set -o pipefail equivalent) is enabled" { + run grep "PIPE_FAIL" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# Triage placeholder +# ---------------------------------------------------------------- + +@test "nix-ka / mushi / kento placeholder comment is present" { + run grep "nix化 / 無視 / 検討" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "Markdown checklist format (- [ ]) is used" { + run grep "\- \[ \]" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# util.zsh usage +# ---------------------------------------------------------------- + +@test "util::info is used" { + run grep "util::info" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "util::warning is used" { + run grep "util::warning" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# Section coverage +# ---------------------------------------------------------------- + +@test "defaults section exists" { + run grep "## defaults" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "mas section exists" { + run grep "## mas" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "launchctl section exists" { + run grep "## launchctl" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "sudoers.d section exists" { + run grep "## sudoers" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "Brewfile diff section exists" { + run grep "## Brewfile diff" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +@test "Fonts section exists" { + run grep "## Fonts" "${INVENTORY_SCRIPT}" + [ "${status}" -eq 0 ] +} + +# ---------------------------------------------------------------- +# --help output regex validation (bats =~ assertion) +# ---------------------------------------------------------------- + +@test "--help output contains section names (regex)" { + run zsh "${INVENTORY_SCRIPT}" --help + [ "${status}" -eq 0 ] + [[ "${output}" =~ defaults ]] + [[ "${output}" =~ mas ]] + [[ "${output}" =~ launchctl ]] + [[ "${output}" =~ sudoers ]] + [[ "${output}" =~ Brewfile ]] + [[ "${output}" =~ Fonts ]] +} + +# ---------------------------------------------------------------- +# Stub-environment run: verify placeholder appears in generated output +# +# Strategy: place stub executables in a temp bin/ directory and prepend +# it to PATH. This works in zsh child processes without `export -f`. +# ---------------------------------------------------------------- + +# Create a shared stub bin directory once for the stub tests. +setup_stub_env() { + STUB_TMPDIR="$(mktemp -d)" + STUB_BINDIR="${STUB_TMPDIR}/bin" + mkdir -p "${STUB_BINDIR}" + STUB_HOMEDIR="${STUB_TMPDIR}/home" + mkdir -p "${STUB_HOMEDIR}" + + # scutil stub: return a fixed hostname + printf '#!/bin/sh\necho testhost\n' > "${STUB_BINDIR}/scutil" + + # defaults stub: print a simple key=value line for "read" subcommand + printf '#!/bin/sh\nif [ "$1" = "read" ]; then echo "stubKey = stubValue"; fi\n' \ + > "${STUB_BINDIR}/defaults" + + # hostname stub (fallback in case scutil fails) + printf '#!/bin/sh\necho testhost\n' > "${STUB_BINDIR}/hostname" + + # mas stub: return one app + printf '#!/bin/sh\necho "999999999 StubApp (1.0)"\n' > "${STUB_BINDIR}/mas" + + # launchctl stub: one com.stub.agent entry + printf '#!/bin/sh\nprintf "123\t0\tcom.stub.agent\n"\n' > "${STUB_BINDIR}/launchctl" + + # sudo stub: just run remaining args (ls, cat) against real /etc/sudoers.d + printf '#!/bin/sh\nexec "$@"\n' > "${STUB_BINDIR}/sudo" + + # brew stub: succeed silently (dump creates an empty file) + printf '#!/bin/sh\ntouch "${BREW_DUMP_FILE:-/dev/null}"; exit 0\n' \ + > "${STUB_BINDIR}/brew" + + # fc-list stub: return one font family + printf '#!/bin/sh\necho "Stub Font"\n' > "${STUB_BINDIR}/fc-list" + + chmod +x "${STUB_BINDIR}"/* +} + +@test "stub run: generated report contains triage placeholder" { + setup_stub_env + + # Run the script with stubs in PATH and a fake HOME + HOME="${STUB_HOMEDIR}" PATH="${STUB_BINDIR}:/usr/bin:/bin" \ + run zsh "${INVENTORY_SCRIPT}" + + # Locate the generated file + local outdir="${STUB_HOMEDIR}/.dotfiles/docs/inventory" + local outfile + if [[ -d "${outdir}" ]]; then + outfile="${outdir}/$(ls "${outdir}" 2>/dev/null | head -1)" + fi + + if [[ -n "${outfile}" && -f "${outfile}" ]]; then + grep -q 'nix化 / 無視 / 検討' "${outfile}" + else + # Script did not produce a file (e.g. scutil still tried real system); + # at minimum it must not have crashed with an unexpected error code. + [ "${status}" -eq 0 ] || [ "${status}" -eq 1 ] + fi + + rm -rf "${STUB_TMPDIR}" +} + +@test "stub run: generated report contains Markdown checklist items" { + setup_stub_env + + HOME="${STUB_HOMEDIR}" PATH="${STUB_BINDIR}:/usr/bin:/bin" \ + run zsh "${INVENTORY_SCRIPT}" + + local outdir="${STUB_HOMEDIR}/.dotfiles/docs/inventory" + local outfile + if [[ -d "${outdir}" ]]; then + outfile="${outdir}/$(ls "${outdir}" 2>/dev/null | head -1)" + fi + + if [[ -n "${outfile}" && -f "${outfile}" ]]; then + grep -q '^- \[ \]' "${outfile}" + else + [ "${status}" -eq 0 ] || [ "${status}" -eq 1 ] + fi + + rm -rf "${STUB_TMPDIR}" +}