From 7ad0ac0547dc4df023f3bec8b7997708a29a6068 Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:35:47 +0900 Subject: [PATCH 1/7] =?UTF-8?q?env-init=20=E3=82=92=20flake=20=E3=83=91?= =?UTF-8?q?=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8=E3=81=A8=E3=81=97=E3=81=A6?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=20devbox=20=E9=96=8B=E7=99=BA?= =?UTF-8?q?=E7=92=B0=E5=A2=83=E3=82=92=E6=95=B4=E5=82=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 汎用 dev ツールの第一弾として env-init を Nix flake で配布する形に移植し、 devbox で Nix の test/lint を含む開発環境を整備した。 - pkgs/env-init: 生スクリプトを verbatim 移植し makeWrapper で PATH のみ wrap - flake.nix: packages / checks (shellcheck・bats・statix・deadnix・nixfmt) / formatter - devbox.json: dev ツール + path:.#env-init の dogfood + init_hook + scripts - ルート .env.template: 書式の参照例(本 repo の dogfood でも使用) - CI: check.yml(nix flake check) と release.yml(タグ駆動の Release 自動生成) - README/CLAUDE.md: 構成・開発・バージョニング(SemVer)を追記 Co-Authored-By: Claude Opus 4.8 --- .env.template | 21 ++ .github/workflows/check.yml | 20 ++ .github/workflows/release.yml | 26 +++ .gitignore | 7 + CLAUDE.md | 8 + README.md | 63 ++++- devbox.json | 25 ++ devbox.lock | 377 ++++++++++++++++++++++++++++++ flake.lock | 27 +++ flake.nix | 82 +++++++ pkgs/env-init/env-init | 229 ++++++++++++++++++ pkgs/env-init/package.nix | 42 ++++ pkgs/env-init/tests/env-init.bats | 119 ++++++++++ 13 files changed, 1044 insertions(+), 2 deletions(-) create mode 100644 .env.template create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/release.yml create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 flake.lock create mode 100644 flake.nix create mode 100755 pkgs/env-init/env-init create mode 100644 pkgs/env-init/package.nix create mode 100644 pkgs/env-init/tests/env-init.bats diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..806f35e --- /dev/null +++ b/.env.template @@ -0,0 +1,21 @@ +# env-init が現在の git worktree 用に .env を生成するためのテンプレート(書式の参照例)。 +# このファイルは bash として 1 回だけ source される。書式の要点: +# - env-init が worktree 番号 N を export 済み(primary=0 / 派生 worktree=1..99)。 +# - 代入行 NAME=... のみが「評価後の値」で .env に出力される。 +# - コメント行・空行はそのまま素通し。代入以外のロジック行は .env に出力されない。 +# - 先に宣言した変数は後続行から ${NAME} で展開できる(bash 評価のため)。 +# これは書式の参照例。利用側 repo は自分のポート・secret に合わせて独自の .env.template を用意する +# (.env.sample を一次ソースにして .env.template を作る運用も good practice。プロジェクト次第)。 + +# 文字列リテラル +APP_NAME="devtools" + +# N を使った算術。worktree ごとにポートをずらす(例: primary=3000, N=1 の worktree=3001)。 +PORT_APP=$((3000 + N)) + +# 先に宣言した変数の展開。APP_NAME と PORT_APP を参照して URL / DB 名を組み立てる。 +APP_URL="http://localhost:${PORT_APP}" +DATABASE_URL="postgres://localhost:$((5432 + N))/${APP_NAME}" + +# コマンド置換による乱数 secret 生成。openssl は利用側 repo の環境(devbox 等)が供給する。 +SESSION_SECRET=$(openssl rand -base64 32) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..74b3698 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,20 @@ +name: check + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install devbox + uses: jetify-com/devbox-install-action@8c6a66ed6273138b1915457069de78cb52fe3bd7 # v0.15.0 + - name: nix flake check + run: devbox run check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9cc3a37 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: release + +# SemVer タグ (vX.Y.Z) を push すると、check を通したうえで +# GitHub Release を自動生成ノート付きで作成する。 +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install devbox + uses: jetify-com/devbox-install-action@8c6a66ed6273138b1915457069de78cb52fe3bd7 # v0.15.0 + - name: nix flake check + run: devbox run check + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "$GITHUB_REF_NAME" --generate-notes --verify-tag diff --git a/.gitignore b/.gitignore index ac36766..1b594d5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,10 @@ result-* # devbox .devbox/ .local/ + +# エディタ / ローカルツール +.obsidian/ + +# Claude Code のローカル状態 +.claude/settings.local.json +.claude/scheduled_tasks.lock diff --git a/CLAUDE.md b/CLAUDE.md index 20a0716..3c44d8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,14 @@ org 横断で共有する汎用 dev ツールの集約先。ツールはまず各利用 repo で実運用に揉んで枯らし、repo 非依存(特定のポート名・secret 名・repo 固有パスを知らない)になったものから本リポジトリへ移設する。 +## 構成と開発の前提 + +- パッケージは `pkgs//` に `package.nix`(`writeShellApplication` 派生)と生スクリプトを同居させる。生スクリプトは + `#!/usr/bin/env bash` 始まりで単体実行可能なまま保ち、wrap は外側に被せるだけで中身を書き換えない。 +- test / lint の真実は `flake.nix` の `checks`(bats・statix・deadnix・nixfmt、および writeShellApplication build 内包の + shellcheck)に一元化する。`devbox.json` の scripts はそれへの薄い委譲。 +- 開発環境は devbox(`devbox run check` など)。ツール一覧の二重管理を避けるため flake の devShell は設けない。 + ## 公開リポジトリの制約 本リポジトリは public 公開する。成果物(ドキュメント・コード・コミット等)に、プライベートリポジトリや非公開 Issue への言及を入れない。設計の背景が非公開リソースにある場合でも、公開成果物は自己完結した記述にする。 diff --git a/README.md b/README.md index 238efbf..9081493 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,65 @@ org 横断で共有する汎用 dev ツールの集約リポジトリ。各利 Nix flake × devbox。言語非依存でツールを PATH へ配布し、`flake.lock` / `devbox.lock` で版とコンテナ build を再現可能に固定する。利用側 repo は同一の flake 参照を `devbox.json` の `packages` に足すだけでよい。 -## 状態 +## 構成 -初期化フェーズ。worktree 用の `.env` 生成エンジン `env-init` の移設を先頭バッチに、repo 非依存に枯れたツールから順次集約していく。 +``` +flake.nix # packages / checks (test・lint) / formatter を公開 +devbox.json # 開発環境(Nix の test/lint ツール)と自己 dogfooding +.env.template # env-init 用テンプレートの書式例(本 repo の dogfood でも使用) +pkgs/ + env-init/ + package.nix # env-init を makeWrapper で wrap する派生 + env-init # エンジン本体(単体実行可能な生スクリプト) + tests/ # bats テスト +``` + +## 開発 + +devbox 経由で開発する。 + +```sh +devbox shell # 開発シェルに入る(初回は .env を自動生成し env-init を dogfood) +devbox run check # nix flake check(shellcheck・bats・statix・deadnix・nixfmt) +devbox run build # nix build .#env-init +devbox run fmt # nix fmt(nixfmt-tree でツリー全体を整形) +``` + +## バージョニング + +リリースはリポジトリ単位の [SemVer](https://semver.org/lang/ja/) タグ `vMAJOR.MINOR.PATCH`(例 `v1.0.0`)で行う。複数ツールが入るが**タグは共通の 1 本**で、ツールは flake output 名(`#env-init` 等)で区別する。利用側は `` にタグ(または commit SHA)をピンし、実際の再現性は利用側の lock が固定する。 + +安定版は [GitHub Releases](https://github.com/airs/devtools/releases) の最新(Latest)を参照し、その `vX.Y.Z` を `` に pin する。 + +SemVer は各ツールの CLI 契約(引数・出力・終了コード)で判断する。 + +- **MAJOR**: 破壊的変更(フラグ改名・出力フォーマット変更・終了コード変更・ツール削除) +- **MINOR**: 後方互換の追加(新ツール追加・新しい任意フラグ) +- **PATCH**: 挙動を変えないバグ修正 + +## ツール + +### env-init + +現在の git worktree 用に `.env` を生成する汎用エンジン。worktree 番号 N を計算し、リポジトリルートの +`.env.template`(bash として 1 回評価される)から `.env` を書き出す。プロジェクト非依存設計で、実行時依存は +`bash` / `git` / `gawk` / `gnused` + coreutils。 + +**利用側 repo での使い方**: + +1. `devbox.json` の `packages` に flake 参照を足し、init_hook で起動する。 + + ```jsonc + { + "packages": ["github:airs/devtools/#env-init"], + "shell": { "init_hook": ["[ -f .env ] || env-init"] } + } + ``` + + `` は tag / commit でピンする。本リポジトリ自身の `devbox.json` は、ローカル flake を dogfood するため + `path:.#env-init` を使う点だけが利用側と異なる。Nix を使わない環境では `pkgs/env-init/env-init` を直接実行できる + (生スクリプトは単体実行可能なまま)。 + +2. **利用側 repo がルートに `.env.template` を用意する**(中央には持ち込まない repo 固有ファイル)。各 worktree で + `N` を参照し、ポートを `$((BASE + N))` でずらし、secret を `openssl rand` / `op read` 等で書く。書式は本リポジトリ + ルートの [`.env.template`](.env.template) を参照(コピー用ではなく書式例)。 diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..a50184e --- /dev/null +++ b/devbox.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json", + "packages": [ + "git@2", + "shellcheck@latest", + "statix@latest", + "deadnix@latest", + "nixfmt@latest", + "bats@latest", + "path:.#env-init" + ], + "env": { + "NIX_CONFIG": "experimental-features = nix-command flakes" + }, + "shell": { + "init_hook": [ + "[ -f .env ] || env-init" + ], + "scripts": { + "check": "nix flake check", + "build": "nix build .#env-init", + "fmt": "nix fmt" + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..78059c7 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,377 @@ +{ + "lockfile_version": "1", + "packages": { + "bats@latest": { + "last_modified": "2026-05-21T08:15:18Z", + "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#bats", + "source": "devbox-search", + "version": "1.12.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jkp1n6vf5kj5z709n5l8mgjzkby97rr7-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/jkp1n6vf5kj5z709n5l8mgjzkby97rr7-bats-1.12.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/7figc61fkvp1v1rbgi77f0bkc71c8hh2-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/7figc61fkvp1v1rbgi77f0bkc71c8hh2-bats-1.12.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1xrr8g08d7mmbg9q205hxbv3fryspnai-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/1xrr8g08d7mmbg9q205hxbv3fryspnai-bats-1.12.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/n0md2c3y0pfqjfz8rh3vdwkn4bkvv2bq-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/n0md2c3y0pfqjfz8rh3vdwkn4bkvv2bq-bats-1.12.0" + } + } + }, + "deadnix@latest": { + "last_modified": "2026-05-21T08:15:18Z", + "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#deadnix", + "source": "devbox-search", + "version": "1.3.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/f57v2z446b4c9prhr80834zsx2hdv2fc-deadnix-1.3.1", + "default": true + } + ], + "store_path": "/nix/store/f57v2z446b4c9prhr80834zsx2hdv2fc-deadnix-1.3.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8wnv32hbvyvsqdifq1hc1n1la96a0m0j-deadnix-1.3.1", + "default": true + } + ], + "store_path": "/nix/store/8wnv32hbvyvsqdifq1hc1n1la96a0m0j-deadnix-1.3.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d7vjscazqmm02s14zkk42cs4ngzy0shr-deadnix-1.3.1", + "default": true + } + ], + "store_path": "/nix/store/d7vjscazqmm02s14zkk42cs4ngzy0shr-deadnix-1.3.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ddy7ba32ww1hz9x4pvir84s5x3np5i5c-deadnix-1.3.1", + "default": true + } + ], + "store_path": "/nix/store/ddy7ba32ww1hz9x4pvir84s5x3np5i5c-deadnix-1.3.1" + } + } + }, + "git@2": { + "last_modified": "2026-05-21T08:15:18Z", + "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#git", + "source": "devbox-search", + "version": "2.54.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/zn8gmbxxf5398hxg6y6nk5jimj85qj69-git-2.54.0-doc" + } + ], + "store_path": "/nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ixp98f9avf8ikpdrmp40cj33g0dazyp9-git-2.54.0", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/c7mafmqf28l7898rpbqqkd0p3m42hc03-git-2.54.0-debug" + }, + { + "name": "doc", + "path": "/nix/store/klwr4n2snj7ldlpkhwarwlif80myx0vj-git-2.54.0-doc" + } + ], + "store_path": "/nix/store/ixp98f9avf8ikpdrmp40cj33g0dazyp9-git-2.54.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/s9lsrlgwr3sabw9a2dl3laykas0ijk85-git-2.54.0", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/agyxy5ay4x06b52mj24rz44fc82328ha-git-2.54.0-doc" + } + ], + "store_path": "/nix/store/s9lsrlgwr3sabw9a2dl3laykas0ijk85-git-2.54.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/bcnisk3ydfgv26v2gw3zlky24g00yww2-git-2.54.0", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/72fyakk8w8idjbksk0h8jbkq6sgvcm85-git-2.54.0-debug" + }, + { + "name": "doc", + "path": "/nix/store/l444690y78nfdwyzm6mghagl7xa97myf-git-2.54.0-doc" + } + ], + "store_path": "/nix/store/bcnisk3ydfgv26v2gw3zlky24g00yww2-git-2.54.0" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-06-01T17:55:45Z", + "resolved": "github:NixOS/nixpkgs/4df1b885d76a54e1aa1a318f8d16fd6005b6401f?lastModified=1780336545&narHash=sha256-vhVhuXzFrIOfcssC%2F9hDHx7MHzDKjF3keHuREOQqQiQ%3D" + }, + "nixfmt@latest": { + "last_modified": "2026-05-21T08:15:18Z", + "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#nixfmt", + "source": "devbox-search", + "version": "1.2.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/yhpmf2683h44jjbglcchs41fhsrcbbpa-nixfmt-1.2.0", + "default": true + } + ], + "store_path": "/nix/store/yhpmf2683h44jjbglcchs41fhsrcbbpa-nixfmt-1.2.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/c67jrm6j21rw35f41fa2pdlqmqmjh055-nixfmt-1.2.0", + "default": true + } + ], + "store_path": "/nix/store/c67jrm6j21rw35f41fa2pdlqmqmjh055-nixfmt-1.2.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ps7n29w4rrym3vblr5msmwwh80b35j25-nixfmt-1.2.0", + "default": true + } + ], + "store_path": "/nix/store/ps7n29w4rrym3vblr5msmwwh80b35j25-nixfmt-1.2.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/01945jlqwsgq7a9nbw00y0p0x6027yy3-nixfmt-1.2.0", + "default": true + } + ], + "store_path": "/nix/store/01945jlqwsgq7a9nbw00y0p0x6027yy3-nixfmt-1.2.0" + } + } + }, + "shellcheck@latest": { + "last_modified": "2026-05-21T08:15:18Z", + "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#shellcheck", + "source": "devbox-search", + "version": "0.11.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/5lr3z3mqgj186p8drycnnkxh7zs13kf0-shellcheck-0.11.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/5gzk96ghybkcjzhnz6gjk0k4szymwvp9-shellcheck-0.11.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/4wz8ih8li10v4s4q1rya1m85wc4aw4wd-shellcheck-0.11.0-doc", + "default": true + }, + { + "name": "out", + "path": "/nix/store/ivfr09ssl368lcdw8jswrb653s43cvxb-shellcheck-0.11.0" + } + ], + "store_path": "/nix/store/5lr3z3mqgj186p8drycnnkxh7zs13kf0-shellcheck-0.11.0-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/hqajavk4av9v6l08h1xsfz19rwsir85y-shellcheck-0.11.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/jpdzs6ywlqmxbmzim0b8flrwc9ac94vz-shellcheck-0.11.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/gdjj2q6jwgjj14c72hwss2zxww5hxvbm-shellcheck-0.11.0-doc", + "default": true + }, + { + "name": "out", + "path": "/nix/store/bg01hij699rpkg2q0fhghdyma1zf3vmc-shellcheck-0.11.0" + } + ], + "store_path": "/nix/store/hqajavk4av9v6l08h1xsfz19rwsir85y-shellcheck-0.11.0-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/g02yfjc7lgig0fnv666ii9rz1vx6mvs2-shellcheck-0.11.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/nc5lj21np4za87piqf5nyax0ibimlzss-shellcheck-0.11.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/iymch2lfdskwpzgka1igia981k76wp9w-shellcheck-0.11.0-doc", + "default": true + }, + { + "name": "out", + "path": "/nix/store/ipidk29kr6a49xk0vl6ygz6dx2vd9ss0-shellcheck-0.11.0" + } + ], + "store_path": "/nix/store/g02yfjc7lgig0fnv666ii9rz1vx6mvs2-shellcheck-0.11.0-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/2lfacwyvxpllrq9jpbyz7z4v4lc3wnr9-shellcheck-0.11.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/0504zfbbnxm2dgnkj0pal11nv09i1g2p-shellcheck-0.11.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/qbmc0kv3ddridry1w7rdc124an8fk895-shellcheck-0.11.0-doc", + "default": true + }, + { + "name": "out", + "path": "/nix/store/pmk1wsbpsxkaa33mrigc72iln3pvy9r2-shellcheck-0.11.0" + } + ], + "store_path": "/nix/store/2lfacwyvxpllrq9jpbyz7z4v4lc3wnr9-shellcheck-0.11.0-bin" + } + } + }, + "statix@latest": { + "last_modified": "2026-05-21T08:15:18Z", + "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#statix", + "source": "devbox-search", + "version": "0-unstable-2026-05-14", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vrhdp9md3ypsixzb6dpg10xba9lq1sy5-statix-0-unstable-2026-05-14", + "default": true + } + ], + "store_path": "/nix/store/vrhdp9md3ypsixzb6dpg10xba9lq1sy5-statix-0-unstable-2026-05-14" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/nkan616sp6six5r73c4329288j5djn98-statix-0-unstable-2026-05-14", + "default": true + } + ], + "store_path": "/nix/store/nkan616sp6six5r73c4329288j5djn98-statix-0-unstable-2026-05-14" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/pyq9yhr64ngnk9qr27cjfwb94x6zfxdq-statix-0-unstable-2026-05-14", + "default": true + } + ], + "store_path": "/nix/store/pyq9yhr64ngnk9qr27cjfwb94x6zfxdq-statix-0-unstable-2026-05-14" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/v5wmprihi3ywkh9ksdvwgiixrxj29r66-statix-0-unstable-2026-05-14", + "default": true + } + ], + "store_path": "/nix/store/v5wmprihi3ywkh9ksdvwgiixrxj29r66-statix-0-unstable-2026-05-14" + } + } + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bffd844 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1780243769, + "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a93538d --- /dev/null +++ b/flake.nix @@ -0,0 +1,82 @@ +{ + description = "org 横断で共有する汎用 dev ツールの集約 flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + in + { + # 複数ツールが入るため default は設けない。利用側は常に `#` を明示する。 + packages = forAllSystems (pkgs: { + env-init = pkgs.callPackage ./pkgs/env-init/package.nix { }; + }); + + # test / lint の単一ソース。`nix flake check` で全て走る。 + checks = forAllSystems ( + pkgs: + let + system = pkgs.stdenv.hostPlatform.system; + in + { + # パッケージ build が通ること。 + env-init = self.packages.${system}.env-init; + + shellcheck = pkgs.runCommand "shellcheck" { nativeBuildInputs = [ pkgs.shellcheck ]; } '' + shellcheck ${./pkgs/env-init/env-init} + touch "$out" + ''; + + # env-init を PATH に載せて bats フルセットを実行。 + bats = + pkgs.runCommand "env-init-bats" + { + nativeBuildInputs = [ + pkgs.bats + pkgs.bash + pkgs.git + pkgs.gawk + pkgs.gnused + pkgs.coreutils + self.packages.${system}.env-init + ]; + } + '' + cp -r ${./pkgs/env-init/tests} tests + export HOME="$TMPDIR" + bats tests + touch "$out" + ''; + + statix = pkgs.runCommand "statix-check" { nativeBuildInputs = [ pkgs.statix ]; } '' + statix check ${./.} + touch "$out" + ''; + + deadnix = pkgs.runCommand "deadnix-check" { nativeBuildInputs = [ pkgs.deadnix ]; } '' + deadnix --fail ${./.} + touch "$out" + ''; + + nixfmt = pkgs.runCommand "nixfmt-check" { nativeBuildInputs = [ pkgs.nixfmt ]; } '' + nixfmt --check ${./flake.nix} ${./pkgs/env-init/package.nix} + touch "$out" + ''; + } + ); + + # nixfmt-tree (treefmt + nixfmt) を formatter にすることで `nix fmt`(引数なし)が + # ツリー全体を再帰整形できる(素の nixfmt は引数なしだと stdin 待ちでハングする)。 + formatter = forAllSystems (pkgs: pkgs.nixfmt-tree); + }; +} diff --git a/pkgs/env-init/env-init b/pkgs/env-init/env-init new file mode 100755 index 0000000..402969b --- /dev/null +++ b/pkgs/env-init/env-init @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# 現在の git worktree 用に .env を生成する汎用エンジン。 +# +# このエンジンはプロジェクト非依存。ポート名も secret 名も一切知らず、 +# 「worktree 番号 N の計算」だけを担う。サブドメイン等の名前解決はプロキシ層 +# (portless 等) の責務に委譲し、このエンジンは数値ポート stride 用の N を提供するだけにする。 +# 実際の値 (ポート・secret) はリポジトリルートの `.env.template` に bash 構文で書く。 +# `.env.template` を bash として 1 回だけ評価し、評価後の値を `.env` に literal で書き出す。 +# +# 仕組み: +# - primary worktree (main clone) は常に N=0。 +# - それ以外は既存 worktree の .env に書かれた `WORKTREE_N` マーカー行を読んで +# 使用済み N を集め、{1..99} の最小未使用値を割り当てる。これにより worktree の +# 削除・追加で出現順が変わっても、既存 worktree の N と衝突しない。 +# - -n で明示指定した場合は衝突チェックせずその値を使う (責任はユーザ側)。 +# +# `.env.template` から参照できるもの: +# - N : worktree 番号 (例: `$((6100 + N))`) +# +# 使い方: +# bin/env-init # 冪等。既存 .env があれば skip (--force で再生成) +# bin/env-init --force # 既存 .env を上書き (N は WORKTREE_N から復元) +# bin/env-init -n 3 # worktree 番号 N を手動指定 +# +# devbox タスク経由: `task env:init` (引数は `-- --force` のように渡す) + +set -eu + +FORCE=0 +N_OVERRIDE="" + +while [ $# -gt 0 ]; do + case "$1" in + -f|--force) + FORCE=1 + shift + ;; + -n) + if [ $# -lt 2 ]; then + echo "error: -n requires a worktree number" >&2 + exit 2 + fi + N_OVERRIDE="$2" + shift 2 + ;; + -h|--help) + # 先頭ヘッダーコメント (shebang 直後の連続する # 行) を usage として出す。 + # 行数を固定するとコメント増減で set -eu などの実装行まで混ざるため、 + # 最初の非コメント行で打ち切る。 + awk 'NR==1 { next } /^#/ { print; next } { exit }' "$0" + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +# リポジトリルート (このスクリプトを呼んだ worktree のルート) +REPO_ROOT=$(git rev-parse --show-toplevel) +ENV_PATH="$REPO_ROOT/.env" +TEMPLATE_PATH="$REPO_ROOT/.env.template" + +if [ ! -f "$TEMPLATE_PATH" ]; then + echo "error: $TEMPLATE_PATH not found. The engine generates .env from .env.template." >&2 + exit 1 +fi + +if [ -e "$ENV_PATH" ] && [ "$FORCE" -ne 1 ]; then + echo "$ENV_PATH already exists; skipping (use --force to regenerate)." + exit 0 +fi + +# 既存 .env から WORKTREE_N マーカーを読む (N 真実ソース)。見つからなければ空。 +read_worktree_n() { + sed -n 's/^WORKTREE_N=\([0-9][0-9]*\).*/\1/p' "$1" 2>/dev/null | head -1 +} + +# worktree 番号 N を決める +if [ -n "$N_OVERRIDE" ]; then + N="$N_OVERRIDE" + case "$N" in + ''|*[!0-9]*) + echo "error: -n value must be a non-negative integer (got '$N')" >&2 + exit 2 + ;; + esac + # 先頭ゼロ (例 `-n 08`) を剥がす。算術展開が `08` / `09` を 8 進数として解釈して + # パースエラーになるのを避ける。全ゼロ入力 (`0` / `00`) は `0` に戻す。 + N=$(printf '%s' "$N" | sed 's/^0*//') + [ -z "$N" ] && N=0 +else + # primary worktree (main clone) は `git worktree list --porcelain` の先頭に現れる。 + # そのパスと現在の REPO_ROOT が一致するなら N=0 を固定で使う。 + PRIMARY_PATH=$(git worktree list --porcelain | awk '/^worktree / { sub(/^worktree /, ""); print; exit }') + if [ -z "$PRIMARY_PATH" ]; then + echo "error: could not determine primary worktree path" >&2 + exit 1 + fi + + if [ "$REPO_ROOT" = "$PRIMARY_PATH" ]; then + N=0 + else + # 他の worktree (自分自身と primary を除く) の .env から WORKTREE_N を読み、 + # 使用済み N を集めて {1..99} の最小未使用値を割り当てる。 + USED_NS="" + # プロセス置換で直接読む (while を current shell で回し USED_NS を親に残す。 + # bash 必須だが本スクリプトは元から bash 前提)。 + while IFS= read -r line; do + case "$line" in + "worktree "*) + wt_path=${line#worktree } + if [ "$wt_path" = "$REPO_ROOT" ] || [ "$wt_path" = "$PRIMARY_PATH" ]; then + continue + fi + wt_env="$wt_path/.env" + if [ -f "$wt_env" ]; then + used_n=$(read_worktree_n "$wt_env") + if [ -n "$used_n" ]; then + USED_NS="$USED_NS $used_n" + fi + fi + ;; + esac + done < <(git worktree list --porcelain) + + # --force で既存 .env を再生成する場合は、そこから WORKTREE_N を読んで以前の N を + # 復元し、他 worktree と衝突していなければそのまま維持する (ポート・hostname が + # 勝手にズレるのを防ぐ)。 + N="" + if [ "$FORCE" -eq 1 ] && [ -f "$ENV_PATH" ]; then + preferred_n=$(read_worktree_n "$ENV_PATH") + if [ -n "$preferred_n" ] && [ "$preferred_n" -ge 1 ] && [ "$preferred_n" -le 99 ]; then + collision=0 + for used in $USED_NS; do + if [ "$preferred_n" = "$used" ]; then + collision=1 + break + fi + done + if [ "$collision" -eq 0 ]; then + N="$preferred_n" + fi + fi + fi + + if [ -z "$N" ]; then + for candidate in {1..99}; do + collision=0 + for used in $USED_NS; do + if [ "$candidate" = "$used" ]; then + collision=1 + break + fi + done + if [ "$collision" -eq 0 ]; then + N="$candidate" + break + fi + done + fi + + if [ -z "$N" ]; then + echo "error: all worktree numbers 1..99 are in use by sibling worktrees. Free an unused worktree or pass -n explicitly." >&2 + exit 1 + fi + fi +fi + +if [ "$N" -gt 99 ]; then + # worktree 番号は 0..99 の 100 個まで。上限の根拠 (ポート帯の予約等) はプロジェクト側 + # (.env.template / README) に閉じており、エンジンは範囲だけを強制する (汎用性のため)。 + echo "error: worktree number $N exceeds maximum. Worktree numbers must be 0..99." >&2 + exit 1 +fi + +# N を export して .env.template から参照可能にする。サブドメイン等の名前解決は +# プロキシ層 (portless) が worktree のブランチ名から自動付与するため、ここで slug や +# worktree 名を計算・export することはしない。 +export N + +# .env.template を bash として 1 回だけ評価する。set -a で全代入を環境に載せ、 +# 評価後の最終値を間接展開 `${!NAME}` で取り出せるようにする。 +# 1 行の失敗で全体が落ちないよう errexit / nounset は評価中だけ無効化する +# (堅牢性は .env.template 著者の責務)。 +set -a +set +eu +# shellcheck disable=SC1090 +. "$TEMPLATE_PATH" +set -eu +set +a + +# 値を `NAME="..."` の double-quote literal で安全に出力するためのエスケープ。 +# 生成 .env は devbox init_hook の `set -a; . ./.env` (bash source) で読まれるため、 +# double-quote 内で特別扱いされる \ " ` $ をバックスラッシュエスケープする。 +# (改行を含む値は想定外。base64 secret / API キー / URL は対象文字を含まない。) +escape_env_value() { + local v=$1 + v=${v//\\/\\\\} + v=${v//\"/\\\"} + v=${v//\`/\\\`} + v=${v//\$/\\\$} + printf '%s' "$v" +} + +# .env を生成。secret を含むため umask 077 で作成し other/group の読取を落とす。 +umask 077 +# 先頭に WORKTREE_N マーカー行を出力 (N 真実ソース)。 +printf 'WORKTREE_N=%s\n' "$N" > "$ENV_PATH" + +# .env.template を行単位で走査して .env に書き出す。 +# - 代入行 NAME=... → 評価後の最終値を ${!NAME} で取り NAME="" 形式で出力 +# - コメント行 / 空行 → そのまま素通し +# - それ以外 (if/echo/exit/関数定義などの評価専用ロジック) → .env に出力しない +# (素通しするとシェル構文が .env に混入し、`. ./.env` や dotenv パーサを壊す) +while IFS= read -r line || [ -n "$line" ]; do + if [[ $line =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]]; then + name=${BASH_REMATCH[1]} + value=${!name-} + printf '%s="%s"\n' "$name" "$(escape_env_value "$value")" >> "$ENV_PATH" + elif [[ $line =~ ^[[:space:]]*(#|$) ]]; then + printf '%s\n' "$line" >> "$ENV_PATH" + fi +done < "$TEMPLATE_PATH" + +chmod 600 "$ENV_PATH" + +echo "Generated $ENV_PATH for worktree N=$N" diff --git a/pkgs/env-init/package.nix b/pkgs/env-init/package.nix new file mode 100644 index 0000000..335255b --- /dev/null +++ b/pkgs/env-init/package.nix @@ -0,0 +1,42 @@ +# env-init を makeWrapper で wrap する。生スクリプト (./env-init) は libexec にそのまま置き、 +# 外側の wrapper で runtimeInputs を PATH 先頭に prefix して exec するだけ(中身は無改変)。 +# +# writeShellApplication を使わない理由: あれはスクリプト本文の前にプリアンブル +# (shebang・set -o・PATH 設定) を挿入するため、env-init の `--help` が `$0` から読む +# 先頭ヘッダコメントが押し下げられて usage が空になる。makeWrapper なら wrapper が +# 生スクリプトを exec し `$0` が生スクリプトを指すため --help が正しく動く。 +# +# PATH には bash も含める。生スクリプトの `#!/usr/bin/env bash` が wrapper の設定した +# PATH で bash を解決するため、決定的な bash を使わせる。runtimeInputs を prefix しても +# 既存 PATH は残る (suffix) ので、.env.template が呼ぶ openssl / op は消費側 PATH で解決できる。 +{ + lib, + runCommand, + makeWrapper, + bash, + coreutils, + git, + gawk, + gnused, +}: +runCommand "env-init" + { + nativeBuildInputs = [ makeWrapper ]; + meta = { + description = "現在の git worktree 用に .env を生成する汎用エンジン"; + mainProgram = "env-init"; + }; + } + '' + install -Dm755 ${./env-init} "$out/libexec/env-init" + makeWrapper "$out/libexec/env-init" "$out/bin/env-init" \ + --prefix PATH : ${ + lib.makeBinPath [ + bash + coreutils + git + gawk + gnused + ] + } + '' diff --git a/pkgs/env-init/tests/env-init.bats b/pkgs/env-init/tests/env-init.bats new file mode 100644 index 0000000..31c9a9a --- /dev/null +++ b/pkgs/env-init/tests/env-init.bats @@ -0,0 +1,119 @@ +#!/usr/bin/env bats +# env-init のフルセットテスト。env-init は PATH 上にある前提(ENV_INIT で上書き可)。 + +ENV_INIT="${ENV_INIT:-env-init}" + +setup() { + # macOS の $TMPDIR は /var → /private/var の symlink。git rev-parse --show-toplevel は + # 物理パスを返すため、worktree のパス比較が一致するよう base を物理パスに正規化する。 + BASE="$(cd "$BATS_TEST_TMPDIR" && pwd -P)" + REPO="$BASE/repo" + mkdir -p "$REPO" + cd "$REPO" + git init -q -b main + git config user.email test@example.com + git config user.name test + git commit -q --allow-empty -m init +} + +# 評価専用ロジック行・コメント・空行・算術・空値代入を含むテンプレートを書く。 +write_template() { + cat > "$1/.env.template" <<'EOF' +# comment line +APP_NAME="demo" +PORT_APP=$((3000 + N)) +EMPTY= + +if true; then :; fi +EOF +} + +@test "--help はヘッダを出して exit 0" { + run "$ENV_INIT" --help + [ "$status" -eq 0 ] + [[ "$output" == *"worktree"* ]] +} + +@test "未知の引数は exit 2" { + run "$ENV_INIT" --nope + [ "$status" -eq 2 ] +} + +@test ".env.template が無ければ exit 1" { + run "$ENV_INIT" + [ "$status" -eq 1 ] + [[ "$output" == *".env.template not found"* ]] +} + +@test "primary は N=0 で .env を生成し権限 600・評価専用行は出力しない" { + write_template "$REPO" + run "$ENV_INIT" + [ "$status" -eq 0 ] + [[ "$output" == *"Generated"* ]] + [ -f "$REPO/.env" ] + grep -qx 'WORKTREE_N=0' "$REPO/.env" + grep -qx 'APP_NAME="demo"' "$REPO/.env" + grep -qx 'PORT_APP="3000"' "$REPO/.env" + grep -qx 'EMPTY=""' "$REPO/.env" + grep -qx '# comment line' "$REPO/.env" + ! grep -q 'if true' "$REPO/.env" + perm="$(stat -c '%a' "$REPO/.env" 2>/dev/null || stat -f '%Lp' "$REPO/.env")" + [ "$perm" = "600" ] +} + +@test "冪等性: 再実行で skip、--force で再生成" { + write_template "$REPO" + run "$ENV_INIT" + [ "$status" -eq 0 ] + run "$ENV_INIT" + [ "$status" -eq 0 ] + [[ "$output" == *"already exists"* ]] + run "$ENV_INIT" --force + [ "$status" -eq 0 ] + [[ "$output" == *"Generated"* ]] +} + +@test "-n は N を設定し先頭ゼロを除去、不正値は exit 2" { + write_template "$REPO" + run "$ENV_INIT" -n 5 + [ "$status" -eq 0 ] + grep -qx 'WORKTREE_N=5' "$REPO/.env" + grep -qx 'PORT_APP="3005"' "$REPO/.env" + + run "$ENV_INIT" --force -n 08 + [ "$status" -eq 0 ] + grep -qx 'WORKTREE_N=8' "$REPO/.env" + + run "$ENV_INIT" --force -n abc + [ "$status" -eq 2 ] + + run "$ENV_INIT" -n + [ "$status" -eq 2 ] +} + +@test "N が 99 を超えると exit 1" { + write_template "$REPO" + run "$ENV_INIT" -n 100 + [ "$status" -eq 1 ] +} + +@test "sibling worktree は最小未使用 N を得て、解放後の N を再利用する" { + write_template "$REPO" + "$ENV_INIT" + + git worktree add -q "$BASE/wtA" -b branchA + write_template "$BASE/wtA" + ( cd "$BASE/wtA" && "$ENV_INIT" ) + grep -qx 'WORKTREE_N=1' "$BASE/wtA/.env" + + git worktree add -q "$BASE/wtB" -b branchB + write_template "$BASE/wtB" + ( cd "$BASE/wtB" && "$ENV_INIT" ) + grep -qx 'WORKTREE_N=2' "$BASE/wtB/.env" + + git worktree remove --force "$BASE/wtA" + git worktree add -q "$BASE/wtC" -b branchC + write_template "$BASE/wtC" + ( cd "$BASE/wtC" && "$ENV_INIT" ) + grep -qx 'WORKTREE_N=1' "$BASE/wtC/.env" +} From 2a946013f8ff167f7783ede69a0e19e0f37ae56a Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:41:17 +0900 Subject: [PATCH 2/7] =?UTF-8?q?env-init=20=E3=81=AE=20shebang=20=E3=82=92?= =?UTF-8?q?=20nix=20store=20=E3=81=AE=20bash=20=E3=81=AB=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux の hermetic な nix ビルドサンドボックスには /usr/bin/env が無く、 生スクリプトの `#!/usr/bin/env bash` が bad interpreter で落ちて CI が失敗していた。 インストールした libexec のコピーのみ shebang を bashNonInteractive の絶対パスへ 置換する(リポジトリ上の原本は #!/usr/bin/env bash のまま=非 Nix 利用者向けに可搬)。 Co-Authored-By: Claude Opus 4.8 --- pkgs/env-init/package.nix | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkgs/env-init/package.nix b/pkgs/env-init/package.nix index 335255b..bb7900a 100644 --- a/pkgs/env-init/package.nix +++ b/pkgs/env-init/package.nix @@ -6,14 +6,17 @@ # 先頭ヘッダコメントが押し下げられて usage が空になる。makeWrapper なら wrapper が # 生スクリプトを exec し `$0` が生スクリプトを指すため --help が正しく動く。 # -# PATH には bash も含める。生スクリプトの `#!/usr/bin/env bash` が wrapper の設定した -# PATH で bash を解決するため、決定的な bash を使わせる。runtimeInputs を prefix しても +# shebang の書き換え: 生スクリプトの `#!/usr/bin/env bash` は Linux の hermetic な nix ビルド +# サンドボックス (/usr/bin/env が無い) で bad interpreter になる。インストールした libexec の +# コピーだけ shebang を nix store のプレーン bash に固定する (リポジトリ上の原本は #!/usr/bin/env +# bash のまま=非 Nix 利用者向けに可搬)。patchShebangs はビルド環境次第で bash-interactive を +# 拾い closure を太らせるため、明示の bash に決定的に置換する。runtimeInputs を prefix しても # 既存 PATH は残る (suffix) ので、.env.template が呼ぶ openssl / op は消費側 PATH で解決できる。 { lib, runCommand, makeWrapper, - bash, + bashNonInteractive, coreutils, git, gawk, @@ -29,10 +32,11 @@ runCommand "env-init" } '' install -Dm755 ${./env-init} "$out/libexec/env-init" + substituteInPlace "$out/libexec/env-init" \ + --replace-fail '#!/usr/bin/env bash' '#!${bashNonInteractive}/bin/bash' makeWrapper "$out/libexec/env-init" "$out/bin/env-init" \ --prefix PATH : ${ lib.makeBinPath [ - bash coreutils git gawk From f961ae62066cbe874f3118ff380edf36220fb189 Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:53:34 +0900 Subject: [PATCH 3/7] =?UTF-8?q?CLAUDE.md=20=E3=81=AE=E3=83=91=E3=83=83?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B8=E6=96=B9=E9=87=9D=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit writeShellApplication 前提の記述だったが、実装は runCommand + makeWrapper で shellcheck も独立 check。将来のパッケージ作成者を誤導しないよう実態に合わせて 一般化した(Copilot レビュー指摘)。 Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3c44d8a..700ed8b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,10 +8,12 @@ org 横断で共有する汎用 dev ツールの集約先。ツールはまず ## 構成と開発の前提 -- パッケージは `pkgs//` に `package.nix`(`writeShellApplication` 派生)と生スクリプトを同居させる。生スクリプトは - `#!/usr/bin/env bash` 始まりで単体実行可能なまま保ち、wrap は外側に被せるだけで中身を書き換えない。 -- test / lint の真実は `flake.nix` の `checks`(bats・statix・deadnix・nixfmt、および writeShellApplication build 内包の - shellcheck)に一元化する。`devbox.json` の scripts はそれへの薄い委譲。 +- パッケージは `pkgs//` に `package.nix` と生スクリプトを同居させる。生スクリプトは `#!/usr/bin/env bash` + 始まりで単体実行可能なまま保ち、wrap は外側に被せるだけで中身を書き換えない(env-init は `runCommand` + `makeWrapper` + で PATH を被せ、shebang のみビルド時に絶対 bash へ固定する)。`writeShellApplication` はスクリプト本文前に + プリアンブルを差し込み `$0` 依存の挙動を壊しうるため、本文を保ちたいツールでは避ける。 +- test / lint の真実は `flake.nix` の `checks`(shellcheck・bats・statix・deadnix・nixfmt)に一元化する。 + `devbox.json` の scripts はそれへの薄い委譲。 - 開発環境は devbox(`devbox run check` など)。ツール一覧の二重管理を避けるため flake の devShell は設けない。 ## 公開リポジトリの制約 From cfadb4ee3add4375824f640d30024bf33be72c99 Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:02:42 +0900 Subject: [PATCH 4/7] =?UTF-8?q?Copilot=20=E6=8C=87=E6=91=98=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C:=20env-init=20=E3=83=98=E3=83=83=E3=83=80=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E3=81=A8=20dogfood=20=E3=81=AE=20openssl=20=E6=8F=90?= =?UTF-8?q?=E4=BE=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - env-init の --help ヘッダを本リポジトリの実態に合わせて更新(存在しない `task env:init` を削除し、コマンド名を `env-init`・起動は devbox init_hook に)。 移行後は本リポジトリが正典のため生スクリプトの記述を実態へ更新。 - devbox.json に openssl@3 を追加。ルート .env.template が `openssl rand` を使うため、 dogfood が環境非依存で SESSION_SECRET を生成できるようにする(空生成の silent failure を防止)。 Co-Authored-By: Claude Opus 4.8 --- devbox.json | 1 + devbox.lock | 124 +++++++++++++++++++++++++++++++++++++++++ pkgs/env-init/env-init | 8 +-- 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/devbox.json b/devbox.json index a50184e..a907e34 100644 --- a/devbox.json +++ b/devbox.json @@ -2,6 +2,7 @@ "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json", "packages": [ "git@2", + "openssl@3", "shellcheck@latest", "statix@latest", "deadnix@latest", diff --git a/devbox.lock b/devbox.lock index 78059c7..ba4e8fc 100644 --- a/devbox.lock +++ b/devbox.lock @@ -221,6 +221,130 @@ } } }, + "openssl@3": { + "last_modified": "2025-12-05T06:24:47Z", + "resolved": "github:NixOS/nixpkgs/42e29df35be6ef54091d3a3b4e97056ce0a98ce8#openssl", + "source": "devbox-search", + "version": "3.6.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/z9prisxci5h5lsk3rdknd4jzq7k9q13d-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/ii9mnzr3i92mgk9dkgg65739mavd0j6f-openssl-3.6.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/h0qgqik0mk0wn7rmm2kk3grfi1wzly74-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/yx3ip21fdaaxpjn5fbir02mqnaw9cm4f-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/3z54dgks2mz3dhwddj158sdibll8xmq5-openssl-3.6.0" + } + ], + "store_path": "/nix/store/z9prisxci5h5lsk3rdknd4jzq7k9q13d-openssl-3.6.0-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/wb6q44n9kcb5acmaa4rgqsajadx1fhhl-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/c9n1alb7ypzjvzd47m16fiwfczz23qs3-openssl-3.6.0-man", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/ci6d4k1sj4bnr892lsrqqmjiihqsk0bl-openssl-3.6.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/pq8b7fb3282g68pmk14mbyi20qn6chid-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/vaplp6w56dyz38986bgkf0pbg3r486b2-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/nj50gkyx813dxvfmsg1q8m330hmf3h86-openssl-3.6.0" + } + ], + "store_path": "/nix/store/wb6q44n9kcb5acmaa4rgqsajadx1fhhl-openssl-3.6.0-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/m3xwn9n0jypwjgi256idfzs979g30j29-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/hw43f3y1vl7ydrd4samnwnrwqqwkpisv-openssl-3.6.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/dirjrfjk8jgsbdpslgb51cav6qaxn2vm-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/va1zhkz0nfmycvd0h239hi4w40qgaxcx-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/q9a4wssx24xsy28w8kifdqizc01fh7sc-openssl-3.6.0" + } + ], + "store_path": "/nix/store/m3xwn9n0jypwjgi256idfzs979g30j29-openssl-3.6.0-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/a9jdl6xq9fc98ykpvqmc9kf0b0j9y8wh-openssl-3.6.0-man", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/sqv8kbdgfxlr2d6nysr8c2715qpsi6f5-openssl-3.6.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev" + }, + { + "name": "doc", + "path": "/nix/store/cgp9ig35iwicfb9spcrgyg2m5dmlcgrv-openssl-3.6.0-doc" + }, + { + "name": "out", + "path": "/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0" + } + ], + "store_path": "/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin" + } + } + }, "shellcheck@latest": { "last_modified": "2026-05-21T08:15:18Z", "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#shellcheck", diff --git a/pkgs/env-init/env-init b/pkgs/env-init/env-init index 402969b..7e217fd 100755 --- a/pkgs/env-init/env-init +++ b/pkgs/env-init/env-init @@ -18,11 +18,11 @@ # - N : worktree 番号 (例: `$((6100 + N))`) # # 使い方: -# bin/env-init # 冪等。既存 .env があれば skip (--force で再生成) -# bin/env-init --force # 既存 .env を上書き (N は WORKTREE_N から復元) -# bin/env-init -n 3 # worktree 番号 N を手動指定 +# env-init # 冪等。既存 .env があれば skip (--force で再生成) +# env-init --force # 既存 .env を上書き (N は WORKTREE_N から復元) +# env-init -n 3 # worktree 番号 N を手動指定 # -# devbox タスク経由: `task env:init` (引数は `-- --force` のように渡す) +# devbox の init_hook 経由: `[ -f .env ] || env-init` (PATH 上の env-init を起動) set -eu From d37813ff49b6a6171e8be8d60385906bd198541e Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:05:55 +0900 Subject: [PATCH 5/7] =?UTF-8?q?devbox=20packages=20=E3=82=92=20@latest=20?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=83=A1=E3=82=B8=E3=83=A3=E3=83=BC=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 意図しないメジャーアップグレードで破壊的変更を取り込むのを防ぐため、 shellcheck@0 / statix@0 / deadnix@1 / nixfmt@1 / bats@1 にピン。 解決されるバージョンは従来と同一で、devbox update 時の major 跳ねを抑止する。 Co-Authored-By: Claude Opus 4.8 --- devbox.json | 10 +++++----- devbox.lock | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/devbox.json b/devbox.json index a907e34..c79b8aa 100644 --- a/devbox.json +++ b/devbox.json @@ -3,11 +3,11 @@ "packages": [ "git@2", "openssl@3", - "shellcheck@latest", - "statix@latest", - "deadnix@latest", - "nixfmt@latest", - "bats@latest", + "shellcheck@0", + "statix@0", + "deadnix@1", + "nixfmt@1", + "bats@1", "path:.#env-init" ], "env": { diff --git a/devbox.lock b/devbox.lock index ba4e8fc..5e86c61 100644 --- a/devbox.lock +++ b/devbox.lock @@ -1,7 +1,7 @@ { "lockfile_version": "1", "packages": { - "bats@latest": { + "bats@1": { "last_modified": "2026-05-21T08:15:18Z", "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#bats", "source": "devbox-search", @@ -49,7 +49,7 @@ } } }, - "deadnix@latest": { + "deadnix@1": { "last_modified": "2026-05-21T08:15:18Z", "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#deadnix", "source": "devbox-search", @@ -173,7 +173,7 @@ "last_modified": "2026-06-01T17:55:45Z", "resolved": "github:NixOS/nixpkgs/4df1b885d76a54e1aa1a318f8d16fd6005b6401f?lastModified=1780336545&narHash=sha256-vhVhuXzFrIOfcssC%2F9hDHx7MHzDKjF3keHuREOQqQiQ%3D" }, - "nixfmt@latest": { + "nixfmt@1": { "last_modified": "2026-05-21T08:15:18Z", "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#nixfmt", "source": "devbox-search", @@ -345,7 +345,7 @@ } } }, - "shellcheck@latest": { + "shellcheck@0": { "last_modified": "2026-05-21T08:15:18Z", "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#shellcheck", "source": "devbox-search", @@ -449,7 +449,7 @@ } } }, - "statix@latest": { + "statix@0": { "last_modified": "2026-05-21T08:15:18Z", "resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#statix", "source": "devbox-search", From c254ace1a4b9d6b002263b1357ce56f96c6a1d37 Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:33:59 +0900 Subject: [PATCH 6/7] =?UTF-8?q?env-init:=20worktree=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E6=AF=94=E8=BC=83=E3=82=92=E7=89=A9=E7=90=86=E3=83=91=E3=82=B9?= =?UTF-8?q?=E3=81=B8=E6=AD=A3=E8=A6=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git rev-parse --show-toplevel と git worktree list --porcelain のパス表現が symlink でズレる環境(macOS の /var → /private/var 等)で、primary 判定や sibling 除外を誤り --force 時に worktree N がズレ得るのを修正。canonicalize_dir で REPO_ROOT / PRIMARY_PATH / 各 worktree パスを pwd -P 正規化してから比較する。 bats も BATS_TEST_TMPDIR を正規化せず使い、symlink 跨ぎの判定を実地検証する。 Co-Authored-By: Claude Opus 4.8 --- pkgs/env-init/env-init | 12 ++++++++++-- pkgs/env-init/tests/env-init.bats | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pkgs/env-init/env-init b/pkgs/env-init/env-init index 7e217fd..fd929c2 100755 --- a/pkgs/env-init/env-init +++ b/pkgs/env-init/env-init @@ -57,8 +57,15 @@ while [ $# -gt 0 ]; do esac done +# パスを物理パス (symlink 解決済み) に正規化する。`git rev-parse --show-toplevel` と +# `git worktree list --porcelain` のパス表現が symlink でズレる (macOS の /var → /private/var 等) +# と、primary 判定や sibling 除外を誤って N が意図せずズレる。比較に使うパスは全てこれを通す。 +canonicalize_dir() { + ( cd "$1" 2>/dev/null && pwd -P ) || printf '%s' "$1" +} + # リポジトリルート (このスクリプトを呼んだ worktree のルート) -REPO_ROOT=$(git rev-parse --show-toplevel) +REPO_ROOT=$(canonicalize_dir "$(git rev-parse --show-toplevel)") ENV_PATH="$REPO_ROOT/.env" TEMPLATE_PATH="$REPO_ROOT/.env.template" @@ -98,6 +105,7 @@ else echo "error: could not determine primary worktree path" >&2 exit 1 fi + PRIMARY_PATH=$(canonicalize_dir "$PRIMARY_PATH") if [ "$REPO_ROOT" = "$PRIMARY_PATH" ]; then N=0 @@ -110,7 +118,7 @@ else while IFS= read -r line; do case "$line" in "worktree "*) - wt_path=${line#worktree } + wt_path=$(canonicalize_dir "${line#worktree }") if [ "$wt_path" = "$REPO_ROOT" ] || [ "$wt_path" = "$PRIMARY_PATH" ]; then continue fi diff --git a/pkgs/env-init/tests/env-init.bats b/pkgs/env-init/tests/env-init.bats index 31c9a9a..6fcea7c 100644 --- a/pkgs/env-init/tests/env-init.bats +++ b/pkgs/env-init/tests/env-init.bats @@ -4,9 +4,10 @@ ENV_INIT="${ENV_INIT:-env-init}" setup() { - # macOS の $TMPDIR は /var → /private/var の symlink。git rev-parse --show-toplevel は - # 物理パスを返すため、worktree のパス比較が一致するよう base を物理パスに正規化する。 - BASE="$(cd "$BATS_TEST_TMPDIR" && pwd -P)" + # base を正規化せず $BATS_TEST_TMPDIR をそのまま使う。macOS では symlink (/var → /private/var) + # を含むため、env-init 側の canonicalize_dir によるパス正規化(symlink 跨ぎでの primary 判定・ + # sibling 除外)を実地で検証することになる。 + BASE="$BATS_TEST_TMPDIR" REPO="$BASE/repo" mkdir -p "$REPO" cd "$REPO" From 0e2b04170dec6e379a6bf4ff68abbce7a0107bbe Mon Sep 17 00:00:00 2001 From: Masakuni Kato <7091+mackato@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:50:18 +0900 Subject: [PATCH 7/7] =?UTF-8?q?env-init:=20git=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=A4=96=E5=AE=9F=E8=A1=8C=E3=82=92=E6=98=8E=E7=A2=BA=E3=81=AA?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=A7=E7=B5=82=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git 管理外で実行された場合に git の生 fatal だけが出ていた問題を修正し、 「git worktree 内で実行する」旨のメッセージで exit 1 する。あわせて、直前の canonicalize_dir 変更が git 失敗時の set -e 即時終了を握り潰して的外れな 「.env.template not found」を出していた退行も解消。bats に git 管理外ケースを追加。 Co-Authored-By: Claude Opus 4.8 --- pkgs/env-init/env-init | 9 +++++++-- pkgs/env-init/tests/env-init.bats | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkgs/env-init/env-init b/pkgs/env-init/env-init index fd929c2..40de76e 100755 --- a/pkgs/env-init/env-init +++ b/pkgs/env-init/env-init @@ -64,8 +64,13 @@ canonicalize_dir() { ( cd "$1" 2>/dev/null && pwd -P ) || printf '%s' "$1" } -# リポジトリルート (このスクリプトを呼んだ worktree のルート) -REPO_ROOT=$(canonicalize_dir "$(git rev-parse --show-toplevel)") +# リポジトリルート (このスクリプトを呼んだ worktree のルート)。git 管理外で実行された場合は +# git の生エラーではなく、何をすべきか分かるメッセージで終了する。 +if ! REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); then + echo "error: not inside a git worktree. Run env-init from within a git repository." >&2 + exit 1 +fi +REPO_ROOT=$(canonicalize_dir "$REPO_ROOT") ENV_PATH="$REPO_ROOT/.env" TEMPLATE_PATH="$REPO_ROOT/.env.template" diff --git a/pkgs/env-init/tests/env-init.bats b/pkgs/env-init/tests/env-init.bats index 6fcea7c..78e7627 100644 --- a/pkgs/env-init/tests/env-init.bats +++ b/pkgs/env-init/tests/env-init.bats @@ -40,6 +40,14 @@ EOF [ "$status" -eq 2 ] } +@test "git 管理外で実行すると明確なエラーで exit 1" { + mkdir -p "$BASE/nongit" + cd "$BASE/nongit" + run "$ENV_INIT" + [ "$status" -eq 1 ] + [[ "$output" == *"git worktree"* ]] +} + @test ".env.template が無ければ exit 1" { run "$ENV_INIT" [ "$status" -eq 1 ]