@c-time/frelio-cli 1.3.13 → 1.4.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.
@@ -1,27 +1,50 @@
1
1
  /**
2
2
  * frelio init - 新規プロジェクトの対話式セットアップ
3
+ *
4
+ * core/* の関数を順番に呼ぶ薄いオーケストレーター。
5
+ * UI 層(プロンプト、ログ出力、process.exit)はここに閉じる。
3
6
  */
4
7
  import prompts from 'prompts';
5
8
  import fs from 'node:fs';
6
9
  import path from 'node:path';
7
- import os from 'node:os';
8
- import { exec, commandExists, log, logStep, logSuccess, logError } from '../lib/shell.js';
9
- import { getLatestRelease, downloadTarball, extractNpmTarball } from '../lib/npm-registry.js';
10
- import { generateInitialContent } from '../lib/initial-content.js';
11
- import { generateConfigJson, generateWranglerToml, generateUsersIndex, generateVersionJson, generateRedirects, generateRoutesJson, generateStorageFunction, generateViteConfig, generatePackageJson, generateTsConfig, generateTsConfigNode, generateStagingDomain, writeFile, ensureDir, } from '../lib/templates.js';
10
+ import { log, logStep, logSuccess, logError } from '../lib/shell.js';
11
+ import { generateStagingDomain } from '../lib/templates.js';
12
12
  import { validateContentRepo, validateR2PublicUrl, validateRequired } from '../lib/validators.js';
