@c-time/frelio-cli 1.3.13 → 1.4.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.
Files changed (44) hide show
  1. package/README.md +18 -14
  2. package/dist/commands/add-staging.d.ts +2 -3
  3. package/dist/commands/add-staging.js +38 -184
  4. package/dist/commands/init.d.ts +4 -0
  5. package/dist/commands/init.js +164 -373
  6. package/dist/commands/update.d.ts +2 -0
  7. package/dist/commands/update.js +11 -67
  8. package/dist/core/bundle.d.ts +14 -0
  9. package/dist/core/bundle.js +122 -0
  10. package/dist/core/cloudflare.d.ts +26 -0
  11. package/dist/core/cloudflare.js +60 -0
  12. package/dist/core/config.d.ts +26 -0
  13. package/dist/core/config.js +120 -0
  14. package/dist/core/content-structure.d.ts +13 -0
  15. package/dist/core/content-structure.js +13 -0
  16. package/dist/core/file-generators.d.ts +28 -0
  17. package/dist/core/file-generators.js +93 -0
  18. package/dist/core/git-operations.d.ts +15 -0
  19. package/dist/core/git-operations.js +78 -0
  20. package/dist/core/github.d.ts +16 -0
  21. package/dist/core/github.js +43 -0
  22. package/dist/core/index.d.ts +22 -0
  23. package/dist/core/index.js +30 -0
  24. package/dist/core/prerequisites.d.ts +22 -0
  25. package/dist/core/prerequisites.js +107 -0
  26. package/dist/core/status.d.ts +18 -0
  27. package/dist/core/status.js +122 -0
  28. package/dist/core/template-scaffold.d.ts +14 -0
  29. package/dist/core/template-scaffold.js +99 -0
  30. package/dist/core/terraform.d.ts +7 -0
  31. package/dist/core/terraform.js +47 -0
  32. package/dist/core/types.d.ts +48 -0
  33. package/dist/core/types.js +21 -0
  34. package/dist/core/workflows.d.ts +11 -0
  35. package/dist/core/workflows.js +345 -0
  36. package/dist/index.js +2 -4
  37. package/dist/lib/github-release.d.ts +15 -0
  38. package/dist/lib/github-release.js +41 -0
  39. package/dist/lib/initial-content.js +87 -55
  40. package/dist/lib/template-renderer.d.ts +16 -0
  41. package/dist/lib/template-renderer.js +32 -0
  42. package/dist/lib/templates.d.ts +7 -7
  43. package/dist/lib/templates.js +311 -214
  44. package/package.json +2 -3
