@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,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;