@oyster-lib/cli 2.0.0
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 +107 -0
- package/dist/index.js +25507 -0
- package/jest.config.ts +23 -0
- package/package.json +33 -0
- package/src/__tests__/templates.test.ts +283 -0
- package/src/commands/deploy.ts +109 -0
- package/src/commands/init.ts +154 -0
- package/src/commands/login.ts +78 -0
- package/src/commands/migrate.ts +81 -0
- package/src/config.ts +86 -0
- package/src/index.ts +22 -0
- package/src/templates/backend.ts +119 -0
- package/src/templates/frontend.ts +139 -0
- package/src/templates/index.ts +149 -0
- package/src/utils/http.ts +131 -0
- package/src/utils/input.ts +89 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import FormData from 'form-data'
|
|
5
|
+
import { loadConfig, requireLogin } from '../config.js'
|
|
6
|
+
import { apiPostFormData } from '../utils/http.js'
|
|
7
|
+
import { chooseDomainAsync, chooseAppAsync } from '../utils/input.js'
|
|
8
|
+
|
|
9
|
+
export function setMigrate(program: Command) {
|
|
10
|
+
program
|
|
11
|
+
.command('migrate')
|
|
12
|
+
.description('データベースマイグレーションを実行する (prisma migrate deploy)')
|
|
13
|
+
.option('-s, --staging', 'ステージング環境に対して実行')
|
|
14
|
+
.option('-d, --domain <name>', 'ドメイン名を直接指定')
|
|
15
|
+
.option('-a, --app <name>', 'アプリ名を直接指定')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
requireLogin()
|
|
19
|
+
const config = loadConfig()
|
|
20
|
+
|
|
21
|
+
// --- ドメイン選択 ---
|
|
22
|
+
let domainName = options.domain
|
|
23
|
+
if (!domainName) {
|
|
24
|
+
const domain = await chooseDomainAsync()
|
|
25
|
+
if (!domain) {
|
|
26
|
+
console.log(chalk.yellow('キャンセルしました'))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
domainName = domain.name
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- アプリ選択 ---
|
|
33
|
+
let appName = options.app
|
|
34
|
+
if (!appName) {
|
|
35
|
+
const app = await chooseAppAsync(domainName)
|
|
36
|
+
if (!app) {
|
|
37
|
+
console.log(chalk.yellow('キャンセルしました'))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
appName = app.name
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isStaging: boolean = options.staging ?? false
|
|
44
|
+
const stage = isStaging ? 'staging' : 'release'
|
|
45
|
+
|
|
46
|
+
console.log()
|
|
47
|
+
console.log(chalk.cyan('マイグレーション設定:'))
|
|
48
|
+
console.log(chalk.gray(` ドメイン : ${domainName}`))
|
|
49
|
+
console.log(chalk.gray(` アプリ : ${appName}`))
|
|
50
|
+
console.log(chalk.gray(` 環境 : ${stage}`))
|
|
51
|
+
console.log()
|
|
52
|
+
|
|
53
|
+
// --- FormData 構築 ---
|
|
54
|
+
const formData = new FormData()
|
|
55
|
+
const data = {
|
|
56
|
+
domain: domainName,
|
|
57
|
+
appName,
|
|
58
|
+
email: config.user?.email ?? '',
|
|
59
|
+
id: config.user?.id ?? '',
|
|
60
|
+
user: config.user?.name ?? '',
|
|
61
|
+
isStaging
|
|
62
|
+
}
|
|
63
|
+
formData.append('data', JSON.stringify(data))
|
|
64
|
+
|
|
65
|
+
// --- マイグレーション実行 ---
|
|
66
|
+
const spinner = ora('マイグレーション実行中...').start()
|
|
67
|
+
try {
|
|
68
|
+
await apiPostFormData('migrate', formData, { timeout: 300_000 })
|
|
69
|
+
spinner.succeed(chalk.green('マイグレーションが完了しました'))
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
spinner.fail(chalk.red('マイグレーションに失敗しました'))
|
|
72
|
+
const msg = error.response?.data?.message ?? error.message
|
|
73
|
+
console.error(chalk.red(` ${msg}`))
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
console.error(chalk.red(`✗ ${error.message}`))
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
export interface OysterConfig {
|
|
6
|
+
api_key?: string
|
|
7
|
+
server_url?: string
|
|
8
|
+
user?: {
|
|
9
|
+
id: string
|
|
10
|
+
email: string
|
|
11
|
+
name: string
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getConfigDir(): string {
|
|
16
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config')
|
|
17
|
+
return path.join(xdgConfig, 'oyster')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONFIG_DIR = getConfigDir()
|
|
21
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 設定を読み込む
|
|
25
|
+
*/
|
|
26
|
+
export function loadConfig(): OysterConfig {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
29
|
+
return {}
|
|
30
|
+
}
|
|
31
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8')
|
|
32
|
+
return JSON.parse(content) as OysterConfig
|
|
33
|
+
} catch {
|
|
34
|
+
return {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 設定を保存する(ファイル権限 600)
|
|
40
|
+
*/
|
|
41
|
+
export function saveConfig(config: OysterConfig): void {
|
|
42
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
43
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* API Key を取得する(環境変数 > 設定ファイル)
|
|
50
|
+
*/
|
|
51
|
+
export function getApiKey(): string | null {
|
|
52
|
+
if (process.env.OYSTER_API_KEY) {
|
|
53
|
+
return process.env.OYSTER_API_KEY
|
|
54
|
+
}
|
|
55
|
+
const config = loadConfig()
|
|
56
|
+
return config.api_key ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* サーバー URL を取得する(環境変数 > 設定ファイル > デフォルト)
|
|
61
|
+
*/
|
|
62
|
+
export function getServerUrl(): string {
|
|
63
|
+
if (process.env.OYSTER_SERVER_URL) {
|
|
64
|
+
return process.env.OYSTER_SERVER_URL
|
|
65
|
+
}
|
|
66
|
+
const config = loadConfig()
|
|
67
|
+
return config.server_url ?? 'https://oyster.craifapps.com'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* CLI API のベース URL
|
|
72
|
+
*/
|
|
73
|
+
export function getApiBaseUrl(): string {
|
|
74
|
+
return `${getServerUrl()}/api/v1/cli`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* ログイン済みか確認
|
|
79
|
+
*/
|
|
80
|
+
export function requireLogin(): string {
|
|
81
|
+
const apiKey = getApiKey()
|
|
82
|
+
if (!apiKey) {
|
|
83
|
+
throw new Error('ログインしていません。先に `oyster login` を実行してください。')
|
|
84
|
+
}
|
|
85
|
+
return apiKey
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander'
|
|
3
|
+
import { setLogin } from './commands/login.js'
|
|
4
|
+
import { setInit } from './commands/init.js'
|
|
5
|
+
import { setDeploy } from './commands/deploy.js'
|
|
6
|
+
import { setMigrate } from './commands/migrate.js'
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('oyster')
|
|
10
|
+
.version('2.0.0')
|
|
11
|
+
.description('Oyster CLI v2 - deploy and manage your Oyster applications')
|
|
12
|
+
|
|
13
|
+
setLogin(program)
|
|
14
|
+
setInit(program)
|
|
15
|
+
setDeploy(program)
|
|
16
|
+
setMigrate(program)
|
|
17
|
+
|
|
18
|
+
program.parse(process.argv)
|
|
19
|
+
|
|
20
|
+
if (!process.argv.slice(2).length) {
|
|
21
|
+
program.outputHelp()
|
|
22
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* バックエンドテンプレートファイルを生成する
|
|
3
|
+
*
|
|
4
|
+
* Oyster プラットフォームが期待する以下の構造を生成:
|
|
5
|
+
* backend/
|
|
6
|
+
* ├── server.ts
|
|
7
|
+
* ├── package.json
|
|
8
|
+
* ├── tsconfig.json
|
|
9
|
+
* └── prisma/{domain}/schema.prisma
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* バックエンドのファイルマップを生成する
|
|
14
|
+
* @param domain ドメイン名(例: example.com)
|
|
15
|
+
* @returns ファイルパス -> ファイル内容 のマップ
|
|
16
|
+
*/
|
|
17
|
+
export function generateBackendFiles(domain: string): Record<string, string> {
|
|
18
|
+
if (!domain) throw new Error('domain は必須です')
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
'backend/server.ts': generateServerTs(),
|
|
22
|
+
'backend/package.json': generateBackendPackageJson(),
|
|
23
|
+
'backend/tsconfig.json': generateBackendTsconfig(),
|
|
24
|
+
[`backend/prisma/${domain}/schema.prisma`]: generatePrismaSchema(domain)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateServerTs(): string {
|
|
29
|
+
return `import express from 'express'
|
|
30
|
+
|
|
31
|
+
const app = express()
|
|
32
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 1616
|
|
33
|
+
|
|
34
|
+
app.use(express.json())
|
|
35
|
+
|
|
36
|
+
app.get('/health', (_req, res) => {
|
|
37
|
+
res.json({ status: 'ok' })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
app.listen(port, () => {
|
|
41
|
+
console.log(\`Action Server listening on port \${port}\`)
|
|
42
|
+
})
|
|
43
|
+
`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function generateBackendPackageJson(): string {
|
|
47
|
+
const pkg = {
|
|
48
|
+
name: '@oyster-apps/backend',
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
private: true,
|
|
51
|
+
scripts: {
|
|
52
|
+
start: 'TS_NODE_TRANSPILE_ONLY=true node --max-old-space-size=2048 -r ts-node/register -r tsconfig-paths/register server.ts'
|
|
53
|
+
},
|
|
54
|
+
dependencies: {
|
|
55
|
+
'express': '^4.21.0',
|
|
56
|
+
'@prisma/client': '^6.0.0'
|
|
57
|
+
},
|
|
58
|
+
devDependencies: {
|
|
59
|
+
'typescript': '^5.8.0',
|
|
60
|
+
'ts-node': '^10.9.0',
|
|
61
|
+
'tsconfig-paths': '^4.2.0',
|
|
62
|
+
'@types/express': '^5.0.0',
|
|
63
|
+
'@types/node': '^22.0.0',
|
|
64
|
+
'prisma': '^6.0.0'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return JSON.stringify(pkg, null, 2) + '\n'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function generateBackendTsconfig(): string {
|
|
71
|
+
const tsconfig = {
|
|
72
|
+
compilerOptions: {
|
|
73
|
+
target: 'ES2022',
|
|
74
|
+
module: 'CommonJS',
|
|
75
|
+
moduleResolution: 'node',
|
|
76
|
+
esModuleInterop: true,
|
|
77
|
+
strict: true,
|
|
78
|
+
skipLibCheck: true,
|
|
79
|
+
forceConsistentCasingInFileNames: true,
|
|
80
|
+
resolveJsonModule: true,
|
|
81
|
+
outDir: './dist',
|
|
82
|
+
rootDir: '.',
|
|
83
|
+
declaration: true,
|
|
84
|
+
baseUrl: '.',
|
|
85
|
+
paths: {
|
|
86
|
+
'prisma/*': ['./prisma/*']
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
include: ['**/*.ts'],
|
|
90
|
+
exclude: ['node_modules', 'dist']
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(tsconfig, null, 2) + '\n'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function generatePrismaSchema(domain: string): string {
|
|
96
|
+
return `// Prisma Schema for ${domain}
|
|
97
|
+
// https://www.prisma.io/docs/prisma-schema
|
|
98
|
+
|
|
99
|
+
generator client {
|
|
100
|
+
provider = "prisma-client-js"
|
|
101
|
+
output = "../../node_modules/.prisma/${domain}"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
datasource db {
|
|
105
|
+
provider = "postgresql"
|
|
106
|
+
url = env("DATABASE_URL")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add your models here
|
|
110
|
+
// model Example {
|
|
111
|
+
// id Int @id @default(autoincrement())
|
|
112
|
+
// name String
|
|
113
|
+
// createdAt DateTime @default(now()) @map("created_at")
|
|
114
|
+
// updatedAt DateTime @updatedAt @map("updated_at")
|
|
115
|
+
//
|
|
116
|
+
// @@map("examples")
|
|
117
|
+
// }
|
|
118
|
+
`
|
|
119
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* フロントエンドテンプレートファイルを生成する
|
|
3
|
+
*
|
|
4
|
+
* Oyster プラットフォームが期待する以下の構造を生成:
|
|
5
|
+
* frontend/{domain}/
|
|
6
|
+
* ├── src/
|
|
7
|
+
* │ ├── apps/{appName}/index.ts
|
|
8
|
+
* │ ├── shared/index.ts
|
|
9
|
+
* │ └── index.css
|
|
10
|
+
* ├── package.json
|
|
11
|
+
* ├── tsconfig.json
|
|
12
|
+
* └── tailwind.config.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* フロントエンドのファイルマップを生成する
|
|
17
|
+
* @param domain ドメイン名(例: example.com)
|
|
18
|
+
* @param appName アプリ名(例: myapp)
|
|
19
|
+
* @returns ファイルパス -> ファイル内容 のマップ
|
|
20
|
+
*/
|
|
21
|
+
export function generateFrontendFiles(
|
|
22
|
+
domain: string,
|
|
23
|
+
appName: string
|
|
24
|
+
): Record<string, string> {
|
|
25
|
+
if (!domain) throw new Error('domain は必須です')
|
|
26
|
+
if (!appName) throw new Error('appName は必須です')
|
|
27
|
+
|
|
28
|
+
const base = `frontend/${domain}`
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
[`${base}/src/apps/${appName}/index.ts`]: generateAppEntryPoint(appName),
|
|
32
|
+
[`${base}/src/shared/index.ts`]: generateSharedIndex(),
|
|
33
|
+
[`${base}/src/index.css`]: generateIndexCss(),
|
|
34
|
+
[`${base}/package.json`]: generateFrontendPackageJson(domain),
|
|
35
|
+
[`${base}/tsconfig.json`]: generateFrontendTsconfig(),
|
|
36
|
+
[`${base}/tailwind.config.js`]: generateTailwindConfig()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* アプリのエントリーポイントを生成する(外部からも使用可能)
|
|
42
|
+
*/
|
|
43
|
+
export function generateAppEntryPoint(appName: string): string {
|
|
44
|
+
const componentName = appName.charAt(0).toUpperCase() + appName.slice(1)
|
|
45
|
+
return `import React from 'react'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ${componentName} - フロントエンドエントリーポイント
|
|
49
|
+
*/
|
|
50
|
+
export default function ${componentName}() {
|
|
51
|
+
return (
|
|
52
|
+
<div className="p-4">
|
|
53
|
+
<h1 className="text-2xl font-bold">${componentName}</h1>
|
|
54
|
+
<p>Welcome to ${appName}</p>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generateSharedIndex(): string {
|
|
62
|
+
return `/**
|
|
63
|
+
* 共有モジュール
|
|
64
|
+
* アプリ間で共有するユーティリティや型をここからエクスポートする
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export {}
|
|
68
|
+
`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateIndexCss(): string {
|
|
72
|
+
return `@import 'tailwindcss';
|
|
73
|
+
|
|
74
|
+
/* カスタムスタイルをここに追加 */
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function generateFrontendPackageJson(domain: string): string {
|
|
79
|
+
const pkg = {
|
|
80
|
+
name: `@oyster-apps/${domain}`,
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
private: true,
|
|
83
|
+
dependencies: {
|
|
84
|
+
'react': '^19.0.0',
|
|
85
|
+
'react-dom': '^19.0.0',
|
|
86
|
+
'@oyster-lib/core': '*'
|
|
87
|
+
},
|
|
88
|
+
devDependencies: {
|
|
89
|
+
'typescript': '^5.8.0',
|
|
90
|
+
'@types/react': '^19.0.0',
|
|
91
|
+
'@types/react-dom': '^19.0.0',
|
|
92
|
+
'esbuild': '^0.24.0',
|
|
93
|
+
'tailwindcss': '^4.0.0'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify(pkg, null, 2) + '\n'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function generateFrontendTsconfig(): string {
|
|
100
|
+
const tsconfig = {
|
|
101
|
+
compilerOptions: {
|
|
102
|
+
target: 'ES2022',
|
|
103
|
+
module: 'ESNext',
|
|
104
|
+
moduleResolution: 'bundler',
|
|
105
|
+
jsx: 'react-jsx',
|
|
106
|
+
esModuleInterop: true,
|
|
107
|
+
strict: true,
|
|
108
|
+
skipLibCheck: true,
|
|
109
|
+
forceConsistentCasingInFileNames: true,
|
|
110
|
+
resolveJsonModule: true,
|
|
111
|
+
declaration: true,
|
|
112
|
+
outDir: './.build',
|
|
113
|
+
baseUrl: '.',
|
|
114
|
+
paths: {
|
|
115
|
+
'components/*': ['./src/apps/*/index.ts'],
|
|
116
|
+
'components': ['./src/apps/*/index.ts'],
|
|
117
|
+
'shared/*': ['./src/shared/index.ts'],
|
|
118
|
+
'shared': ['./src/shared/index.ts']
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
include: ['src/**/*'],
|
|
122
|
+
exclude: ['node_modules', '.build']
|
|
123
|
+
}
|
|
124
|
+
return JSON.stringify(tsconfig, null, 2) + '\n'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function generateTailwindConfig(): string {
|
|
128
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
129
|
+
module.exports = {
|
|
130
|
+
content: [
|
|
131
|
+
'./src/**/*.{js,ts,jsx,tsx}'
|
|
132
|
+
],
|
|
133
|
+
theme: {
|
|
134
|
+
extend: {}
|
|
135
|
+
},
|
|
136
|
+
plugins: []
|
|
137
|
+
}
|
|
138
|
+
`
|
|
139
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { generateFrontendFiles, generateAppEntryPoint } from './frontend.js'
|
|
4
|
+
import { generateBackendFiles } from './backend.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 初回初期化: 全ファイルを生成する(frontend + backend + 共通ファイル)
|
|
8
|
+
*/
|
|
9
|
+
export function generateAllFiles(
|
|
10
|
+
domain: string,
|
|
11
|
+
appName: string
|
|
12
|
+
): Record<string, string> {
|
|
13
|
+
if (!domain) throw new Error('domain は必須です')
|
|
14
|
+
if (!appName) throw new Error('appName は必須です')
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
...generateFrontendFiles(domain, appName),
|
|
18
|
+
...generateBackendFiles(domain),
|
|
19
|
+
...generateCommonFiles(domain, appName)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 追加アプリ: 新しいアプリのエントリーポイントのみ生成する
|
|
25
|
+
* 既存の package.json, tsconfig.json, backend 等は含まない
|
|
26
|
+
*/
|
|
27
|
+
export function generateAppFiles(
|
|
28
|
+
domain: string,
|
|
29
|
+
appName: string
|
|
30
|
+
): Record<string, string> {
|
|
31
|
+
if (!domain) throw new Error('domain は必須です')
|
|
32
|
+
if (!appName) throw new Error('appName は必須です')
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
[`frontend/${domain}/src/apps/${appName}/index.ts`]: generateAppEntryPoint(appName)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 共通ファイル(.gitignore, README.md)を生成する
|
|
41
|
+
*/
|
|
42
|
+
function generateCommonFiles(
|
|
43
|
+
domain: string,
|
|
44
|
+
appName: string
|
|
45
|
+
): Record<string, string> {
|
|
46
|
+
return {
|
|
47
|
+
'.gitignore': generateGitignore(),
|
|
48
|
+
'README.md': generateReadme(domain, appName)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function generateGitignore(): string {
|
|
53
|
+
return `# Dependencies
|
|
54
|
+
node_modules/
|
|
55
|
+
|
|
56
|
+
# Build output
|
|
57
|
+
.build/
|
|
58
|
+
dist/
|
|
59
|
+
|
|
60
|
+
# Environment
|
|
61
|
+
.env
|
|
62
|
+
.env.local
|
|
63
|
+
.env.*.local
|
|
64
|
+
|
|
65
|
+
# IDE
|
|
66
|
+
.vscode/
|
|
67
|
+
.idea/
|
|
68
|
+
|
|
69
|
+
# OS
|
|
70
|
+
.DS_Store
|
|
71
|
+
Thumbs.db
|
|
72
|
+
|
|
73
|
+
# Logs
|
|
74
|
+
*.log
|
|
75
|
+
npm-debug.log*
|
|
76
|
+
yarn-debug.log*
|
|
77
|
+
pnpm-debug.log*
|
|
78
|
+
`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generateReadme(domain: string, appName: string): string {
|
|
82
|
+
return `# Oyster Apps - ${domain}
|
|
83
|
+
|
|
84
|
+
This repository contains the frontend and backend code for the **${domain}** domain on the Oyster platform.
|
|
85
|
+
|
|
86
|
+
## Structure
|
|
87
|
+
|
|
88
|
+
\`\`\`
|
|
89
|
+
frontend/${domain}/ # Frontend applications
|
|
90
|
+
src/apps/${appName}/ # ${appName} app entry point
|
|
91
|
+
src/shared/ # Shared modules
|
|
92
|
+
backend/ # Backend (Action Server)
|
|
93
|
+
prisma/${domain}/ # Prisma schema
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
## Apps
|
|
97
|
+
|
|
98
|
+
- **${appName}** - \`frontend/${domain}/src/apps/${appName}/index.ts\`
|
|
99
|
+
|
|
100
|
+
## Deploy
|
|
101
|
+
|
|
102
|
+
\`\`\`bash
|
|
103
|
+
oyster deploy -d ${domain} -a ${appName}
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Migrate
|
|
107
|
+
|
|
108
|
+
\`\`\`bash
|
|
109
|
+
oyster migrate -d ${domain} -a ${appName}
|
|
110
|
+
\`\`\`
|
|
111
|
+
`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* ファイルマップをディスクに書き込む
|
|
116
|
+
* @param rootDir 書き込み先のルートディレクトリ
|
|
117
|
+
* @param files ファイルパス -> 内容のマップ
|
|
118
|
+
* @param force true の場合は既存ファイルを上書き
|
|
119
|
+
* @returns 作成されたファイルとスキップされたファイルのリスト
|
|
120
|
+
*/
|
|
121
|
+
export function writeFiles(
|
|
122
|
+
rootDir: string,
|
|
123
|
+
files: Record<string, string>,
|
|
124
|
+
force: boolean
|
|
125
|
+
): { created: string[]; skipped: string[] } {
|
|
126
|
+
const created: string[] = []
|
|
127
|
+
const skipped: string[] = []
|
|
128
|
+
|
|
129
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
130
|
+
const fullPath = path.join(rootDir, relativePath)
|
|
131
|
+
const dir = path.dirname(fullPath)
|
|
132
|
+
|
|
133
|
+
// ディレクトリを再帰的に作成
|
|
134
|
+
if (!fs.existsSync(dir)) {
|
|
135
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 既存ファイルのチェック
|
|
139
|
+
if (fs.existsSync(fullPath) && !force) {
|
|
140
|
+
skipped.push(relativePath)
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fs.writeFileSync(fullPath, content, 'utf-8')
|
|
145
|
+
created.push(relativePath)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { created, skipped }
|
|
149
|
+
}
|