@@ -1,27 +1,49 @@
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 { generateTerraformFiles } from '../core/terraform.js';
21
+ import { installBundle } from '../core/bundle.js';
22
+ import { initialCommitAndBranches } from '../core/git-operations.js';
13
23
  export async function initCommand(options) {
14
24
  log('');
15
25
  log('🚀 Frelio CMS プロジェクトセットアップ');
16
26
  log('');
17
- // 前提チェック
27
+ // ── 前提チェック ──
18
28
  log('🔍 前提チェック...');
19
- const checks = checkPrerequisites(options);
20
- if (!checks.ok) {
29
+ const targets = ['git'];
30
+ if (!options.skipGithub)
31
+ targets.push('github');
32
+ if (!options.skipCloudflare && !options.terraform)
33
+ targets.push('cloudflare');
34
+ const prereqs = checkPrerequisitesFor(targets);
35
+ if (prereqs.success) {
36
+ for (const [name, r] of Object.entries(prereqs.data.results)) {
37
+ if (r.success)
38
+ logSuccess(name);
39
+ }
40
+ }
41
+ else {
42
+ logError(prereqs.error);
21
43
  process.exit(1);
22
44
  }
23
45
  log('');
24
- // 対話式プロンプト
46
+ // ── 対話式プロンプト ──
25
47
  log('📝 プロジェクト設定:');
26
48
  const config = await promptConfig(options);
27
49
  if (!config) {
@@ -29,7 +51,7 @@ export async function initCommand(options) {
29
51
  process.exit(0);
30
52
  }
31
53
  log('');
32
- // OAuth App 案内
54
+ // ── OAuth App 案内 ──
33
55
  log('🔑 GitHub OAuth App:');
34
56
  if (!config.githubClientId) {
35
57
  const isInteractive = process.stdin.isTTY === true;
@@ -40,9 +62,9 @@ export async function initCommand(options) {
40
62
  log(' ⚠ OAuth App は GitHub の Web UI で作成が必要です。');
41
63
  log(' → https://github.com/settings/developers');
42
64
  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`);
65
+ log(` - Application name: ${config.siteTitle || config.adminPagesProjectName} CMS`);
66
+ log(` - Homepage URL: https://${config.adminPagesProjectName}.pages.dev`);
67
+ log(` - Callback URL: https://${config.adminPagesProjectName}.pages.dev/api/auth/callback`);
46
68
  log('');
47
69
  const oauthResponse = await prompts([
48
70
  {
@@ -67,26 +89,20 @@ export async function initCommand(options) {
67
89
  oauthResponse.clientSecret;
68
90
  }
69
91
  log('');
70
- // セットアップ実行
92
+ // ── セットアップ実行 ──
71
93
  const totalSteps = getTotalSteps(options);
72
94
  let step = 0;
73
95
  // GitHub リポジトリ作成
74
96
  if (!options.skipGithub) {
75
97
  step++;
76
98
  logStep(step, totalSteps, 'GitHub リポジトリ作成...');
77
- try {
78
- exec(`gh repo create ${config.contentRepo} --private --confirm`, { silent: true });
79
- logSuccess('リポジトリ作成完了');
99
+ const repoResult = createRepo(config.contentRepo);
100
+ if (repoResult.success) {
101
+ logSuccess(repoResult.data.alreadyExisted ? 'リポジトリは既に存在します' : 'リポジトリ作成完了');
80
102
  }
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
- }
103
+ else {
104
+ logError(repoResult.error);
105
+ process.exit(1);
90
106
  }
91
107
  }
92
108
  // 作業ディレクトリ作成
@@ -95,18 +111,13 @@ export async function initCommand(options) {
95
111
  if (!options.skipGithub) {
96
112
  step++;
97
113
  logStep(step, totalSteps, 'リポジトリをクローン...');
98
- try {
99
- exec(`gh repo clone ${config.contentRepo} ${repoName}`, { silent: true });
100
- logSuccess('クローン完了');
114
+ const cloneResult = cloneRepo(config.contentRepo, projectDir);
115
+ if (cloneResult.success) {
116
+ logSuccess(cloneResult.data.alreadyExisted ? 'ディレクトリは既に存在します' : 'クローン完了');
101
117
  }
102
- catch (error) {
103
- if (fs.existsSync(projectDir)) {
104
- logSuccess('ディレクトリは既に存在します');
105
- }
106
- else {
107
- logError(`クローン失敗: ${error.message}`);
108
- process.exit(1);
109
- }
118
+ else {
119
+ logError(cloneResult.error);
120
+ process.exit(1);
110
121
  }
111
122
  }
112
123
  else {
@@ -114,104 +125,90 @@ export async function initCommand(options) {
114
125
  fs.mkdirSync(projectDir, { recursive: true });
115
126
  }
116
127
  }
117
- // コンテンツリポジトリ初期構造
128
+ // コンテンツリポジトリ初期構造(ワークフロー含む)
118
129
  step++;
119
130
  logStep(step, totalSteps, 'コンテンツリポジトリ初期構造作成...');
120
- createContentStructure(projectDir, config);
121
- generateInitialContent(projectDir);
122
- logSuccess('初期構造作成完了(デモサイトテンプレート付き)');
123
- // GitHub Actions ワークフロー
124
- step++;
125
- logStep(step, totalSteps, 'GitHub Actions ワークフロー配置...');
126
- copyWorkflows(projectDir);
127
- logSuccess('ワークフロー配置完了');
131
+ const structResult = await createFullContentStructure(projectDir, config);
132
+ if (structResult.success) {
133
+ logSuccess('初期構造作成完了(デモサイトテンプレート + ワークフロー)');
134
+ }
135
+ else {
136
+ logError(structResult.error);
137
+ process.exit(1);
138
+ }
128
139
  // CMS Admin バンドル展開
129
140
  step++;
130
141
  logStep(step, totalSteps, 'CMS Admin バンドルをダウンロード・展開...');
131
- try {
132
- await extractBundle(projectDir);
142
+ const bundleResult = await installBundle(projectDir);
143
+ if (bundleResult.success) {
133
144
  logSuccess('バンドル展開完了');
134
145
  }
135
- catch (error) {
136
- logError(`バンドル展開失敗: ${error.message}`);
146
+ else {
147
+ logError(bundleResult.error);
137
148
  log(' ℹ リリースが公開されていない場合は、手動でバンドルを展開してください。');
138
149
  log(' scripts/build-distributable.sh を実行してから dist-release/ の内容をコピーします。');
139
150
  }
140
151
  // 設定ファイル生成
141
152
  step++;
142
153
  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) {
154
+ const configResult = regenerateAllConfigFiles(projectDir, config);
155
+ if (configResult.success) {
156
+ logSuccess('設定ファイル生成完了');
157
+ }
158
+ else {
159
+ logError(configResult.error);
160
+ process.exit(1);
161
+ }
162
+ // Cloudflare セットアップ or Terraform ファイル生成
163
+ if (options.terraform) {
152
164
  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
- }
165
+ logStep(step, totalSteps, 'Terraform ファイル生成...');
166
+ const tfResult = generateTerraformFiles(projectDir, config);
167
+ if (tfResult.success) {
168
+ logSuccess('Terraform ファイル生成完了(terraform/)');
166
169
  }
167
- try {
168
- exec(`wrangler pages project create ${config.pagesProjectName} --production-branch main`, {
169
- silent: true,
170
- });
171
- logSuccess(`Pages プロジェクト "${config.pagesProjectName}" 作成完了`);
170
+ else {
171
+ logError(tfResult.error);
172
172
  }
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}" は既に存在します`);
173
+ }
174
+ else if (!options.skipCloudflare) {
175
+ step++;
176
+ logStep(step, totalSteps, 'Cloudflare セットアップ...');
177
+ const secret = config.githubClientSecret;
178
+ const cfResult = setupCloudflareResources(config, secret);
179
+ if (cfResult.success) {
180
+ const { r2Bucket, contentPages, adminPages, secrets } = cfResult.data;
181
+ if (r2Bucket.success) {
182
+ logSuccess(r2Bucket.alreadyExisted
183
+ ? `R2 バケット "${config.r2BucketName}" は既に存在します`
184
+ : `R2 バケット "${config.r2BucketName}" 作成完了`);
177
185
  }
178
186
  else {
179
- logError(`Pages プロジェクト作成失敗: ${msg}`);
187
+ logError(r2Bucket.error);
180
188
  }
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}" は既に存在します`);
189
+ if (contentPages.success) {
190
+ logSuccess(contentPages.alreadyExisted
191
+ ? `Pages プロジェクト "${config.pagesProjectName}" は既に存在します`
192
+ : `Pages プロジェクト "${config.pagesProjectName}"(コンテンツ配信)作成完了`);
194
193
  }
195
194
  else {
196
- logError(`ステージング Pages プロジェクト作成失敗: ${msg}`);
197
- }
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 設定完了');
195
+ logError(contentPages.error);
205
196
  }
206
- catch {
207
- logError('GITHUB_CLIENT_SECRET の設定に失敗しました。CF ダッシュボードから手動設定してください。');
197
+ if (adminPages.success) {
198
+ logSuccess(adminPages.alreadyExisted
199
+ ? `Pages プロジェクト "${config.adminPagesProjectName}" は既に存在します`
200
+ : `Pages プロジェクト "${config.adminPagesProjectName}"(管理画面)作成完了`);
208
201
  }
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 設定完了');
202
+ else {
203
+ logError(adminPages.error);
212
204
  }
213
- catch {
214
- logError('GITHUB_CLIENT_ID の設定に失敗しました。CF ダッシュボードから手動設定してください。');
205
+ for (const s of secrets) {
206
+ if (s.success) {
207
+ logSuccess('シークレット設定完了');
208
+ }
209
+ else {
210
+ logError(s.error);
211
+ }
215
212
  }
216
213
  }
217
214
  }
@@ -219,84 +216,47 @@ export async function initCommand(options) {
219
216
  if (!options.skipGithub) {
220
217
  step++;
221
218
  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 });
219
+ const gitResult = initialCommitAndBranches(projectDir, config.contentRepo);
220
+ if (gitResult.success) {
234
221
  logSuccess('ブランチ作成・プッシュ完了');
235
222
  }
236
- catch (error) {
237
- logError(`Git 操作失敗: ${error.message}`);
223
+ else {
224
+ logError(gitResult.error);
238
225
  }
239
226
  }
240
- // 完了
227
+ // ── 完了 ──
241
228
  log('');
242
229
  log('✅ セットアップ完了!');
243
230
  log('');
244
- log(` 管理画面: https://${config.pagesProjectName}.pages.dev/admin/`);
231
+ log(` 管理画面: https://${config.adminPagesProjectName}.pages.dev/admin/`);
232
+ log(` プレビュー: https://staging.${config.pagesProjectName}.pages.dev`);
245
233
  log('');
246
- log(' 残りの手動作業:');
247
- log(` 1. 本番 Pages(${config.pagesProjectName})にリポジトリを接続`);
248
- log(` 2. ステージング Pages(${config.pagesProjectName}-staging)にリポジトリを接続`);
234
+ if (options.terraform) {
235
+ log(' Terraform でインフラを構築:');
236
+ log(` 1. cd ${repoName}/terraform`);
237
+ log(' 2. cp terraform.tfvars.example terraform.tfvars && vim terraform.tfvars');
238
+ log(' 3. terraform init');
239
+ log(' 4. terraform plan');
240
+ log(' 5. terraform apply');
241
+ log('');
242
+ log(' 残りの手動作業:');
243
+ log(` 1. コンテンツ配信 Pages(${config.pagesProjectName})にリポジトリを接続`);
244
+ log(` 2. 管理画面 Pages(${config.adminPagesProjectName})にリポジトリを接続(admin ブランチ)`);
245
+ }
246
+ else {
247
+ log(' 残りの手動作業:');
248
+ log(` 1. コンテンツ配信 Pages(${config.pagesProjectName})にリポジトリを接続`);
249
+ log(` 2. 管理画面 Pages(${config.adminPagesProjectName})にリポジトリを接続(admin ブランチ)`);
250
+ }
249
251
  if (config.stagingDomain) {
250
- log(` 3. ステージング Pages にカスタムドメインを設定: ${config.stagingDomain}`);
252
+ log(` 3. プレビュー用カスタムドメインを設定: ${config.stagingDomain}`);
251
253
  }
252
254
  log(` ${config.stagingDomain ? '4' : '3'}. ステージングのアクセス制限を設定(Cloudflare Access 推奨)`);
253
255
  log('');
254
256
  }
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
- }
257
+ // ---------------------------------------------------------------------------
258
+ // UI helpers (prompts — commands 層に閉じる)
259
+ // ---------------------------------------------------------------------------
300
260
  async function promptConfig(options) {
301
261
  const isInteractive = process.stdin.isTTY === true;
302
262
  // 必須オプションが揃っている場合 → プロンプトスキップ
@@ -316,7 +276,7 @@ async function promptConfig(options) {
316
276
  logError('使用例: frelio init --content-repo owner/repo --client-id xxx --client-secret yyy');
317
277
  process.exit(1);
318
278
  }
319
- // 対話モード(渡されたオプションはスキップ)
279
+ // 対話モード
320
280
  const response = await prompts([
321
281
  {
322
282
  type: options.contentRepo ? null : 'text',
@@ -366,12 +326,8 @@ async function promptConfig(options) {
366
326
  name: 'ownerUsername',
367
327
  message: '管理者の GitHub ユーザー名:',
368
328
  initial: () => {
369
- try {
370
- return exec('gh api user -q .login', { silent: true });
371
- }
372
- catch {
373
- return '';
374
- }
329
+ const userResult = getAuthenticatedUser();
330
+ return userResult.success ? userResult.data.username : '';
375
331
  },
376
332
  },
377
333
  ], { onCancel: () => process.exit(0) });
@@ -390,6 +346,7 @@ async function promptConfig(options) {
390
346
  r2BucketName: options.r2BucketName ?? response.r2BucketName ?? `${repoName}-files`,
391
347
  r2PublicUrl: options.r2PublicUrl ?? response.r2PublicUrl ?? '',
392
348
  pagesProjectName: repoName,
349
+ adminPagesProjectName: `${repoName}-admin`,
393
350
  ownerUsername: options.ownerUsername ?? response.ownerUsername ?? '',
394
351
  stagingDomain,
395
352
  };
@@ -399,206 +356,40 @@ async function promptConfig(options) {
399
356
  return config;
400
357
  }
401
358
  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));
359
+ const result = buildConfig({
360
+ contentRepo: options.contentRepo,
361
+ githubClientId: options.clientId,
362
+ siteTitle: options.siteTitle,
363
+ productionUrl: options.productionUrl,
364
+ stagingDomain: options.stagingDomain,
365
+ r2BucketName: options.r2BucketName,
366
+ r2PublicUrl: options.r2PublicUrl,
367
+ ownerUsername: options.ownerUsername,
368
+ });
369
+ if (!result.success) {
370
+ logError(result.error);
414
371
  process.exit(1);
415
372
  }
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 = '';
373
+ // ownerUsername gh api から補完
374
+ const config = result.data.config;
375
+ if (!config.ownerUsername) {
376
+ const userResult = getAuthenticatedUser();
377
+ if (userResult.success) {
378
+ config.ownerUsername = userResult.data.username;
428
379
  }
429
380
  }
430
381
  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,
382
+ ...config,
441
383
  githubClientSecret: options.clientSecret,
442
384
  };
443
385
  }
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
386
  function getTotalSteps(options) {
598
- let steps = 4; // content structure, workflows, bundle, config files
387
+ let steps = 3; // content structure (includes workflows), bundle, config files
599
388
  if (!options.skipGithub)
600
389
  steps += 3; // create repo, clone, commit/push
601
- if (!options.skipCloudflare)
602
- steps += 1; // R2 + Pages
390
+ if (options.terraform)
391
+ steps += 1;
392
+ else if (!options.skipCloudflare)
393
+ steps += 1;
603
394
  return steps;
604
395
  }