@einja/dev-cli 0.1.40 → 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 +2 -2
  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
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export interface AppConfig {
5
+ id: string;
6
+ portRangeStart: number;
7
+ rangeSize: number;
8
+ }
9
+
10
+ export interface PostgresConfig {
11
+ port: number;
12
+ containerName: string;
13
+ }
14
+
15
+ export interface WorktreeConfig {
16
+ schemaVersion: number;
17
+ postgres: PostgresConfig;
18
+ apps: AppConfig[];
19
+ }
20
+
21
+ const defaultWorktreeConfig: WorktreeConfig = {
22
+ schemaVersion: 1,
23
+ postgres: { port: 25432, containerName: "einja-management-postgres" },
24
+ apps: [{ id: "web", portRangeStart: 3000, rangeSize: 1000 }],
25
+ };
26
+
27
+ function findProjectRoot(startDir: string = process.cwd()): string | null {
28
+ let currentDir = startDir;
29
+ while (currentDir !== path.dirname(currentDir)) {
30
+ if (fs.existsSync(path.join(currentDir, "package.json"))) {
31
+ return currentDir;
32
+ }
33
+ currentDir = path.dirname(currentDir);
34
+ }
35
+ return null;
36
+ }
37
+
38
+ export function loadWorktreeConfig(projectRoot?: string): WorktreeConfig {
39
+ const root = projectRoot ?? findProjectRoot();
40
+ if (!root) return defaultWorktreeConfig;
41
+
42
+ const configPath = path.join(root, "worktree.config.json");
43
+ if (!fs.existsSync(configPath)) return defaultWorktreeConfig;
44
+
45
+ try {
46
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
47
+ return {
48
+ schemaVersion: raw.schemaVersion ?? 1,
49
+ postgres: {
50
+ port: typeof raw.postgres?.port === "number" ? raw.postgres.port : 25432,
51
+ containerName: typeof raw.postgres?.containerName === "string"
52
+ ? raw.postgres.containerName : "einja-management-postgres",
53
+ },
54
+ apps: Array.isArray(raw.apps)
55
+ ? raw.apps.filter((a: unknown) =>
56
+ typeof a === "object" && a !== null && "id" in a && "portRangeStart" in a
57
+ )
58
+ : defaultWorktreeConfig.apps,
59
+ };
60
+ } catch {
61
+ console.warn("worktree.config.json の読み込みに失敗。デフォルト設定を使用します。");
62
+ return defaultWorktreeConfig;
63
+ }
64
+ }
@@ -0,0 +1,640 @@
1
+ import { execSync, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ // ANSI color codes
7
+ const colors = {
8
+ blue: (text: string) => `\x1b[34m${text}\x1b[0m`,
9
+ green: (text: string) => `\x1b[32m${text}\x1b[0m`,
10
+ yellow: (text: string) => `\x1b[33m${text}\x1b[0m`,
11
+ gray: (text: string) => `\x1b[90m${text}\x1b[0m`,
12
+ red: (text: string) => `\x1b[31m${text}\x1b[0m`,
13
+ cyan: (text: string) => `\x1b[36m${text}\x1b[0m`,
14
+ };
15
+
16
+ function getPlatform(): "macos" | "linux" | "windows" | "unknown" {
17
+ switch (process.platform) {
18
+ case "darwin":
19
+ return "macos";
20
+ case "linux":
21
+ return "linux";
22
+ case "win32":
23
+ return "windows";
24
+ default:
25
+ return "unknown";
26
+ }
27
+ }
28
+
29
+ function commandExists(cmd: string): boolean {
30
+ try {
31
+ // Use 'command -v' for POSIX compatibility, 'where' for Windows
32
+ const checkCmd =
33
+ process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`;
34
+ execSync(checkCmd, { stdio: "ignore", shell: true });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * git worktreeのメインリポジトリのパスを取得
43
+ * worktreeでない場合はnullを返す
44
+ */
45
+ function getMainWorktreePath(): string | null {
46
+ try {
47
+ const result = execSync("git worktree list --porcelain", {
48
+ encoding: "utf-8",
49
+ stdio: ["pipe", "pipe", "pipe"],
50
+ });
51
+ const lines = result.trim().split("\n");
52
+ // 最初の "worktree /path/to/main" がメインリポジトリ
53
+ for (const line of lines) {
54
+ if (line.startsWith("worktree ")) {
55
+ const mainPath = line.substring("worktree ".length);
56
+ // 現在のディレクトリと異なる場合のみ返す(worktree環境の場合)
57
+ if (mainPath !== process.cwd()) {
58
+ return mainPath;
59
+ }
60
+ break;
61
+ }
62
+ }
63
+ return null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * worktreeの親から.env.keysをコピー
71
+ * 成功した場合はtrue、失敗またはworktreeでない場合はfalseを返す
72
+ */
73
+ function copyEnvKeysFromMainWorktree(targetPath: string): boolean {
74
+ const mainPath = getMainWorktreePath();
75
+ if (!mainPath) {
76
+ return false;
77
+ }
78
+
79
+ const sourceEnvKeysPath = path.join(mainPath, ".env.keys");
80
+ if (!fs.existsSync(sourceEnvKeysPath)) {
81
+ return false;
82
+ }
83
+
84
+ try {
85
+ fs.copyFileSync(sourceEnvKeysPath, targetPath);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ function getShellConfig(): { rcFile: string; hookCmd: string } | null {
93
+ const shell = process.env.SHELL || "";
94
+ const shellName = path.basename(shell);
95
+ const home = os.homedir();
96
+
97
+ switch (shellName) {
98
+ case "zsh":
99
+ return {
100
+ rcFile: path.join(home, ".zshrc"),
101
+ hookCmd: 'eval "$(direnv hook zsh)"',
102
+ };
103
+ case "bash":
104
+ return {
105
+ rcFile: path.join(home, ".bashrc"),
106
+ hookCmd: 'eval "$(direnv hook bash)"',
107
+ };
108
+ case "fish":
109
+ return {
110
+ rcFile: path.join(home, ".config", "fish", "config.fish"),
111
+ hookCmd: "direnv hook fish | source",
112
+ };
113
+ default:
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function ensureFileExists(filePath: string): void {
119
+ const dir = path.dirname(filePath);
120
+ if (!fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ }
123
+ if (!fs.existsSync(filePath)) {
124
+ fs.writeFileSync(filePath, "");
125
+ }
126
+ }
127
+
128
+ function appendToRcFile(rcFile: string, content: string): void {
129
+ ensureFileExists(rcFile);
130
+ fs.appendFileSync(rcFile, content);
131
+ }
132
+
133
+ function log(prefix: string, message: string): void {
134
+ console.log(`${prefix} ${message}`);
135
+ }
136
+
137
+ function succeed(message: string): void {
138
+ log(colors.green("✓"), message);
139
+ }
140
+
141
+ function warn(message: string): void {
142
+ log(colors.yellow("⚠"), message);
143
+ }
144
+
145
+ function fail(message: string): void {
146
+ log(colors.red("✗"), message);
147
+ }
148
+
149
+ function step(num: number, message: string): void {
150
+ console.log(`\n${colors.blue(`Step ${num}:`)} ${message}`);
151
+ }
152
+
153
+ async function setupVolta(): Promise<void> {
154
+ const platform = getPlatform();
155
+ const home = os.homedir();
156
+
157
+ // 1. Voltaインストール確認
158
+ step(1, "Voltaの確認...");
159
+
160
+ const hasVolta = commandExists("volta");
161
+
162
+ if (!hasVolta) {
163
+ if (platform !== "macos") {
164
+ warn("Voltaがインストールされていません");
165
+ console.log(colors.yellow(" 手動でインストールしてください:"));
166
+ console.log(colors.gray(" curl https://get.volta.sh | bash"));
167
+ console.log(
168
+ colors.gray(" インストール後、再度このスクリプトを実行してください\n"),
169
+ );
170
+ process.exit(1);
171
+ }
172
+
173
+ console.log(" Voltaをインストール中...");
174
+ try {
175
+ execSync("curl -fsSL https://get.volta.sh | bash", {
176
+ stdio: "inherit",
177
+ shell: "/bin/bash",
178
+ });
179
+ succeed("Voltaをインストールしました");
180
+ } catch {
181
+ fail("Voltaのインストールに失敗しました");
182
+ console.log(
183
+ colors.yellow(
184
+ " 手動でインストールしてください: curl https://get.volta.sh | bash",
185
+ ),
186
+ );
187
+ process.exit(1);
188
+ }
189
+ } else {
190
+ succeed("Voltaは既にインストールされています");
191
+ }
192
+
193
+ // 2. シェル設定確認(VOLTA_FEATURE_PNPM)
194
+ step(2, "Voltaシェル設定の確認...");
195
+
196
+ const shellConfig = getShellConfig();
197
+ if (shellConfig) {
198
+ const { rcFile } = shellConfig;
199
+ const rcContent = fs.existsSync(rcFile)
200
+ ? fs.readFileSync(rcFile, "utf-8")
201
+ : "";
202
+
203
+ if (!rcContent.includes("VOLTA_FEATURE_PNPM")) {
204
+ const voltaConfig = `
205
+ # Volta - pnpm support
206
+ export VOLTA_FEATURE_PNPM=1
207
+ `;
208
+ appendToRcFile(rcFile, voltaConfig);
209
+ succeed(`${rcFile} にVOLTA_FEATURE_PNPMを追加しました`);
210
+ } else {
211
+ succeed("Voltaシェル設定は既に存在します");
212
+ }
213
+ }
214
+
215
+ // 3. Node.js/pnpmインストール
216
+ step(3, "Node.js/pnpmのインストール...");
217
+
218
+ const packageJsonPath = path.join(process.cwd(), "package.json");
219
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
220
+ const voltaConfig = packageJson.volta as
221
+ | { node?: string; pnpm?: string }
222
+ | undefined;
223
+
224
+ if (voltaConfig) {
225
+ const voltaPath = path.join(home, ".volta", "bin", "volta");
226
+ const voltaCmd = fs.existsSync(voltaPath) ? voltaPath : "volta";
227
+
228
+ try {
229
+ if (voltaConfig.node) {
230
+ execSync(`${voltaCmd} install node@${voltaConfig.node}`, {
231
+ stdio: "inherit",
232
+ env: { ...process.env, VOLTA_FEATURE_PNPM: "1" },
233
+ });
234
+ }
235
+ if (voltaConfig.pnpm) {
236
+ execSync(`${voltaCmd} install pnpm@${voltaConfig.pnpm}`, {
237
+ stdio: "inherit",
238
+ env: { ...process.env, VOLTA_FEATURE_PNPM: "1" },
239
+ });
240
+ }
241
+ succeed(
242
+ `Node.js ${voltaConfig.node}, pnpm ${voltaConfig.pnpm} をインストールしました`,
243
+ );
244
+ } catch {
245
+ warn("Node.js/pnpmのインストールに失敗しました(シェル再起動後に再実行してください)");
246
+ }
247
+ } else {
248
+ warn("package.jsonにvoltaフィールドがありません");
249
+ }
250
+ }
251
+
252
+ async function promptPassword(message: string): Promise<string> {
253
+ const stdin = process.stdin;
254
+
255
+ // Non-TTY environment (e.g., piped input, CI) - skip interactive prompt
256
+ if (!stdin.isTTY) {
257
+ return "";
258
+ }
259
+
260
+ // Dynamic import for readline (ESM)
261
+ const readline = await import("node:readline");
262
+ const rl = readline.createInterface({
263
+ input: stdin,
264
+ output: process.stdout,
265
+ });
266
+
267
+ return new Promise((resolve) => {
268
+ // Hide input for password
269
+ process.stdout.write(`${message} `);
270
+ let input = "";
271
+
272
+ const wasRaw = stdin.isRaw;
273
+ stdin.setRawMode(true);
274
+ stdin.resume();
275
+ stdin.setEncoding("utf8");
276
+
277
+ const onData = (char: string) => {
278
+ switch (char) {
279
+ case "\n":
280
+ case "\r":
281
+ case "\u0004": // Ctrl+D
282
+ stdin.setRawMode(wasRaw ?? false);
283
+ stdin.pause();
284
+ stdin.removeListener("data", onData);
285
+ console.log();
286
+ rl.close();
287
+ resolve(input);
288
+ break;
289
+ case "\u0003": // Ctrl+C
290
+ stdin.setRawMode(wasRaw ?? false);
291
+ stdin.pause();
292
+ stdin.removeListener("data", onData);
293
+ rl.close();
294
+ process.exit(1);
295
+ break;
296
+ case "\u007F": // Backspace
297
+ if (input.length > 0) {
298
+ input = input.slice(0, -1);
299
+ process.stdout.write("\b \b");
300
+ }
301
+ break;
302
+ default:
303
+ input += char;
304
+ process.stdout.write("*");
305
+ break;
306
+ }
307
+ };
308
+
309
+ stdin.on("data", onData);
310
+ });
311
+ }
312
+
313
+ async function main(): Promise<void> {
314
+ const cwd = process.cwd();
315
+ const platform = getPlatform();
316
+
317
+ console.log(colors.blue("\n🚀 開発環境セットアップを開始します...\n"));
318
+
319
+ // Step 1-3: Voltaセットアップ
320
+ await setupVolta();
321
+
322
+ // 4. direnvインストール確認・実行
323
+ step(4, "direnvの確認...");
324
+
325
+ const hasDirenv = commandExists("direnv");
326
+
327
+ if (!hasDirenv) {
328
+ if (platform === "macos") {
329
+ console.log(" direnvをインストール中...");
330
+ const result = spawnSync("brew", ["install", "direnv"], {
331
+ stdio: "inherit",
332
+ });
333
+ if (result.error || result.status !== 0) {
334
+ fail("direnvのインストールに失敗しました");
335
+ console.log(
336
+ colors.yellow(" 手動でインストールしてください: brew install direnv"),
337
+ );
338
+ process.exit(1);
339
+ }
340
+ succeed("direnvをインストールしました");
341
+ } else {
342
+ warn("direnvがインストールされていません");
343
+ console.log(colors.yellow(" 手動でインストールしてください:"));
344
+ console.log(colors.gray(" Linux: sudo apt install direnv"));
345
+ console.log(
346
+ colors.gray(" 詳細: https://direnv.net/docs/installation.html"),
347
+ );
348
+ console.log(
349
+ colors.gray(" インストール後、再度このスクリプトを実行してください\n"),
350
+ );
351
+ process.exit(1);
352
+ }
353
+ } else {
354
+ succeed("direnvは既にインストールされています");
355
+ }
356
+
357
+ // 5. シェル設定
358
+ step(5, "シェル設定の確認...");
359
+
360
+ const shellConfig = getShellConfig();
361
+
362
+ if (shellConfig) {
363
+ const { rcFile, hookCmd } = shellConfig;
364
+ const rcContent = fs.existsSync(rcFile)
365
+ ? fs.readFileSync(rcFile, "utf-8")
366
+ : "";
367
+
368
+ if (!rcContent.includes("direnv hook")) {
369
+ appendToRcFile(rcFile, `\n# direnv\n${hookCmd}\n`);
370
+ succeed(`${rcFile} に設定を追加しました`);
371
+ } else {
372
+ succeed("シェル設定は既に存在します");
373
+ }
374
+ } else {
375
+ warn("未対応のシェルです。手動でdirenvフックを設定してください");
376
+ }
377
+
378
+ // 6. dotenvxインストール
379
+ step(6, "dotenvxの確認...");
380
+
381
+ const hasDotenvx = commandExists("dotenvx");
382
+
383
+ if (!hasDotenvx) {
384
+ if (platform === "macos" || platform === "linux") {
385
+ console.log(" dotenvxをインストール中...");
386
+ try {
387
+ // 公式インストールスクリプトを使用
388
+ execSync("curl -sfS https://dotenvx.sh/install.sh | sh", {
389
+ stdio: "inherit",
390
+ shell: "/bin/bash",
391
+ });
392
+ succeed("dotenvxをインストールしました");
393
+ } catch {
394
+ // フォールバック: npm経由でインストール
395
+ try {
396
+ execSync("npm install -g @dotenvx/dotenvx", { stdio: "inherit" });
397
+ succeed("dotenvxをnpm経由でインストールしました");
398
+ } catch {
399
+ warn("dotenvxのインストールに失敗しました");
400
+ console.log(colors.yellow(" 手動でインストールしてください:"));
401
+ console.log(
402
+ colors.gray(" curl -sfS https://dotenvx.sh/install.sh | sh"),
403
+ );
404
+ console.log(
405
+ colors.gray(" または: npm install -g @dotenvx/dotenvx"),
406
+ );
407
+ }
408
+ }
409
+ } else {
410
+ warn("dotenvxがインストールされていません");
411
+ console.log(colors.yellow(" 手動でインストールしてください:"));
412
+ console.log(
413
+ colors.gray(" curl -sfS https://dotenvx.sh/install.sh | sh"),
414
+ );
415
+ console.log(colors.gray(" または: npm install -g @dotenvx/dotenvx"));
416
+ }
417
+ } else {
418
+ succeed("dotenvxは既にインストールされています");
419
+ }
420
+
421
+ // 7. .env.local復号 → .env作成
422
+ step(7, ".envファイルの作成(.env.localから復号)...");
423
+
424
+ const envPath = path.join(cwd, ".env");
425
+ const envLocalPath = path.join(cwd, ".env.local");
426
+ const envKeysPath = path.join(cwd, ".env.keys");
427
+ const envExamplePath = path.join(cwd, ".env.example");
428
+
429
+ if (!fs.existsSync(envPath)) {
430
+ // .env.local(暗号化済み)から復号して.envを作成
431
+ if (fs.existsSync(envLocalPath) && fs.existsSync(envKeysPath)) {
432
+ try {
433
+ // .env.keysから秘密鍵を読み込む
434
+ const keysContent = fs.readFileSync(envKeysPath, "utf-8");
435
+ const localKeyMatch = keysContent.match(
436
+ /DOTENV_PRIVATE_KEY_LOCAL=["']?([^"'\n]+)["']?/,
437
+ );
438
+ if (!localKeyMatch) {
439
+ throw new Error("DOTENV_PRIVATE_KEY_LOCAL が .env.keys に見つかりません");
440
+ }
441
+ const privateKey = localKeyMatch[1];
442
+
443
+ // dotenvxで復号
444
+ execSync("npx dotenvx decrypt -f .env.local --stdout > .env", {
445
+ shell: true,
446
+ cwd,
447
+ stdio: "pipe",
448
+ env: { ...process.env, DOTENV_PRIVATE_KEY_LOCAL: privateKey },
449
+ });
450
+ succeed(".env.local を復号して .env を作成しました");
451
+ } catch (error) {
452
+ fail(".env.local の復号に失敗しました");
453
+ console.log(colors.yellow(" 秘密鍵が正しいか確認してください"));
454
+ console.log(
455
+ colors.gray(" .env.keys はチームから共有を受けてください"),
456
+ );
457
+ // フォールバック: .env.exampleからコピー
458
+ if (fs.existsSync(envExamplePath)) {
459
+ fs.copyFileSync(envExamplePath, envPath);
460
+ warn("フォールバック: .env.example から .env を作成しました");
461
+ }
462
+ }
463
+ } else if (!fs.existsSync(envKeysPath)) {
464
+ // .env.keysがない場合、まずworktreeの親からコピーを試みる
465
+ if (copyEnvKeysFromMainWorktree(envKeysPath)) {
466
+ succeed("worktreeのメインリポジトリから .env.keys をコピーしました");
467
+ // コピー成功後、復号を再試行
468
+ try {
469
+ const keysContent = fs.readFileSync(envKeysPath, "utf-8");
470
+ const localKeyMatch = keysContent.match(
471
+ /DOTENV_PRIVATE_KEY_LOCAL=["']?([^"'\n]+)["']?/,
472
+ );
473
+ if (localKeyMatch) {
474
+ const privateKey = localKeyMatch[1];
475
+ execSync("npx dotenvx decrypt -f .env.local --stdout > .env", {
476
+ shell: true,
477
+ cwd,
478
+ stdio: "pipe",
479
+ env: { ...process.env, DOTENV_PRIVATE_KEY_LOCAL: privateKey },
480
+ });
481
+ succeed(".env.local を復号して .env を作成しました");
482
+ } else {
483
+ throw new Error("DOTENV_PRIVATE_KEY_LOCAL が見つかりません");
484
+ }
485
+ } catch {
486
+ warn(".env.keys のコピー後、復号に失敗しました");
487
+ if (fs.existsSync(envExamplePath)) {
488
+ fs.copyFileSync(envExamplePath, envPath);
489
+ warn("フォールバック: .env.example から .env を作成しました");
490
+ }
491
+ }
492
+ } else {
493
+ // worktreeからのコピーも失敗した場合
494
+ warn(".env.keys が見つかりません(秘密鍵ファイル)");
495
+ console.log(
496
+ colors.yellow(" チームから .env.keys を共有してもらってください"),
497
+ );
498
+ console.log(colors.gray(" または 1Password 等で共有されています"));
499
+ // フォールバック: .env.exampleからコピー
500
+ if (fs.existsSync(envExamplePath)) {
501
+ fs.copyFileSync(envExamplePath, envPath);
502
+ warn("フォールバック: .env.example から .env を作成しました");
503
+ }
504
+ }
505
+ } else if (!fs.existsSync(envLocalPath)) {
506
+ // .env.localがない場合(古いリポジトリ)
507
+ if (fs.existsSync(envExamplePath)) {
508
+ fs.copyFileSync(envExamplePath, envPath);
509
+ succeed(".env.example から .env を作成しました(レガシーモード)");
510
+ } else {
511
+ fail(".env.example が見つかりません");
512
+ process.exit(1);
513
+ }
514
+ }
515
+ } else {
516
+ succeed(".env は既に存在します");
517
+ }
518
+
519
+ // 8. .env.personal作成 & GITHUB_TOKEN設定(対話的)
520
+ step(8, "個人用トークン設定(.env.personal)...");
521
+
522
+ const envPersonalPath = path.join(cwd, ".env.personal");
523
+ const envPersonalExamplePath = path.join(cwd, ".env.personal.example");
524
+
525
+ // .env.personalがなければテンプレートからコピー
526
+ if (!fs.existsSync(envPersonalPath)) {
527
+ if (fs.existsSync(envPersonalExamplePath)) {
528
+ fs.copyFileSync(envPersonalExamplePath, envPersonalPath);
529
+ succeed(".env.personal.example から .env.personal を作成しました");
530
+ } else {
531
+ // テンプレートがない場合は最小限の内容で作成
532
+ fs.writeFileSync(
533
+ envPersonalPath,
534
+ "# 個人用トークン\nGITHUB_TOKEN=\n",
535
+ );
536
+ succeed(".env.personal を新規作成しました");
537
+ }
538
+ }
539
+
540
+ // GITHUB_TOKENの確認
541
+ const envPersonalContent = fs.readFileSync(envPersonalPath, "utf-8");
542
+ const hasGithubToken =
543
+ envPersonalContent.includes("GITHUB_TOKEN=") &&
544
+ !envPersonalContent.match(/GITHUB_TOKEN=\s*$/m) &&
545
+ !envPersonalContent.match(/GITHUB_TOKEN=\s*\n/);
546
+
547
+ if (!hasGithubToken) {
548
+ console.log(colors.yellow("\n ⚠️ GITHUB_TOKENが設定されていません"));
549
+ console.log(colors.gray(" GitHub Personal Access Token が必要です"));
550
+ console.log(
551
+ colors.gray(" 取得方法: https://github.com/settings/tokens/new"),
552
+ );
553
+ console.log(colors.gray(" 必要なスコープ: repo, read:org\n"));
554
+
555
+ const token = await promptPassword(
556
+ " GITHUB_TOKENを入力してください(スキップはEnter):",
557
+ );
558
+
559
+ const trimmedToken = token.trim();
560
+ if (trimmedToken) {
561
+ let updatedContent: string;
562
+ if (envPersonalContent.includes("GITHUB_TOKEN=")) {
563
+ // Replace existing line
564
+ updatedContent = envPersonalContent.replace(
565
+ /GITHUB_TOKEN=.*/,
566
+ `GITHUB_TOKEN=${trimmedToken}`,
567
+ );
568
+ } else {
569
+ // Append new line
570
+ updatedContent = envPersonalContent.endsWith("\n")
571
+ ? `${envPersonalContent}GITHUB_TOKEN=${trimmedToken}\n`
572
+ : `${envPersonalContent}\nGITHUB_TOKEN=${trimmedToken}\n`;
573
+ }
574
+ fs.writeFileSync(envPersonalPath, updatedContent);
575
+ succeed("GITHUB_TOKENを .env.personal に設定しました");
576
+ } else {
577
+ console.log(
578
+ colors.yellow(
579
+ " スキップしました。後で .env.personal を編集してください",
580
+ ),
581
+ );
582
+ }
583
+ } else {
584
+ succeed("GITHUB_TOKENは既に設定されています");
585
+ }
586
+
587
+ // 9. direnv有効化
588
+ step(9, "direnvの有効化...");
589
+ try {
590
+ execSync("direnv allow", { cwd, stdio: "ignore" });
591
+ succeed("direnvを有効化しました");
592
+ } catch {
593
+ warn("direnv allowに失敗(シェル再起動後に再実行してください)");
594
+ }
595
+
596
+ // 10. データベース起動
597
+ step(10, "データベース起動...");
598
+ const hasDocker = commandExists("docker");
599
+
600
+ if (hasDocker) {
601
+ try {
602
+ execSync("docker-compose up -d postgres", { cwd, stdio: "inherit" });
603
+ succeed("PostgreSQLを起動しました");
604
+
605
+ // 起動を待つ
606
+ console.log(colors.gray(" データベースの起動を待機中..."));
607
+ await new Promise((resolve) => setTimeout(resolve, 3000));
608
+
609
+ // 11. Prismaセットアップ
610
+ step(11, "データベース初期化...");
611
+ execSync("pnpm db:generate", { cwd, stdio: "inherit" });
612
+ execSync("pnpm db:push", { cwd, stdio: "inherit" });
613
+ succeed("データベースを初期化しました");
614
+ } catch {
615
+ warn("データベースの起動または初期化に失敗しました");
616
+ console.log(colors.gray(" 手動で実行してください:"));
617
+ console.log(colors.gray(" docker-compose up -d postgres"));
618
+ console.log(colors.gray(" pnpm db:generate && pnpm db:push"));
619
+ }
620
+ } else {
621
+ warn("Dockerがインストールされていません");
622
+ console.log(colors.gray(" Dockerをインストール後、以下を実行してください:"));
623
+ console.log(colors.gray(" docker-compose up -d postgres"));
624
+ console.log(colors.gray(" pnpm db:generate && pnpm db:push"));
625
+ }
626
+
627
+ // 完了
628
+ console.log(colors.green("\n=========================================="));
629
+ console.log(colors.green("✅ 環境セットアップが完了しました!"));
630
+ console.log(colors.green("==========================================\n"));
631
+
632
+ console.log("開発を始めるには:");
633
+ console.log(colors.cyan(" pnpm dev"));
634
+ console.log("");
635
+ }
636
+
637
+ main().catch((error: unknown) => {
638
+ console.error(colors.red("エラーが発生しました:"), error);
639
+ process.exit(1);
640
+ });