@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
package/README.md CHANGED
@@ -142,12 +142,11 @@ frelio init
142
142
  2. **対話式プロンプト** — リポジトリ名、サイトタイトル、R2 設定などを入力
143
143
  3. **GitHub OAuth App の案内** — OAuth App の作成手順を表示し、Client ID / Secret を入力
144
144
  4. **GitHub リポジトリ作成** — `gh repo create` でプライベートリポジトリを作成・クローン
145
- 5. **コンテンツ初期構造の作成** — `frelio-data/` ディレクトリとメタデータファイルを生成
146
- 6. **GitHub Actions ワークフロー配置**ステージングビルド、本番プロモート、直接デプロイの3つ
147
- 7. **CMS Admin バンドルの展開** 最新リリースから `admin/`、`functions/`、`workers/` を配置
148
- 8. **設定ファイル生成**`admin/config.json`、`wrangler.toml`、`_redirects`、`_routes.json`
149
- 9. **Cloudflare セットアップ** R2 バケット作成、本番用 Pages プロジェクト作成、ステージング用 Pages プロジェクト作成、OAuth シークレット設定
150
- 10. **ブランチ構造の作成** — `main`、`develop`、`staging` ブランチを作成・プッシュ、デフォルトブランチを `develop` に設定
145
+ 5. **コンテンツリポジトリのスキャフォールド**GitHub から [`c-time/frelio-content-template`](https://github.com/c-time/frelio-content-template) をダウンロードし、設定値を埋め込んで展開(`frelio-data/`、`scripts/`、`.github/workflows/`、`vite.config.ts` 等)
146
+ 6. **CMS Admin バンドルの展開**最新リリースから `admin/`、`functions/`、`workers/` を配置
147
+ 7. **設定ファイル生成** — `admin/config.json`、`wrangler.toml`、`_redirects`、`_routes.json`
148
+ 8. **Cloudflare セットアップ** R2 バケット作成、本番用 Pages プロジェクト作成、ステージング用 Pages プロジェクト作成、OAuth シークレット設定
149
+ 9. **ブランチ構造の作成**`main`、`develop`、`staging` ブランチを作成・プッシュ、デフォルトブランチを `develop` に設定
151
150
 
152
151
  #### プロンプトで聞かれること
153
152
 
@@ -212,7 +211,7 @@ frelio init \
212
211
 
213
212
  ```
214
213
  my-site/
215
- ├── admin/ # CMS 管理画面(ビルド済み)
214
+ ├── admin/ # CMS 管理画面(ビルド済み、frelio update で更新)
216
215
  │ ├── index.html
217
216
  │ ├── config.json # 実行時設定
218
217
  │ └── assets/
@@ -231,19 +230,24 @@ my-site/
231
230
  │ │ ├── contents/
232
231
  │ │ │ ├── published/ # 公開済み
233
232
  │ │ │ └── private/ # 下書き
234
- │ │ ├── templates/ # HTML テンプレート
235
- │ │ │ └── assets/
236
- │ │ │ ├── scss/ # スタイルソース
237
- │ │ │ └── ts/ # スクリプトソース
233
+ │ │ ├── templates/ # HTML テンプレート + アセットソース
234
+ │ │ │ ├── _parts/ # 共通パーツ (head.htm, header.htm, footer.htm)
235
+ │ │ │ ├── common/ # 全ページ共通 (scripts/ + styles/)
236
+ │ │ │ ├── about/ # ページ別 (index.html + scripts/ + styles/)
237
+ │ │ │ ├── contact/
238
+ │ │ │ └── news/
238
239
  │ │ └── data/data-json/ # SSG 中間データ
239
240
  │ └── admin/
240
- │ ├── metadata/ # メタデータ
241
+ │ ├── content_types/ # UI/Views 設定
242
+ │ ├── metadata/ # content_types.json, _dashboard.json
241
243
  │ ├── recipes/ # ビルドレシピ
242
- │ └── users/ # ユーザー管理
243
- ├── .github/workflows/ # GitHub Actions
244
+ │ └── structure/ # users.json, stages.json
245
+ ├── scripts/ # SSG ビルドスクリプト (tsx)
246
+ ├── .github/workflows/ # GitHub Actions(4ワークフロー)
244
247
  ├── vite.config.ts # テンプレートアセットビルド(SCSS/TS)
245
248
  ├── package.json # vite + sass + typescript
246
249
  ├── tsconfig.json
250
+ ├── tsconfig.node.json
247
251
  ├── wrangler.toml # Cloudflare 設定(R2 バインディング)
248
252
  ├── _redirects # /admin/* → SPA, /* → /public/:splat
249
253
  ├── _routes.json # /api/*, /storage/* → Functions
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * frelio add-staging - ステージング環境(プレビュー用ブランチ)の追加
3
+ *
4
+ * core/* の関数を呼ぶ薄いオーケストレーター。
3
5
  */
4
6
  type AddStagingOptions = {
5
7
  name?: string;
6
- pagesProject?: string;
7
- domain?: string;
8
- skipCloudflare?: boolean;
9
8
  };
10
9
  export declare function addStagingCommand(options: AddStagingOptions): Promise<void>;
11
10
  export {};
@@ -1,136 +1,52 @@
1
1
  /**
2
2
  * frelio add-staging - ステージング環境(プレビュー用ブランチ)の追加
3
+ *
4
+ * core/* の関数を呼ぶ薄いオーケストレーター。
3
5
  */
4
6
  import prompts from 'prompts';
5
- import { exec, commandExists, log, logStep, logSuccess, logError } from '../lib/shell.js';
6
- import { generateHash } from '../lib/templates.js';
7
- import { validateStagingName, validateRequired } from '../lib/validators.js';
7
+ import { log, logStep, logSuccess, logError } from '../lib/shell.js';
8
+ import { validateStagingName } from '../lib/validators.js';
9
+ import { checkGit } from '../core/prerequisites.js';
10
+ import { isGitRepo, createBranch, checkWorkflowBranchCoverage } from '../core/git-operations.js';
11
+ import { readConfig } from '../core/config.js';
8
12
  export async function addStagingCommand(options) {
9
13
  log('');
10
14
  log('🌿 ステージング環境の追加');
11
15
  log('');
12
16
  // 前提チェック
13
- if (!commandExists('git')) {
14
- logError('git が見つかりません。');
17
+ const gitCheck = checkGit();
18
+ if (!gitCheck.success) {
19
+ logError(gitCheck.error);
15
20
  process.exit(1);
16
21
  }
17
- // Git リポジトリ内か確認
18
- try {
19
- exec('git rev-parse --is-inside-work-tree', { silent: true });
20
- }
21
- catch {
22
+ const projectDir = process.cwd();
23
+ if (!isGitRepo(projectDir)) {
22
24
  logError('Git リポジトリ内で実行してください。');
23
25
  process.exit(1);
24
26
  }
25
- // wrangler チェック
26
- if (!options.skipCloudflare) {
27
- if (!commandExists('wrangler')) {
28
- logError('wrangler CLI が見つかりません。`npm i -g wrangler` でインストールしてください。');
29
- process.exit(1);
30
- }
31
- try {
32
- exec('wrangler whoami', { silent: true });
33
- }
34
- catch {
35
- logError('wrangler にログインしていません。`wrangler login` を実行してください。');
36
- process.exit(1);
37
- }
27
+ // Pages プロジェクト名を config.json から取得
28
+ let pagesProjectName = '';
29
+ const configResult = readConfig(projectDir);
30
+ if (configResult.success && configResult.data.config) {
31
+ pagesProjectName = configResult.data.config.pagesProjectName || '';
38
32
  }
39
- // 本番 Pages プロジェクト名とドメインを推定
40
- let basePagesProject = '';
41
- let baseDomain = '';
42
- try {
43
- const wranglerToml = exec('cat wrangler.toml', { silent: true });
44
- const nameMatch = wranglerToml.match(/^name\s*=\s*"([^"]+)"/m);
45
- if (nameMatch) {
46
- basePagesProject = nameMatch[1];
47
- }
48
- }
49
- catch {
50
- // wrangler.toml がなくても続行
51
- }
52
- try {
53
- const configJson = exec('cat admin/config.json', { silent: true });
54
- const config = JSON.parse(configJson);
55
- if (config.productionUrl) {
56
- const url = new URL(config.productionUrl);
57
- baseDomain = url.hostname;
58
- }
59
- else if (config.previewUrl) {
60
- // previewUrl から親ドメインを推定(staging-xxx.example.com → example.com)
61
- const url = new URL(config.previewUrl);
62
- const parts = url.hostname.split('.');
63
- if (parts.length > 2) {
64
- baseDomain = parts.slice(-2).join('.');
65
- }
66
- }
67
- }
68
- catch {
69
- // config.json がなくても続行
70
- }
71
- // 非対話 / 対話モード判定
33
+ // ステージング名の取得
72
34
  const isInteractive = process.stdin.isTTY === true;
73
35
  let stagingName;
74
- let pagesProject;
75
- let stagingDomain;
76
- if (options.name && options.pagesProject) {
77
- // 全必須オプション指定 → プロンプトスキップ
78
- const nameCheck = validateStagingName(options.name);
79
- if (nameCheck !== true) {
80
- logError(`--name: ${nameCheck}`);
81
- process.exit(1);
82
- }
83
- stagingName = options.name;
84
- pagesProject = options.pagesProject;
85
- stagingDomain = options.domain ?? generateDefaultDomain(stagingName, basePagesProject, baseDomain);
86
- }
87
- else if (options.name && !options.pagesProject) {
88
- // --name のみ指定 → pagesProject はデフォルト算出 or プロンプト
36
+ if (options.name) {
89
37
  const nameCheck = validateStagingName(options.name);
90
38
  if (nameCheck !== true) {
91
39
  logError(`--name: ${nameCheck}`);
92
40
  process.exit(1);
93
41
  }
94
42
  stagingName = options.name;
95
- const defaultPagesProject = basePagesProject ? `${basePagesProject}-staging-${stagingName}` : '';
96
- if (!isInteractive) {
97
- // 非 TTY → デフォルト値で続行
98
- pagesProject = defaultPagesProject;
99
- if (!pagesProject) {
100
- logError('--pages-project が必要です(wrangler.toml からプロジェクト名を推定できません)');
101
- process.exit(1);
102
- }
103
- stagingDomain = options.domain ?? generateDefaultDomain(stagingName, basePagesProject, baseDomain);
104
- }
105
- else {
106
- // TTY → 残りをプロンプト
107
- const response = await prompts([
108
- {
109
- type: 'text',
110
- name: 'pagesProject',
111
- message: 'ステージング用 Pages プロジェクト名:',
112
- initial: defaultPagesProject,
113
- validate: (v) => validateRequired(v, 'プロジェクト名'),
114
- },
115
- {
116
- type: 'text',
117
- name: 'domain',
118
- message: 'カスタムドメイン(推測困難なハッシュ付き推奨):',
119
- initial: generateDefaultDomain(stagingName, basePagesProject, baseDomain),
120
- },
121
- ], { onCancel: () => process.exit(0) });
122
- pagesProject = response.pagesProject;
123
- stagingDomain = response.domain;
124
- }
125
43
  }
126
44
  else {
127
- // --name 未指定
128
45
  if (!isInteractive) {
129
46
  logError('非対話モードでは --name が必須です。');
130
- logError('使用例: frelio add-staging --name preview1 [--pages-project <name>] [--domain <domain>]');
47
+ logError('使用例: frelio add-staging --name preview1');
131
48
  process.exit(1);
132
49
  }
133
- // 従来通りの対話式プロンプト
134
50
  const response = await prompts([
135
51
  {
136
52
  type: 'text',
@@ -138,86 +54,36 @@ export async function addStagingCommand(options) {
138
54
  message: 'ステージング名(staging-{name} のブランチが作成されます):',
139
55
  validate: (v) => validateStagingName(v),
140
56
  },
141
- {
142
- type: 'text',
143
- name: 'pagesProject',
144
- message: 'ステージング用 Pages プロジェクト名:',
145
- initial: (_prev, values) => {
146
- const name = values.name || '';
147
- return basePagesProject ? `${basePagesProject}-staging-${name}` : '';
148
- },
149
- validate: (v) => validateRequired(v, 'プロジェクト名'),
150
- },
151
- {
152
- type: 'text',
153
- name: 'domain',
154
- message: 'カスタムドメイン(推測困難なハッシュ付き推奨):',
155
- initial: (_prev, values) => {
156
- const name = values.name || '';
157
- return generateDefaultDomain(name, basePagesProject, baseDomain);
158
- },
159
- },
160
57
  ], { onCancel: () => process.exit(0) });
161
58
  if (!response.name) {
162
59
  log('キャンセルしました。');
163
60
  process.exit(0);
164
61
  }
165
62
  stagingName = response.name;
166
- pagesProject = response.pagesProject;
167
- stagingDomain = response.domain;
168
63
  }
169
64
  const branchName = `staging-${stagingName}`;
170
65
  log('');
171
- const totalSteps = options.skipCloudflare ? 2 : 3;
66
+ const totalSteps = 2;
172
67
  let step = 0;
173
68
  // Step 1: Git ブランチ作成
174
69
  step++;
175
70
  logStep(step, totalSteps, `ブランチ "${branchName}" を作成...`);
176
- try {
177
- // develop から分岐
178
- exec('git fetch origin', { silent: true });
179
- // ブランチが既に存在するか確認
180
- try {
181
- exec(`git rev-parse --verify origin/${branchName}`, { silent: true });
182
- logSuccess(`ブランチ "${branchName}" はリモートに既に存在します`);
183
- }
184
- catch {
185
- // ローカルブランチ作成 & プッシュ
186
- exec(`git branch ${branchName} origin/develop`, { silent: true });
187
- exec(`git push -u origin ${branchName}`, { silent: true });
188
- logSuccess(`ブランチ "${branchName}" を作成・プッシュしました`);
189
- }
71
+ const branchResult = createBranch(projectDir, branchName, 'origin/develop');
72
+ if (branchResult.success) {
73
+ logSuccess(branchResult.data.alreadyExisted
74
+ ? `ブランチ "${branchName}" はリモートに既に存在します`
75
+ : `ブランチ "${branchName}" を作成・プッシュしました`);
190
76
  }
191
- catch (error) {
192
- logError(`ブランチ作成失敗: ${error.message}`);
77
+ else {
78
+ logError(branchResult.error);
193
79
  process.exit(1);
194
80
  }
195
- // Step 2: Cloudflare Pages プロジェクト作成
196
- if (!options.skipCloudflare) {
197
- step++;
198
- logStep(step, totalSteps, `Pages プロジェクト "${pagesProject}" を作成...`);
199
- try {
200
- exec(`wrangler pages project create ${pagesProject} --production-branch ${branchName}`, {
201
- silent: true,
202
- });
203
- logSuccess(`Pages プロジェクト "${pagesProject}" を作成しました`);
204
- }
205
- catch (error) {
206
- const msg = error.message;
207
- if (msg.includes('already exists')) {
208
- logSuccess(`Pages プロジェクト "${pagesProject}" は既に存在します`);
209
- }
210
- else {
211
- logError(`Pages プロジェクト作成失敗: ${msg}`);
212
- }
213
- }
214
- }
215
- // Step 3: build-staging.yml のブランチリスト確認
81
+ // Step 2: build-staging.yml のブランチリスト確認
216
82
  step++;
217
83
  logStep(step, totalSteps, 'GitHub Actions ワークフローを確認...');
218
- try {
219
- const workflow = exec('cat .github/workflows/build-staging.yml', { silent: true });
220
- if (workflow.includes('staging-*') || workflow.includes(`staging-${stagingName}`)) {
84
+ const workflowCheck = checkWorkflowBranchCoverage(projectDir, branchName);
85
+ if (workflowCheck.success) {
86
+ if (workflowCheck.data.covered) {
221
87
  logSuccess('ワークフローは既に対応済みです(ワイルドカードまたは明示列挙)');
222
88
  }
223
89
  else {
@@ -227,10 +93,10 @@ export async function addStagingCommand(options) {
227
93
  log(' - staging');
228
94
  log(` - ${branchName} # ← 追加`);
229
95
  log('');
230
- log(' または、ワイルドカード "staging-*" を使用するとすべてのカスタム staging に対応できます。');
96
+ log(' または、CMS 管理画面の基本設定 ファイル所在一覧からワークフローを再生成してください。');
231
97
  }
