@einja/dev-cli 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +89 -1
  2. package/dist/cli.js +1 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +71 -1
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/list.js.map +1 -1
  8. package/dist/commands/sync.d.ts.map +1 -1
  9. package/dist/commands/sync.js +187 -13
  10. package/dist/commands/sync.js.map +1 -1
  11. package/dist/lib/dependency-checker.d.ts.map +1 -1
  12. package/dist/lib/merger.d.ts +12 -0
  13. package/dist/lib/merger.d.ts.map +1 -1
  14. package/dist/lib/merger.js +28 -0
  15. package/dist/lib/merger.js.map +1 -1
  16. package/dist/lib/preset-update/cli-repo-detector.d.ts.map +1 -1
  17. package/dist/lib/preset-update/file-copier.d.ts.map +1 -1
  18. package/dist/lib/preset-update/preset-finder.d.ts.map +1 -1
  19. package/dist/lib/preset.d.ts.map +1 -1
  20. package/dist/lib/sync/category-validator.d.ts +1 -1
  21. package/dist/lib/sync/category-validator.d.ts.map +1 -1
  22. package/dist/lib/sync/category-validator.js +2 -1
  23. package/dist/lib/sync/category-validator.js.map +1 -1
  24. package/dist/lib/sync/category-validator.test.js +3 -1
  25. package/dist/lib/sync/category-validator.test.js.map +1 -1
  26. package/dist/lib/sync/conflict-reporter.d.ts.map +1 -1
  27. package/dist/lib/sync/diff-engine.d.ts.map +1 -1
  28. package/dist/lib/sync/file-filter.d.ts.map +1 -1
  29. package/dist/lib/sync/file-filter.js +1 -0
  30. package/dist/lib/sync/file-filter.js.map +1 -1
  31. package/dist/lib/sync/integration.test.js +255 -69
  32. package/dist/lib/sync/integration.test.js.map +1 -1
  33. package/dist/lib/sync/json-processor.d.ts +4 -4
  34. package/dist/lib/sync/json-processor.d.ts.map +1 -1
  35. package/dist/lib/sync/json-processor.js +11 -11
  36. package/dist/lib/sync/json-processor.js.map +1 -1
  37. package/dist/lib/sync/marker-processor.d.ts +60 -8
  38. package/dist/lib/sync/marker-processor.d.ts.map +1 -1
  39. package/dist/lib/sync/marker-processor.js +117 -26
  40. package/dist/lib/sync/marker-processor.js.map +1 -1
  41. package/dist/lib/sync/marker-processor.test.js +261 -40
  42. package/dist/lib/sync/marker-processor.test.js.map +1 -1
  43. package/dist/lib/sync/metadata-manager.d.ts +4 -0
  44. package/dist/lib/sync/metadata-manager.d.ts.map +1 -1
  45. package/dist/lib/sync/metadata-manager.js +15 -0
  46. package/dist/lib/sync/metadata-manager.js.map +1 -1
  47. package/dist/lib/sync/metadata-manager.test.js +68 -0
  48. package/dist/lib/sync/metadata-manager.test.js.map +1 -1
  49. package/dist/lib/sync/orphan-cleaner.d.ts +29 -0
  50. package/dist/lib/sync/orphan-cleaner.d.ts.map +1 -0
  51. package/dist/lib/sync/orphan-cleaner.js +80 -0
  52. package/dist/lib/sync/orphan-cleaner.js.map +1 -0
  53. package/dist/lib/sync/orphan-cleaner.test.d.ts +2 -0
  54. package/dist/lib/sync/orphan-cleaner.test.d.ts.map +1 -0
  55. package/dist/lib/sync/orphan-cleaner.test.js +169 -0
  56. package/dist/lib/sync/orphan-cleaner.test.js.map +1 -0
  57. package/dist/lib/sync/project-private-synchronizer.d.ts +52 -0
  58. package/dist/lib/sync/project-private-synchronizer.d.ts.map +1 -0
  59. package/dist/lib/sync/project-private-synchronizer.js +106 -0
  60. package/dist/lib/sync/project-private-synchronizer.js.map +1 -0
  61. package/dist/lib/sync/project-private-synchronizer.test.d.ts +2 -0
  62. package/dist/lib/sync/project-private-synchronizer.test.d.ts.map +1 -0
  63. package/dist/lib/sync/project-private-synchronizer.test.js +348 -0
  64. package/dist/lib/sync/project-private-synchronizer.test.js.map +1 -0
  65. package/dist/types/index.d.ts +1 -0
  66. package/dist/types/index.d.ts.map +1 -1
  67. package/dist/types/sync.d.ts +36 -6
  68. package/dist/types/sync.d.ts.map +1 -1
  69. package/dist/types/sync.js +2 -2
  70. package/dist/types/sync.js.map +1 -1
  71. package/package.json +5 -4
  72. package/presets/default/.claude/agents/einja/Explore.md +140 -0
  73. package/presets/default/.claude/agents/einja/backend-architect.md +4 -0
  74. package/presets/default/.claude/agents/einja/codex-agent.md +4 -0
  75. package/presets/default/.claude/agents/einja/design-engineer.md +4 -0
  76. package/presets/default/.claude/agents/einja/docs/docs-updater.md +4 -0
  77. package/presets/default/.claude/agents/einja/frontend-architect.md +4 -0
  78. package/presets/default/.claude/agents/einja/frontend-coder.md +4 -0
  79. package/presets/default/.claude/agents/einja/git/conflict-resolver.md +4 -0
  80. package/presets/default/.claude/agents/einja/specs/spec-design-generator.md +4 -1
  81. package/presets/default/.claude/agents/einja/specs/spec-qa-generator.md +4 -0
  82. package/presets/default/.claude/agents/einja/specs/spec-requirements-generator.md +4 -1
  83. package/presets/default/.claude/agents/einja/specs/spec-tasks-generator.md +6 -2
  84. package/presets/default/.claude/agents/einja/specs/spec-tasks-validator.md +4 -0
  85. package/presets/default/.claude/agents/einja/task/task-executer.md +57 -115
  86. package/presets/default/.claude/agents/einja/task/task-modification-analyzer.md +4 -0
  87. package/presets/default/.claude/agents/einja/task/task-qa.md +4 -0
  88. package/presets/default/.claude/agents/einja/task/task-reviewer.md +4 -0
  89. package/presets/default/.claude/commands/einja/einja-sync.md +5 -1
  90. package/presets/default/.claude/commands/einja/frontend-implement.md +3 -1
  91. package/presets/default/.claude/commands/einja/issue-exec.md +403 -0
  92. package/presets/default/.claude/commands/einja/spec-create.md +15 -1
  93. package/presets/default/.claude/commands/einja/start-dev.md +4 -0
  94. package/presets/default/.claude/commands/einja/sync-cursor-commands.md +4 -0
  95. package/presets/default/.claude/commands/einja/task-exec.md +106 -14
  96. package/presets/default/.claude/commands/einja/update-docs-by-task-specs.md +4 -0
  97. package/presets/default/.claude/hooks/einja/plan-mode-skill-loader.sh +23 -0
  98. package/presets/default/.claude/settings.json +15 -1
  99. package/presets/default/.claude/skills/einja-conflict-resolver/SKILL.md +4 -0
  100. package/presets/default/.claude/skills/einja-general-context-loader/SKILL.md +4 -0
  101. package/presets/default/.claude/skills/einja-output-format/SKILL.md +4 -0
  102. package/presets/default/.claude/skills/einja-project-overview/SKILL.md +7 -3
  103. package/presets/default/.claude/skills/einja-skill-creator/SKILL.md +266 -274
  104. package/presets/default/.claude/skills/einja-skill-creator/agents/analyzer.md +274 -0
  105. package/presets/default/.claude/skills/einja-skill-creator/agents/comparator.md +202 -0
  106. package/presets/default/.claude/skills/einja-skill-creator/agents/grader.md +195 -0
  107. package/presets/default/.claude/skills/einja-skill-creator/assets/eval_review.html +146 -0
  108. package/presets/default/.claude/skills/einja-skill-creator/eval-viewer/generate_review.py +471 -0
  109. package/presets/default/.claude/skills/einja-skill-creator/eval-viewer/viewer.html +1325 -0
  110. package/presets/default/.claude/skills/einja-skill-creator/references/schemas.md +430 -0
  111. package/presets/default/.claude/skills/einja-skill-creator/scripts/aggregate_benchmark.py +154 -0
  112. package/presets/default/.claude/skills/einja-skill-creator/scripts/generate_report.py +265 -0
  113. package/presets/default/.claude/skills/einja-skill-creator/scripts/improve_description.py +252 -0
  114. package/presets/default/.claude/skills/einja-skill-creator/scripts/init_skill.py +13 -19
  115. package/presets/default/.claude/skills/einja-skill-creator/scripts/package_skill.py +36 -7
  116. package/presets/default/.claude/skills/einja-skill-creator/scripts/run_eval.py +310 -0
  117. package/presets/default/.claude/skills/einja-skill-creator/scripts/run_loop.py +295 -0
  118. package/presets/default/.claude/skills/einja-skill-creator/scripts/utils.py +48 -0
  119. package/presets/default/.claude/skills/einja-spec-context-loader/SKILL.md +4 -0
  120. package/presets/default/.claude/skills/einja-task-commit/SKILL.md +4 -0
  121. package/presets/default/.claude/skills/einja-task-qa/SKILL.md +4 -0
  122. package/presets/default/.envrc +5 -0
  123. package/presets/default/.mcp.json +2 -12
  124. package/presets/default/CLAUDE.md.template +26 -4
  125. package/presets/default/docs/einja/example/specs/issues/issue999-example-task/tasks.md +1 -1
  126. package/presets/default/docs/einja/instructions/deployment-setup.md +3 -8
  127. package/presets/default/docs/einja/instructions/environment-setup.md +3 -8
  128. package/presets/default/docs/einja/instructions/issue-exec-workflow.md +276 -0
  129. package/presets/default/docs/einja/instructions/local-server-environment-and-worktree.md +70 -8
  130. package/presets/default/docs/einja/instructions/neon-cli-reference.md +3 -8
  131. package/presets/default/docs/einja/instructions/task-execute.md +23 -28
  132. package/presets/default/docs/einja/instructions/vercel-cli-reference.md +17 -10
  133. package/presets/default/docs/einja/steering/README.md +11 -11
  134. package/presets/default/docs/einja/steering/acceptance-criteria-and-qa-guide.md +3 -8
  135. package/presets/default/docs/einja/steering/architecture.md +3 -8
  136. package/presets/default/docs/einja/steering/branch-strategy.md +63 -70
  137. package/presets/default/docs/einja/steering/commit-rules.md +3 -8
  138. package/presets/default/docs/einja/steering/db-schema-design.md +3 -8
  139. package/presets/default/docs/einja/steering/development/api-development.md +3 -8
  140. package/presets/default/docs/einja/steering/development/backend-architecture.md +3 -8
  141. package/presets/default/docs/einja/steering/development/coding-standards.md +723 -0
  142. package/presets/default/docs/einja/steering/development/component-design.md +502 -0
  143. package/presets/default/docs/einja/steering/development/database-guidelines.md +54 -5
  144. package/presets/default/docs/einja/steering/development/frontend-development.md +3 -8
  145. package/presets/default/docs/einja/steering/development/playwright-guidelines.md +59 -0
  146. package/presets/default/docs/einja/steering/development/review-guidelines.md +3 -8
  147. package/presets/default/docs/einja/steering/development/testing-strategy.md +3 -8
  148. package/presets/default/docs/einja/steering/development-workflow.md +71 -124
  149. package/presets/default/docs/einja/steering/infrastructure/deployment.md +49 -55
  150. package/presets/default/docs/einja/steering/infrastructure/environment-variables.md +4 -8
  151. package/presets/default/docs/einja/steering/product.md +3 -8
  152. package/presets/default/docs/einja/steering/task-management.md +14 -98
  153. package/presets/default/scripts/ensure-serena.sh +75 -0
  154. package/presets/default/scripts/env-rotate-secrets.ts +336 -0
  155. package/presets/default/scripts/env-show.ts +130 -0
  156. package/presets/default/scripts/env.ts +479 -0
  157. package/presets/default/scripts/init.sh +92 -0
  158. package/presets/default/scripts/lib/env-common.ts +108 -0
  159. package/presets/default/scripts/lib/worktree-config.ts +64 -0
  160. package/presets/default/scripts/setup-dev.ts +640 -0
  161. package/presets/default/scripts/stop-serena.sh +25 -0
  162. package/presets/default/scripts/worktree/dev.ts +872 -0
  163. package/dist/lib/sync/seed-synchronizer.d.ts +0 -27
  164. package/dist/lib/sync/seed-synchronizer.d.ts.map +0 -1
  165. package/dist/lib/sync/seed-synchronizer.js +0 -72
  166. package/dist/lib/sync/seed-synchronizer.js.map +0 -1
  167. package/dist/lib/sync/seed-synchronizer.test.d.ts +0 -2
  168. package/dist/lib/sync/seed-synchronizer.test.d.ts.map +0 -1
  169. package/dist/lib/sync/seed-synchronizer.test.js +0 -147
  170. package/dist/lib/sync/seed-synchronizer.test.js.map +0 -1
  171. package/presets/default/.claude/skills/einja-api-development/SKILL.md +0 -14
  172. package/presets/default/.claude/skills/einja-backend-architecture/SKILL.md +0 -18
  173. package/presets/default/.claude/skills/einja-coding-standards/SKILL.md +0 -132
  174. package/presets/default/.claude/skills/einja-coding-standards/references/import-conventions.md +0 -69
  175. package/presets/default/.claude/skills/einja-coding-standards/references/naming-conventions.md +0 -107
  176. package/presets/default/.claude/skills/einja-coding-standards/references/prohibited-patterns.md +0 -169
  177. package/presets/default/.claude/skills/einja-coding-standards/references/typescript-rules.md +0 -247
  178. package/presets/default/.claude/skills/einja-component-design/SKILL.md +0 -109
  179. package/presets/default/.claude/skills/einja-component-design/references/directory-structure.md +0 -117
  180. package/presets/default/.claude/skills/einja-component-design/references/props-patterns.md +0 -159
  181. package/presets/default/.claude/skills/einja-component-design/references/styling-guide.md +0 -122
  182. package/presets/default/.claude/skills/einja-frontend-development/SKILL.md +0 -14
  183. package/presets/default/docs/einja/instructions/task-vibe-kanban-loop.md +0 -565