13
+ // core functions
14
+ import { checkPrerequisitesFor } from '../core/prerequisites.js';
15
+ import { buildConfig } from '../core/config.js';
16
+ import { createRepo, cloneRepo, getAuthenticatedUser } from '../core/github.js';
17
+ import { setupCloudflareResources } from '../core/cloudflare.js';
18
+ import { createFullContentStructure } from '../core/content-structure.js';
19
+ import { regenerateAllConfigFiles } from '../core/file-generators.js';
20
+ import { generateWorkflows } from '../core/workflows.js';
21
+ import { generateTerraformFiles } from '../core/terraform.js';
22
+ import { installBundle } from '../core/bundle.js';
23
+ import { initialCommitAndBranches } from '../core/git-operations.js';
13
24
  export async function initCommand(options) {
14
25
  log('');
15
26
  log('🚀 Frelio CMS プロジェクトセットアップ');
16
27
  log('');
17
- // 前提チェック
28
+ // ── 前提チェック ──
18
29
  log('🔍 前提チェック...');
19
- const checks = checkPrerequisites(options);
20
- if (!checks.ok) {
30
+ const targets = ['git'];
31
+ if (!options.skipGithub)
32
+ targets.push('github');
33
+ if (!options.skipCloudflare && !options.terraform)
34
+ targets.push('cloudflare');
35
+ const prereqs = checkPrerequisitesFor(targets);
36
+ if (prereqs.success) {
37
+ for (const [name, r] of Object.entries(prereqs.data.results)) {
38
+ if (r.success)
39
+ logSuccess(name);
40
+ }
41
+ }
42
+ else {
43
+ logError(prereqs.error);
21
44
  process.exit(1);
22
45
  }
23
46
  log('');
24
- // 対話式プロンプト
47
+ // ── 対話式プロンプト ──
25
48
  log('📝 プロジェクト設定:');
26
49
  const config = await promptConfig(options);
27
50
  if (!config) {
@@ -29,7 +52,7 @@ export async function initCommand(options) {
29
52
  process.exit(0);
30
53
  }
31
54
  log('');
32
- // OAuth App 案内
55
+ // ── OAuth App 案内 ──
33
56
  log('🔑 GitHub OAuth App:');
34
57
  if (!config.githubClientId) {
35
58
  const isInteractive = process.stdin.isTTY === true;
@@ -40,9 +63,9 @@ export async function initCommand(options) {
40
63
  log(' ⚠ OAuth App は GitHub の Web UI で作成が必要です。');
41
64
  log(' → https://github.com/settings/developers');
42
65
  log(' → New OAuth App:');
43
- log(` - Application name: ${config.siteTitle || config.pagesProjectName} CMS`);
44
- log(` - Homepage URL: https://${config.pagesProjectName}.pages.dev`);
45
- log(` - Callback URL: https://${config.pagesProjectName}.pages.dev/api/auth/callback`);
66
+ log(` - Application name: ${config.siteTitle || config.adminPagesProjectName} CMS`);
67
+ log(` - Homepage URL: https://${config.adminPagesProjectName}.pages.dev`);
68
+ log(` - Callback URL: https://${config.adminPagesProjectName}.pages.dev/api/auth/callback`);
46
69
  log('');
47
70
  const oauthResponse = await prompts([
48
71
  {
@@ -67,26 +90,20 @@ export async function initCommand(options) {
67
90
  oauthResponse.clientSecret;
68
91
  }
69
92
  log('');
70
- // セットアップ実行
93
+ // ── セットアップ実行 ──
71
94
  const totalSteps = getTotalSteps(options);
72
95
  let step = 0;
73
96
  // GitHub リポジトリ作成
74
97
  if (!options.skipGithub) {
75
98
  step++;
76
99
  logStep(step, totalSteps, 'GitHub リポジトリ作成...');
77
- try {
78
- exec(`gh repo create ${config.contentRepo} --private --confirm`, { silent: true });
79
- logSuccess('リポジトリ作成完了');
100
+ const repoResult = createRepo(config.contentRepo);
101
+ if (repoResult.success) {
102
+ logSuccess(repoResult.data.alreadyExisted ? 'リポジトリは既に存在します' : 'リポジトリ作成完了');
80
103
  }
81
- catch (error) {
82
- const msg = error.message;
83
- if (msg.includes('already exists')) {
84
- logSuccess('リポジトリは既に存在します');
85
- }
86
- else {
87
- logError(`リポジトリ作成失敗: ${msg}`);
88
- process.exit(1);
89
- }
104
+ else {
105
+ logError(repoResult.error);
106
+ process.exit(1);
90
107
  }
91
108
  }
92
109
  // 作業ディレクトリ作成
@@ -95,18 +112,13 @@ export async function initCommand(options) {
95
112
  if (!options.skipGithub) {
96
113
  step++;
97
114
  logStep(step, totalSteps, 'リポジトリをクローン...');
98
- try {
99
- exec(`gh repo clone ${config.contentRepo} ${repoName}`, { silent: true });
100
- logSuccess('クローン完了');
115
+ const cloneResult = cloneRepo(config.contentRepo, projectDir);
116
+ if (cloneResult.success) {
117
+ logSuccess(cloneResult.data.alreadyExisted ? 'ディレクトリは既に存在します' : 'クローン完了');
101
118
  }
102
- catch (error) {
103
- if (fs.existsSync(projectDir)) {
104
- logSuccess('ディレクトリは既に存在します');
105
- }
106
- else {
107
- logError(`クローン失敗: ${error.message}`);
108
- process.exit(1);
109
- }
119
+ else {
120
+ logError(cloneResult.error);
121
+ process.exit(1);
110
122
  }
111
123
  }
112
124
  else {
@@ -117,101 +129,98 @@ export async function initCommand(options) {
117
129
  // コンテンツリポジトリ初期構造
118
130
  step++;
119
131
  logStep(step, totalSteps, 'コンテンツリポジトリ初期構造作成...');
120
- createContentStructure(projectDir, config);
121
- generateInitialContent(projectDir);
122
- logSuccess('初期構造作成完了(デモサイトテンプレート付き)');
132
+ const structResult = createFullContentStructure(projectDir, config);
133
+ if (structResult.success) {
134
+ logSuccess('初期構造作成完了(デモサイトテンプレート付き)');
135
+ }
136
+ else {
137
+ logError(structResult.error);
138
+ process.exit(1);
139
+ }
123
140
  // GitHub Actions ワークフロー
124
141
  step++;
125
142
  logStep(step, totalSteps, 'GitHub Actions ワークフロー配置...');
126
- copyWorkflows(projectDir);
127
- logSuccess('ワークフロー配置完了');
143
+ const workflowResult = generateWorkflows(projectDir, config);
144
+ if (workflowResult.success) {
145
+ logSuccess('ワークフロー配置完了');
146
+ }
147
+ else {
148
+ logError(workflowResult.error);
149
+ process.exit(1);
150
+ }
128
151
  // CMS Admin バンドル展開
129
152
  step++;
130
153
  logStep(step, totalSteps, 'CMS Admin バンドルをダウンロード・展開...');
131
- try {
132
- await extractBundle(projectDir);
154
+ const bundleResult = await installBundle(projectDir);
155
+ if (bundleResult.success) {
133
156
  logSuccess('バンドル展開完了');
134
157
  }
135
- catch (error) {
136
- logError(`バンドル展開失敗: ${error.message}`);
158
+ else {
159
+ logError(bundleResult.error);
137
160
  log(' ℹ リリースが公開されていない場合は、手動でバンドルを展開してください。');
138
161
  log(' scripts/build-distributable.sh を実行してから dist-release/ の内容をコピーします。');
139
162
  }
140
163
  // 設定ファイル生成
141
164
  step++;
142
165
  logStep(step, totalSteps, '設定ファイル生成...');
143
- writeFile(path.join(projectDir, 'admin', 'config.json'), generateConfigJson(config));
144
- writeFile(path.join(projectDir, 'wrangler.toml'), generateWranglerToml(config));
145
- writeFile(path.join(projectDir, '_redirects'), generateRedirects());
146
- writeFile(path.join(projectDir, '_routes.json'), generateRoutesJson());
147
- // staging Pages 用(build output: public/)
148
- writeFile(path.join(projectDir, 'public', '_routes.json'), JSON.stringify({ version: 1, include: ['/storage/*'], exclude: [] }, null, 2));
149
- logSuccess('設定ファイル生成完了');
150
- // Cloudflare セットアップ
151
- if (!options.skipCloudflare) {
166
+ const configResult = regenerateAllConfigFiles(projectDir, config);
167
+ if (configResult.success) {
168
+ logSuccess('設定ファイル生成完了');
169
+ }
170
+ else {
171
+ logError(configResult.error);
172
+ process.exit(1);
173
+ }
174
+ // Cloudflare セットアップ or Terraform ファイル生成
175
+ if (options.terraform) {
152
176
  step++;
153
- logStep(step, totalSteps, 'Cloudflare セットアップ...');
154
- try {
155
- exec(`wrangler r2 bucket create ${config.r2BucketName}`, { silent: true });
156
- logSuccess(`R2 バケット "${config.r2BucketName}" 作成完了`);
157
- }
158
- catch (error) {
159
- const msg = error.message;
160
- if (msg.includes('already exists')) {
161
- logSuccess(`R2 バケット "${config.r2BucketName}" は既に存在します`);
162
- }
163
- else {
164
- logError(`R2 バケット作成失敗: ${msg}`);
165
- }
177
+ logStep(step, totalSteps, 'Terraform ファイル生成...');
178
+ const tfResult = generateTerraformFiles(projectDir, config);
179
+ if (tfResult.success) {
180
+ logSuccess('Terraform ファイル生成完了(terraform/)');
166
181
  }
167
- try {
168
- exec(`wrangler pages project create ${config.pagesProjectName} --production-branch main`, {
169
- silent: true,
170
- });
171
- logSuccess(`Pages プロジェクト "${config.pagesProjectName}" 作成完了`);
182
+ else {
183
+ logError(tfResult.error);
172
184
  }
173
- catch (error) {
174
- const msg = error.message;
175
- if (msg.includes('already exists') || msg.includes('A project with this name already exists')) {
176
- logSuccess(`Pages プロジェクト "${config.pagesProjectName}" は既に存在します`);
185
+ }
186
+ else if (!options.skipCloudflare) {
187
+ step++;
188
+ logStep(step, totalSteps, 'Cloudflare セットアップ...');
189
+ const secret = config.githubClientSecret;
190
+ const cfResult = setupCloudflareResources(config, secret);
191
+ if (cfResult.success) {
192
+ const { r2Bucket, contentPages, adminPages, secrets } = cfResult.data;
193
+ if (r2Bucket.success) {
194
+ logSuccess(r2Bucket.alreadyExisted
195
+ ? `R2 バケット "${config.r2BucketName}" は既に存在します`
196
+ : `R2 バケット "${config.r2BucketName}" 作成完了`);
177
197
  }
178
198
  else {
179
- logError(`Pages プロジェクト作成失敗: ${msg}`);
199
+ logError(r2Bucket.error);
180
200
  }
181
- }
182
- // ステージング用 Pages プロジェクト作成
183
- const stagingProjectName = `${config.pagesProjectName}-staging`;
184
- try {
185
- exec(`wrangler pages project create ${stagingProjectName} --production-branch staging`, {
186
- silent: true,
187
- });
188
- logSuccess(`Pages プロジェクト "${stagingProjectName}"(ステージング)作成完了`);
189
- }
190
- catch (error) {
191
- const msg = error.message;
192
- if (msg.includes('already exists') || msg.includes('A project with this name already exists')) {
193
- logSuccess(`Pages プロジェクト "${stagingProjectName}" は既に存在します`);
201
+ if (contentPages.success) {
202
+ logSuccess(contentPages.alreadyExisted
203
+ ? `Pages プロジェクト "${config.pagesProjectName}" は既に存在します`
204
+ : `Pages プロジェクト "${config.pagesProjectName}"(コンテンツ配信)作成完了`);
194
205
  }
195
206
  else {
196
- logError(`ステージング Pages プロジェクト作成失敗: ${msg}`);
207
+ logError(contentPages.error);
197
208
  }
198
- }
199
- // シークレット設定
200
- const secret = config.githubClientSecret;
201
- if (secret) {
202
- try {
203
- exec(`echo "${secret}" | wrangler pages secret put GITHUB_CLIENT_SECRET --project-name ${config.pagesProjectName}`, { silent: true });
204
- logSuccess('GITHUB_CLIENT_SECRET 設定完了');
209
+ if (adminPages.success) {
210
+ logSuccess(adminPages.alreadyExisted
211
+ ? `Pages プロジェクト "${config.adminPagesProjectName}" は既に存在します`
212
+ : `Pages プロジェクト "${config.adminPagesProjectName}"(管理画面)作成完了`);
205
213
  }
206
- catch {
207
- logError('GITHUB_CLIENT_SECRET の設定に失敗しました。CF ダッシュボードから手動設定してください。');
208
- }
209
- try {
210
- exec(`echo "${config.githubClientId}" | wrangler pages secret put GITHUB_CLIENT_ID --project-name ${config.pagesProjectName}`, { silent: true });
211
- logSuccess('GITHUB_CLIENT_ID 設定完了');
214
+ else {
215
+ logError(adminPages.error);
212
216
  }
213
- catch {
214
- logError('GITHUB_CLIENT_ID の設定に失敗しました。CF ダッシュボードから手動設定してください。');
217
+ for (const s of secrets) {
218
+ if (s.success) {
219
+ logSuccess('シークレット設定完了');
220
+ }
221
+ else {
222
+ logError(s.error);
223
+ }
215
224
  }
216
225
  }
217
226
  }
@@ -219,84 +228,47 @@ export async function initCommand(options) {
219
228
  if (!options.skipGithub) {
220
229
  step++;
221
230
  logStep(step, totalSteps, '初回コミット & ブランチ作成...');
222
- try {
223
- exec('git add -A', { cwd: projectDir, silent: true });
224
- exec('git commit -m "Initial Frelio CMS setup"', { cwd: projectDir, silent: true });
225
- // develop, staging ブランチ作成
226
- exec('git branch develop', { cwd: projectDir, silent: true });
227
- exec('git branch staging', { cwd: projectDir, silent: true });
228
- // プッシュ
229
- exec('git push -u origin main', { cwd: projectDir, silent: true });
230
- exec('git push -u origin develop', { cwd: projectDir, silent: true });
231
- exec('git push -u origin staging', { cwd: projectDir, silent: true });
232
- // デフォルトブランチを develop に変更
233
- exec(`gh repo edit ${config.contentRepo} --default-branch develop`, { silent: true });
231
+ const gitResult = initialCommitAndBranches(projectDir, config.contentRepo);
232
+ if (gitResult.success) {
234
233
  logSuccess('ブランチ作成・プッシュ完了');
235
234
  }
236
- catch (error) {
237
- logError(`Git 操作失敗: ${error.message}`);
235
+ else {
236
+ logError(gitResult.error);
238
237
  }
239
238
  }
240
- // 完了
239
+ // ── 完了 ──
241
240
  log('');
242
241
  log('✅ セットアップ完了!');
243
242
  log('');
244
- log(` 管理画面: https://${config.pagesProjectName}.pages.dev/admin/`);
243
+ log(` 管理画面: https://${config.adminPagesProjectName}.pages.dev/admin/`);
244
+ log(` プレビュー: https://staging.${config.pagesProjectName}.pages.dev`);
245
245
  log('');
246
- log(' 残りの手動作業:');
247
- log(` 1. 本番 Pages(${config.pagesProjectName})にリポジトリを接続`);
248
- log(` 2. ステージング Pages(${config.pagesProjectName}-staging)にリポジトリを接続`);
246
+ if (options.terraform) {
247
+ log(' Terraform でインフラを構築:');
248
+ log(` 1. cd ${repoName}/terraform`);
249
+ log(' 2. cp terraform.tfvars.example terraform.tfvars && vim terraform.tfvars');
250
+ log(' 3. terraform init');
251
+ log(' 4. terraform plan');
252
+ log(' 5. terraform apply');
253
+ log('');
254
+ log(' 残りの手動作業:');
255
+ log(` 1. コンテンツ配信 Pages(${config.pagesProjectName})にリポジトリを接続`);
256
+ log(` 2. 管理画面 Pages(${config.adminPagesProjectName})にリポジトリを接続(admin ブランチ)`);
257
+ }
258
+ else {
259
+ log(' 残りの手動作業:');
260
+ log(` 1. コンテンツ配信 Pages(${config.pagesProjectName})にリポジトリを接続`);
261
+ log(` 2. 管理画面 Pages(${config.adminPagesProjectName})にリポジトリを接続(admin ブランチ)`);
262
+ }
249
263
  if (config.stagingDomain) {
250
- log(` 3. ステージング Pages にカスタムドメインを設定: ${config.stagingDomain}`);
264
+ log(` 3. プレビュー用カスタムドメインを設定: ${config.stagingDomain}`);
251
265
  }
252
266
  log(` ${config.stagingDomain ? '4' : '3'}. ステージングのアクセス制限を設定(Cloudflare Access 推奨)`);
253
267
  log('');
254
268
  }
255
- function checkPrerequisites(options) {
256
- let ok = true;
257
- if (!options.skipGithub) {
258
- if (commandExists('gh')) {
259
- logSuccess('gh CLI (GitHub)');
260
- try {
261
- exec('gh auth status', { silent: true });
262
- logSuccess('gh auth status (ログイン済み)');
263
- }
264
- catch {
265
- logError('gh にログインしていません。`gh auth login` を実行してください。');
266
- ok = false;
267
- }
268
- }
269
- else {
270
- logError('gh CLI が見つかりません。https://cli.github.com/ からインストールしてください。');
271
- ok = false;
272
- }
273
- }
274
- if (!options.skipCloudflare) {
275
- if (commandExists('wrangler')) {
276
- logSuccess('wrangler CLI (Cloudflare)');
277
- try {
278
- exec('wrangler whoami', { silent: true });
279
- logSuccess('wrangler whoami (ログイン済み)');
280
- }
281
- catch {
282
- logError('wrangler にログインしていません。`wrangler login` を実行してください。');
283
- ok = false;
284
- }
285
- }
286
- else {
287
- logError('wrangler CLI が見つかりません。`npm i -g wrangler` でインストールしてください。');
288
- ok = false;
289
- }
290
- }
291
- if (commandExists('git')) {
292
- logSuccess('git');
293
- }
294
- else {
295
- logError('git が見つかりません。');
296
- ok = false;
297
- }
298
- return { ok };
299
- }
269
+ // ---------------------------------------------------------------------------
270
+ // UI helpers (prompts — commands 層に閉じる)
271
+ // ---------------------------------------------------------------------------
300
272
  async function promptConfig(options) {
301
273
  const isInteractive = process.stdin.isTTY === true;
302
274
  // 必須オプションが揃っている場合 → プロンプトスキップ
@@ -316,7 +288,7 @@ async function promptConfig(options) {
316
288
  logError('使用例: frelio init --content-repo owner/repo --client-id xxx --client-secret yyy');
317
289
  process.exit(1);
318
290
  }
319
- // 対話モード(渡されたオプションはスキップ)
291
+ // 対話モード
320
292
  const response = await prompts([
321
293
  {
322
294
  type: options.contentRepo ? null : 'text',
@@ -366,12 +338,8 @@ async function promptConfig(options) {
366
338
  name: 'ownerUsername',
367
339
  message: '管理者の GitHub ユーザー名:',
368
340
  initial: () => {
369
- try {
370
- return exec('gh api user -q .login', { silent: true });
371
- }
372
- catch {
373
- return '';
374
- }
341
+ const userResult = getAuthenticatedUser();
342
+ return userResult.success ? userResult.data.username : '';
375
343
  },
376
344
  },
377
345
  ], { onCancel: () => process.exit(0) });
@@ -390,6 +358,7 @@ async function promptConfig(options) {
390
358
  r2BucketName: options.r2BucketName ?? response.r2BucketName ?? `${repoName}-files`,
391
359
  r2PublicUrl: options.r2PublicUrl ?? response.r2PublicUrl ?? '',
392
360
  pagesProjectName: repoName,
361
+ adminPagesProjectName: `${repoName}-admin`,
393
362
  ownerUsername: options.ownerUsername ?? response.ownerUsername ?? '',
394
363
  stagingDomain,
395
364
  };
@@ -399,206 +368,40 @@ async function promptConfig(options) {
399
368
  return config;
400
369
  }
401
370
  function buildConfigFromOptions(options) {
402
- // バリデーション
403
- const errors = [];
404
- const repoCheck = validateContentRepo(options.contentRepo);
405
- if (repoCheck !== true)
406
- errors.push(`--content-repo: ${repoCheck}`);
407
- if (options.r2PublicUrl) {
408
- const urlCheck = validateR2PublicUrl(options.r2PublicUrl);
409
- if (urlCheck !== true)
410
- errors.push(`--r2-public-url: ${urlCheck}`);
411
- }
412
- if (errors.length > 0) {
413
- errors.forEach((e) => logError(e));
371
+ const result = buildConfig({
372
+ contentRepo: options.contentRepo,
373
+ githubClientId: options.clientId,
374
+ siteTitle: options.siteTitle,
375
+ productionUrl: options.productionUrl,
376
+ stagingDomain: options.stagingDomain,
377
+ r2BucketName: options.r2BucketName,
378
+ r2PublicUrl: options.r2PublicUrl,
379
+ ownerUsername: options.ownerUsername,
380
+ });
381
+ if (!result.success) {
382
+ logError(result.error);
414
383
  process.exit(1);
415
384
  }
416
- const repoName = options.contentRepo.split('/')[1];
417
- // デフォルト算出
418
- const stagingDomain = options.stagingDomain ??
419
- generateStagingDomain(options.productionUrl || '', repoName);
420
- const previewUrl = stagingDomain ? `https://${stagingDomain}` : '';
421
- let ownerUsername = options.ownerUsername ?? '';
422
- if (!ownerUsername) {
423
- try {
424
- ownerUsername = exec('gh api user -q .login', { silent: true });
425
- }
426
- catch {
427
- ownerUsername = '';
385
+ // ownerUsername gh api から補完
386
+ const config = result.data.config;
387
+ if (!config.ownerUsername) {
388
+ const userResult = getAuthenticatedUser();
389
+ if (userResult.success) {
390
+ config.ownerUsername = userResult.data.username;
428
391
  }
429
392
  }
430
393
  return {
431
- contentRepo: options.contentRepo,
432
- githubClientId: options.clientId || '',
433
- siteTitle: options.siteTitle || '',
434
- productionUrl: options.productionUrl || '',
435
- previewUrl,
436
- r2BucketName: options.r2BucketName ?? `${repoName}-files`,
437
- r2PublicUrl: options.r2PublicUrl || '',
438
- pagesProjectName: repoName,
439
- ownerUsername,
440
- stagingDomain,
394
+ ...config,
441
395
  githubClientSecret: options.clientSecret,
442
396
  };
443
397
  }
444
- function createContentStructure(projectDir, config) {
445
- // frelio-data ディレクトリ構造
446
- const dirs = [
447
- 'frelio-data/site/content_types',
448
- 'frelio-data/site/contents/published',
449
- 'frelio-data/site/contents/private',
450
- 'frelio-data/site/templates/assets/scss',
451
- 'frelio-data/site/templates/assets/ts',
452
- 'frelio-data/site/templates/assets/entries',
453
- 'frelio-data/site/data/data-json',
454
- 'frelio-data/admin/metadata',
455
- 'frelio-data/admin/users',
456
- 'frelio-data/admin/recipes',
457
- 'scripts',
458
- 'public',
459
- ];
460
- for (const dir of dirs) {
461
- ensureDir(path.join(projectDir, dir));
462
- }
463
- // メタデータファイル
464
- writeFile(path.join(projectDir, 'frelio-data/admin/users/_index.json'), generateUsersIndex(config));
465
- // version.json
466
- writeFile(path.join(projectDir, 'version.json'), generateVersionJson());
467
- // vite.config.ts + tsconfig.json + tsconfig.node.json + package.json
468
- writeFile(path.join(projectDir, 'vite.config.ts'), generateViteConfig());
469
- writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
470
- writeFile(path.join(projectDir, 'tsconfig.node.json'), generateTsConfigNode());
471
- writeFile(path.join(projectDir, 'package.json'), generatePackageJson(config));
472
- // R2 ファイル配信用 Pages Function (/storage/*)
473
- writeFile(path.join(projectDir, 'functions', 'storage', '[[path]].ts'), generateStorageFunction());
474
- // .gitignore
475
- const gitignore = [
476
- 'node_modules/',
477
- '.wrangler/',
478
- '.dev.vars',
479
- '',
480
- ].join('\n');
481
- writeFile(path.join(projectDir, '.gitignore'), gitignore);
482
- }
483
- function copyWorkflows(projectDir) {
484
- const workflowsDir = path.join(projectDir, '.github', 'workflows');
485
- fs.mkdirSync(workflowsDir, { recursive: true });
486
- // build-staging.yml
487
- writeFile(path.join(workflowsDir, 'build-staging.yml'), `name: Build Staging
488
- on:
489
- push:
490
- branches: [staging, 'staging-*']
491
-
492
- permissions:
493
- contents: write
494
-
495
- jobs:
496
- build:
497
- runs-on: ubuntu-latest
498
- steps:
499
- - uses: actions/checkout@v4
500
- - uses: actions/setup-node@v4
501
- with:
502
- node-version: 20
503
- - run: echo "TODO: Add SSG build steps"
504
- `);
505
- // promote-production.yml
506
- writeFile(path.join(workflowsDir, 'promote-production.yml'), `name: Promote to Production
507
- on:
508
- push:
509
- branches: [main]
510
-
511
- permissions:
512
- contents: write
513
-
514
- jobs:
515
- tag:
516
- runs-on: ubuntu-latest
517
- steps:
518
- - uses: actions/checkout@v4
519
- with:
520
- fetch-depth: 0
521
- - name: Create deploy tag
522
- run: |
523
- VERSION=$(cat version.json | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).version))")
524
- EXISTING=\$(git tag -l "d\${VERSION}.*" | wc -l)
525
- NEXT=\$((EXISTING + 1))
526
- TAG="d\${VERSION}.\${NEXT}"
527
- git tag "\$TAG"
528
- git push origin "\$TAG"
529
- `);
530
- // direct-deploy.yml
531
- writeFile(path.join(workflowsDir, 'direct-deploy.yml'), `name: Direct Deploy
532
- on:
533
- workflow_dispatch:
534
-
535
- permissions:
536
- contents: write
537
-
538
- jobs:
539
- deploy:
540
- runs-on: ubuntu-latest
541
- steps:
542
- - uses: actions/checkout@v4
543
- with:
544
- fetch-depth: 0
545
- - name: Merge develop to staging
546
- run: |
547
- git config user.name "github-actions[bot]"
548
- git config user.email "github-actions[bot]@users.noreply.github.com"
549
- git checkout staging
550
- git merge origin/develop --no-edit
551
- git push origin staging
552
- - name: Fast-forward main
553
- run: |
554
- git checkout main
555
- git merge staging --ff-only
556
- git push origin main
557
- `);
558
- }
559
- async function extractBundle(projectDir) {
560
- const release = await getLatestRelease();
561
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-'));
562
- try {
563
- const tarPath = await downloadTarball(release, tmpDir);
564
- const { adminDir, functionsDir, workersDir } = await extractNpmTarball(tarPath, tmpDir);
565
- // admin/ をコピー(dist/ の中身から functions/ を除く)
566
- if (fs.existsSync(adminDir)) {
567
- copyDir(adminDir, path.join(projectDir, 'admin'), ['functions']);
568
- }
569
- // functions/ をコピー(dist/functions/ → functions/)
570
- if (fs.existsSync(functionsDir)) {
571
- copyDir(functionsDir, path.join(projectDir, 'functions'));
572
- }
573
- // workers/ をコピー(Pages Functions が import する)
574
- if (fs.existsSync(workersDir)) {
575
- copyDir(workersDir, path.join(projectDir, 'workers'));
576
- }
577
- }
578
- finally {
579
- fs.rmSync(tmpDir, { recursive: true, force: true });
580
- }
581
- }
582
- function copyDir(src, dest, exclude = []) {
583
- fs.mkdirSync(dest, { recursive: true });
584
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
585
- if (exclude.includes(entry.name))
586
- continue;
587
- const srcPath = path.join(src, entry.name);
588
- const destPath = path.join(dest, entry.name);
589
- if (entry.isDirectory()) {
590
- copyDir(srcPath, destPath);
591
- }
592
- else {
593
- fs.copyFileSync(srcPath, destPath);
594
- }
595
- }
596
- }
597
398
  function getTotalSteps(options) {
598
399
  let steps = 4; // content structure, workflows, bundle, config files
599
400
  if (!options.skipGithub)
600
401
  steps += 3; // create repo, clone, commit/push
601
- if (!options.skipCloudflare)
602
- steps += 1; // R2 + Pages
402
+ if (options.terraform)
403
+ steps += 1;
404
+ else if (!options.skipCloudflare)
405
+ steps += 1;
603
406
  return steps;
604
407
  }