create-einja-app 0.2.4 → 0.2.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-einja-app",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
4
4
  "description": "CLI tool to create new projects with Einja Management Template",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,19 @@
1
1
  # Auto detect text files and perform LF normalization
2
2
  * text=auto
3
3
 
4
+ # Binary files - prevent CRLF conversion
5
+ *.ico binary
6
+ *.png binary
7
+ *.jpg binary
8
+ *.jpeg binary
9
+ *.gif binary
10
+ *.webp binary
11
+ *.svg binary
12
+ *.woff binary
13
+ *.woff2 binary
14
+ *.ttf binary
15
+ *.eot binary
16
+ *.pdf binary
17
+
4
18
  # Symlink handling for Windows compatibility
5
19
  docs/templates symlink=auto
@@ -0,0 +1,128 @@
1
+ name: タスク実装
2
+ description: 新機能・バグ修正のタスク管理用Issue
3
+ title: "[機能名]"
4
+ labels: ["task"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ ## タスク実装用Issue
10
+
11
+ このテンプレートは新機能やバグ修正のタスク管理に使用します。
12
+
13
+ - type: textarea
14
+ id: as-is
15
+ attributes:
16
+ label: AS-IS(現状)
17
+ description: requirements.mdから抽出した現在の状態・問題点
18
+ placeholder: |
19
+ 現在の実装状況と課題を記載
20
+
21
+ **現在の実装状況**:
22
+ - 状況1
23
+ - 状況2
24
+
25
+ **現状の課題**:
26
+ - 課題1
27
+ - 課題2
28
+ validations:
29
+ required: true
30
+
31
+ - type: textarea
32
+ id: to-be
33
+ attributes:
34
+ label: TO-BE(目標状態)
35
+ description: requirements.mdから抽出した実装後の期待する状態
36
+ placeholder: |
37
+ 実現したい姿と期待される改善を記載
38
+
39
+ **実現したい姿**:
40
+ - 姿1
41
+ - 姿2
42
+
43
+ **期待される改善**:
44
+ - 改善1
45
+ - 改善2
46
+ validations:
47
+ required: true
48
+
49
+ - type: textarea
50
+ id: approach
51
+ attributes:
52
+ label: 対応方針
53
+ description: design.mdから抽出した技術的アプローチ、使用ライブラリ、実装方針
54
+ placeholder: |
55
+ 技術選定、アーキテクチャ、実装方針を記載
56
+
57
+ **技術スタック**:
58
+ - フレームワーク: Next.js 15
59
+ - データベース: PostgreSQL
60
+ - 認証: NextAuth.js
61
+
62
+ **主要な実装方針**:
63
+ - 方針1
64
+ - 方針2
65
+ validations:
66
+ required: true
67
+
68
+ - type: textarea
69
+ id: tasks
70
+ attributes:
71
+ label: タスク一覧
72
+ description: 実装するタスクグループのリスト
73
+ placeholder: |
74
+ ### Phase 1: [フェーズ名]
75
+
76
+ - [ ] 1.1 [タスクグループ名]
77
+
78
+ - 1.1.1 [タスク名]
79
+ - サブタスク内容
80
+ - **要件**: Story 1
81
+ - **依存関係**: なし
82
+ - **完了条件**: [テスト条件]が通ること(AC1.1を満たす)
83
+ - **対応設計**: design.md「[セクション名]」セクション
84
+ - **シナリオテスト**: なし(基盤構築タスク、UIフロー未実装のため)
85
+
86
+ - 1.1.2 [タスク名]
87
+ - サブタスク内容
88
+ - **要件**: Story 1
89
+ - **依存関係**: 1.1.1
90
+ - **完了条件**: [テスト条件]が通ること(AC1.2を満たす)
91
+ - **対応設計**: design.md「[セクション名]」セクション
92
+ - **シナリオテスト**: シナリオ1 Step 1-3(部分実行)
93
+
94
+ - [ ] 1.2 [タスクグループ名]
95
+
96
+ - 1.2.1 [タスク名]
97
+ - サブタスク内容
98
+ - **要件**: Story 2
99
+ - **依存関係**: 1.1
100
+ - **完了条件**: [テスト条件]が通ること(AC2.1を満たす)
101
+ - **対応設計**: design.md「[セクション名]」セクション
102
+ - **シナリオテスト**: シナリオ1 全Step(フル実行)
103
+
104
+ ### Phase 2: [フェーズ名]
105
+
106
+ - [ ] 2.1 [タスクグループ名]
107
+
108
+ - 2.1.1 [タスク名]
109
+ - サブタスク内容
110
+ - **要件**: Story 3
111
+ - **依存関係**: Phase 1完了
112
+ - **完了条件**: [テスト条件]が通ること(AC3.1を満たす)
113
+ - **対応設計**: design.md「[セクション名]」セクション
114
+ - **シナリオテスト**: シナリオ2 全Step
115
+ value: |
116
+ ### Phase 1: [フェーズ名]
117
+
118
+ - [ ] 1.1 [タスクグループ名]
119
+
120
+ - 1.1.1 [タスク名]
121
+ - [サブタスク内容]
122
+ - **要件**: Story 1
123
+ - **依存関係**: なし
124
+ - **完了条件**: [テスト条件](AC1.1を満たす)
125
+ - **対応設計**: design.md「[セクション名]」セクション
126
+ - **シナリオテスト**: なし(理由を記載)
127
+ validations:
128
+ required: true
@@ -3,101 +3,6 @@
3
3
  Turborepo + Next.js 15 + Auth.js + Prisma 構成のプロジェクトテンプレートと、Claude Code用のATDDワークフロー設定を提供します。
4
4
 
5
5
  ---
6
-
7
- ## パッケージ利用者向け
8
-
9
- ### create-einja-app - 新規プロジェクト作成
10
-
11
- 新しいプロジェクトを作成したい場合に使用します。
12
-
13
- ```bash
14
- npx create-einja-app my-project
15
- ```
16
-
17
- **何が起きるか:**
18
-
19
- 1. `my-project/` ディレクトリが作成される
20
- 2. Turborepo + Next.js 15 + Prisma のモノレポ構成が展開される
21
- 3. `.claude/` ディレクトリ(Claude Code設定)が自動セットアップされる
22
- 4. 依存関係がインストールされる
23
- 5. Gitリポジトリが初期化される
24
-
25
- **作成後の開始手順:**
26
-
27
- ```bash
28
- cd my-project
29
- docker-compose up -d postgres # PostgreSQL起動
30
- pnpm dev # 開発サーバー起動
31
- ```
32
-
33
- ブラウザで http://localhost:3000 にアクセス
34
-
35
- **オプション:**
36
-
37
- | オプション | 説明 |
38
- |-----------|------|
39
- | `--yes` | 対話プロンプトをスキップ(デフォルト値使用) |
40
- | `--skip-git` | Git初期化をスキップ |
41
- | `--skip-install` | 依存関係インストールをスキップ |
42
-
43
- 📖 詳細: [packages/create-einja-app/README.md](./packages/create-einja-app/README.md)
44
-
45
- ---
46
-
47
- ### @einja/dev-cli - 既存プロジェクトにClaude Code設定を追加
48
-
49
- 既存のプロジェクトにClaude Code用のATDDワークフロー設定を追加したい場合に使用します。
50
-
51
- ```bash
52
- cd your-existing-project
53
- npx @einja/dev-cli init
54
- ```
55
-
56
- **何が起きるか:**
57
-
58
- 1. `.claude/` ディレクトリが作成される
59
- - `agents/` - タスク実行、仕様書生成、フロントエンド開発用サブエージェント
60
- - `commands/` - `/spec-create`, `/task-exec` などのスラッシュコマンド
61
- - `skills/` - コーディング規約、コンポーネント設計ガイド
62
- - `hooks/` - Biomeフォーマット、型チェックなどのGit Hooks
63
- - `settings.json` - MCPサーバー設定(GitHub, Playwright, Serena等)
64
- 2. `docs/einja/` ディレクトリが作成される
65
- - `steering/` - コミットルール、テスト戦略、レビューガイドライン
66
- - `templates/` - 仕様書テンプレート
67
- 3. `CLAUDE.md` テンプレートが作成される
68
- 4. `package.json` にスクリプトが追加される
69
-
70
- **追加されるnpm scripts:**
71
-
72
- ```bash
73
- pnpm task:loop 123 # GitHub Issue #123のタスクを自動実行
74
- pnpm einja:sync # テンプレートから最新設定を同期
75
- ```
76
-
77
- **その他のコマンド:**
78
-
79
- ```bash
80
- # テンプレートから設定を同期(更新があった場合)
81
- npx @einja/dev-cli sync
82
-
83
- # 特定カテゴリのみ同期
84
- npx @einja/dev-cli sync --only commands,agents
85
- ```
86
-
87
- 📖 詳細: [packages/cli/README.md](./packages/cli/README.md)
88
-
89
- ---
90
-
91
- ### 使い分けガイド
92
-
93
- | やりたいこと | 使うパッケージ |
94
- |-------------|---------------|
95
- | 新規プロジェクトを作成したい | `npx create-einja-app my-project` |
96
- | 既存プロジェクトにClaude設定を追加したい | `npx @einja/dev-cli init` |
97
- | Claude設定を最新に更新したい | `npx @einja/dev-cli sync` |
98
-
99
- ---
100
-
101
6
  ## パッケージ開発者向け
102
7
 
103
8
  以下は、このリポジトリ自体を開発する場合の情報です。
@@ -318,28 +223,6 @@ pnpm db:studio
318
223
  - **@repo/server-core**: バックエンド共通層(Prismaクライアント・スキーマ、ドメインロジック)
319
224
  - **@repo/ui**: 共通UIコンポーネント(shadcn/ui)
320
225
 
321
- ### CLIパッケージの開発
322
-
323
- #### @einja/dev-cli
324
-
325
- ```bash
326
- cd packages/cli
327
- pnpm build # ビルド
328
- pnpm test # テスト
329
- pnpm typecheck # 型チェック
330
- ```
331
-
332
- 📖 [ビルドプロセス](./packages/cli/docs/BUILD.md) | [NPM公開手順](./packages/cli/docs/PUBLISHING.md) | [リリース手順](./packages/cli/RELEASING.md)
333
-
334
- #### create-einja-app
335
-
336
- ```bash
337
- cd packages/create-einja-app
338
- pnpm build # ビルド(テンプレート更新含む)
339
- pnpm test # テスト
340
- pnpm typecheck # 型チェック
341
- ```
342
-
343
226
  ### 開発ワークフロー
344
227
 
345
228
  1. ブランチを作成
@@ -38,13 +38,11 @@
38
38
  "db:migrate": "turbo run db:migrate",
39
39
  "db:studio": "turbo run db:studio",
40
40
  "dev:setup": "tsx scripts/setup-dev.ts",
41
- "env:prepare": "tsx scripts/worktree/dev.ts --setup-only",
42
- "cli-template:update": "tsx scripts/cli-template-update.ts",
43
- "template:update": "tsx scripts/template-update.ts",
44
41
  "dotenvx": "dotenvx",
45
42
  "env:update": "tsx scripts/env.ts",
46
43
  "env:encrypt": "dotenvx encrypt",
47
44
  "env:show": "tsx scripts/env-show.ts",
45
+ "env:rotate-secrets": "tsx scripts/env-rotate-secrets.ts",
48
46
  "task:loop": "npx @einja/dev-cli task:loop",
49
47
  "einja:sync": "npx @einja/dev-cli sync"
50
48
  },
@@ -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
+ });