@@ -356,7 +356,7 @@ TDDは**3タスク分割(X.Y.1 テスト / X.Y.2 実装 / X.Y.3 リファク
356
356
 
357
357
  ### 依存関係の記述形式(重要)
358
358
 
359
- **🔴 `pnpm task:loop`スクリプトが認識できる形式のみ使用すること**
359
+ **🔴 `/einja:issue-exec` が認識できる形式のみ使用すること**
360
360
 
361
361
  | 記述形式 | 意味 | 例 |
362
362
  |---------|------|-----|
@@ -377,7 +377,7 @@ TDDは**3タスク分割(X.Y.1 テスト / X.Y.2 実装 / X.Y.3 リファク
377
377
  ### 1. タスクグループを選定・実行
378
378
  - `/einja:task-exec #{issue_number} {タスクグループ番号}`コマンドを実行
379
379
  - タスクグループ番号は必須引数(例: `1.1`, `2.3`)
380
- - **注意**: `pnpm task:loop <issue番号>`が何らかの理由で使えない場合のオプションの単発実行として`/einja:task-exec`を使用
380
+ - **注意**: 単一タスクグループを品質重視で確実に完了させたい場合に使用。`/einja:issue-exec` の Worker 内部でも呼ばれる
381
381
  - executer → reviewer → qa の3段階で実行
382
382
 
383
383
  ### 2. タスクを順次実装し、コミット
@@ -396,85 +396,6 @@ TDDは**3タスク分割(X.Y.1 テスト / X.Y.2 実装 / X.Y.3 リファク
396
396
  - または、ユーザーが明示的に指示した場合のみ自動更新
397
397
  - すべてのタスクグループが完了したら、IssueをClose
398
398
 
399
- ## Vibe-Kanbanタスク作成時の必須情報
400
-
401
- ### タスクタイトル形式
402
- ```
403
- [タスクグループ番号] タスクグループ名
404
- ```
405
-
406
- **例**: `[1.1] Server Core構築とDB設定`
407
-
408
- ### タスク説明形式
409
-
410
- ```markdown
411
- ## GitHub Issue
412
- #{issue_number}
413
-
414
- ## タスクグループ番号
415
- <タスクグループ番号>
416
-
417
- ## 概要
418
- <タスクグループの概要>
419
-
420
- ## タスク
421
- <GitHub Issueから抽出したタスク一覧>
422
-
423
- ## 依存関係
424
- <依存するタスクグループ番号>
425
-
426
- ## 完了条件
427
- <受け入れ基準を含む完了条件>
428
-
429
- ## 対応設計
430
- <design.mdの参照箇所>
431
- ```
432
-
433
- **例**:
434
- ```markdown
435
- ## GitHub Issue
436
- #17
437
-
438
- ## タスクグループ番号
439
- 1.1
440
-
441
- ## 概要
442
- packages/server-coreの初期化からDB接続設定まで、Server Coreの基盤構築を完了
443
-
444
- ## タスク
445
- - 1.1.1 packages/server-core初期化とtsconfig設定
446
- - 1.1.2 DB接続設定とマイグレーション
447
-
448
- ## 依存関係
449
- なし
450
-
451
- ## 完了条件
452
- server-coreパッケージが動作し、DBに接続できること(AC1.1を満たす)
453
-
454
- ## 対応設計
455
- design.md「Server Core構築」セクション
456
- ```
457
-
458
- ### 親Issue/サブIssue階層(task:loop使用時)
459
-
460
- `pnpm task:loop` 使用時は、Vibe-Kanban上でPhase→親Issue、タスクグループ→サブIssueの階層構造で管理されます。
461
-
462
- **親Issueタイトル形式**:
463
- ```
464
- [Issue{N} Phase{M}] {Phase名}
465
- ```
466
- 例: `[Issue17 Phase1] 基盤構築`
467
-
468
- **サブIssueタイトル形式**:
469
- ```
470
- [Issue{N} {X.Y}] {タスクグループ名}
471
- ```
472
- 例: `[Issue17 1.1] Server Core構築とDB設定`
473
-
474
- **親子関係の設定**: サブIssue作成後、REST API(PATCH `/api/remote/issues/{id}`)で `parent_issue_id` を設定します。
475
-
476
- **注意**: `/einja:task-exec` による単発実行時はこの階層構造は使用されません。`pnpm task:loop` 使用時のみ適用されます。
477
-
478
399
  ## コマンドリファレンス
479
400
 
480
401
  ### タスク管理関連コマンド
@@ -493,18 +414,18 @@ design.md「Server Core構築」セクション
493
414
  - executer → reviewer → qa の3段階で実行
494
415
  - QA合格後は追加指示待ち状態に入る
495
416
  - GitHub Issue更新はユーザーの明示的指示時のみ
496
- - **位置づけ**: `pnpm task:loop <issue番号>`が使えない場合の代替となる単発実行コマンド
417
+ - **位置づけ**: 単一タスクグループを品質重視で実行するコマンド。`/einja:issue-exec` Worker 内部でも使用される
497
418
 
498
- **自動ループ実行**:
419
+ **Issue全体の並列実行**:
499
420
  ```bash
500
- pnpm task:loop <issue番号>
501
- pnpm task:loop <issue番号> --max-group <番号> # 指定番号まで実行
502
- pnpm task:loop <issue番号> --branch <ブランチ> # ベースブランチ指定
421
+ /einja:issue-exec #<issue番号>
422
+ /einja:issue-exec #<issue番号> --merge-mode auto # 全自動モード
423
+ /einja:issue-exec #<issue番号> --max-phase <番号> # 指定Phaseまで実行
424
+ /einja:issue-exec #<issue番号> --base <ブランチ> # ベースブランチ指定
503
425
  ```
504
- - 着手可能なタスクグループを並列でVibe-Kanbanに登録
505
- - Done状態を監視して次のタスクを自動開始
506
- - Phase毎に親Issueを作成し、タスクグループをサブIssueとして管理(詳細は「親Issue/サブIssue階層」を参照)
507
- - **前提**: `npx @einja/cli init` 実行済み、Claude Code インストール済み
426
+ - Manager → Director → Worker の3階層で並列実行
427
+ - tmux セッションで全プロセスを監視可能
428
+ - マージモードで自動化レベルを制御
508
429
 
509
430
  **仕様書からドキュメント更新**:
510
431
  ```bash
@@ -513,11 +434,6 @@ pnpm task:loop <issue番号> --branch <ブランチ> # ベースブランチ指
513
434
  - タスク仕様書の内容をfeature仕様書とsteering仕様書に反映
514
435
  <!-- @einja:managed:end -->
515
436
 
516
- ---
517
-
518
- <!-- @einja:seed:start id="task-management-project" -->
519
- ## プロジェクト固有の設定
520
-
521
- <!-- このセクションはプロジェクト固有の内容を追記する場所です -->
522
- <!-- einja syncで上書きされません -->
523
- <!-- @einja:seed:end -->
437
+ <!-- @einja:project-private:start id="task-management-project" -->
438
+ <!-- プロジェクト固有の情報を記入 -->
439
+ <!-- @einja:project-private:end -->
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # Serena MCP サーバーの冪等起動
3
+ # .envrc から source して使用
4
+
5
+ # メインワークツリーをベースにする(worktree 間で共有)
6
+ _SERENA_BASE="${1:-$(pwd)}"
7
+ _SERENA_PORT_FILE="$_SERENA_BASE/.serena-port"
8
+ _SERENA_DEFAULT_PORT="${SERENA_PORT:-9850}"
9
+
10
+ # --- 既存インスタンスチェック(PIDベース) ---
11
+ if [ -f "$_SERENA_PORT_FILE" ]; then
12
+ read -r _saved_port _saved_pid < "$_SERENA_PORT_FILE"
13
+ if [ -n "$_saved_pid" ] && kill -0 "$_saved_pid" 2>/dev/null; then
14
+ # PIDが生存 → 自プロジェクトのSerena
15
+ export SERENA_PORT="$_saved_port"
16
+ return 0 2>/dev/null || true
17
+ fi
18
+ # PID死亡 → クリーンアップ
19
+ rm -f "$_SERENA_PORT_FILE"
20
+ fi
21
+
22
+ # --- uvx 確認 ---
23
+ if ! command -v uvx &> /dev/null; then
24
+ echo "[ensure-serena] Warning: uvx not found. Serena will not auto-start." >&2
25
+ echo "[ensure-serena] Install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh" >&2
26
+ return 0 2>/dev/null || true
27
+ fi
28
+
29
+ # --- 空きポート検出 ---
30
+ _port="$_SERENA_DEFAULT_PORT"
31
+ _port_found=false
32
+ for _i in $(seq 1 10); do
33
+ if ! lsof -i ":$_port" -sTCP:LISTEN > /dev/null 2>&1; then
34
+ _port_found=true
35
+ break
36
+ fi
37
+ _port=$((_port + 1))
38
+ done
39
+
40
+ if [ "$_port_found" = false ]; then
41
+ echo "[ensure-serena] Error: No available port found (tried $_SERENA_DEFAULT_PORT-$_port)" >&2
42
+ return 0 2>/dev/null || true
43
+ fi
44
+
45
+ # --- バックグラウンド起動 ---
46
+ echo "[ensure-serena] Starting Serena on port $_port..."
47
+ uvx --from git+https://github.com/oraios/serena \
48
+ serena start-mcp-server \
49
+ --transport streamable-http \
50
+ --host 127.0.0.1 \
51
+ --port "$_port" \
52
+ --context claude-code \
53
+ --project "$_SERENA_BASE" \
54
+ > /dev/null 2>&1 &
55
+ _serena_pid=$!
56
+ disown
57
+
58
+ # --- 起動待機(PID生存 + ポートLISTEN、最大30秒) ---
59
+ for _i in $(seq 1 60); do
60
+ if ! kill -0 "$_serena_pid" 2>/dev/null; then
61
+ echo "[ensure-serena] Warning: Serena process exited unexpectedly" >&2
62
+ return 0 2>/dev/null || true
63
+ fi
64
+ if lsof -p "$_serena_pid" -i ":$_port" -sTCP:LISTEN > /dev/null 2>&1; then
65
+ echo "$_port $_serena_pid" > "$_SERENA_PORT_FILE"
66
+ export SERENA_PORT="$_port"
67
+ echo "[ensure-serena] Serena ready on port $_port (PID: $_serena_pid)"
68
+ return 0 2>/dev/null || true
69
+ fi
70
+ sleep 0.5
71
+ done
72
+
73
+ # タイムアウト(起動失敗してもdirenvはブロックしない)
74
+ echo "[ensure-serena] Warning: Serena failed to start within 30s" >&2
75
+ return 0 2>/dev/null || true
@@ -0,0 +1,336 @@
1
+ /**
2
+ * 秘密鍵ローテーションスクリプト
3
+ *
4
+ * AUTH_SECRETとDOTENV_PRIVATE_KEY_*のローテーションを可能にする
5
+ * 使用方法: pnpm env:rotate-secrets
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import crypto from "node:crypto";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import * as p from "@clack/prompts";
13
+ import {
14
+ type EnvironmentConfig,
15
+ ENVIRONMENTS,
16
+ parseEnvFile,
17
+ getPrivateKey,
18
+ ENV_KEYS_PATH,
19
+ } from "./lib/env-common.js";
20
+
21
+ const cwd = process.cwd();
22
+
23
+ /**
24
+ * ローテーションタイプ
25
+ */
26
+ type RotationType = "auth" | "dotenv" | "both";
27
+
28
+ /**
29
+ * AUTH_SECRETを生成
30
+ *
31
+ * @returns 生成されたAUTH_SECRET(64文字のランダム16進数文字列)
32
+ */
33
+ function generateAuthSecret(): string {
34
+ return crypto.randomBytes(32).toString("hex");
35
+ }
36
+
37
+ /**
38
+ * ローテーションする秘密鍵を選択
39
+ *
40
+ * @returns 選択されたローテーションタイプ
41
+ */
42
+ async function selectRotationType(): Promise<RotationType> {
43
+ const rotationType = await p.select({
44
+ message: "ローテーションする秘密鍵を選択してください",
45
+ options: [
46
+ {
47
+ value: "auth" as const,
48
+ label: "AUTH_SECRET",
49
+ hint: "NextAuth署名鍵",
50
+ },
51
+ {
52
+ value: "dotenv" as const,
53
+ label: "DOTENV_PRIVATE_KEY",
54
+ hint: "dotenvx暗号化鍵",
55
+ },
56
+ {
57
+ value: "both" as const,
58
+ label: "両方",
59
+ hint: "AUTH_SECRET と DOTENV_PRIVATE_KEY",
60
+ },
61
+ ],
62
+ });
63
+
64
+ if (p.isCancel(rotationType)) {
65
+ p.cancel("キャンセルしました");
66
+ process.exit(0);
67
+ }
68
+
69
+ return rotationType;
70
+ }
71
+
72
+ /**
73
+ * 対象環境を選択(複数選択可)
74
+ *
75
+ * @returns 選択された環境の配列
76
+ */
77
+ async function selectTargetEnvironments(): Promise<EnvironmentConfig[]> {
78
+ // 利用可能な環境をチェック
79
+ const availableEnvs = ENVIRONMENTS.filter((env) => {
80
+ const envFilePath = path.join(cwd, env.file);
81
+ const hasFile = fs.existsSync(envFilePath);
82
+ const hasKey = getPrivateKey(env.privateKeyEnv) !== null;
83
+ return hasFile && hasKey;
84
+ });
85
+
86
+ if (availableEnvs.length === 0) {
87
+ p.log.error("ローテーション可能な環境がありません");
88
+ p.log.info("環境ファイルと秘密鍵が必要です");
89
+ process.exit(1);
90
+ }
91
+
92
+ const envOptions = availableEnvs.map((env) => ({
93
+ value: env.name,
94
+ label: env.description,
95
+ hint: env.file,
96
+ }));
97
+
98
+ const selectedEnvs = await p.multiselect({
99
+ message: "対象環境を選択してください(スペースで選択、Enterで確定)",
100
+ options: envOptions,
101
+ required: true,
102
+ });
103
+
104
+ if (p.isCancel(selectedEnvs)) {
105
+ p.cancel("キャンセルしました");
106
+ process.exit(0);
107
+ }
108
+
109
+ const selected = ENVIRONMENTS.filter((env) =>
110
+ (selectedEnvs as string[]).includes(env.name),
111
+ );
112
+
113
+ // 本番環境が含まれる場合は追加確認
114
+ const includesProduction = selected.some((env) => env.name === "production");
115
+ if (includesProduction) {
116
+ p.log.warn("⚠️ 本番環境の秘密鍵を変更します");
117
+ const confirmProd = await p.confirm({
118
+ message: "本当に本番環境の秘密鍵を変更しますか?",
119
+ initialValue: false,
120
+ });
121
+
122
+ if (p.isCancel(confirmProd) || !confirmProd) {
123
+ p.cancel("キャンセルしました");
124
+ process.exit(0);
125
+ }
126
+ }
127
+
128
+ return selected;
129
+ }
130
+
131
+ /**
132
+ * AUTH_SECRETをローテーション
133
+ *
134
+ * @param env - 対象環境
135
+ */
136
+ async function rotateAuthSecret(env: EnvironmentConfig): Promise<void> {
137
+ const envFilePath = path.join(cwd, env.file);
138
+ const privateKey = getPrivateKey(env.privateKeyEnv);
139
+
140
+ if (!privateKey) {
141
+ throw new Error(
142
+ `.env.keys に ${env.privateKeyEnv} が見つかりません`,
143
+ );
144
+ }
145
+
146
+ // dotenvx実行時の環境変数
147
+ const dotenvxEnv = { ...process.env, [env.privateKeyEnv]: privateKey };
148
+
149
+ // 復号
150
+ const decrypted = execSync(`dotenvx decrypt -f ${env.file} --stdout`, {
151
+ cwd,
152
+ encoding: "utf-8",
153
+ env: dotenvxEnv,
154
+ });
155
+
156
+ // 新しいAUTH_SECRETを生成
157
+ const newAuthSecret = generateAuthSecret();
158
+
159
+ // AUTH_SECRET行を置換
160
+ const updatedContent = decrypted.replace(
161
+ /^AUTH_SECRET=.*$/m,
162
+ `AUTH_SECRET=${newAuthSecret}`,
163
+ );
164
+
165
+ // テンポラリファイルに書き込み
166
+ const tmpPath = path.join(cwd, `${env.file}.tmp`);
167
+ fs.writeFileSync(tmpPath, updatedContent);
168
+
169
+ try {
170
+ // 元のファイルを削除してリネーム
171
+ fs.unlinkSync(envFilePath);
172
+ fs.renameSync(tmpPath, envFilePath);
173
+
174
+ // 再暗号化
175
+ execSync(`dotenvx encrypt -f ${env.file}`, {
176
+ cwd,
177
+ stdio: "pipe",
178
+ env: dotenvxEnv,
179
+ });
180
+
181
+ p.log.success(`✅ AUTH_SECRET をローテーションしました (${env.name})`);
182
+ } catch (error) {
183
+ // テンポラリファイルを削除
184
+ if (fs.existsSync(tmpPath)) {
185
+ fs.unlinkSync(tmpPath);
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * DOTENV_PRIVATE_KEYをローテーション
193
+ *
194
+ * @param env - 対象環境
195
+ */
196
+ async function rotateDotenvKey(env: EnvironmentConfig): Promise<void> {
197
+ const envFilePath = path.join(cwd, env.file);
198
+ const privateKey = getPrivateKey(env.privateKeyEnv);
199
+
200
+ if (!privateKey) {
201
+ throw new Error(
202
+ `.env.keys に ${env.privateKeyEnv} が見つかりません`,
203
+ );
204
+ }
205
+
206
+ // dotenvx実行時の環境変数
207
+ const dotenvxEnv = { ...process.env, [env.privateKeyEnv]: privateKey };
208
+
209
+ // dotenvx rotate を実行(新しいキーペアが自動生成され、.env.keysが自動更新される)
210
+ execSync(`dotenvx rotate -f ${env.file}`, {
211
+ cwd,
212
+ stdio: "pipe",
213
+ env: dotenvxEnv,
214
+ });
215
+
216
+ p.log.success(`✅ DOTENV_PRIVATE_KEY をローテーションしました (${env.name})`);
217
+ }
218
+
219
+ /**
220
+ * エラー時復元付きでローテーションを実行
221
+ *
222
+ * @param env - 対象環境
223
+ * @param type - ローテーションタイプ
224
+ */
225
+ async function rotateWithRecovery(
226
+ env: EnvironmentConfig,
227
+ type: RotationType,
228
+ ): Promise<void> {
229
+ const envFilePath = path.join(cwd, env.file);
230
+ const backupPath = path.join(cwd, `${env.file}.bak`);
231
+ const keysBackupPath = path.join(cwd, ".env.keys.bak");
232
+
233
+ // バックアップ作成
234
+ fs.copyFileSync(envFilePath, backupPath);
235
+ fs.copyFileSync(ENV_KEYS_PATH, keysBackupPath);
236
+
237
+ try {
238
+ if (type === "auth" || type === "both") {
239
+ await rotateAuthSecret(env);
240
+ }
241
+
242
+ if (type === "dotenv" || type === "both") {
243
+ await rotateDotenvKey(env);
244
+ }
245
+
246
+ // 成功したらバックアップを削除
247
+ fs.unlinkSync(backupPath);
248
+ if (type === "dotenv" || type === "both") {
249
+ fs.unlinkSync(keysBackupPath);
250
+ }
251
+ } catch (error) {
252
+ // エラー発生時はバックアップから復元
253
+ p.log.error(`❌ エラーが発生しました: ${error}`);
254
+ if (fs.existsSync(backupPath)) {
255
+ fs.copyFileSync(backupPath, envFilePath);
256
+ fs.unlinkSync(backupPath);
257
+ }
258
+ if (fs.existsSync(keysBackupPath)) {
259
+ fs.copyFileSync(keysBackupPath, ENV_KEYS_PATH);
260
+ fs.unlinkSync(keysBackupPath);
261
+ }
262
+ p.log.info("元のファイルを復元しました");
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * 次のステップを表示
269
+ *
270
+ * @param envs - ローテーションした環境の配列
271
+ */
272
+ function showNextSteps(envs: EnvironmentConfig[]): void {
273
+ const envFiles = envs.map((env) => env.file).join(" ");
274
+
275
+ p.note(
276
+ [
277
+ `git diff ${envFiles} .env.keys`,
278
+ `git add ${envFiles} .env.keys`,
279
+ 'git commit -m "chore: 秘密鍵をローテーション"',
280
+ "",
281
+ ".env.keys をチームと共有(1Password等)",
282
+ ].join("\n"),
283
+ "💡 次のステップ",
284
+ );
285
+ }
286
+
287
+ /**
288
+ * メイン処理
289
+ */
290
+ async function main(): Promise<void> {
291
+ p.intro("🔐 秘密鍵ローテーション");
292
+
293
+ // ローテーションタイプを選択
294
+ const rotationType = await selectRotationType();
295
+
296
+ // 対象環境を選択
297
+ const targetEnvs = await selectTargetEnvironments();
298
+
299
+ // 確認
300
+ const proceed = await p.confirm({
301
+ message: "ローテーションを実行しますか?",
302
+ initialValue: true,
303
+ });
304
+
305
+ if (p.isCancel(proceed) || !proceed) {
306
+ p.cancel("キャンセルしました");
307
+ process.exit(0);
308
+ }
309
+
310
+ const spinner = p.spinner();
311
+
312
+ // バックアップを作成中
313
+ spinner.start("バックアップを作成中...");
314
+ await new Promise((resolve) => setTimeout(resolve, 500));
315
+ spinner.stop("バックアップを作成しました");
316
+
317
+ // 各環境でローテーション実行
318
+ for (const env of targetEnvs) {
319
+ try {
320
+ await rotateWithRecovery(env, rotationType);
321
+ } catch (error) {
322
+ p.log.error(`${env.name} のローテーションに失敗しました`);
323
+ process.exit(1);
324
+ }
325
+ }
326
+
327
+ // 次のステップを表示
328
+ showNextSteps(targetEnvs);
329
+
330
+ p.outro("✅ 完了");
331
+ }
332
+
333
+ main().catch((error: unknown) => {
334
+ p.log.error(`エラーが発生しました: ${error}`);
335
+ process.exit(1);
336
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * 環境変数表示スクリプト(非対話式)
3
+ *
4
+ * 指定した環境の.envファイルを復号して表示
5
+ * 使用方法:
6
+ * pnpm env:show → .env.local
7
+ * pnpm env:show staging → .env.staging
8
+ * pnpm env:show production → .env.production
9
+ */
10
+
11
+ import { execSync } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+
15
+ const cwd = process.cwd();
16
+ const ENV_KEYS_PATH = path.join(cwd, ".env.keys");
17
+
18
+ interface EnvironmentConfig {
19
+ name: string;
20
+ file: string;
21
+ privateKeyEnv: string;
22
+ }
23
+
24
+ const ENVIRONMENTS: EnvironmentConfig[] = [
25
+ { name: "local", file: ".env.local", privateKeyEnv: "DOTENV_PRIVATE_KEY_LOCAL" },
26
+ { name: "develop", file: ".env.develop", privateKeyEnv: "DOTENV_PRIVATE_KEY_DEVELOP" },
27
+ { name: "staging", file: ".env.staging", privateKeyEnv: "DOTENV_PRIVATE_KEY_STAGING" },
28
+ { name: "preview", file: ".env.preview", privateKeyEnv: "DOTENV_PRIVATE_KEY_PREVIEW" },
29
+ { name: "production", file: ".env.production", privateKeyEnv: "DOTENV_PRIVATE_KEY_PRODUCTION" },
30
+ { name: "ci", file: ".env.ci", privateKeyEnv: "DOTENV_PRIVATE_KEY_CI" },
31
+ ];
32
+
33
+ function parseEnvFile(filePath: string): Record<string, string> {
34
+ if (!fs.existsSync(filePath)) {
35
+ return {};
36
+ }
37
+ const content = fs.readFileSync(filePath, "utf-8");
38
+ const result: Record<string, string> = {};
39
+
40
+ for (const line of content.split("\n")) {
41
+ const trimmed = line.trim();
42
+ if (!trimmed || trimmed.startsWith("#")) continue;
43
+
44
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
45
+ if (match) {
46
+ const key = match[1].trim();
47
+ let value = match[2].trim();
48
+ if (
49
+ (value.startsWith('"') && value.endsWith('"')) ||
50
+ (value.startsWith("'") && value.endsWith("'"))
51
+ ) {
52
+ value = value.slice(1, -1);
53
+ }
54
+ result[key] = value;
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ function getPrivateKey(privateKeyEnv: string): string | null {
61
+ if (!fs.existsSync(ENV_KEYS_PATH)) {
62
+ return null;
63
+ }
64
+ const keys = parseEnvFile(ENV_KEYS_PATH);
65
+ return keys[privateKeyEnv] || null;
66
+ }
67
+
68
+ function showUsage(): void {
69
+ console.log("使用方法: pnpm env:show [環境名]");
70
+ console.log("");
71
+ console.log("環境名:");
72
+ for (const env of ENVIRONMENTS) {
73
+ console.log(` ${env.name.padEnd(12)} → ${env.file}`);
74
+ }
75
+ console.log("");
76
+ console.log("例:");
77
+ console.log(" pnpm env:show # .env.local を表示");
78
+ console.log(" pnpm env:show staging # .env.staging を表示");
79
+ }
80
+
81
+ function main(): void {
82
+ const arg = process.argv[2];
83
+
84
+ // ヘルプ
85
+ if (arg === "-h" || arg === "--help") {
86
+ showUsage();
87
+ process.exit(0);
88
+ }
89
+
90
+ // 環境を特定
91
+ const envName = arg || "local";
92
+ const env = ENVIRONMENTS.find((e) => e.name === envName);
93
+
94
+ if (!env) {
95
+ console.error(`❌ 不明な環境: ${envName}`);
96
+ console.error("");
97
+ showUsage();
98
+ process.exit(1);
99
+ }
100
+
101
+ const envFilePath = path.join(cwd, env.file);
102
+
103
+ if (!fs.existsSync(envFilePath)) {
104
+ console.error(`❌ ${env.file} が見つかりません`);
105
+ process.exit(1);
106
+ }
107
+
108
+ const privateKey = getPrivateKey(env.privateKeyEnv);
109
+ if (!privateKey) {
110
+ console.error(`❌ .env.keys に ${env.privateKeyEnv} が見つかりません`);
111
+ console.error(" チームから .env.keys を共有してもらってください");
112
+ process.exit(1);
113
+ }
114
+
115
+ // dotenvx で復号
116
+ try {
117
+ const decrypted = execSync(`dotenvx decrypt -f ${env.file} --stdout`, {
118
+ cwd,
119
+ encoding: "utf-8",
120
+ env: { ...process.env, [env.privateKeyEnv]: privateKey },
121
+ });
122
+ console.log(decrypted);
123
+ } catch (error) {
124
+ console.error("❌ 復号に失敗しました");
125
+ console.error(error);
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ main();