232
98
  }
233
- catch {
99
+ else {
234
100
  log(' ⚠ .github/workflows/build-staging.yml が見つかりません。手動で確認してください。');
235
101
  }
236
102
  // 完了
@@ -238,25 +104,13 @@ export async function addStagingCommand(options) {
238
104
  log('✅ ステージング環境の追加が完了しました!');
239
105
  log('');
240
106
  log(` ブランチ: ${branchName}`);
241
- if (!options.skipCloudflare) {
242
- const domain = stagingDomain || `${pagesProject}.pages.dev`;
243
- log(` URL: https://${domain}`);
107
+ if (pagesProjectName) {
108
+ log(` プレビュー URL: https://${branchName}.${pagesProjectName}.pages.dev`);
244
109
  }
245
110
  log('');
246
111
  log(' 残りの手動作業:');
247
- log(' - Cloudflare Pages でリポジトリを接続(GitHub integration)');
248
- if (stagingDomain) {
249
- log(` - Pages プロジェクトにカスタムドメイン "${stagingDomain}" を設定`);
250
- }
251
- log(' - Cloudflare Access でアクセス制限を設定(推奨)');
252
112
  log(' - CMS 管理画面の /staging ページでステージングブランチを登録');
113
+ log(' - カスタムドメインが必要な場合は Cloudflare Pages のカスタムドメイン設定');
114
+ log(' - Cloudflare Access でアクセス制限を設定(推奨)');
253
115
  log('');
254
116
  }
255
- function generateDefaultDomain(name, basePagesProject, baseDomain) {
256
- const hash = generateHash();
257
- if (baseDomain) {
258
- return `${name}-${hash}.${baseDomain}`;
259
- }
260
- const project = basePagesProject || 'site';
261
- return `${project}-${name}-${hash}.pages.dev`;
262
- }
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * frelio init - 新規プロジェクトの対話式セットアップ
3
+ *
4
+ * core/* の関数を順番に呼ぶ薄いオーケストレーター。
5
+ * UI 層(プロンプト、ログ出力、process.exit)はここに閉じる。
3
6
  */
4
7
  type InitOptions = {
5
8
  skipGithub?: boolean;
6
9
  skipCloudflare?: boolean;
10
+ terraform?: boolean;
7
11
  contentRepo?: string;
8
12
  siteTitle?: string;
9
13
  productionUrl?: string;