create-einja-app 0.1.1
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/README.md +307 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1041 -0
- package/dist/cli.js.map +1 -0
- package/package.json +62 -0
- package/templates/turborepo-pandacss/.biomeignore +15 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/biome-format.sh +49 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/design-doc-check.sh +61 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/detect-secrets.sh +62 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/large-file-warning.sh +42 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/playwright-resize.sh +36 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/typecheck.sh +37 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/unset-volta-recursion.sh +32 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/validate-git-commit.sh +239 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/warn-index-ts.sh +34 -0
- package/templates/turborepo-pandacss/.claude/hooks/einja/warn-relative-import.sh +48 -0
- package/templates/turborepo-pandacss/.claude/settings.json +174 -0
- package/templates/turborepo-pandacss/.claude/skills/create-einja-app-release/SKILL.md +186 -0
- package/templates/turborepo-pandacss/.claude/skills/dev-cli-release/SKILL.md +173 -0
- package/templates/turborepo-pandacss/.cursor/commands/spec-create.md +227 -0
- package/templates/turborepo-pandacss/.cursor/commands/start-dev.md +98 -0
- package/templates/turborepo-pandacss/.cursor/commands/task-exec.md +287 -0
- package/templates/turborepo-pandacss/.cursor/commands/task-vibe-kanban-loop.md +532 -0
- package/templates/turborepo-pandacss/.cursor/commands/update-docs-by-task-specs.md +448 -0
- package/templates/turborepo-pandacss/.cursor/mcp.json +45 -0
- package/templates/turborepo-pandacss/.cursor/rules/api-rules.mdc +171 -0
- package/templates/turborepo-pandacss/.cursor/rules/api-test-rules.mdc +181 -0
- package/templates/turborepo-pandacss/.cursor/rules/base-code.mdc +70 -0
- package/templates/turborepo-pandacss/.cursor/rules/base-commit-rules.mdc +174 -0
- package/templates/turborepo-pandacss/.cursor/rules/base-design.mdc +12 -0
- package/templates/turborepo-pandacss/.cursor/rules/base-rules.mdc +231 -0
- package/templates/turborepo-pandacss/.cursor/rules/error-handling-rules.mdc +188 -0
- package/templates/turborepo-pandacss/.cursor/rules/refactor-rules.mdc +93 -0
- package/templates/turborepo-pandacss/.dockerignore +126 -0
- package/templates/turborepo-pandacss/.einja-sync.json +35 -0
- package/templates/turborepo-pandacss/.env.ci +25 -0
- package/templates/turborepo-pandacss/.env.example +35 -0
- package/templates/turborepo-pandacss/.env.personal.example +27 -0
- package/templates/turborepo-pandacss/.envrc +4 -0
- package/templates/turborepo-pandacss/.gitattributes +5 -0
- package/templates/turborepo-pandacss/.husky/pre-commit +1 -0
- package/templates/turborepo-pandacss/.lintstagedrc.js +24 -0
- package/templates/turborepo-pandacss/.mcp.json +45 -0
- package/templates/turborepo-pandacss/.node-version +1 -0
- package/templates/turborepo-pandacss/.templateignore +60 -0
- package/templates/turborepo-pandacss/.vscode/extensions.json +3 -0
- package/templates/turborepo-pandacss/CLAUDE.md +415 -0
- package/templates/turborepo-pandacss/README.md +322 -0
- package/templates/turborepo-pandacss/apps/web/middleware.ts +28 -0
- package/templates/turborepo-pandacss/apps/web/next.config.ts +10 -0
- package/templates/turborepo-pandacss/apps/web/package.json +80 -0
- package/templates/turborepo-pandacss/apps/web/panda.config.ts +114 -0
- package/templates/turborepo-pandacss/apps/web/postcss.config.cjs +6 -0
- package/templates/turborepo-pandacss/apps/web/public/file.svg +1 -0
- package/templates/turborepo-pandacss/apps/web/public/globe.svg +1 -0
- package/templates/turborepo-pandacss/apps/web/public/next.svg +1 -0
- package/templates/turborepo-pandacss/apps/web/public/vercel.svg +1 -0
- package/templates/turborepo-pandacss/apps/web/public/window.svg +1 -0
- package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/dashboard/page.tsx +79 -0
- package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/data/_components/UserTable.tsx +203 -0
- package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/data/page.tsx +57 -0
- package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/layout-client.tsx +31 -0
- package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/layout.tsx +17 -0
- package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/profile/page.tsx +59 -0
- package/templates/turborepo-pandacss/apps/web/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/templates/turborepo-pandacss/apps/web/src/app/api/auth/signup/route.ts +70 -0
- package/templates/turborepo-pandacss/apps/web/src/app/error.tsx +106 -0
- package/templates/turborepo-pandacss/apps/web/src/app/favicon.ico +0 -0
- package/templates/turborepo-pandacss/apps/web/src/app/global-error.tsx +110 -0
- package/templates/turborepo-pandacss/apps/web/src/app/globals.css +121 -0
- package/templates/turborepo-pandacss/apps/web/src/app/layout.tsx +28 -0
- package/templates/turborepo-pandacss/apps/web/src/app/not-found.tsx +54 -0
- package/templates/turborepo-pandacss/apps/web/src/app/page.module.css +165 -0
- package/templates/turborepo-pandacss/apps/web/src/app/page.test.tsx +52 -0
- package/templates/turborepo-pandacss/apps/web/src/app/page.tsx +284 -0
- package/templates/turborepo-pandacss/apps/web/src/app/signin/page.tsx +296 -0
- package/templates/turborepo-pandacss/apps/web/src/app/signup/page.tsx +395 -0
- package/templates/turborepo-pandacss/apps/web/src/application/use-cases/UserUseCases.test.ts +229 -0
- package/templates/turborepo-pandacss/apps/web/src/application/use-cases/UserUseCases.ts +115 -0
- package/templates/turborepo-pandacss/apps/web/src/components/auth/login-button.tsx +35 -0
- package/templates/turborepo-pandacss/apps/web/src/components/auth/logout-button.tsx +24 -0
- package/templates/turborepo-pandacss/apps/web/src/components/auth/user-avatar.test.tsx +68 -0
- package/templates/turborepo-pandacss/apps/web/src/components/auth/user-avatar.tsx +43 -0
- package/templates/turborepo-pandacss/apps/web/src/components/dashboard/dashboard-stats.tsx +128 -0
- package/templates/turborepo-pandacss/apps/web/src/components/providers/query-provider.tsx +30 -0
- package/templates/turborepo-pandacss/apps/web/src/components/providers/session-provider.tsx +12 -0
- package/templates/turborepo-pandacss/apps/web/src/components/shared/Sidebar.tsx +175 -0
- package/templates/turborepo-pandacss/apps/web/src/components/shared/header.tsx +166 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/accordion.tsx +64 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/alert-dialog.tsx +135 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/alert.tsx +60 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/aspect-ratio.tsx +9 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/avatar.tsx +41 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/badge.tsx +39 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/breadcrumb.tsx +101 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/button.tsx +56 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/card.tsx +75 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/checkbox.tsx +29 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/data-table.tsx +189 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/dialog-hook.tsx +210 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/dialog.tsx +129 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/drawer.tsx +124 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/dropdown-menu.tsx +228 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/form.tsx +152 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/hover-card.tsx +38 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/input.tsx +21 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/label.tsx +21 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/pagination.tsx +105 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/popover.tsx +42 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/progress.tsx +28 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/select.tsx +170 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/separator.tsx +28 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/sonner.tsx +25 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/table.tsx +92 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/tabs.tsx +54 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/textarea.tsx +18 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/tooltip.tsx +57 -0
- package/templates/turborepo-pandacss/apps/web/src/components/ui/typography.tsx +158 -0
- package/templates/turborepo-pandacss/apps/web/src/lib/auth/guard.ts +36 -0
- package/templates/turborepo-pandacss/apps/web/src/lib/auth/index.ts +22 -0
- package/templates/turborepo-pandacss/apps/web/src/lib/prisma.ts +3 -0
- package/templates/turborepo-pandacss/apps/web/src/lib/utils.ts +6 -0
- package/templates/turborepo-pandacss/apps/web/test/globals.d.ts +1 -0
- package/templates/turborepo-pandacss/apps/web/test/matchers.d.ts +1 -0
- package/templates/turborepo-pandacss/apps/web/test/setup.ts +22 -0
- package/templates/turborepo-pandacss/apps/web/tsconfig.json +37 -0
- package/templates/turborepo-pandacss/apps/web/vitest.config.ts +20 -0
- package/templates/turborepo-pandacss/apps/web/vitest.d.ts +2 -0
- package/templates/turborepo-pandacss/biome.json +60 -0
- package/templates/turborepo-pandacss/components.json +21 -0
- package/templates/turborepo-pandacss/docker-compose.yml +27 -0
- package/templates/turborepo-pandacss/middleware.ts +32 -0
- package/templates/turborepo-pandacss/next.config.ts +10 -0
- package/templates/turborepo-pandacss/package-lock.json +9346 -0
- package/templates/turborepo-pandacss/package.json +64 -0
- package/templates/turborepo-pandacss/packages/config/package.json +41 -0
- package/templates/turborepo-pandacss/packages/config/panda.config.ts +114 -0
- package/templates/turborepo-pandacss/packages/config/src/index.ts +24 -0
- package/templates/turborepo-pandacss/packages/config/src/worktree-config-loader.ts +129 -0
- package/templates/turborepo-pandacss/packages/config/src/worktree-config.ts +75 -0
- package/templates/turborepo-pandacss/packages/config/tsconfig.build.json +19 -0
- package/templates/turborepo-pandacss/packages/config/tsconfig.json +24 -0
- package/templates/turborepo-pandacss/packages/config/typescript/base.json +19 -0
- package/templates/turborepo-pandacss/packages/front-core/package.json +24 -0
- package/templates/turborepo-pandacss/packages/front-core/src/auth/config.ts +84 -0
- package/templates/turborepo-pandacss/packages/front-core/src/auth/index.ts +5 -0
- package/templates/turborepo-pandacss/packages/front-core/src/auth/types/next-auth.d.ts +20 -0
- package/templates/turborepo-pandacss/packages/front-core/src/auth/utils.ts +29 -0
- package/templates/turborepo-pandacss/packages/front-core/src/context/index.ts +2 -0
- package/templates/turborepo-pandacss/packages/front-core/src/hooks/index.ts +2 -0
- package/templates/turborepo-pandacss/packages/front-core/src/index.ts +4 -0
- package/templates/turborepo-pandacss/packages/front-core/src/utils/index.ts +2 -0
- package/templates/turborepo-pandacss/packages/front-core/tsconfig.json +14 -0
- package/templates/turborepo-pandacss/packages/server-core/package.json +32 -0
- package/templates/turborepo-pandacss/packages/server-core/prisma/schema.prisma +102 -0
- package/templates/turborepo-pandacss/packages/server-core/prisma/seed.ts +67 -0
- package/templates/turborepo-pandacss/packages/server-core/prisma.config.ts +8 -0
- package/templates/turborepo-pandacss/packages/server-core/src/__generated__/fabbrica/index.d.ts +270 -0
- package/templates/turborepo-pandacss/packages/server-core/src/__generated__/fabbrica/index.js +484 -0
- package/templates/turborepo-pandacss/packages/server-core/src/core/result.test.ts +78 -0
- package/templates/turborepo-pandacss/packages/server-core/src/core/result.ts +53 -0
- package/templates/turborepo-pandacss/packages/server-core/src/domain/.gitkeep +0 -0
- package/templates/turborepo-pandacss/packages/server-core/src/domain/entities/User.test.ts +232 -0
- package/templates/turborepo-pandacss/packages/server-core/src/domain/entities/User.ts +105 -0
- package/templates/turborepo-pandacss/packages/server-core/src/domain/repository-interfaces/IUserRepository.ts +101 -0
- package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/client.ts +15 -0
- package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/mappers/UserMapper.test.ts +278 -0
- package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/mappers/UserMapper.ts +103 -0
- package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/repositories/UserRepository.test.ts +317 -0
- package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/repositories/UserRepository.ts +169 -0
- package/templates/turborepo-pandacss/packages/server-core/src/testing/factories/index.ts +22 -0
- package/templates/turborepo-pandacss/packages/server-core/src/testing/factories/user.factory.ts +123 -0
- package/templates/turborepo-pandacss/packages/server-core/src/testing/fixtures/users.ts +92 -0
- package/templates/turborepo-pandacss/packages/server-core/src/testing/helpers/date.ts +49 -0
- package/templates/turborepo-pandacss/packages/server-core/src/testing/helpers/password.ts +50 -0
- package/templates/turborepo-pandacss/packages/server-core/src/testing/index.ts +26 -0
- package/templates/turborepo-pandacss/packages/server-core/tsconfig.json +14 -0
- package/templates/turborepo-pandacss/packages/server-core/vitest.config.ts +15 -0
- package/templates/turborepo-pandacss/packages/ui/package.json +37 -0
- package/templates/turborepo-pandacss/packages/ui/src/accordion.tsx +66 -0
- package/templates/turborepo-pandacss/packages/ui/src/alert-dialog.tsx +157 -0
- package/templates/turborepo-pandacss/packages/ui/src/alert.tsx +66 -0
- package/templates/turborepo-pandacss/packages/ui/src/aspect-ratio.tsx +11 -0
- package/templates/turborepo-pandacss/packages/ui/src/avatar.tsx +53 -0
- package/templates/turborepo-pandacss/packages/ui/src/badge.tsx +46 -0
- package/templates/turborepo-pandacss/packages/ui/src/breadcrumb.tsx +108 -0
- package/templates/turborepo-pandacss/packages/ui/src/button.tsx +59 -0
- package/templates/turborepo-pandacss/packages/ui/src/card.tsx +92 -0
- package/templates/turborepo-pandacss/packages/ui/src/checkbox.tsx +32 -0
- package/templates/turborepo-pandacss/packages/ui/src/data-table.tsx +216 -0
- package/templates/turborepo-pandacss/packages/ui/src/dialog-hook.tsx +226 -0
- package/templates/turborepo-pandacss/packages/ui/src/dialog.tsx +143 -0
- package/templates/turborepo-pandacss/packages/ui/src/drawer.tsx +135 -0
- package/templates/turborepo-pandacss/packages/ui/src/dropdown-menu.tsx +257 -0
- package/templates/turborepo-pandacss/packages/ui/src/form.tsx +168 -0
- package/templates/turborepo-pandacss/packages/ui/src/hover-card.tsx +44 -0
- package/templates/turborepo-pandacss/packages/ui/src/input.tsx +21 -0
- package/templates/turborepo-pandacss/packages/ui/src/label.tsx +24 -0
- package/templates/turborepo-pandacss/packages/ui/src/lib/utils.ts +6 -0
- package/templates/turborepo-pandacss/packages/ui/src/pagination.tsx +126 -0
- package/templates/turborepo-pandacss/packages/ui/src/popover.tsx +48 -0
- package/templates/turborepo-pandacss/packages/ui/src/progress.tsx +31 -0
- package/templates/turborepo-pandacss/packages/ui/src/select.tsx +185 -0
- package/templates/turborepo-pandacss/packages/ui/src/separator.tsx +28 -0
- package/templates/turborepo-pandacss/packages/ui/src/skeleton.tsx +13 -0
- package/templates/turborepo-pandacss/packages/ui/src/sonner.tsx +25 -0
- package/templates/turborepo-pandacss/packages/ui/src/table.tsx +116 -0
- package/templates/turborepo-pandacss/packages/ui/src/tabs.tsx +66 -0
- package/templates/turborepo-pandacss/packages/ui/src/textarea.tsx +18 -0
- package/templates/turborepo-pandacss/packages/ui/src/tooltip.tsx +61 -0
- package/templates/turborepo-pandacss/packages/ui/src/typography.tsx +187 -0
- package/templates/turborepo-pandacss/packages/ui/tsconfig.json +20 -0
- package/templates/turborepo-pandacss/panda.config.ts +114 -0
- package/templates/turborepo-pandacss/pnpm-lock.yaml +9032 -0
- package/templates/turborepo-pandacss/pnpm-workspace.yaml +11 -0
- package/templates/turborepo-pandacss/postcss.config.cjs +6 -0
- package/templates/turborepo-pandacss/prisma/schema.prisma +82 -0
- package/templates/turborepo-pandacss/public/file.svg +1 -0
- package/templates/turborepo-pandacss/public/globe.svg +1 -0
- package/templates/turborepo-pandacss/public/next.svg +1 -0
- package/templates/turborepo-pandacss/public/vercel.svg +1 -0
- package/templates/turborepo-pandacss/public/window.svg +1 -0
- package/templates/turborepo-pandacss/scripts/cli-template-update.ts +387 -0
- package/templates/turborepo-pandacss/scripts/env-show.ts +129 -0
- package/templates/turborepo-pandacss/scripts/env.ts +555 -0
- package/templates/turborepo-pandacss/scripts/init.sh +92 -0
- package/templates/turborepo-pandacss/scripts/setup-dev.ts +640 -0
- package/templates/turborepo-pandacss/scripts/template-update.ts +277 -0
- package/templates/turborepo-pandacss/scripts/worktree/dev.ts +872 -0
- package/templates/turborepo-pandacss/test/globals.d.ts +1 -0
- package/templates/turborepo-pandacss/test/setup.ts +22 -0
- package/templates/turborepo-pandacss/tsconfig.json +46 -0
- package/templates/turborepo-pandacss/turbo.json +57 -0
- package/templates/turborepo-pandacss/vitest.config.ts +20 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree対応の開発サーバー起動スクリプト
|
|
3
|
+
*
|
|
4
|
+
* ブランチ名から一意なポート番号を計算し、複数のWorktreeを並行して起動可能にする。
|
|
5
|
+
* PostgreSQLは共有インスタンスを使用し、database名で分離する。
|
|
6
|
+
* 設定はworktree.config.jsonから読み込む。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import readline from "node:readline";
|
|
14
|
+
import {
|
|
15
|
+
type AppConfig,
|
|
16
|
+
type WorktreeConfig,
|
|
17
|
+
loadWorktreeConfig,
|
|
18
|
+
} from "{{packageName}}/config";
|
|
19
|
+
|
|
20
|
+
/** 設定を保持するグローバル変数 */
|
|
21
|
+
let config: WorktreeConfig;
|
|
22
|
+
|
|
23
|
+
/** ログファイルのファイルディスクリプタ(バックグラウンドモード時のみ使用) */
|
|
24
|
+
let logFd: number | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ログファイルを初期化(バックグラウンドモード時)
|
|
28
|
+
* @param logFile - ログファイルパス
|
|
29
|
+
*/
|
|
30
|
+
function initLogFile(logFile: string): void {
|
|
31
|
+
// ログディレクトリを作成
|
|
32
|
+
const logDir = path.dirname(logFile);
|
|
33
|
+
if (!fs.existsSync(logDir)) {
|
|
34
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
// ログファイルをクリアして新規作成
|
|
37
|
+
logFd = fs.openSync(logFile, "w");
|
|
38
|
+
// タイムスタンプ付きヘッダーを書き込み
|
|
39
|
+
const header = `${"=".repeat(60)}\n[${new Date().toISOString()}] 開発サーバー起動\n${"=".repeat(60)}\n`;
|
|
40
|
+
fs.writeSync(logFd, header);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ログを出力(標準出力 + ログファイル)
|
|
45
|
+
* @param message - ログメッセージ
|
|
46
|
+
*/
|
|
47
|
+
function log(message: string): void {
|
|
48
|
+
console.log(message);
|
|
49
|
+
if (logFd !== null) {
|
|
50
|
+
fs.writeSync(logFd, message + "\n");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* エラーログを出力(標準エラー + ログファイル)
|
|
56
|
+
* @param message - エラーメッセージ
|
|
57
|
+
*/
|
|
58
|
+
function logError(message: string): void {
|
|
59
|
+
console.error(message);
|
|
60
|
+
if (logFd !== null) {
|
|
61
|
+
fs.writeSync(logFd, message + "\n");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* コマンドを実行し、出力をログに記録
|
|
67
|
+
* @param command - 実行するコマンド
|
|
68
|
+
* @param options - execSync のオプション(envなど)
|
|
69
|
+
* @returns コマンドの出力
|
|
70
|
+
*/
|
|
71
|
+
function execWithLog(
|
|
72
|
+
command: string,
|
|
73
|
+
options: Parameters<typeof execSync>[1] = {},
|
|
74
|
+
): string {
|
|
75
|
+
try {
|
|
76
|
+
const result = execSync(command, {
|
|
77
|
+
...options,
|
|
78
|
+
encoding: "utf-8",
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
+
});
|
|
81
|
+
if (result) {
|
|
82
|
+
log(result.trim());
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Error && "stdout" in error) {
|
|
87
|
+
const execError = error as { stdout: string; stderr: string };
|
|
88
|
+
if (execError.stdout) {
|
|
89
|
+
log(execError.stdout.trim());
|
|
90
|
+
}
|
|
91
|
+
if (execError.stderr) {
|
|
92
|
+
logError(execError.stderr.trim());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 設定を取得(遅延読み込み)
|
|
101
|
+
*/
|
|
102
|
+
function getConfig(): WorktreeConfig {
|
|
103
|
+
if (!config) {
|
|
104
|
+
config = loadWorktreeConfig();
|
|
105
|
+
}
|
|
106
|
+
return config;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ブランチ名からハッシュベースで一意なポート番号を計算
|
|
111
|
+
*
|
|
112
|
+
* 各アプリは設定されたポート範囲を使用し、競合を回避。
|
|
113
|
+
* PostgreSQLは共有インスタンスを使用(設定されたポート)。
|
|
114
|
+
*
|
|
115
|
+
* @param branchName - Gitブランチ名
|
|
116
|
+
* @param apps - アプリケーション設定の配列
|
|
117
|
+
* @returns アプリIDをキーとするポート番号のRecord
|
|
118
|
+
*/
|
|
119
|
+
export function calculatePorts(
|
|
120
|
+
branchName: string,
|
|
121
|
+
apps: AppConfig[],
|
|
122
|
+
): Record<string, number> {
|
|
123
|
+
// ブランチ名をSHA-256でハッシュ化
|
|
124
|
+
const hash = crypto.createHash("sha256").update(branchName).digest("hex");
|
|
125
|
+
|
|
126
|
+
// ハッシュの最初の8文字を16進数として数値化
|
|
127
|
+
const hashNum = Number.parseInt(hash.slice(0, 8), 16);
|
|
128
|
+
|
|
129
|
+
const ports: Record<string, number> = {};
|
|
130
|
+
for (const app of apps) {
|
|
131
|
+
const offset = hashNum % app.rangeSize;
|
|
132
|
+
ports[app.id] = app.portRangeStart + offset;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return ports;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* ブランチ名からデータベース名を生成
|
|
140
|
+
* PostgreSQLのデータベース名として有効な形式に変換
|
|
141
|
+
*
|
|
142
|
+
* @param branchName - Gitブランチ名
|
|
143
|
+
* @returns データベース名(例: main, feature_auth)
|
|
144
|
+
*/
|
|
145
|
+
export function generateDatabaseName(branchName: string): string {
|
|
146
|
+
// ブランチ名を正規化(英数字とアンダースコアのみ)
|
|
147
|
+
const normalized = branchName
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.replace(/[^a-z0-9]/g, "_")
|
|
150
|
+
.replace(/_+/g, "_")
|
|
151
|
+
.replace(/^_|_$/g, "")
|
|
152
|
+
.slice(0, 32);
|
|
153
|
+
|
|
154
|
+
return normalized || "main";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* ポートが使用中かチェック(LISTENまたはESTABLISHED状態のみ)
|
|
159
|
+
*
|
|
160
|
+
* @param port - チェックするポート番号
|
|
161
|
+
* @returns ポートが使用中の場合true
|
|
162
|
+
*/
|
|
163
|
+
export function isPortInUse(port: number): boolean {
|
|
164
|
+
try {
|
|
165
|
+
// lsofコマンドでポートをLISTENしているプロセスを確認
|
|
166
|
+
const result = execSync(`lsof -i :${port} -s TCP:LISTEN`, {
|
|
167
|
+
encoding: "utf-8",
|
|
168
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
169
|
+
});
|
|
170
|
+
return result.trim().length > 0;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* ポートを使用しているプロセスのPIDを取得(LISTEN状態のみ)
|
|
178
|
+
*
|
|
179
|
+
* @param port - チェックするポート番号
|
|
180
|
+
* @returns PIDの配列(見つからない場合は空配列)
|
|
181
|
+
*/
|
|
182
|
+
export function getProcessesOnPort(port: number): number[] {
|
|
183
|
+
try {
|
|
184
|
+
// lsofコマンドでポートをLISTENしているプロセスのPIDを取得
|
|
185
|
+
const result = execSync(`lsof -ti :${port} -s TCP:LISTEN`, { encoding: "utf-8" }).trim();
|
|
186
|
+
if (!result) return [];
|
|
187
|
+
return result.split("\n").map((pid) => Number.parseInt(pid, 10)).filter((pid) => !Number.isNaN(pid));
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 指定したポートを使用しているプロセスを終了
|
|
195
|
+
*
|
|
196
|
+
* @param port - 終了するプロセスのポート番号
|
|
197
|
+
* @param signal - 送信するシグナル(デフォルト: SIGTERM)
|
|
198
|
+
* @returns 終了したプロセス数
|
|
199
|
+
*/
|
|
200
|
+
export function killProcessesOnPort(port: number, signal: NodeJS.Signals = "SIGTERM"): number {
|
|
201
|
+
const pids = getProcessesOnPort(port);
|
|
202
|
+
if (pids.length === 0) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`🔪 ポート ${port} を使用しているプロセス (PID: ${pids.join(", ")}) を終了します...`);
|
|
207
|
+
|
|
208
|
+
let killedCount = 0;
|
|
209
|
+
for (const pid of pids) {
|
|
210
|
+
try {
|
|
211
|
+
process.kill(pid, signal);
|
|
212
|
+
killedCount++;
|
|
213
|
+
console.log(` ✓ PID ${pid} にシグナル ${signal} を送信`);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn(` ⚠ PID ${pid} の終了に失敗: ${error}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// プロセス終了を待機(最大5秒)
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
while (Date.now() - startTime < 5000) {
|
|
222
|
+
if (!isPortInUse(port)) {
|
|
223
|
+
console.log(`✅ ポート ${port} が解放されました`);
|
|
224
|
+
return killedCount;
|
|
225
|
+
}
|
|
226
|
+
spawnSync("sleep", ["0.2"]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// まだ使用中ならSIGKILLを送信
|
|
230
|
+
const remainingPids = getProcessesOnPort(port);
|
|
231
|
+
if (remainingPids.length > 0) {
|
|
232
|
+
console.log(`⚠ ポート ${port} がまだ使用中です。SIGKILLを送信します...`);
|
|
233
|
+
for (const pid of remainingPids) {
|
|
234
|
+
try {
|
|
235
|
+
process.kill(pid, "SIGKILL");
|
|
236
|
+
} catch {
|
|
237
|
+
// 無視
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
spawnSync("sleep", ["0.5"]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return killedCount;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* ログファイルのパスを取得
|
|
248
|
+
*
|
|
249
|
+
* 各worktreeは独立したディレクトリなので、シンプルに log/dev.log を使用。
|
|
250
|
+
* 同じブランチ名のworktreeは実用上存在しないため、ブランチ名での分離は不要。
|
|
251
|
+
*
|
|
252
|
+
* @returns ログファイルのパス
|
|
253
|
+
*/
|
|
254
|
+
export function getLogFilePath(): string {
|
|
255
|
+
const projectRoot = process.cwd();
|
|
256
|
+
const logDir = path.join(projectRoot, "log");
|
|
257
|
+
|
|
258
|
+
// ログディレクトリを作成
|
|
259
|
+
if (!fs.existsSync(logDir)) {
|
|
260
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return path.join(logDir, "dev.log");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* ポートを使用しているプロセスのコマンドパスを取得(LISTEN状態のみ)
|
|
268
|
+
*
|
|
269
|
+
* @param port - チェックするポート番号
|
|
270
|
+
* @returns コマンドパスの配列
|
|
271
|
+
*/
|
|
272
|
+
export function getProcessCommandsOnPort(port: number): { pid: number; command: string }[] {
|
|
273
|
+
try {
|
|
274
|
+
const result = execSync(`lsof -i :${port} -s TCP:LISTEN -Fp -Fc`, { encoding: "utf-8" }).trim();
|
|
275
|
+
if (!result) return [];
|
|
276
|
+
|
|
277
|
+
const processes: { pid: number; command: string }[] = [];
|
|
278
|
+
let currentPid = 0;
|
|
279
|
+
|
|
280
|
+
for (const line of result.split("\n")) {
|
|
281
|
+
if (line.startsWith("p")) {
|
|
282
|
+
currentPid = Number.parseInt(line.slice(1), 10);
|
|
283
|
+
} else if (line.startsWith("c") && currentPid) {
|
|
284
|
+
processes.push({ pid: currentPid, command: line.slice(1) });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return processes;
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* プロセスがこのリポジトリに属するか判定
|
|
295
|
+
*
|
|
296
|
+
* @param pid - プロセスID
|
|
297
|
+
* @returns このリポジトリのプロセスならtrue
|
|
298
|
+
*/
|
|
299
|
+
export function isProcessFromThisRepo(pid: number): boolean {
|
|
300
|
+
try {
|
|
301
|
+
const cwd = execSync(`lsof -p ${pid} -Fn | grep "^n" | grep "cwd" || true`, {
|
|
302
|
+
encoding: "utf-8",
|
|
303
|
+
}).trim();
|
|
304
|
+
|
|
305
|
+
// cwdが取得できない場合はコマンドラインで判定
|
|
306
|
+
if (!cwd) {
|
|
307
|
+
const cmdline = execSync(`ps -p ${pid} -o command=`, { encoding: "utf-8" }).trim();
|
|
308
|
+
return cmdline.includes(process.cwd()) || cmdline.includes("turbo") || cmdline.includes("next");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return cwd.includes(process.cwd());
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* ポートを確保する(必要に応じて自リポジトリのプロセスをkill)
|
|
319
|
+
*
|
|
320
|
+
* 他のワークツリーや外部アプリは誤killしないよう、
|
|
321
|
+
* 自リポジトリのプロセスのみを対象とする。
|
|
322
|
+
*
|
|
323
|
+
* @param ports - 確保するポート番号セット
|
|
324
|
+
* @returns 確保したポート番号セット
|
|
325
|
+
*/
|
|
326
|
+
export function ensurePorts(ports: Record<string, number>): Record<string, number> {
|
|
327
|
+
for (const [appId, port] of Object.entries(ports)) {
|
|
328
|
+
if (!isPortInUse(port)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const processes = getProcessCommandsOnPort(port);
|
|
333
|
+
const ownProcesses = processes.filter((p) => isProcessFromThisRepo(p.pid));
|
|
334
|
+
|
|
335
|
+
if (ownProcesses.length > 0) {
|
|
336
|
+
// 自リポジトリのプロセスならkill
|
|
337
|
+
console.log(`🔄 ポート ${port} を使用中の自プロセスを停止します...`);
|
|
338
|
+
for (const p of ownProcesses) {
|
|
339
|
+
try {
|
|
340
|
+
process.kill(p.pid, "SIGTERM");
|
|
341
|
+
console.log(` ✓ PID ${p.pid} (${p.command}) を停止`);
|
|
342
|
+
} catch {
|
|
343
|
+
// 既に終了している
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// 終了待機
|
|
347
|
+
spawnSync("sleep", ["0.5"]);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// まだ使用中なら外部プロセス
|
|
351
|
+
if (isPortInUse(port)) {
|
|
352
|
+
const remaining = getProcessCommandsOnPort(port);
|
|
353
|
+
console.error(`❌ ポート ${port} は外部プロセスが使用中です:`);
|
|
354
|
+
for (const p of remaining) {
|
|
355
|
+
console.error(` - PID ${p.pid}: ${p.command}`);
|
|
356
|
+
}
|
|
357
|
+
console.error(` 手動で停止するか、別のブランチ名を使用してください`);
|
|
358
|
+
throw new Error(`ポート ${port} を確保できません(外部プロセスが使用中)`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return ports;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 現在のGitブランチ名を取得
|
|
367
|
+
*
|
|
368
|
+
* @returns ブランチ名
|
|
369
|
+
*/
|
|
370
|
+
export function getCurrentBranch(): string {
|
|
371
|
+
try {
|
|
372
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
373
|
+
encoding: "utf-8",
|
|
374
|
+
}).trim();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error("Gitブランチの取得に失敗しました:", error);
|
|
377
|
+
return "main";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* AUTH_SECRETを生成
|
|
383
|
+
*
|
|
384
|
+
* @returns 生成されたAUTH_SECRET(32文字以上のランダム文字列)
|
|
385
|
+
*/
|
|
386
|
+
export function generateAuthSecret(): string {
|
|
387
|
+
return crypto.randomBytes(32).toString("hex");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* .envファイルを生成
|
|
392
|
+
*
|
|
393
|
+
* .env.exampleをベースに、ワークツリー固有の値を設定した.envを生成。
|
|
394
|
+
*
|
|
395
|
+
* @param ports - アプリIDをキーとするポート番号のRecord
|
|
396
|
+
* @param databaseName - データベース名
|
|
397
|
+
*/
|
|
398
|
+
export function writeEnvFile(
|
|
399
|
+
ports: Record<string, number>,
|
|
400
|
+
databaseName: string,
|
|
401
|
+
): void {
|
|
402
|
+
const cfg = getConfig();
|
|
403
|
+
const projectRoot = process.cwd();
|
|
404
|
+
const envExamplePath = path.join(projectRoot, ".env.example");
|
|
405
|
+
const envPath = path.join(projectRoot, ".env");
|
|
406
|
+
|
|
407
|
+
// .env.exampleをベースに読み込む
|
|
408
|
+
let envContent = "";
|
|
409
|
+
if (fs.existsSync(envExamplePath)) {
|
|
410
|
+
envContent = fs.readFileSync(envExamplePath, "utf-8");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ヘッダーを追加
|
|
414
|
+
const branch = getCurrentBranch();
|
|
415
|
+
const header = `# ============================================
|
|
416
|
+
# Auto-generated by pnpm dev - DO NOT EDIT
|
|
417
|
+
# ============================================
|
|
418
|
+
# Branch: ${branch}
|
|
419
|
+
# Generated: ${new Date().toISOString()}
|
|
420
|
+
#
|
|
421
|
+
# このファイルは pnpm dev 実行時に上書きされます
|
|
422
|
+
# 手動で編集しても次回実行時にリセットされます
|
|
423
|
+
# 秘密情報は .env.local に設定してください
|
|
424
|
+
# ============================================
|
|
425
|
+
|
|
426
|
+
`;
|
|
427
|
+
|
|
428
|
+
// DATABASE_URLを生成
|
|
429
|
+
const databaseUrl = `postgresql://postgres:postgres@localhost:${cfg.postgres.port}/${databaseName}?schema=public`;
|
|
430
|
+
|
|
431
|
+
// 環境変数設定
|
|
432
|
+
const envSettings: Record<string, string> = {
|
|
433
|
+
DATABASE_URL: `"${databaseUrl}"`,
|
|
434
|
+
AUTH_SECRET: generateAuthSecret(),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// 各アプリのポートを動的に追加(PORT_WEB, PORT_ADMIN等)
|
|
438
|
+
for (const [appId, port] of Object.entries(ports)) {
|
|
439
|
+
envSettings[`PORT_${appId.toUpperCase()}`] = port.toString();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// webアプリがあればNEXTAUTH_URLを設定
|
|
443
|
+
if (ports.web) {
|
|
444
|
+
envSettings.NEXTAUTH_URL = `http://localhost:${ports.web}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 環境変数の更新または追加
|
|
448
|
+
for (const [key, value] of Object.entries(envSettings)) {
|
|
449
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
450
|
+
if (regex.test(envContent)) {
|
|
451
|
+
envContent = envContent.replace(regex, `${key}=${value}`);
|
|
452
|
+
} else {
|
|
453
|
+
envContent += `\n${key}=${value}`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fs.writeFileSync(envPath, header + envContent.trim() + "\n", "utf-8");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 設定されたポートでPostgreSQLが起動しているか確認
|
|
462
|
+
*
|
|
463
|
+
* @returns 起動していればコンテナ名、なければnull
|
|
464
|
+
*/
|
|
465
|
+
export function getRunningPostgresContainer(): string | null {
|
|
466
|
+
const cfg = getConfig();
|
|
467
|
+
try {
|
|
468
|
+
// 設定されたポートを使用しているコンテナを検索
|
|
469
|
+
const result = execSync(
|
|
470
|
+
`docker ps --filter "publish=${cfg.postgres.port}" --format "{{.Names}}"`,
|
|
471
|
+
{
|
|
472
|
+
encoding: "utf-8",
|
|
473
|
+
},
|
|
474
|
+
).trim();
|
|
475
|
+
return result.length > 0 ? result.split("\n")[0] : null;
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* PostgreSQLコンテナが起動しているか確認(後方互換性)
|
|
483
|
+
*
|
|
484
|
+
* @returns 起動していればtrue
|
|
485
|
+
*/
|
|
486
|
+
export function isPostgresRunning(): boolean {
|
|
487
|
+
return getRunningPostgresContainer() !== null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* PostgreSQLコンテナを起動(または既存を再利用)
|
|
492
|
+
*
|
|
493
|
+
* @returns 使用するコンテナ名
|
|
494
|
+
*/
|
|
495
|
+
export function startPostgres(): string {
|
|
496
|
+
const cfg = getConfig();
|
|
497
|
+
|
|
498
|
+
// 既存のPostgreSQLコンテナを確認
|
|
499
|
+
const existingContainer = getRunningPostgresContainer();
|
|
500
|
+
if (existingContainer) {
|
|
501
|
+
log(`✅ PostgreSQL(${existingContainer})は既に起動しています`);
|
|
502
|
+
return existingContainer;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
log("🐘 PostgreSQLコンテナを起動します...");
|
|
506
|
+
|
|
507
|
+
// 環境変数を設定してdocker compose up
|
|
508
|
+
execWithLog("docker compose up -d postgres", {
|
|
509
|
+
env: {
|
|
510
|
+
...process.env,
|
|
511
|
+
POSTGRES_PORT: cfg.postgres.port.toString(),
|
|
512
|
+
POSTGRES_CONTAINER_NAME: cfg.postgres.containerName,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// 起動待機
|
|
517
|
+
log("⏳ PostgreSQL起動を待機中...");
|
|
518
|
+
let retries = 0;
|
|
519
|
+
const maxRetries = 30;
|
|
520
|
+
|
|
521
|
+
while (retries < maxRetries) {
|
|
522
|
+
const container = getRunningPostgresContainer();
|
|
523
|
+
if (container) {
|
|
524
|
+
try {
|
|
525
|
+
execSync(`docker exec ${container} pg_isready -U postgres`, {
|
|
526
|
+
stdio: "ignore",
|
|
527
|
+
});
|
|
528
|
+
log("✅ PostgreSQL起動完了");
|
|
529
|
+
return container;
|
|
530
|
+
} catch {
|
|
531
|
+
// まだ準備中
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
retries++;
|
|
535
|
+
execSync("sleep 1");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
throw new Error("PostgreSQLの起動に失敗しました");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* データベースが存在するか確認し、なければ作成
|
|
543
|
+
*
|
|
544
|
+
* @param containerName - PostgreSQLコンテナ名
|
|
545
|
+
* @param databaseName - 作成するデータベース名
|
|
546
|
+
*/
|
|
547
|
+
export function ensureDatabaseExists(
|
|
548
|
+
containerName: string,
|
|
549
|
+
databaseName: string,
|
|
550
|
+
): void {
|
|
551
|
+
log(`🗄️ データベース「${databaseName}」を確認中...`);
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
// データベースの存在確認
|
|
555
|
+
const result = execSync(
|
|
556
|
+
`docker exec ${containerName} psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${databaseName}'"`,
|
|
557
|
+
{ encoding: "utf-8" },
|
|
558
|
+
).trim();
|
|
559
|
+
|
|
560
|
+
if (result === "1") {
|
|
561
|
+
log(`✅ データベース「${databaseName}」は既に存在します`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
// コマンド失敗時は作成を試みる
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// データベース作成
|
|
569
|
+
log(`📦 データベース「${databaseName}」を作成します...`);
|
|
570
|
+
execWithLog(
|
|
571
|
+
`docker exec ${containerName} psql -U postgres -c "CREATE DATABASE ${databaseName}"`,
|
|
572
|
+
);
|
|
573
|
+
log(`✅ データベース「${databaseName}」を作成しました`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* マイグレーションを実行
|
|
578
|
+
*
|
|
579
|
+
* @param databaseName - データベース名
|
|
580
|
+
*/
|
|
581
|
+
export function runMigration(databaseName: string): void {
|
|
582
|
+
const cfg = getConfig();
|
|
583
|
+
log("🔄 マイグレーションを実行します...");
|
|
584
|
+
const databaseUrl = `postgresql://postgres:postgres@localhost:${cfg.postgres.port}/${databaseName}?schema=public`;
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
execWithLog("pnpm db:push", {
|
|
588
|
+
env: {
|
|
589
|
+
...process.env,
|
|
590
|
+
DATABASE_URL: databaseUrl,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
log("✅ マイグレーション完了");
|
|
594
|
+
} catch (error) {
|
|
595
|
+
logError("❌ マイグレーションに失敗しました");
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* 開発サーバーを起動(turbo経由)
|
|
602
|
+
*
|
|
603
|
+
* @param envVars - 追加の環境変数
|
|
604
|
+
* @param options - 起動オプション
|
|
605
|
+
*/
|
|
606
|
+
function startDevServer(
|
|
607
|
+
envVars: Record<string, string> = {},
|
|
608
|
+
options: { background?: boolean; logFile?: string } = {},
|
|
609
|
+
): void {
|
|
610
|
+
const { background = false, logFile } = options;
|
|
611
|
+
|
|
612
|
+
// ポートの確保は ensurePorts() で実施済み
|
|
613
|
+
|
|
614
|
+
if (background && logFile) {
|
|
615
|
+
log("🚀 開発サーバーをバックグラウンドで起動します...");
|
|
616
|
+
log(`📄 ログファイル: ${logFile}`);
|
|
617
|
+
|
|
618
|
+
// 既存のlogFdを使用するか、新規作成
|
|
619
|
+
let logStream: number;
|
|
620
|
+
if (logFd !== null) {
|
|
621
|
+
// 既存のログファイルディスクリプタを使用(前処理ログと連続)
|
|
622
|
+
logStream = logFd;
|
|
623
|
+
// turbo開始のセパレータを追加
|
|
624
|
+
const separator = `\n${"=".repeat(60)}\n[${new Date().toISOString()}] turbo run dev 開始\n${"=".repeat(60)}\n`;
|
|
625
|
+
fs.writeSync(logStream, separator);
|
|
626
|
+
} else {
|
|
627
|
+
// ログファイルをクリアして新規作成(setupなしの場合)
|
|
628
|
+
logStream = fs.openSync(logFile, "w");
|
|
629
|
+
const header = `${"=".repeat(60)}\n[${new Date().toISOString()}] 開発サーバー起動\n${"=".repeat(60)}\n`;
|
|
630
|
+
fs.writeSync(logStream, header);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// バックグラウンドプロセスとして起動
|
|
634
|
+
const child = spawn("pnpm", ["turbo", "run", "dev"], {
|
|
635
|
+
stdio: ["ignore", logStream, logStream],
|
|
636
|
+
shell: true,
|
|
637
|
+
detached: true,
|
|
638
|
+
env: {
|
|
639
|
+
...process.env,
|
|
640
|
+
...envVars,
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// 親プロセスから切り離す
|
|
645
|
+
child.unref();
|
|
646
|
+
|
|
647
|
+
// PIDをファイルに保存(後でstop/statusで使用)
|
|
648
|
+
const pidFile = logFile.replace(".log", ".pid");
|
|
649
|
+
fs.writeFileSync(pidFile, child.pid?.toString() ?? "");
|
|
650
|
+
|
|
651
|
+
log(`✅ 開発サーバーが起動しました (PID: ${child.pid})`);
|
|
652
|
+
log(`\n📋 ログを確認: tail -f ${logFile}`);
|
|
653
|
+
log(`🛑 停止: pnpm dev:stop`);
|
|
654
|
+
|
|
655
|
+
// 親プロセスは終了
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
log("🚀 開発サーバーを起動します...");
|
|
660
|
+
|
|
661
|
+
// spawn を使用してプロセスを実行(環境変数を渡す)
|
|
662
|
+
const child = spawn("pnpm", ["turbo", "run", "dev"], {
|
|
663
|
+
stdio: "inherit",
|
|
664
|
+
shell: true,
|
|
665
|
+
env: {
|
|
666
|
+
...process.env,
|
|
667
|
+
...envVars,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
child.on("error", (error) => {
|
|
672
|
+
logError(`開発サーバーの起動に失敗しました: ${error}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
child.on("exit", (code) => {
|
|
677
|
+
process.exit(code ?? 0);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* メイン実行関数
|
|
683
|
+
*
|
|
684
|
+
* @param options - 実行オプション
|
|
685
|
+
*/
|
|
686
|
+
export function main(options: {
|
|
687
|
+
setupOnly?: boolean;
|
|
688
|
+
skipSetup?: boolean;
|
|
689
|
+
background?: boolean;
|
|
690
|
+
} = {}): void {
|
|
691
|
+
const { setupOnly = false, skipSetup = false, background = false } = options;
|
|
692
|
+
const cfg = getConfig();
|
|
693
|
+
|
|
694
|
+
// バックグラウンドモードの場合、ログファイルを初期化
|
|
695
|
+
const logFile = getLogFilePath();
|
|
696
|
+
if (background) {
|
|
697
|
+
initLogFile(logFile);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// バックグラウンドモードの場合、既存のサーバーを停止
|
|
701
|
+
if (background) {
|
|
702
|
+
const pidFile = logFile.replace(".log", ".pid");
|
|
703
|
+
|
|
704
|
+
if (fs.existsSync(pidFile)) {
|
|
705
|
+
const pid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
706
|
+
if (!Number.isNaN(pid)) {
|
|
707
|
+
try {
|
|
708
|
+
process.kill(pid, 0); // プロセス存在確認
|
|
709
|
+
log(`⚠️ 既存の開発サーバー (PID: ${pid}) が起動中です`);
|
|
710
|
+
log("🔄 既存サーバーを停止して再起動します...");
|
|
711
|
+
stopDevServer();
|
|
712
|
+
} catch {
|
|
713
|
+
// プロセスが存在しない場合はPIDファイルを削除
|
|
714
|
+
fs.unlinkSync(pidFile);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// --skip-setup: 環境準備をスキップして直接turbo run dev
|
|
721
|
+
if (skipSetup) {
|
|
722
|
+
startDevServer();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const branch = getCurrentBranch();
|
|
727
|
+
log(`現在のブランチ: ${branch}`);
|
|
728
|
+
|
|
729
|
+
// ブランチ名からデータベース名を生成
|
|
730
|
+
const databaseName = generateDatabaseName(branch);
|
|
731
|
+
log(`データベース名: ${databaseName}`);
|
|
732
|
+
|
|
733
|
+
// ブランチ名からポート番号を計算(固定)
|
|
734
|
+
const calculatedPorts = calculatePorts(branch, cfg.apps);
|
|
735
|
+
log(`ポート: ${JSON.stringify(calculatedPorts)}`);
|
|
736
|
+
|
|
737
|
+
// ポートを確保(自リポジトリのプロセスのみkill対象)
|
|
738
|
+
const availablePorts = ensurePorts(calculatedPorts);
|
|
739
|
+
|
|
740
|
+
// .envに書き込み
|
|
741
|
+
writeEnvFile(availablePorts, databaseName);
|
|
742
|
+
log(".envに書き込みました");
|
|
743
|
+
|
|
744
|
+
// PostgreSQLの起動確認・起動(コンテナ名を取得)
|
|
745
|
+
const containerName = startPostgres();
|
|
746
|
+
|
|
747
|
+
// データベースの存在確認・作成
|
|
748
|
+
ensureDatabaseExists(containerName, databaseName);
|
|
749
|
+
|
|
750
|
+
// マイグレーション実行
|
|
751
|
+
runMigration(databaseName);
|
|
752
|
+
|
|
753
|
+
// webアプリのポートを取得(表示用)
|
|
754
|
+
const webPort = availablePorts.web ?? Object.values(availablePorts)[0];
|
|
755
|
+
|
|
756
|
+
log(`
|
|
757
|
+
===========================================
|
|
758
|
+
Worktree環境設定完了
|
|
759
|
+
===========================================
|
|
760
|
+
Web: http://localhost:${webPort}
|
|
761
|
+
PostgreSQL: localhost:${cfg.postgres.port}
|
|
762
|
+
Database: ${databaseName}
|
|
763
|
+
${Object.entries(availablePorts)
|
|
764
|
+
.map(([id, port]) => ` PORT_${id.toUpperCase()}: ${port}`)
|
|
765
|
+
.join("\n")}
|
|
766
|
+
===========================================
|
|
767
|
+
`);
|
|
768
|
+
|
|
769
|
+
if (setupOnly) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// 環境変数を準備
|
|
774
|
+
const envVars: Record<string, string> = {
|
|
775
|
+
DATABASE_URL: `postgresql://postgres:postgres@localhost:${cfg.postgres.port}/${databaseName}?schema=public`,
|
|
776
|
+
NEXTAUTH_URL: `http://localhost:${webPort}`,
|
|
777
|
+
AUTH_SECRET: generateAuthSecret(),
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// 各アプリのポートを追加
|
|
781
|
+
for (const [id, port] of Object.entries(availablePorts)) {
|
|
782
|
+
envVars[`PORT_${id.toUpperCase()}`] = port.toString();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 開発サーバーを起動(logFileは関数冒頭で取得済み)
|
|
786
|
+
startDevServer(envVars, { background, logFile });
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* 開発サーバーを停止
|
|
791
|
+
*/
|
|
792
|
+
export function stopDevServer(): void {
|
|
793
|
+
const branch = getCurrentBranch();
|
|
794
|
+
const logFile = getLogFilePath();
|
|
795
|
+
const pidFile = logFile.replace(".log", ".pid");
|
|
796
|
+
|
|
797
|
+
if (!fs.existsSync(pidFile)) {
|
|
798
|
+
console.log("⚠ 実行中の開発サーバーが見つかりません");
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const pid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
803
|
+
if (Number.isNaN(pid)) {
|
|
804
|
+
console.log("⚠ PIDファイルが無効です");
|
|
805
|
+
fs.unlinkSync(pidFile);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
process.kill(pid, "SIGTERM");
|
|
811
|
+
console.log(`✅ 開発サーバー (PID: ${pid}) を停止しました`);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.log(`⚠ プロセス ${pid} は既に終了しています`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// PIDファイルを削除
|
|
817
|
+
fs.unlinkSync(pidFile);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* 開発サーバーのステータスを表示
|
|
822
|
+
*/
|
|
823
|
+
export function showDevStatus(): void {
|
|
824
|
+
const branch = getCurrentBranch();
|
|
825
|
+
const logFile = getLogFilePath();
|
|
826
|
+
const pidFile = logFile.replace(".log", ".pid");
|
|
827
|
+
const cfg = getConfig();
|
|
828
|
+
|
|
829
|
+
console.log(`\n📊 開発サーバーステータス`);
|
|
830
|
+
console.log(`${"=".repeat(50)}`);
|
|
831
|
+
console.log(`ブランチ: ${branch}`);
|
|
832
|
+
|
|
833
|
+
if (fs.existsSync(pidFile)) {
|
|
834
|
+
const pid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
835
|
+
try {
|
|
836
|
+
process.kill(pid, 0); // シグナル0でプロセス存在確認
|
|
837
|
+
console.log(`状態: 🟢 実行中 (PID: ${pid})`);
|
|
838
|
+
} catch {
|
|
839
|
+
console.log(`状態: 🔴 停止 (古いPIDファイルあり)`);
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
console.log(`状態: ⚪ 未起動`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ポート使用状況
|
|
846
|
+
const calculatedPorts = calculatePorts(branch, cfg.apps);
|
|
847
|
+
console.log(`\nポート使用状況:`);
|
|
848
|
+
for (const [appId, port] of Object.entries(calculatedPorts)) {
|
|
849
|
+
const status = isPortInUse(port) ? "🟢 使用中" : "⚪ 空き";
|
|
850
|
+
console.log(` ${appId}: ${port} ${status}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
console.log(`\nログファイル: ${logFile}`);
|
|
854
|
+
console.log(`${"=".repeat(50)}\n`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// スクリプトとして直接実行された場合
|
|
858
|
+
const args = process.argv.slice(2);
|
|
859
|
+
const setupOnly = args.includes("--setup-only");
|
|
860
|
+
const skipSetup = args.includes("--skip-setup");
|
|
861
|
+
const background = args.includes("--background") || args.includes("-b");
|
|
862
|
+
const noKill = args.includes("--no-kill");
|
|
863
|
+
const stop = args.includes("--stop");
|
|
864
|
+
const status = args.includes("--status");
|
|
865
|
+
|
|
866
|
+
if (stop) {
|
|
867
|
+
stopDevServer();
|
|
868
|
+
} else if (status) {
|
|
869
|
+
showDevStatus();
|
|
870
|
+
} else {
|
|
871
|
+
main({ setupOnly, skipSetup, background, killExisting: !noKill });
|
|
872
|
+
}
|