feat(env): PLAN03-1 PR2 devbase env import#15
Conversation
`devbase env import` を追加し、export で生成したバンドル (age 暗号化済み or 平文 tar.gz) から `.env` 群を復元できるようにする。 主な機能: - merge セマンティクス: --merge keep-existing (既定) / prefer-incoming - --replace-keys: 指定キーのみ上書き - --replace: 対象 .env を丸ごと差し替え (backup 取得) - --dry-run: 差分プレビュー (書き込みなし) - 2 フェーズ書き出し (prepare → commit) で部分適用を最小化、失敗時は backup から best-effort で rollback - --backup-dir / --keep-last N (既定 10) で backup を GC - .env.sources.yml は既定で上書きせず参照用コピーのみ、--merge-metadata で 新規 source エントリのみ追加、--no-metadata で完全無視 - 暗号化判定: gzip magic で平文 / age 暗号化を識別 - 引数バリデーション: SOURCE='-' と --passphrase-stdin の併用拒否、 --passphrase-env と --passphrase-stdin の併用拒否、--replace と --replace-keys の併用拒否 - 既定 identity: ~/.ssh/id_ed25519 → ~/.ssh/id_rsa の順で探索 テスト (tests/cli/test_env_import.py, 17 ケース): - export → import の round-trip、0600 permission 保持、LF 保持 - merge モード別の挙動 (keep-existing / prefer-incoming / replace-keys / replace)、dry-run が変更しないこと、replace 時の backup 作成 - --include-project / --no-metadata / --merge-metadata の挙動 - 未知 manifest version のバンドル拒否、--keep-last による古い backup GC - 平文バンドル import、passphrase round-trip Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | REQUEST_CHANGES
上記のデータ損失、部分適用、merge セマンティクスの修正が必要です。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | gemini | REQUEST_CHANGES
本PRは env import 機能を追加するものですが、バックアップの自動削除ロジックにおける安全性や、特定キー上書き時のマージ挙動に重大な問題が見つかりました。以下の指摘事項の修正をお願いします。
cross-review round 1 で指摘されたデータ損失・部分適用・merge 不整合を修正: - --replace-keys 指定外でも既存に無い incoming 新規キーは追加するように修正 (CLI help の "other keys behave like keep-existing" に整合) - _rollback で op='create' なターゲットは backup が無くても unlink し、 commit 途中失敗時の部分適用残骸を消す - _gc_backups は devbase が生成する timestamp 形式 (YYYYMMDD-HHMMSS) の ディレクトリのみを削除対象にし、--backup-dir 親に置かれた無関係なファイル /ディレクトリを誤って消さないようにする - EnvFile.parse_bytes(data) を新設し、io_import の bytes パースを 一時ファイル経由から直接パースに置き換え (I/O 削減 + 例外安全) - 上記 3 件分の回帰テストを追加 Refs: codex review #4336666744 / gemini review #4336672519
fix-pr round 1 サマリ (--defer-nit)cross-review round 1 (codex review #4336666744 / gemini review #4336672519) で指摘された全 7 件に対応しました。 重要度別件数 (独自再判定後)
修正内容 (commit 62363d7)
重複指摘の集約
いずれも対応コミットを同一 reply で全 thread に通知し、修正済み thread を Resolve しました。 テスト
CI 状態 (push 時点スナップショット)
|
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | gemini | REQUEST_CHANGES
ロールバック時の新規ファイル削除(sources.yml 対応)と、失敗時の一時ファイルクリーンアップの徹底を提案します。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | REQUEST_CHANGES
import は .env の値を変更せず復元できることが中核要件なので、merge/create 経路の parser/formatter round-trip を修正してください。あわせて CLI 追加分の completion と、バックアップディレクトリ名の衝突回避も対応が必要です。
PR #15 round 2 のクロスレビュー指摘 (codex 3 件 + gemini 2 件) に対応。 * EnvFile.parse_bytes に double-quote 内 escape の逆変換 (state machine) を 追加し、save / _format_env_bytes との round-trip を成立させる。これにより backslash / quote / 改行を含む値が import → export を経て二重エスケープ される問題を解消する。 * _plan_env_merge の create パスでは _format_env_bytes による再シリアライズを 避け、incoming_bytes をそのまま採用する。バンドル側のバイト列を完全に 保持し、parse/format に潜む副作用を確実に排除する。 * _rollback で「backup が無い = 元ファイル不在」のケースを op に関係なく unlink するように変更。op='sources-merge' で sources.yml を新規作成した ロールバックでも残骸が残らなくなる。 * _commit 失敗時に、まだ rename されていない .import.tmp ファイルを try-finally で確実に削除する。 * _make_backup_dir に microsecond + 連番フォールバックを付与し、同一秒内に import が複数回走っても backup ディレクトリが衝突しない。 * etc/devbase-completion.bash と etc/_devbase の env サブコマンド一覧に import を追加し、各オプションも補完できるようにする。 テストでは parse_bytes round-trip / create 経路の byte preservation / sources.yml rollback unlink / tmp cleanup / backup 衝突回避を検証する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 修正サマリ (PR #15)クロスレビュー round 2 で挙がった codex 3 件 + gemini 2 件、計 5 件にすべて対応しました。 修正コミット
対応内訳 (5/5 修正済み, deferred: 0, rejected: 0)
追加テスト (6 件)
ローカル品質チェック
|
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | codex | APPROVE
修正必須の指摘はありません。対象テストは uv run pytest /tmp/devbase-worktrees/pr15/tests/env /tmp/devbase-worktrees/pr15/tests/cli/test_env_export.py /tmp/devbase-worktrees/pr15/tests/cli/test_env_import.py /tmp/devbase-worktrees/pr15/tests/cli/test_prefix_resolution.py で 98 passed です。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | gemini | REQUEST_CHANGES
env import における既存の .env ファイルのマージ処理とエスケープ処理に、データの正確性を損なう可能性のある問題が見つかりました。特に、マージ時に既存のコメントや空行がすべて削除されてしまう点と、シェル変数展開を防ぐための $ のエスケープが不足している点は、破壊的な影響を及ぼす可能性があるため修正が必要です。
- ``EnvFile.parse_entries`` を追加し、コメント / 空行 / kv を行単位で トークン化できるようにする (PR #15 gemini 指摘)。 - ``_merge_into_existing_bytes`` で既存 ``.env`` のコメント・空行・キー順を 保持したまま値を差し替えるよう merge 経路を変更。``EnvFile.dump_bytes`` への単純差し替えではコメントが失われていた。 - ダブルクオート値に含まれる ``$`` を ``\$`` にエスケープし、``parse_bytes`` 側でも ``\$`` を ``$`` に戻すように round-trip 対応 (シェル ``source`` 時の 意図しない変数展開を防止 / PR #15 gemini 指摘)。 - ``EnvFile.dump_bytes`` / ``EnvFile.dump_entries_bytes`` にフォーマット ロジックを集約し、``io_import._format_env_bytes`` を廃止。``EnvFile.save`` も ``dump_bytes`` 経由に統一して二重実装を解消 (PR #15 gemini 指摘)。 - 新規テスト: コメント保持マージ (prefer-incoming / keep-existing)、 ``$`` の round-trip、``EnvFile.dump_bytes`` のエスケープ仕様を追加。
ndf:fix round 3 サマリgemini round 3 の指摘 3 件 (major×2, minor×1) を全て対応しました。codex round 3 は APPROVE で対象なし。 修正コミット
対応内容
新規テスト
ローカル検証
Resolve 済み thread
deferred / rejected はなし。再レビューをお願いします。 |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 4 | codex | APPROVE
修正要求に該当する指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 4 | gemini | REQUEST_CHANGES
PR #15 では、.env ファイルのマージ時にコメントや空行を保持する仕組みが導入され、全体の堅牢性が向上しています。特に $ のエスケープ対応はシェル環境での安全な利用に寄与する重要な改善です。
一方で、実装面でコードの重複がいくつか見受けられます。保守性の観点から、共通処理の括り出しを検討してください。
各マージ戦略 (replace_keys / keep-existing / prefer-incoming) で個別に書かれていた new_bytes 生成と _Plan 構築を `_build_merge_plan` にまとめた。各分岐は merged / added / overwritten / skipped の計算だけを担当するように整理。 動作変更なし (PR #15 gemini round4 指摘 / 既存テスト 102 件 PASS)。
fix round 4 サマリこのラウンドの対応
レビュー結果
対応内容
ローカル品質チェック
解決済みスレッド
|
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | gemini | REQUEST_CHANGES
共通処理の _build_merge_plan への括り出しによりコードの見通しは改善されましたが、ファイルの存在判定に existing (パース済み KV 辞書) を使用している箇所に重大なバグがあります。コメントのみの .env ファイルに対してマージを行う際、既存のコメントが全て失われる設計になっています。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | codex | REQUEST_CHANGES
既存prefix互換とコメント保持の回帰があるため、該当箇所の修正をお願いします。
- _build_merge_plan / _plan_env_merge / replace ブランチの op 判定を existing (key=value dict) の有無から target.exists() に変更。 コメント / 空行のみで構成された既存 .env が「create」と誤判定されて incoming_bytes で上書きされ既存コメントが失われる問題を修正 (PR #15 round5 codex/gemini 指摘)。 - SUBCMD_PREFIX_PREFERENCES に i: init を追加。import 追加で devbase env i が init / import の両方にマッチして ambiguous に なっていたため、既存ショートカット (devbase env i → init) を維持する (PR #15 round5 codex 指摘)。 - 回帰テスト追加: - tests/cli/test_env_import.py: コメント/空行のみの既存 .env が prefer-incoming / keep-existing / replace-keys でコメント保持される こと。--replace ブランチでも op='replace' として報告され backup が 取られること - tests/cli/test_prefix_resolution.py: devbase env i → init, devbase env im → import, devbase env in → init Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #15 Round 5 修正対応サマリcross-review round 5 で挙がった指摘 (gemini critical/major/minor + codex minor x2、計 5 thread) に対応しました。実体は 2 個のバグ (バグ A は 4 thread 重複指摘): 修正コミット
バグ A: コメント / 空行のみの既存 .env が create 扱いされる (4 thread)
判定基準を 回帰テストを
バグ B:
|
| 区分 | 件数 |
|---|---|
| 対応 (critical) | 1 |
| 対応 (major) | 1 |
| 対応 (minor) | 3 |
| deferred (nit) | 0 |
| rejected | 0 |
| 合計 thread | 5 (resolve 済み) |
ローカル品質チェック
uv run pytest: 109 passeduv run python -m compileall lib bin: OKuvx ruff check --select=E9,F63,F7,F82 lib: All checks passed
CI 状態: PR にはチェックが登録されていない (gh pr checks 15 → no checks reported)。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 6 | codex | APPROVE
修正が必要な指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 6 | gemini | APPROVE
PR #15 の実装を確認しました。env import の機能追加、アトミックな書き換えとロールバック、コメント・空行の保持ロジックは非常に堅牢に実装されており、テストも網羅的です。既存の env export (release/PLAN03-1 で実装済み) との round-trip 整合性も確保されています。
以下の 2 点、軽微な改善提案がありますが、機能自体は完成しているため APPROVE とします。
gemini round 6 で挙がった minor 指摘 2 件に対応:
1. lib/devbase/env/io_import.py:97 `import sys` がローカル import になっていたのを
ファイル先頭の標準 import セクションに移動 (`import getpass` も同時追加)。
メンテナンス性向上、PEP 8 への準拠。
2. lib/devbase/env/io_import.py:100 TTY 入力時に `sys.stdin.readline()` を使って
いたため、パスフレーズがそのまま画面にエコーバックされていた。
`getpass.getpass(prompt, stream=sys.stderr)` を使うように変更。
パイプ入力時 (非 TTY) は従来どおり `sys.stdin.readline()` で読み (
stdin リダイレクト経由のテストフックとして必要)、getpass の EOFError は
`ImportError("stdin からパスフレーズを読み取れませんでした")` に変換する。
回帰テスト 3 件を追加 (tests/cli/test_env_import.py):
- test_read_passphrase_uses_getpass_on_tty
- test_read_passphrase_falls_back_to_stdin_on_pipe
- test_read_passphrase_tty_eof_raises_import_error
ローカル品質チェック:
- uv run pytest tests/ -> 112 passed (107 + 3 + 既存 review round で +2)
- uv run python -m compileall lib bin -> OK
- uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix round 7 サマリcross-review round 6 で gemini APPROVE と同時に挙がった minor 改善提案 2 件に対応しました (codex round 6 は APPROVE で対象なし)。 修正コミット
対応内訳
件数サマリ
新規テスト (3 件)
ローカル品質チェック
CI 状態
Resolve 済み thread
関連 (本 PR スコープ外)
|
PR #15 round 7 で io_import.py に入れた修正 (commit 454425e) を、対称な io_export.py 側にも適用する。両者は _read_passphrase の構造が同じで、 TTY エコー抑止と import 位置の問題も同じパターンで残っていた。 - lib/devbase/env/io_export.py: - ローカル `import sys` をファイル先頭の標準 import セクションに移動 - `import getpass` を追加 - TTY 入力時に `getpass.getpass("passphrase: ", stream=sys.stderr)` を 使うように変更。EOFError は ExportError("stdin からパスフレーズを 読み取れませんでした") に変換 - パイプ入力時 (非 TTY) は従来どおり `sys.stdin.readline()` を使用 - tests/cli/test_env_export.py: - 旧テスト test_read_passphrase_shows_prompt_on_tty / test_read_passphrase_no_prompt_on_pipe は `print` 経由の prompt 表示を 検証する構造だったため、getpass.getpass をモックする方式に書き換え - 追加: test_read_passphrase_uses_getpass_on_tty (TTY 時に getpass.getpass が呼ばれ stdin は消費されないこと) - 追加: test_read_passphrase_falls_back_to_stdin_on_pipe (非 TTY では getpass は呼ばれず stdin.readline 経路に入ること) - 追加: test_read_passphrase_tty_eof_raises_export_error (EOFError → ExportError 変換) ローカル品質チェック: - uv run pytest tests/ -> 113 passed - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
追補: export 側にも同じ修正を適用
修正コミット
修正内容
スコープ判断PR #14 で merge 済みの
ため本 PR に同梱しました。 ローカル品質チェック
|
PR #15 ユーザーレビュー (lib/devbase/env/store.py:160) で挙がった 「pythonic に書き直してください」への対応。 before: バックスラッシュ + 次文字を 1 文字ずつ走査する hand-rolled state machine (25 行)。 after: re.compile(r'\\.') で \<char> を一括捕捉し、逆引き辞書 {'\\\\': '\\', '\\"': '"', '\\n': '\n', '\\$': '$'} で 1 行置換する re.sub 呼び出し 1 つに集約 (5 行)。 挙動は state machine と完全に同等: - 未知エスケープ (例: \x) は dict.get の default にマッチ文字列 自身を返すことでバックスラッシュごと保持 - 末尾の単独 \ は \\. のドットが 2 文字目を要求するため自然に マッチせず、そのまま保持 - \\\\n (リテラル \\ + n) と \\n (改行) も re.sub は左から非重複で マッチするため state machine と同じ結果 差分: 43 → 15 行 (net -28 行 削除 / +15 行 追加 = -13 行)。 既存テスト (test_envfile_parse_bytes_round_trip_with_escapes 等) で double-quote escape の round-trip は完全に検証済みのため新規テストは追加せず。 ローカル品質チェック: - uv run pytest tests/ -> 113 passed - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed - uv run python -m compileall lib bin -> OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix round 8 サマリユーザーレビュー (round 7 の追補後に追加された 1 件) に対応しました。 修正コミット
対応内訳
件数サマリ
行数削減
既存テストdouble-quote escape の round-trip は既存テストで網羅:
state machine と完全に同等な挙動を確認するため新規テストは追加せず。 ローカル品質チェック
CI 状態
Resolve 済み thread
|
* chore(PLAN03-1): release ブランチ作成 * feat(env): PLAN03-1 PR1 devbase env export (Local + Stdio) (#14) * chore(PLAN03-1-export-local): Draft PR 作成 * feat(env): devbase env export を追加 (PLAN03-1 PR1) - lib/devbase/env/bundle.py: tar.gz + manifest.yml バンドル構築/展開、sha256 検証、未知 version 拒否、パストラバーサル拒否 - lib/devbase/env/cipher.py: pyrage 経由の age 暗号化/復号 (X25519 / OpenSSH ed25519,rsa / passphrase / @path 参照) - lib/devbase/env/storage.py: Local + Stdio backend、s3/gs は本 PR では未実装で明示エラー - lib/devbase/env/io_export.py: 機密キー検知警告、既定鍵 (~/.ssh/id_rsa.pub) 自動利用、--passphrase-stdin と DEST='-' 併用拒否 - cli.py / commands/env.py: env export サブコマンド登録 + SUBCMD_MAP 更新 - pyproject.toml: pyrage>=1.2 を deps、pytest>=8.0 を dev group、tool.pytest.ini_options 追加 - tests/env, tests/cli: ラウンドトリップ + 異常系 28 件 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): レビュー指摘の修正 (storage/bundle/cipher) - storage.py: LocalBackend で file:// URI を url2pathname で実パスへ変換 - bundle.py: manifest.files の要素型 (dict, path: str, sha256: str) を検証 - cipher.py: age 秘密鍵判定をバイト列で行い、UTF-8 デコード失敗を明示エラー化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(env): round 2 レビュー指摘の修正 (堅牢性 + test 追加) - storage: file:// URI の netloc が空/localhost 以外なら StorageError で拒否 (codex major) - bundle: tar 内の重複エントリを BundleError で検出 (codex major) - cipher: _resolve_recipient の @path 再帰に深さ制限 (上限 5) を追加 (gemini minor) - tests/storage: file:// URI roundtrip と remote host 拒否の test を追加 (gemini minor) - tests/bundle: _validate_manifest 不正系 (files が list でない / entry が dict でない / path 不正 / sha256 不正) + 重複エントリの test を追加 (gemini minor) - tests/cipher: @path 循環参照で CipherError を返す test を追加 (gemini minor) * fix(env): sha256 必須化と ed25519 デフォルト鍵対応 (round 3) - bundle.py: manifest.files[*].sha256 を必須の 64 文字 16 進文字列として検証 None / 欠落 / 長さ違い / 非 16 進は BundleError。完全性チェック迂回を防止 - cipher.py: default_recipient_paths / default_identity_paths に ed25519 (id_ed25519.pub / id_ed25519) を追加し、rsa より優先 - tests: sha256 欠落 / None / 長さ違い / 非 16 進の異常系テストを追加 - tests: ed25519 がデフォルトパス候補に含まれ rsa より優先されることを検証 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(env): round 4 レビュー指摘の修正 (異常系の堅牢化) - bundle: yaml.safe_load の結果が dict でない場合に BundleError を送出 (top-level が list/str/数値の場合に AttributeError が漏れるのを防止) - cipher: @path 参照ファイルが UTF-8 でない場合 CipherError に包んで再送出 (UnicodeDecodeError が呼び出し側に漏れていた) - storage.resolve: Windows ドライブレター (C:\path 等) を urlparse が scheme と誤認する問題に対応し LocalBackend にフォールバック 各修正に対応する異常系 test を追加 (合計 +5 test)。 * fix(env): _resolve_identity の OSError を CipherError に包む (round 5) - lib/devbase/env/cipher.py: path.read_bytes() を try/except OSError で ラップし、I/O エラー時も CipherError で統一されたエラー型を返す - tests/env/test_cipher.py: monkeypatch で read_bytes に OSError を 発生させて CipherError に包まれることを検証する test を追加 gemini round 5 指摘 (minor / 堅牢性) に対応。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(env): round 6 レビュー指摘の修正 (決定性 + 完全性 + 堅牢性) - bundle.pack: gzip.GzipFile(mtime=0) でラップし出力を完全に決定的にする - bundle._validate_manifest: tar 内ファイルセットと manifest の完全一致を 検証し、manifest に記載のない未知ファイルを BundleError で拒否する - cipher._resolve_recipient: @path の read_text で発生する OSError を CipherError に包んで一貫したエラーハンドリングにする - cipher._resolve_identity: OpenSSH ヘッダで先に SSH 鍵を判別する分岐を 追加し、鍵形式判別を明示化 (将来の形式追加もしやすくする) - tests: pack 決定性 / unknown file 拒否 / @path OSError ラップ / OpenSSH ヘッダ優先判別の test を追加 * fix(env): @path 参照ファイルのコメント・空行をスキップする (round 6 追加) recipient ファイルにコメント (# 始まり) や空行が混在していても扱えるよう、 有効な最初の行のみを採用する。テストも追加。 * fix(env): round 1 レビュー指摘の修正 (TOCTOU + BundleError 統一 + prefix 互換 + completion) - storage.py: LocalBackend.write_bytes を os.open(mode=0o600, O_CREAT|O_TRUNC|O_WRONLY) で 作成時点から 0600 を強制し、umask に依らない TOCTOU 安全な書き込みに変更 (codex major / gemini minor — 同一指摘)。既存ファイル上書き時も先に chmod で権限を絞る。 read_bytes / write_bytes の OSError を StorageError にラップ (gemini minor)。 - bundle.py: unpack() の tarfile.open / getmembers / extractfile で発生する tarfile.TarError / OSError を BundleError にラップ (gemini major)。 make_entries_from_disk の exists() を is_file() に変更し、対象パスが ディレクトリだった場合の IsADirectoryError を防止 (gemini minor)。 _validate_manifest に manifest.files の path 重複検出を追加 (codex minor)。 - cli.py: SUBCMD_PREFIX_PREFERENCES を追加し、`devbase env e` が引き続き edit に 解決されるように prefix 解決の後方互換を維持 (codex minor)。 - etc/devbase-completion.bash, etc/_devbase: env export サブコマンドと 各オプションを補完に追加 (codex minor)。 - tests: storage の TOCTOU / OSError ラップ / 既存ファイル 0600 上書き、 bundle の path 重複 / 壊れた tar / is_file 切替、CLI prefix の後方互換テストを追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): round 2 advisory レビュー指摘の修正 (docstring / help / stdin prompt) - io_export.py: `_resolve_recipients` の docstring を更新し、既定鍵が `id_ed25519.pub` → `id_rsa.pub` の優先順で探索される実態に合わせる - cli.py: `--recipient` の help を `Default: ~/.ssh/id_ed25519.pub, then ~/.ssh/id_rsa.pub (first existing one)` に修正 - io_export.py: `--passphrase-stdin` で `sys.stdin.isatty()` の場合に `passphrase: ` プロンプトを stderr に表示し、対話実行時のハング感を解消 - 暗号化キー未指定エラーメッセージも ed25519 優先を反映 - tests/cli/test_env_export.py: tty / 非 tty 双方の挙動を検証する 2 ケース追加 Refs: PR #14 review comments 3280597873 / 3280597877 / 3280597881 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(env): PLAN03-1 PR2 devbase env import (#15) * chore: PLAN03-1 PR2 Draft PR 作成 (import 実装) * feat(env): PLAN03-1 PR2 devbase env import (Local + Stdio) `devbase env import` を追加し、export で生成したバンドル (age 暗号化済み or 平文 tar.gz) から `.env` 群を復元できるようにする。 主な機能: - merge セマンティクス: --merge keep-existing (既定) / prefer-incoming - --replace-keys: 指定キーのみ上書き - --replace: 対象 .env を丸ごと差し替え (backup 取得) - --dry-run: 差分プレビュー (書き込みなし) - 2 フェーズ書き出し (prepare → commit) で部分適用を最小化、失敗時は backup から best-effort で rollback - --backup-dir / --keep-last N (既定 10) で backup を GC - .env.sources.yml は既定で上書きせず参照用コピーのみ、--merge-metadata で 新規 source エントリのみ追加、--no-metadata で完全無視 - 暗号化判定: gzip magic で平文 / age 暗号化を識別 - 引数バリデーション: SOURCE='-' と --passphrase-stdin の併用拒否、 --passphrase-env と --passphrase-stdin の併用拒否、--replace と --replace-keys の併用拒否 - 既定 identity: ~/.ssh/id_ed25519 → ~/.ssh/id_rsa の順で探索 テスト (tests/cli/test_env_import.py, 17 ケース): - export → import の round-trip、0600 permission 保持、LF 保持 - merge モード別の挙動 (keep-existing / prefer-incoming / replace-keys / replace)、dry-run が変更しないこと、replace 時の backup 作成 - --include-project / --no-metadata / --merge-metadata の挙動 - 未知 manifest version のバンドル拒否、--keep-last による古い backup GC - 平文バンドル import、passphrase round-trip Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): import の merge/rollback/GC 安全性を改善 cross-review round 1 で指摘されたデータ損失・部分適用・merge 不整合を修正: - --replace-keys 指定外でも既存に無い incoming 新規キーは追加するように修正 (CLI help の "other keys behave like keep-existing" に整合) - _rollback で op='create' なターゲットは backup が無くても unlink し、 commit 途中失敗時の部分適用残骸を消す - _gc_backups は devbase が生成する timestamp 形式 (YYYYMMDD-HHMMSS) の ディレクトリのみを削除対象にし、--backup-dir 親に置かれた無関係なファイル /ディレクトリを誤って消さないようにする - EnvFile.parse_bytes(data) を新設し、io_import の bytes パースを 一時ファイル経由から直接パースに置き換え (I/O 削減 + 例外安全) - 上記 3 件分の回帰テストを追加 Refs: codex review #4336666744 / gemini review #4336672519 * fix(env): import の二重エスケープ / rollback / tmp 残骸 / completion を修正 PR #15 round 2 のクロスレビュー指摘 (codex 3 件 + gemini 2 件) に対応。 * EnvFile.parse_bytes に double-quote 内 escape の逆変換 (state machine) を 追加し、save / _format_env_bytes との round-trip を成立させる。これにより backslash / quote / 改行を含む値が import → export を経て二重エスケープ される問題を解消する。 * _plan_env_merge の create パスでは _format_env_bytes による再シリアライズを 避け、incoming_bytes をそのまま採用する。バンドル側のバイト列を完全に 保持し、parse/format に潜む副作用を確実に排除する。 * _rollback で「backup が無い = 元ファイル不在」のケースを op に関係なく unlink するように変更。op='sources-merge' で sources.yml を新規作成した ロールバックでも残骸が残らなくなる。 * _commit 失敗時に、まだ rename されていない .import.tmp ファイルを try-finally で確実に削除する。 * _make_backup_dir に microsecond + 連番フォールバックを付与し、同一秒内に import が複数回走っても backup ディレクトリが衝突しない。 * etc/devbase-completion.bash と etc/_devbase の env サブコマンド一覧に import を追加し、各オプションも補完できるようにする。 テストでは parse_bytes round-trip / create 経路の byte preservation / sources.yml rollback unlink / tmp cleanup / backup 衝突回避を検証する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): import の merge でコメントを保持し $ をエスケープする - ``EnvFile.parse_entries`` を追加し、コメント / 空行 / kv を行単位で トークン化できるようにする (PR #15 gemini 指摘)。 - ``_merge_into_existing_bytes`` で既存 ``.env`` のコメント・空行・キー順を 保持したまま値を差し替えるよう merge 経路を変更。``EnvFile.dump_bytes`` への単純差し替えではコメントが失われていた。 - ダブルクオート値に含まれる ``$`` を ``\$`` にエスケープし、``parse_bytes`` 側でも ``\$`` を ``$`` に戻すように round-trip 対応 (シェル ``source`` 時の 意図しない変数展開を防止 / PR #15 gemini 指摘)。 - ``EnvFile.dump_bytes`` / ``EnvFile.dump_entries_bytes`` にフォーマット ロジックを集約し、``io_import._format_env_bytes`` を廃止。``EnvFile.save`` も ``dump_bytes`` 経由に統一して二重実装を解消 (PR #15 gemini 指摘)。 - 新規テスト: コメント保持マージ (prefer-incoming / keep-existing)、 ``$`` の round-trip、``EnvFile.dump_bytes`` のエスケープ仕様を追加。 * refactor(env): _plan_env_merge の重複を _build_merge_plan に共通化 各マージ戦略 (replace_keys / keep-existing / prefer-incoming) で個別に書かれていた new_bytes 生成と _Plan 構築を `_build_merge_plan` にまとめた。各分岐は merged / added / overwritten / skipped の計算だけを担当するように整理。 動作変更なし (PR #15 gemini round4 指摘 / 既存テスト 102 件 PASS)。 * fix(env): コメントのみ既存 .env を merge 経路に通す + env i 短縮維持 - _build_merge_plan / _plan_env_merge / replace ブランチの op 判定を existing (key=value dict) の有無から target.exists() に変更。 コメント / 空行のみで構成された既存 .env が「create」と誤判定されて incoming_bytes で上書きされ既存コメントが失われる問題を修正 (PR #15 round5 codex/gemini 指摘)。 - SUBCMD_PREFIX_PREFERENCES に i: init を追加。import 追加で devbase env i が init / import の両方にマッチして ambiguous に なっていたため、既存ショートカット (devbase env i → init) を維持する (PR #15 round5 codex 指摘)。 - 回帰テスト追加: - tests/cli/test_env_import.py: コメント/空行のみの既存 .env が prefer-incoming / keep-existing / replace-keys でコメント保持される こと。--replace ブランチでも op='replace' として報告され backup が 取られること - tests/cli/test_prefix_resolution.py: devbase env i → init, devbase env im → import, devbase env in → init Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): import の sys を先頭 import に / TTY 時 getpass.getpass でエコー抑止 gemini round 6 で挙がった minor 指摘 2 件に対応: 1. lib/devbase/env/io_import.py:97 `import sys` がローカル import になっていたのを ファイル先頭の標準 import セクションに移動 (`import getpass` も同時追加)。 メンテナンス性向上、PEP 8 への準拠。 2. lib/devbase/env/io_import.py:100 TTY 入力時に `sys.stdin.readline()` を使って いたため、パスフレーズがそのまま画面にエコーバックされていた。 `getpass.getpass(prompt, stream=sys.stderr)` を使うように変更。 パイプ入力時 (非 TTY) は従来どおり `sys.stdin.readline()` で読み ( stdin リダイレクト経由のテストフックとして必要)、getpass の EOFError は `ImportError("stdin からパスフレーズを読み取れませんでした")` に変換する。 回帰テスト 3 件を追加 (tests/cli/test_env_import.py): - test_read_passphrase_uses_getpass_on_tty - test_read_passphrase_falls_back_to_stdin_on_pipe - test_read_passphrase_tty_eof_raises_import_error ローカル品質チェック: - uv run pytest tests/ -> 112 passed (107 + 3 + 既存 review round で +2) - uv run python -m compileall lib bin -> OK - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): export 側にも TTY エコー抑止と先頭 import を適用 (import と対称化) PR #15 round 7 で io_import.py に入れた修正 (commit 454425e) を、対称な io_export.py 側にも適用する。両者は _read_passphrase の構造が同じで、 TTY エコー抑止と import 位置の問題も同じパターンで残っていた。 - lib/devbase/env/io_export.py: - ローカル `import sys` をファイル先頭の標準 import セクションに移動 - `import getpass` を追加 - TTY 入力時に `getpass.getpass("passphrase: ", stream=sys.stderr)` を 使うように変更。EOFError は ExportError("stdin からパスフレーズを 読み取れませんでした") に変換 - パイプ入力時 (非 TTY) は従来どおり `sys.stdin.readline()` を使用 - tests/cli/test_env_export.py: - 旧テスト test_read_passphrase_shows_prompt_on_tty / test_read_passphrase_no_prompt_on_pipe は `print` 経由の prompt 表示を 検証する構造だったため、getpass.getpass をモックする方式に書き換え - 追加: test_read_passphrase_uses_getpass_on_tty (TTY 時に getpass.getpass が呼ばれ stdin は消費されないこと) - 追加: test_read_passphrase_falls_back_to_stdin_on_pipe (非 TTY では getpass は呼ばれず stdin.readline 経路に入ること) - 追加: test_read_passphrase_tty_eof_raises_export_error (EOFError → ExportError 変換) ローカル品質チェック: - uv run pytest tests/ -> 113 passed - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(env): _unescape_double_quoted を re.sub + 逆引き辞書に書き換え PR #15 ユーザーレビュー (lib/devbase/env/store.py:160) で挙がった 「pythonic に書き直してください」への対応。 before: バックスラッシュ + 次文字を 1 文字ずつ走査する hand-rolled state machine (25 行)。 after: re.compile(r'\\.') で \<char> を一括捕捉し、逆引き辞書 {'\\\\': '\\', '\\"': '"', '\\n': '\n', '\\$': '$'} で 1 行置換する re.sub 呼び出し 1 つに集約 (5 行)。 挙動は state machine と完全に同等: - 未知エスケープ (例: \x) は dict.get の default にマッチ文字列 自身を返すことでバックスラッシュごと保持 - 末尾の単独 \ は \\. のドットが 2 文字目を要求するため自然に マッチせず、そのまま保持 - \\\\n (リテラル \\ + n) と \\n (改行) も re.sub は左から非重複で マッチするため state machine と同じ結果 差分: 43 → 15 行 (net -28 行 削除 / +15 行 追加 = -13 行)。 既存テスト (test_envfile_parse_bytes_round_trip_with_escapes 等) で double-quote escape の round-trip は完全に検証済みのため新規テストは追加せず。 ローカル品質チェック: - uv run pytest tests/ -> 113 passed - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed - uv run python -m compileall lib bin -> OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(env): PLAN03-1 PR3 devbase env export/import S3 backend (#19) * chore: PLAN03-1 PR3 Draft PR 作成 (S3 backend) * feat(env): PLAN03-1 PR3 devbase env export/import S3 backend - `s3://bucket/key` を `devbase env export` / `devbase env import` の 入出力先として指定できるようにする - export 時は ServerSideEncryption (`aws:kms` 既定, `AES256` 切替可) を 常に PutObject に付与し、加えて GetBucketEncryption で **バケット側の 既定暗号化** も事前確認する - 暗号化未設定 / 確認不可 (AccessDenied) のバケットへは `--unsafe-allow-unencrypted-bucket` を明示しない限り export を拒否する (オブジェクト単位の SSE はこのフラグに関係なく常に付与される) - SSE 種別 / KMS 鍵 / エンドポイント / リージョンは環境変数 (`DEVBASE_S3_SSE`, `DEVBASE_S3_SSE_KMS_KEY_ID`, `DEVBASE_S3_ENDPOINT_URL`, `DEVBASE_S3_REGION`) で上書きできる - `boto3` は optional dep として `[project.optional-dependencies] s3` に追加 (`pip install 'devbase[s3]'` でインストール) - `gs://` (GCS) は PLAN03-1 PR4 廃案のため明示エラーで拒否する Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): PLAN03-1 PR3 storage.py minor 修正 (cross-review round 1) PR #19 のクロスレビュー (codex / gemini) で指摘された minor 3 件に対応。 - `_parse_s3_uri`: `urlparse` は S3 キーに含まれる `?` / `#` を query / fragment として落としてしまうため、AWS CLI と同じ挙動になるよう スキームを除去した上で `partition('/')` で分割する。 - boto3 未インストール時のエラーメッセージを `pip install boto3` から 本プロジェクトの optional dependency 経由 (`pip install 'devbase[s3]'` / `uv add 'devbase[s3]'`) に変更。 - `_verify_bucket_encryption`: MinIO / LocalStack 等の S3 互換ストレージで GetBucketEncryption が NotImplemented を返すケースに備え、 `--unsafe-allow-unencrypted-bucket` 指定時は未知エラーも警告のみで続行する 逃げ道として機能させる (CHANGELOG の S3 互換ストレージ対応との整合)。 新規テスト: query/fragment 保持、未知エラーの拒否、unsafe フラグでの続行を追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(env): PLAN03-1 PR3 boto3 を main dependency に昇格 boto3 を `[project.optional-dependencies].s3` から `[project].dependencies` に移し、ImportError ハンドラとフォローアップ案内文を撤去する。 意図: - S3 URI を初めて指定したユーザに `pip install 'devbase[s3]'` を 打たせる UX を廃する。25MB 程度のコスト増 (botocore 24MB) は 実装複雑度ゼロと引き換え。 - 引数検出 (`s3://` 走査) や lazy 自動 install を採らないのは、 CI / オフライン / read-only コンテナで挙動が安定するため。 storage.py / test_storage.py の boto3-missing 関連コードを削除。 CHANGELOG.md の optional 記述も同期更新。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(env): PLAN03-1 PR5 ドキュメント追加 + import/export リファクタ (#20) * refactor(env): PLAN03-1 PR5 env export/import モジュールを整理 - 711 行に肥大化していた `io_import.py` を以下の 3 モジュールに分割する。 公開 API (`ImportOptions`, `import_bundle`, `ImportError`) は維持し、 テストの `_read_passphrase` 直接 import や `getpass` パッチも継続して動く: - `io_import.py` (209 行): 引数検証 / 復号判定 / 全体オーケストレーション - `_import_merge.py`: `Plan` データクラス、`plan_env_merge` / `plan_sources`、 既存コメント・空行を保持した merge ロジック、ログ整形 - `_import_atomic.py`: 2 フェーズ書き込み (`backup_existing` → `write_atomic` → `commit`)、`gc_backups`、ロールバック - export / import で重複していた共通 helper を `io_common.py` に集約する: - `read_passphrase()` — env / stdin 入力、tty 時の getpass エコー抑止 - `resolve_recipient_specs()` / `resolve_identity_specs()` — 省略時の `~/.ssh/id_ed25519(.pub)` → `id_rsa(.pub)` fallback - `write_secure_bytes()` — `os.open(O_WRONLY|O_CREAT|O_TRUNC, mode=0o600)` で TOCTOU を避けてセキュアにバイト列を書き出す共通実装。`storage.LocalBackend` と `_import_atomic` から呼び出す - `_plan_env_merge()` の 4 段ネスト if/elif を 4 つの小さな関数 (`_plan_replace` / `_plan_replace_keys` / `_plan_keep_existing` / `_plan_prefer_incoming`) に分割し、`plan_env_merge` 本体はモード選択のみに 簡素化する - `storage.LocalBackend.write_bytes` を `io_common.write_secure_bytes` への薄いラッパに置き換え、重複していた `os.open` + chmod パターンを削除 - `io_export.py` (185 → 168 行) の `_read_passphrase` / `_resolve_recipients` は `io_common` への delegation に置き換え、`encrypt_payload` / `validate_options` の helper 関数に export 本体のロジックを分解 - `_commit()` 移動に伴い、テスト 3 件の `monkeypatch.setattr(_io_import.os, 'replace', ...)` パッチ先を `_import_atomic.os` に更新。`log_plans` 移動に伴う caplog の logger 名も `devbase.env._import_merge` に追従 リファクタの動機: - io_import.py が PR1〜PR3 を通じて 711 行まで肥大化し、merge 計画 / atomic 書き込み / orchestration が同居して読みづらかった - io_export と io_import で `_read_passphrase` / 既定鍵 fallback / セキュア書き込みが ほぼ重複していた 挙動の変更は無く、全 136 テストが引き続き green を維持する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(env): PLAN03-1 PR5 env export/import 利用者向けガイドを追加 `docs/user/env-export-import.md` (456 行) を新設し、以下を網羅する: - 対象ファイル一覧 (global / projects/*/.env / .env.sources.yml) と 公開可能な雛形 (`projects/*/env`) を含めない設計理由 - クイックスタート (既定鍵での export → 別マシンでの import) - バンドル構造 (manifest.yml の sha256 検証、version 互換ポリシー) - age 暗号化: recipient / identity / passphrase の 3 方式、対応鍵種別表、 ssh-ecdsa 非対応への対処 (`age-keygen` / `ssh-keygen -t ed25519`)、 既定鍵 (`~/.ssh/id_ed25519` → `id_rsa`) - 入出力先: ローカル / stdio / S3。S3 の SSE 強制と `--unsafe-allow-unencrypted-bucket`、`DEVBASE_S3_*` 環境変数 - export / import の全オプション表、merge モード (keep-existing / prefer-incoming / --replace-keys / --replace) の動作比較 - `.env.sources.yml` の取り扱い (既定スキップ + 参照用コピー、 `--merge-metadata` での新規エントリ追加) - 2 フェーズ書き込み + backup + ロールバックの仕組み、 `--keep-last N` での GC、ACID 非保証の注意 - 典型ワークフロー 4 件 (別マシン移行 / 定期バックアップ / S3 チーム共有 / CI 配布) - トラブルシューティング 8 件 加えて以下のリンクを追加: - `README.md`: 「利用者向け」ドキュメント表に env-export-import.md への リンクを追加し、`env` グループの説明に export / import を併記 - `docs/user/environment-variables.md`: 「別マシンへの移行 / バックアップ」 節を新設して env-export-import.md へ誘導、ベストプラクティスに追記 - `CHANGELOG.md` (Unreleased): docs 追加とリファクタリングを記載 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): warning ログの文面矛盾を解消 (unsafe フラグ続行時) `_verify_bucket_encryption` で `--unsafe-allow-unencrypted-bucket` 指定時に 「export を中止します。(unsafe フラグにより続行)」という矛盾した警告が出ていた。 問題説明 (problem) と対処案内 (guidance) を分離し、warning は problem のみ、 StorageError raise 時は問題+対処案内を出すよう統一。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): PR #13 round1 codex/gemini 指摘対応 - _import_merge: 未対応 arcname を黙って捨てず MergeError で停止 (filter_members の logger.debug+continue → raise MergeError) - _import_merge: merge 経路で値が変更されていないキーは raw 行を温存し、 PATH=$HOME/bin のような未クオート値が PATH="\$HOME/bin" に勝手に エスケープされて source 時の意味が変わるのを防ぐ - cipher: age-keygen 出力の先頭コメント (# created / # public key) を 考慮し、行単位でコメント / 空行を除いて AGE-SECRET-KEY-1 行を検出 - 上記 3 件に対する回帰テストを追加 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): PR #13 round2 gemini 指摘対応 - io_export._validate_options: DEST='-' (stdout) と --passphrase-stdin の 排他チェックを削除。stdin (passphrase) と stdout (bundle) は別ストリーム のため衝突しない (例: `echo pass | devbase env export - --passphrase-stdin > out`) - io_common.read_passphrase: stdin から読んだ行末を rstrip('\n') から rstrip('\r\n') に変更。Windows/WSL 由来 CRLF パイプで末尾 \r が残ると age 復号が無音で失敗するため。 - tests: 排他テストを併用許可テストに置換 + CRLF rstrip 回帰テスト追加 Refs: #13 (review) * fix(env): PR #13 round3 codex 指摘対応 - _import_merge.py の project 名正規表現を厳格化し、`./..`/隠しディレクトリを 弾く (path traversal: `env/projects/./.env` が `$DEVBASE_ROOT/projects/.env` に解決される問題への対策)。 - 上記の回帰テストを `tests/cli/test_env_import.py` に追加。 - zsh/bash 補完に `--unsafe-allow-unencrypted-bucket` (env export) を追加し CLI 定義と同期。 * fix(env): PR #13 round4 gemini 指摘対応 - cipher.py: `@PATH` ファイルに複数行の鍵が含まれる場合は明示的に CipherError を投げる。team_keys.txt のような複数公開鍵列挙を 暗黙に「最初の 1 人」だけ扱う挙動は、チーム運用で暗号化バンドル が壊れる原因になるため誤運用を防ぐ - io_common.py: `resolve_identity_specs` で `~/.ssh/id_ed25519` と `~/.ssh/id_rsa` の両方が存在する場合、両方を返すように変更。 `pyrage.decrypt` は複数 identity を受け付け、バンドルに合致する 鍵だけが使われる。これにより RSA で暗号化されたバンドルを ed25519 鍵だけで開けず失敗する問題を解消 - resolve_recipient_specs は意図的に最初の 1 つを返す挙動を維持 (どの鍵で暗号化するか一意に決める必要があるため) - 複数鍵ファイル拒否と複数 identity 復号の回帰テストを追加 * fix(env): PR #13 round5 codex/gemini 指摘対応 - bundle.is_valid_project_name() を導入し、export 側 (make_entries_from_disk) でも import 側 (_PROJECT_ENV_RE) と同じ project 名 validator を適用する。空白 / 先頭 `.` / `-` 等を含む ディレクトリは警告のみで skip し、round-trip 不能な bundle が 作られるのを防ぐ (codex round 5 指摘)。 - _import_merge._PROJECT_ENV_RE を bundle._VALID_PROJECT_NAME_RE から組み立てるよう変更し、import / export 両側の validator を 契約レベルで同期させる。 - S3Backend._verify_bucket_encryption で NoSuchBucket / 認証・接続 系エラー (code が取れないケース) は --unsafe-allow-unencrypted-bucket の有無に関わらず即 StorageError を投げる。続行しても put_object が 同じエラーで再失敗するだけのため、早期にエラーを返してユーザの トラブルシューティングを助ける (gemini round 5 指摘)。 - 回帰テスト追加: - test_is_valid_project_name / test_make_entries_from_disk_skips_invalid_project_names - test_make_entries_from_disk_invalid_name_explicitly_included_is_still_skipped - test_make_entries_from_disk_validator_matches_import_side (契約同期) - test_s3_backend_rejects_no_such_bucket_even_with_unsafe_flag - test_s3_backend_rejects_auth_or_network_error_without_aws_code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: 不要な migrate_ai_to_home.sh を削除 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): recipient と passphrase 同時指定を拒否 + docs 表現修正 - io_export._encrypt_payload で passphrase ありかつ opts.recipients 指定時に ExportError を送出。従来は recipients=[] に上書きされて cipher.encrypt 側の同時指定チェックを silently バイパスし、ユーザ が明示した --recipient が無視されていた (gemini round 6 指摘)。 - docs/user/env-export-import.md L201 の制約説明を修正。round 2 で export 側の DEST='-' x --passphrase-stdin 排他チェックを撤廃した ため、SOURCE='-' (import) のみ併用不可、export は併用可能であること を明記 (codex round 6 指摘)。 - recipient + passphrase 併用拒否の回帰テストを追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): PR #22 round1 codex/gemini 指摘対応 - bundle.py: manifest version チェックを厳密一致 (!=) に変更し、 0 や負数の未知スキーマを拒否 (codex major 指摘) - cli.py: import --identity の help を "(all existing ones)" に修正 (codex minor 指摘、resolve_identity_specs は全鍵を返す) - io_import.py: --identity と --passphrase-* の同時指定を _validate_options で拒否 (gemini major 指摘) - テスト追加: version=0/-1 の拒否、identity+passphrase 排他チェック Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): export オプション排他チェックを _validate_options に集約 (fail-fast) - --recipient と --passphrase-* の排他チェックを _encrypt_payload から _validate_options へ移動 (ディスク I/O 前に弾く) - --force-unencrypted と鍵指定の排他チェックを export() から _validate_options へ移動 - io_import._validate_options と対称的な構造に統一 - _validate_options 直接テスト 5 件追加 PR #22 round2 gemini [major / 堅牢性] 指摘対応 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): backup GC に dbenv- prefix で安全弁 + export 既定名を microsecond 精度に PR #22 round3 codex 指摘対応: 1. [major] _import_atomic.py: backup ディレクトリ名に `dbenv-` prefix を付与し、 --backup-dir 親に無関係なタイムスタンプ形式ディレクトリがあっても GC で rmtree しないようにした。旧フォーマット (prefix なし) は後方互換で GC 対象。 2. [minor] io_export.py: 既定出力名を秒精度から microsecond 精度に変更し、 同一秒の複数 export による無言上書きを防止。加えて既定名使用時に同名 ファイルが既に存在する場合は ExportError で失敗させる。 テスト追加: - test_gc_backups_ignores_bare_timestamp_dirs_from_other_tools - test_default_dest_includes_microsecond - test_export_default_dest_rejects_existing_file Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): opts.dest 空文字で既定名ガードがバイパスされる問題を修正 `opts.dest` が `""` (空文字) の場合、`opts.dest or _default_dest(...)` で 既定名に置換されるが、`if opts.dest is None` チェックが `False` となり 同名ファイル存在時の ExportError ガードがバイパスされていた。 `if not opts.dest` に修正し、空文字も None と同等に扱うようにした。 テスト `test_export_empty_dest_rejects_existing_file` を追加。 PR #22 round4 gemini 指摘対応 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
issues/PLAN03-1.mddevbase env importサブコマンド実装 (Local + Stdio)スコープ
lib/devbase/env/io_import.py新設--merge keep-existing(既定): 既存キーを保持、新規キーのみ追加--merge prefer-incoming: バンドル側の値で上書き--replace-keys KEY,...: 指定キーのみ上書き--replace: 対象.envを丸ごと差し替え (backup 取得)--dry-runで差分プレビュー--backup-dir/--keep-last N(既定 10) で backup GC.env.sources.yml取り扱いポリシー--no-metadata: 完全無視--merge-metadata: 新規 source のみ追加 (マシン固有値は再計算)SOURCE='-'と--passphrase-stdinの併用拒否Test plan
.env完全一致--dry-runが.envを変更しないこと--replaceで backup ディレクトリが作成されること0600を維持すること--keep-last N後に古い backup が削除されていること