@einja/dev-cli 0.1.40 → 0.1.44

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 (207) 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/commands/task-loop/lib/github-client.test.js.map +1 -1
  12. package/dist/commands/task-loop/lib/vibe-kanban-rest-client.js +2 -2
  13. package/dist/commands/task-loop/lib/vibe-kanban-rest-client.js.map +1 -1
  14. package/dist/lib/dependency-checker.d.ts.map +1 -1
  15. package/dist/lib/merger.d.ts +12 -0
  16. package/dist/lib/merger.d.ts.map +1 -1
  17. package/dist/lib/merger.js +28 -0
  18. package/dist/lib/merger.js.map +1 -1
  19. package/dist/lib/preset-update/cli-repo-detector.d.ts.map +1 -1
  20. package/dist/lib/preset-update/file-copier.d.ts.map +1 -1
  21. package/dist/lib/preset-update/file-copier.js +3 -3
  22. package/dist/lib/preset-update/file-copier.js.map +1 -1
  23. package/dist/lib/preset-update/preset-finder.d.ts.map +1 -1
  24. package/dist/lib/preset.d.ts.map +1 -1
  25. package/dist/lib/sync/category-validator.d.ts +1 -1
  26. package/dist/lib/sync/category-validator.d.ts.map +1 -1
  27. package/dist/lib/sync/category-validator.js +2 -1
  28. package/dist/lib/sync/category-validator.js.map +1 -1
  29. package/dist/lib/sync/category-validator.test.js +3 -1
  30. package/dist/lib/sync/category-validator.test.js.map +1 -1
  31. package/dist/lib/sync/conflict-reporter.d.ts.map +1 -1
  32. package/dist/lib/sync/diff-engine.d.ts.map +1 -1
  33. package/dist/lib/sync/file-filter.d.ts.map +1 -1
  34. package/dist/lib/sync/file-filter.js +1 -0
  35. package/dist/lib/sync/file-filter.js.map +1 -1
  36. package/dist/lib/sync/integration.test.js +255 -69
  37. package/dist/lib/sync/integration.test.js.map +1 -1
  38. package/dist/lib/sync/json-processor.d.ts +4 -4
  39. package/dist/lib/sync/json-processor.d.ts.map +1 -1
  40. package/dist/lib/sync/json-processor.js +11 -11
  41. package/dist/lib/sync/json-processor.js.map +1 -1
  42. package/dist/lib/sync/marker-processor.d.ts +60 -8
  43. package/dist/lib/sync/marker-processor.d.ts.map +1 -1
  44. package/dist/lib/sync/marker-processor.js +117 -26
  45. package/dist/lib/sync/marker-processor.js.map +1 -1
  46. package/dist/lib/sync/marker-processor.test.js +261 -40
  47. package/dist/lib/sync/marker-processor.test.js.map +1 -1
  48. package/dist/lib/sync/metadata-manager.d.ts +4 -0
  49. package/dist/lib/sync/metadata-manager.d.ts.map +1 -1
  50. package/dist/lib/sync/metadata-manager.js +15 -0
  51. package/dist/lib/sync/metadata-manager.js.map +1 -1
  52. package/dist/lib/sync/metadata-manager.test.js +69 -0
  53. package/dist/lib/sync/metadata-manager.test.js.map +1 -1
  54. package/dist/lib/sync/orphan-cleaner.d.ts +29 -0
  55. package/dist/lib/sync/orphan-cleaner.d.ts.map +1 -0
  56. package/dist/lib/sync/orphan-cleaner.js +80 -0
  57. package/dist/lib/sync/orphan-cleaner.js.map +1 -0
  58. package/dist/lib/sync/orphan-cleaner.test.d.ts +2 -0
  59. package/dist/lib/sync/orphan-cleaner.test.d.ts.map +1 -0
  60. package/dist/lib/sync/orphan-cleaner.test.js +169 -0
  61. package/dist/lib/sync/orphan-cleaner.test.js.map +1 -0
  62. package/dist/lib/sync/project-private-synchronizer.d.ts +52 -0
  63. package/dist/lib/sync/project-private-synchronizer.d.ts.map +1 -0
  64. package/dist/lib/sync/project-private-synchronizer.js +110 -0
  65. package/dist/lib/sync/project-private-synchronizer.js.map +1 -0
  66. package/dist/lib/sync/project-private-synchronizer.test.d.ts +2 -0
  67. package/dist/lib/sync/project-private-synchronizer.test.d.ts.map +1 -0
  68. package/dist/lib/sync/project-private-synchronizer.test.js +348 -0
  69. package/dist/lib/sync/project-private-synchronizer.test.js.map +1 -0
  70. package/dist/types/index.d.ts +1 -0
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/dist/types/sync.d.ts +36 -6
  73. package/dist/types/sync.d.ts.map +1 -1
  74. package/dist/types/sync.js +2 -2
  75. package/dist/types/sync.js.map +1 -1
  76. package/package.json +5 -4
  77. package/presets/default/.claude/agents/einja/Explore.md +140 -0
  78. package/presets/default/.claude/agents/einja/backend-architect.md +21 -1
  79. package/presets/default/.claude/agents/einja/codex-agent.md +5 -1
  80. package/presets/default/.claude/agents/einja/design-engineer.md +5 -1
  81. package/presets/default/.claude/agents/einja/docs/docs-updater.md +7 -93
  82. package/presets/default/.claude/agents/einja/frontend-architect.md +21 -1
  83. package/presets/default/.claude/agents/einja/frontend-coder.md +5 -1
  84. package/presets/default/.claude/agents/einja/{specs/spec-design-generator.md → issue-specs/design-generator.md} +16 -8
  85. package/presets/default/.claude/agents/einja/{specs/spec-qa-generator.md → issue-specs/qa-generator.md} +10 -4
  86. package/presets/default/.claude/agents/einja/{specs/spec-requirements-generator.md → issue-specs/requirements-generator.md} +9 -6
  87. package/presets/default/.claude/agents/einja/{specs/spec-tasks-generator.md → issue-specs/tasks-generator.md} +19 -16
  88. package/presets/default/.claude/agents/einja/{specs/spec-tasks-validator.md → issue-specs/tasks-validator.md} +13 -9
  89. package/presets/default/.claude/agents/einja/issue-specs/ui-design-generator.md +114 -0
  90. package/presets/default/.claude/agents/einja/task/task-executer.md +64 -116
  91. package/presets/default/.claude/agents/einja/task/task-modification-analyzer.md +6 -2
  92. package/presets/default/.claude/agents/einja/task/task-qa.md +7 -3
  93. package/presets/default/.claude/agents/einja/task/task-reviewer.md +17 -1
  94. package/presets/default/.claude/commands/einja/einja-sync.md +124 -45
  95. package/presets/default/.claude/commands/einja/frontend-implement.md +3 -1
  96. package/presets/default/.claude/commands/einja/issue-exec.md +413 -0
  97. package/presets/default/.claude/commands/einja/start-dev.md +4 -0
  98. package/presets/default/.claude/commands/einja/sync-cursor-commands.md +10 -6
  99. package/presets/default/.claude/commands/einja/{update-docs-by-task-specs.md → update-docs-by-issue-specs.md} +61 -57
  100. package/presets/default/.claude/hooks/einja/plan-mode-skill-loader.sh +27 -0
  101. package/presets/default/.claude/settings.json +29 -5
  102. package/presets/default/.claude/skills/{einja-general-context-loader → _einja-general-context-loader}/SKILL.md +6 -2
  103. package/presets/default/.claude/skills/{einja-output-format → _einja-output-format}/SKILL.md +5 -1
  104. package/presets/default/.claude/skills/_einja-project-overview/SKILL.md +29 -0
  105. package/presets/default/.claude/skills/{einja-spec-context-loader → _einja-spec-context-loader}/SKILL.md +9 -5
  106. package/presets/default/.claude/skills/einja-coding-standards/references/testing-strategy.md +899 -0
  107. package/presets/default/.claude/skills/einja-conflict-resolver/SKILL.md +5 -1
  108. package/presets/default/.claude/skills/einja-create-pr/SKILL.md +138 -0
  109. package/presets/default/.claude/skills/einja-infra-maintenance/SKILL.md +779 -0
  110. package/presets/default/.claude/{commands/einja/spec-create.md → skills/einja-issue-spec-create/SKILL.md} +60 -23
  111. package/presets/default/.claude/skills/einja-issue-spec-generator/SKILL.md +105 -0
  112. package/presets/default/.claude/skills/einja-issue-spec-generator/references/format-rules.md +35 -0
  113. package/presets/default/.claude/skills/einja-issue-spec-validator/SKILL.md +130 -0
  114. package/presets/default/.claude/skills/einja-issue-spec-validator/references/validation-rules.md +52 -0
  115. package/presets/default/.claude/skills/einja-npm-release/SKILL.md +242 -0
  116. package/presets/default/.claude/skills/einja-skill-creator/SKILL.md +311 -263
  117. package/presets/default/.claude/skills/einja-skill-creator/agents/analyzer.md +274 -0
  118. package/presets/default/.claude/skills/einja-skill-creator/agents/comparator.md +202 -0
  119. package/presets/default/.claude/skills/einja-skill-creator/agents/grader.md +195 -0
  120. package/presets/default/.claude/skills/einja-skill-creator/assets/eval_review.html +146 -0
  121. package/presets/default/.claude/skills/einja-skill-creator/eval-viewer/generate_review.py +471 -0
  122. package/presets/default/.claude/skills/einja-skill-creator/eval-viewer/viewer.html +1325 -0
  123. package/presets/default/.claude/skills/einja-skill-creator/references/schemas.md +430 -0
  124. package/presets/default/.claude/skills/einja-skill-creator/scripts/aggregate_benchmark.py +401 -0
  125. package/presets/default/.claude/skills/einja-skill-creator/scripts/compare_runs.py +154 -0
  126. package/presets/default/.claude/skills/einja-skill-creator/scripts/generate_report.py +272 -0
  127. package/presets/default/.claude/skills/einja-skill-creator/scripts/improve_description.py +247 -0
  128. package/presets/default/.claude/skills/einja-skill-creator/scripts/init_skill.py +13 -19
  129. package/presets/default/.claude/skills/einja-skill-creator/scripts/package_skill.py +36 -7
  130. package/presets/default/.claude/skills/einja-skill-creator/scripts/run_eval.py +310 -0
  131. package/presets/default/.claude/skills/einja-skill-creator/scripts/run_loop.py +375 -0
  132. package/presets/default/.claude/skills/einja-skill-creator/scripts/utils.py +48 -0
  133. package/presets/default/.claude/skills/einja-skill-first/SKILL.md +265 -0
  134. package/presets/default/.claude/skills/einja-subagent-question-protocol/SKILL.md +98 -0
  135. package/presets/default/.claude/skills/einja-task-commit/SKILL.md +11 -7
  136. package/presets/default/.claude/{commands/einja/task-exec.md → skills/einja-task-exec/SKILL.md} +106 -89
  137. package/presets/default/.claude/skills/einja-task-qa/SKILL.md +8 -4
  138. package/presets/default/.claude/skills/einja-task-qa/references/troubleshooting.md +1 -1
  139. package/presets/default/.claude/skills/einja-task-qa/references/usage-patterns.md +2 -2
  140. package/presets/default/.claude/skills/einja-team-exec/SKILL.md +165 -0
  141. package/presets/default/.envrc +5 -0
  142. package/presets/default/.mcp.json +2 -12
  143. package/presets/default/CLAUDE.md.template +45 -8
  144. package/presets/default/docs/einja/example/specs/issues/issue999-example-task/tasks.md +1 -1
  145. package/presets/default/docs/einja/instructions/deployment-setup.md +4 -9
  146. package/presets/default/docs/einja/instructions/environment-setup.md +3 -8
  147. package/presets/default/docs/einja/instructions/issue-exec-workflow.md +276 -0
  148. package/presets/default/docs/einja/instructions/local-server-environment-and-worktree.md +71 -9
  149. package/presets/default/docs/einja/instructions/neon-cli-reference.md +3 -8
  150. package/presets/default/docs/einja/instructions/setup-flow.md +279 -0
  151. package/presets/default/docs/einja/instructions/task-execute.md +63 -68
  152. package/presets/default/docs/einja/instructions/vercel-cli-reference.md +17 -10
  153. package/presets/default/docs/einja/steering/README.md +11 -11
  154. package/presets/default/docs/einja/steering/acceptance-criteria-and-qa-guide.md +4 -9
  155. package/presets/default/docs/einja/steering/architecture.md +3 -8
  156. package/presets/default/docs/einja/steering/branch-strategy.md +63 -70
  157. package/presets/default/docs/einja/steering/commit-rules.md +3 -8
  158. package/presets/default/docs/einja/steering/db-schema-design.md +3 -8
  159. package/presets/default/docs/einja/steering/development/api-development.md +3 -8
  160. package/presets/default/docs/einja/steering/development/backend-architecture.md +3 -8
  161. package/presets/default/docs/einja/steering/development/coding-standards.md +723 -0
  162. package/presets/default/docs/einja/steering/development/component-design.md +502 -0
  163. package/presets/default/docs/einja/steering/development/database-guidelines.md +2 -2
  164. package/presets/default/docs/einja/steering/development/frontend-development.md +3 -8
  165. package/presets/default/docs/einja/steering/development/playwright-guidelines.md +59 -0
  166. package/presets/default/docs/einja/steering/development/review-guidelines.md +3 -8
  167. package/presets/default/docs/einja/steering/development/testing-strategy.md +3 -8
  168. package/presets/default/docs/einja/steering/development-workflow.md +155 -140
  169. package/presets/default/docs/einja/steering/infrastructure/deployment.md +156 -55
  170. package/presets/default/docs/einja/steering/infrastructure/environment-variables.md +4 -8
  171. package/presets/default/docs/einja/steering/product.md +3 -8
  172. package/presets/default/docs/einja/steering/task-management.md +22 -110
  173. package/presets/default/scripts/ensure-serena.sh +75 -0
  174. package/presets/default/scripts/env-rotate-secrets.ts +396 -0
  175. package/presets/default/scripts/env-show.ts +130 -0
  176. package/presets/default/scripts/env.ts +479 -0
  177. package/presets/default/scripts/init-github.ts +363 -0
  178. package/presets/default/scripts/init.sh +98 -0
  179. package/presets/default/scripts/lib/env-common.ts +108 -0
  180. package/presets/default/scripts/lib/worktree-config.ts +64 -0
  181. package/presets/default/scripts/setup-dev.ts +655 -0
  182. package/presets/default/scripts/stop-serena.sh +25 -0
  183. package/presets/default/scripts/worktree/dev.ts +872 -0
  184. package/dist/lib/sync/seed-synchronizer.d.ts +0 -27
  185. package/dist/lib/sync/seed-synchronizer.d.ts.map +0 -1
  186. package/dist/lib/sync/seed-synchronizer.js +0 -72
  187. package/dist/lib/sync/seed-synchronizer.js.map +0 -1
  188. package/dist/lib/sync/seed-synchronizer.test.d.ts +0 -2
  189. package/dist/lib/sync/seed-synchronizer.test.d.ts.map +0 -1
  190. package/dist/lib/sync/seed-synchronizer.test.js +0 -147
  191. package/dist/lib/sync/seed-synchronizer.test.js.map +0 -1
  192. package/presets/default/.claude/agents/einja/git/conflict-resolver.md +0 -148
  193. package/presets/default/.claude/hooks/einja/validate-git-commit.sh +0 -239
  194. package/presets/default/.claude/skills/einja-api-development/SKILL.md +0 -14
  195. package/presets/default/.claude/skills/einja-backend-architecture/SKILL.md +0 -18
  196. package/presets/default/.claude/skills/einja-coding-standards/SKILL.md +0 -132
  197. package/presets/default/.claude/skills/einja-coding-standards/references/import-conventions.md +0 -69
  198. package/presets/default/.claude/skills/einja-coding-standards/references/naming-conventions.md +0 -107
  199. package/presets/default/.claude/skills/einja-coding-standards/references/prohibited-patterns.md +0 -169
  200. package/presets/default/.claude/skills/einja-coding-standards/references/typescript-rules.md +0 -247
  201. package/presets/default/.claude/skills/einja-component-design/SKILL.md +0 -109
  202. package/presets/default/.claude/skills/einja-component-design/references/directory-structure.md +0 -117
  203. package/presets/default/.claude/skills/einja-component-design/references/props-patterns.md +0 -159
  204. package/presets/default/.claude/skills/einja-component-design/references/styling-guide.md +0 -122
  205. package/presets/default/.claude/skills/einja-frontend-development/SKILL.md +0 -14
  206. package/presets/default/.claude/skills/einja-project-overview/SKILL.md +0 -35
  207. package/presets/default/docs/einja/instructions/task-vibe-kanban-loop.md +0 -565
@@ -0,0 +1,396 @@
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
+ * CLIフラグをパース
289
+ */
290
+ function parseArgs(): { all: boolean; nonInteractive: boolean } {
291
+ const args = process.argv.slice(2);
292
+ return {
293
+ all: args.includes("--all"),
294
+ nonInteractive: args.includes("--non-interactive"),
295
+ };
296
+ }
297
+
298
+ /**
299
+ * 非対話モードで全環境のローテーションを実行
300
+ * create-einja-app のセットアップ時に使用
301
+ *
302
+ * @returns 成功した環境数
303
+ */
304
+ async function runNonInteractive(): Promise<number> {
305
+ const rotationType: RotationType = "both";
306
+ let successCount = 0;
307
+
308
+ // 利用可能な環境をフィルタ
309
+ const availableEnvs = ENVIRONMENTS.filter((env) => {
310
+ const envFilePath = path.join(cwd, env.file);
311
+ const hasFile = fs.existsSync(envFilePath);
312
+ const hasKey = getPrivateKey(env.privateKeyEnv) !== null;
313
+ return hasFile && hasKey;
314
+ });
315
+
316
+ if (availableEnvs.length === 0) {
317
+ console.log("[env:rotate-secrets] ローテーション可能な環境がありません");
318
+ return 0;
319
+ }
320
+
321
+ console.log(`[env:rotate-secrets] ${availableEnvs.length}環境の秘密鍵をローテーション中...`);
322
+
323
+ for (const env of availableEnvs) {
324
+ try {
325
+ await rotateWithRecovery(env, rotationType);
326
+ successCount++;
327
+ console.log(`[env:rotate-secrets] ✅ ${env.name}: 完了`);
328
+ } catch (error) {
329
+ console.error(`[env:rotate-secrets] ⚠ ${env.name}: 失敗 - ${error}`);
330
+ // 失敗時も続行(中断しない)
331
+ }
332
+ }
333
+
334
+ console.log(`[env:rotate-secrets] ${successCount}/${availableEnvs.length}環境のローテーション完了`);
335
+ return successCount;
336
+ }
337
+
338
+ /**
339
+ * 対話モードのメイン処理
340
+ */
341
+ async function runInteractive(): Promise<void> {
342
+ p.intro("🔐 秘密鍵ローテーション");
343
+
344
+ // ローテーションタイプを選択
345
+ const rotationType = await selectRotationType();
346
+
347
+ // 対象環境を選択
348
+ const targetEnvs = await selectTargetEnvironments();
349
+
350
+ // 確認
351
+ const proceed = await p.confirm({
352
+ message: "ローテーションを実行しますか?",
353
+ initialValue: true,
354
+ });
355
+
356
+ if (p.isCancel(proceed) || !proceed) {
357
+ p.cancel("キャンセルしました");
358
+ process.exit(0);
359
+ }
360
+
361
+ const spinner = p.spinner();
362
+
363
+ // バックアップを作成中
364
+ spinner.start("バックアップを作成中...");
365
+ await new Promise((resolve) => setTimeout(resolve, 500));
366
+ spinner.stop("バックアップを作成しました");
367
+
368
+ // 各環境でローテーション実行
369
+ for (const env of targetEnvs) {
370
+ try {
371
+ await rotateWithRecovery(env, rotationType);
372
+ } catch (error) {
373
+ p.log.error(`${env.name} のローテーションに失敗しました`);
374
+ process.exit(1);
375
+ }
376
+ }
377
+
378
+ // 次のステップを表示
379
+ showNextSteps(targetEnvs);
380
+
381
+ p.outro("✅ 完了");
382
+ }
383
+
384
+ const flags = parseArgs();
385
+
386
+ if (flags.all && flags.nonInteractive) {
387
+ runNonInteractive().catch((error: unknown) => {
388
+ console.error(`[env:rotate-secrets] エラー: ${error}`);
389
+ // 非対話モードではprocess.exit(1)を呼ばない(呼び出し元で制御)
390
+ });
391
+ } else {
392
+ runInteractive().catch((error: unknown) => {
393
+ p.log.error(`エラーが発生しました: ${error}`);
394
+ process.exit(1);
395
+ });
396
+ }
@@ -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();