@c-time/frelio-cli 1.4.4 → 1.4.6
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 +2 -1
- package/dist/commands/add-staging.js +3 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +131 -11
- package/dist/commands/set-domain.d.ts +16 -0
- package/dist/commands/set-domain.js +141 -0
- package/dist/core/cloudflare.d.ts +1 -1
- package/dist/core/cloudflare.js +7 -1
- package/dist/core/config.js +2 -1
- package/dist/core/domain.d.ts +38 -0
- package/dist/core/domain.js +87 -0
- package/dist/core/git-operations.d.ts +14 -0
- package/dist/core/git-operations.js +40 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/index.js +14 -2
- package/dist/lib/resource-links.d.ts +26 -0
- package/dist/lib/resource-links.js +50 -0
- package/dist/lib/template-renderer.d.ts +1 -1
- package/dist/lib/template-renderer.js +3 -0
- package/dist/lib/templates.d.ts +37 -3
- package/dist/lib/templates.js +83 -15
- package/dist/lib/validators.d.ts +1 -0
- package/dist/lib/validators.js +14 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ frelio init
|
|
|
157
157
|
| 本番 URL | `--production-url` | 公開サイトの URL(任意) | `https://example.com` |
|
|
158
158
|
| ステージングドメイン | `--staging-domain` | ステージング確認用ドメイン(任意) | `staging-abc123.example.com` |
|
|
159
159
|
| R2 バケット名 | `--r2-bucket-name` | ファイルストレージ用バケット | `my-site-files` |
|
|
160
|
-
| R2 公開 URL | `--r2-public-url` |
|
|
160
|
+
| R2 公開 URL | `--r2-public-url` | アップロードファイル(R2)の配信元 URL(任意)。R2 のカスタムドメイン推奨、`pub-xxxx.r2.dev` も可。空欄なら後から設定可 | `https://files.example.com` |
|
|
161
161
|
| 管理者 GitHub ユーザー名 | `--owner-username` | 最初の管理者ユーザー | `your-username` |
|
|
162
162
|
| OAuth Client ID | `--client-id` | GitHub OAuth App の Client ID | `Ov23li...` |
|
|
163
163
|
| OAuth Client Secret | `--client-secret` | GitHub OAuth App の Client Secret | `********` |
|
|
@@ -438,6 +438,7 @@ Pages プロジェクトの設定でプレビューデプロイメントのア
|
|
|
438
438
|
- **Application name**: `My Site CMS`(任意)
|
|
439
439
|
- **Homepage URL**: `https://<pages-project>.pages.dev`
|
|
440
440
|
- **Authorization callback URL**: `https://<pages-project>.pages.dev/api/auth/callback`
|
|
441
|
+
- **Enable Device Flow**: チェックしない(Disable のまま。Frelio は認可コードフローを使用するため不要)
|
|
441
442
|
4. **Register application** をクリック
|
|
442
443
|
5. **Client ID** をメモ
|
|
443
444
|
6. **Generate a new client secret** → **Client Secret** をメモ
|
|
@@ -105,7 +105,10 @@ export async function addStagingCommand(options) {
|
|
|
105
105
|
log('');
|
|
106
106
|
log(` ブランチ: ${branchName}`);
|
|
107
107
|
if (pagesProjectName) {
|
|
108
|
+
// GitHub Actions の wrangler デプロイ(--branch)が生成するブランチエイリアス URL。
|
|
109
|
+
// 初回の Actions デプロイ完了後に有効になる。
|
|
108
110
|
log(` プレビュー URL: https://${branchName}.${pagesProjectName}.pages.dev`);
|
|
111
|
+
log(' (GitHub Actions の初回デプロイ後に有効。Cloudflare の Git 連携プレビューではない)');
|
|
109
112
|
}
|
|
110
113
|
log('');
|
|
111
114
|
log(' 残りの手動作業:');
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -9,6 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { log, logStep, logSuccess, logError } from '../lib/shell.js';
|
|
11
11
|
import { generateStagingDomain } from '../lib/templates.js';
|
|
12
|
+
import { buildResourceLinks } from '../lib/resource-links.js';
|
|
12
13
|
import { validateContentRepo, validateR2PublicUrl, validateRequired } from '../lib/validators.js';
|
|
13
14
|
// core functions
|
|
14
15
|
import { checkPrerequisitesFor } from '../core/prerequisites.js';
|
|
@@ -19,7 +20,7 @@ import { createFullContentStructure } from '../core/content-structure.js';
|
|
|
19
20
|
import { regenerateAllConfigFiles } from '../core/file-generators.js';
|
|
20
21
|
import { generateTerraformFiles } from '../core/terraform.js';
|
|
21
22
|
import { installBundle } from '../core/bundle.js';
|
|
22
|
-
import { initialCommitAndBranches } from '../core/git-operations.js';
|
|
23
|
+
import { initialCommitAndBranches, getGitIdentity, setGitIdentity, } from '../core/git-operations.js';
|
|
23
24
|
export async function initCommand(options) {
|
|
24
25
|
log('');
|
|
25
26
|
log('🚀 Frelio CMS プロジェクトセットアップ');
|
|
@@ -65,6 +66,7 @@ export async function initCommand(options) {
|
|
|
65
66
|
log(` - Application name: ${config.siteTitle || config.adminPagesProjectName} CMS`);
|
|
66
67
|
log(` - Homepage URL: https://${config.adminPagesProjectName}.pages.dev`);
|
|
67
68
|
log(` - Callback URL: https://${config.adminPagesProjectName}.pages.dev/api/auth/callback`);
|
|
69
|
+
log(' - Enable Device Flow: チェックしない(Disable のまま。Frelio は認可コードフローを使用)');
|
|
68
70
|
log('');
|
|
69
71
|
const oauthResponse = await prompts([
|
|
70
72
|
{
|
|
@@ -146,7 +148,9 @@ export async function initCommand(options) {
|
|
|
146
148
|
else {
|
|
147
149
|
logError(bundleResult.error);
|
|
148
150
|
log(' ℹ リリースが公開されていない場合は、手動でバンドルを展開してください。');
|
|
149
|
-
log('
|
|
151
|
+
log(' npm pack @c-time/frelio-cms で取得した tarball を展開し、');
|
|
152
|
+
log(' package/dist/(functions を除く)を admin/、package/dist/functions を functions/、');
|
|
153
|
+
log(' package/workers を workers/ に配置します。');
|
|
150
154
|
}
|
|
151
155
|
// 設定ファイル生成
|
|
152
156
|
step++;
|
|
@@ -175,7 +179,8 @@ export async function initCommand(options) {
|
|
|
175
179
|
step++;
|
|
176
180
|
logStep(step, totalSteps, 'Cloudflare セットアップ...');
|
|
177
181
|
const secret = config.githubClientSecret;
|
|
178
|
-
const
|
|
182
|
+
const cfToken = config.cloudflareApiToken;
|
|
183
|
+
const cfResult = setupCloudflareResources(config, secret, cfToken);
|
|
179
184
|
if (cfResult.success) {
|
|
180
185
|
const { r2Bucket, contentPages, adminPages, secrets } = cfResult.data;
|
|
181
186
|
if (r2Bucket.success) {
|
|
@@ -212,6 +217,12 @@ export async function initCommand(options) {
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
}
|
|
220
|
+
// git identity 確認(commit 直前 — 未設定だとデフォルト値で commit author が埋まるのを防ぐ)
|
|
221
|
+
if (!options.skipGithub) {
|
|
222
|
+
step++;
|
|
223
|
+
logStep(step, totalSteps, 'git identity 確認...');
|
|
224
|
+
await ensureGitIdentity(projectDir, config.ownerUsername);
|
|
225
|
+
}
|
|
215
226
|
// ブランチ構造とプッシュ
|
|
216
227
|
if (!options.skipGithub) {
|
|
217
228
|
step++;
|
|
@@ -229,8 +240,25 @@ export async function initCommand(options) {
|
|
|
229
240
|
log('✅ セットアップ完了!');
|
|
230
241
|
log('');
|
|
231
242
|
log(` 管理画面: https://${config.adminPagesProjectName}.pages.dev/admin/`);
|
|
232
|
-
|
|
243
|
+
// カスタムドメイン未設定時は staging ブランチのプレビューエイリアス(GitHub Actions の
|
|
244
|
+
// wrangler デプロイで生成)を案内する。Cloudflare の Git 連携ブランチプレビューは使わない。
|
|
245
|
+
log(` プレビュー: ${config.previewUrl || `https://staging.${config.pagesProjectName}.pages.dev`}`);
|
|
233
246
|
log('');
|
|
247
|
+
// 作成したリソースへのリンク(確認・設定変更の導線)
|
|
248
|
+
const resourceSections = buildResourceLinks(config, {
|
|
249
|
+
github: !options.skipGithub,
|
|
250
|
+
cloudflare: !options.terraform && !options.skipCloudflare,
|
|
251
|
+
});
|
|
252
|
+
if (resourceSections.length > 0) {
|
|
253
|
+
log('🔗 作成したリソース:');
|
|
254
|
+
for (const section of resourceSections) {
|
|
255
|
+
log(` ${section.title}:`);
|
|
256
|
+
for (const link of section.links) {
|
|
257
|
+
log(` ✓ ${link.label}: ${link.url}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
log('');
|
|
261
|
+
}
|
|
234
262
|
if (options.terraform) {
|
|
235
263
|
log(' Terraform でインフラを構築:');
|
|
236
264
|
log(` 1. cd ${repoName}/terraform`);
|
|
@@ -257,6 +285,54 @@ export async function initCommand(options) {
|
|
|
257
285
|
// ---------------------------------------------------------------------------
|
|
258
286
|
// UI helpers (prompts — commands 層に閉じる)
|
|
259
287
|
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* commit 直前に git identity(user.name / user.email)を確認する。
|
|
290
|
+
* 未設定の場合:
|
|
291
|
+
* - 対話モード: 入力を促し、クローンしたリポジトリのローカルスコープに設定する
|
|
292
|
+
* - 非対話モード: 警告して続行(git のデフォルト値で commit される旨を案内)
|
|
293
|
+
*/
|
|
294
|
+
async function ensureGitIdentity(projectDir, ownerUsername) {
|
|
295
|
+
const { name, email } = getGitIdentity(projectDir);
|
|
296
|
+
if (name && email) {
|
|
297
|
+
logSuccess('git identity 設定済み');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const isInteractive = process.stdin.isTTY === true;
|
|
301
|
+
if (!isInteractive) {
|
|
302
|
+
log(' ⚠ git user.name / user.email が未設定です。');
|
|
303
|
+
log(' このまま進めると commit author にデフォルト値(ホスト名由来)が使われます。');
|
|
304
|
+
log(' 後で `git config user.name` / `user.email` を設定し直してください。');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
log(' ⚠ git user.name / user.email が未設定です。commit author を確定するため入力してください。');
|
|
308
|
+
const response = await prompts([
|
|
309
|
+
{
|
|
310
|
+
type: 'text',
|
|
311
|
+
name: 'name',
|
|
312
|
+
message: 'git user.name:',
|
|
313
|
+
initial: name || ownerUsername || '',
|
|
314
|
+
validate: (v) => validateRequired(v, 'user.name'),
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
type: 'text',
|
|
318
|
+
name: 'email',
|
|
319
|
+
message: 'git user.email:',
|
|
320
|
+
initial: email || '',
|
|
321
|
+
validate: (v) => validateRequired(v, 'user.email'),
|
|
322
|
+
},
|
|
323
|
+
], { onCancel: () => process.exit(0) });
|
|
324
|
+
if (!response.name || !response.email) {
|
|
325
|
+
log('セットアップをキャンセルしました。');
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
328
|
+
const result = setGitIdentity(projectDir, response.name, response.email);
|
|
329
|
+
if (result.success) {
|
|
330
|
+
logSuccess('git identity を設定しました(このリポジトリのみ)');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
logError(result.error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
260
336
|
async function promptConfig(options) {
|
|
261
337
|
const isInteractive = process.stdin.isTTY === true;
|
|
262
338
|
// 必須オプションが揃っている場合 → プロンプトスキップ
|
|
@@ -300,10 +376,7 @@ async function promptConfig(options) {
|
|
|
300
376
|
type: options.stagingDomain !== undefined ? null : 'text',
|
|
301
377
|
name: 'stagingDomain',
|
|
302
378
|
message: 'ステージングのドメイン(推測困難なハッシュ付き推奨):',
|
|
303
|
-
initial: (_prev, values) =>
|
|
304
|
-
const repo = (options.contentRepo || values.contentRepo)?.split('/')[1] || 'site';
|
|
305
|
-
return generateStagingDomain((options.productionUrl || values.productionUrl) || '', repo);
|
|
306
|
-
},
|
|
379
|
+
initial: (_prev, values) => generateStagingDomain((options.productionUrl || values.productionUrl) || ''),
|
|
307
380
|
},
|
|
308
381
|
{
|
|
309
382
|
type: options.r2BucketName !== undefined ? null : 'text',
|
|
@@ -315,9 +388,19 @@ async function promptConfig(options) {
|
|
|
315
388
|
},
|
|
316
389
|
},
|
|
317
390
|
{
|
|
318
|
-
type: options.r2PublicUrl !== undefined
|
|
391
|
+
type: options.r2PublicUrl !== undefined
|
|
392
|
+
? null
|
|
393
|
+
: () => {
|
|
394
|
+
log('');
|
|
395
|
+
log(' ℹ R2 公開 URL(任意)');
|
|
396
|
+
log(' アップロードした画像・ファイル(Cloudflare R2)を配信する公開 URL です。');
|
|
397
|
+
log(' 推奨: R2 に割り当てたカスタムドメイン 例) https://files.example.com');
|
|
398
|
+
log(' 代替: R2.dev の開発用 URL 例) https://pub-xxxxxxxx.r2.dev');
|
|
399
|
+
log(' 未入力なら空欄のまま Enter(後から admin/config.json で設定可)');
|
|
400
|
+
return 'text';
|
|
401
|
+
},
|
|
319
402
|
name: 'r2PublicUrl',
|
|
320
|
-
message: 'R2 公開 URL:',
|
|
403
|
+
message: 'R2 公開 URL (任意, 例: https://files.example.com):',
|
|
321
404
|
initial: '',
|
|
322
405
|
validate: (v) => validateR2PublicUrl(v),
|
|
323
406
|
},
|
|
@@ -330,6 +413,36 @@ async function promptConfig(options) {
|
|
|
330
413
|
return userResult.success ? userResult.data.username : '';
|
|
331
414
|
},
|
|
332
415
|
},
|
|
416
|
+
{
|
|
417
|
+
type: options.cloudflareAccountId !== undefined
|
|
418
|
+
? null
|
|
419
|
+
: () => {
|
|
420
|
+
log('');
|
|
421
|
+
log(' ℹ Cloudflare アカウント ID / API トークン(デプロイ管理機能 / Issue #27)');
|
|
422
|
+
log(' 管理画面のデプロイ履歴で Cloudflare Pages のデプロイ一覧・削除を行うために使用します。');
|
|
423
|
+
log(' アカウント ID: Cloudflare ダッシュボードの URL や任意ページの右サイドバーで確認できます。');
|
|
424
|
+
log(' 未入力なら空欄のまま Enter(後から admin/config.json と wrangler.toml で設定可)');
|
|
425
|
+
return 'text';
|
|
426
|
+
},
|
|
427
|
+
name: 'cloudflareAccountId',
|
|
428
|
+
message: 'Cloudflare アカウント ID (任意):',
|
|
429
|
+
initial: '',
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
type: options.cloudflareApiToken !== undefined
|
|
433
|
+
? null
|
|
434
|
+
: () => {
|
|
435
|
+
log('');
|
|
436
|
+
log(' ℹ Cloudflare API トークン(任意・シークレット)');
|
|
437
|
+
log(' スコープ: Account → Cloudflare Pages: Edit(一覧取得+削除に必要)');
|
|
438
|
+
log(' Dashboard → My Profile → API Tokens → Create Token で発行できます。');
|
|
439
|
+
log(' 管理画面 Pages プロジェクトのシークレットとして登録されます(コミットされません)。');
|
|
440
|
+
return 'password';
|
|
441
|
+
},
|
|
442
|
+
name: 'cloudflareApiToken',
|
|
443
|
+
message: 'Cloudflare API トークン (任意):',
|
|
444
|
+
initial: '',
|
|
445
|
+
},
|
|
333
446
|
], { onCancel: () => process.exit(0) });
|
|
334
447
|
const contentRepo = options.contentRepo || response.contentRepo;
|
|
335
448
|
if (!contentRepo)
|
|
@@ -349,10 +462,15 @@ async function promptConfig(options) {
|
|
|
349
462
|
adminPagesProjectName: `${repoName}-admin`,
|
|
350
463
|
ownerUsername: options.ownerUsername ?? response.ownerUsername ?? '',
|
|
351
464
|
stagingDomain,
|
|
465
|
+
cloudflareAccountId: options.cloudflareAccountId ?? response.cloudflareAccountId ?? '',
|
|
352
466
|
};
|
|
353
467
|
if (options.clientSecret) {
|
|
354
468
|
config.githubClientSecret = options.clientSecret;
|
|
355
469
|
}
|
|
470
|
+
const cloudflareApiToken = options.cloudflareApiToken ?? response.cloudflareApiToken;
|
|
471
|
+
if (cloudflareApiToken) {
|
|
472
|
+
config.cloudflareApiToken = cloudflareApiToken;
|
|
473
|
+
}
|
|
356
474
|
return config;
|
|
357
475
|
}
|
|
358
476
|
function buildConfigFromOptions(options) {
|
|
@@ -365,6 +483,7 @@ function buildConfigFromOptions(options) {
|
|
|
365
483
|
r2BucketName: options.r2BucketName,
|
|
366
484
|
r2PublicUrl: options.r2PublicUrl,
|
|
367
485
|
ownerUsername: options.ownerUsername,
|
|
486
|
+
cloudflareAccountId: options.cloudflareAccountId,
|
|
368
487
|
});
|
|
369
488
|
if (!result.success) {
|
|
370
489
|
logError(result.error);
|
|
@@ -381,12 +500,13 @@ function buildConfigFromOptions(options) {
|
|
|
381
500
|
return {
|
|
382
501
|
...config,
|
|
383
502
|
githubClientSecret: options.clientSecret,
|
|
503
|
+
cloudflareApiToken: options.cloudflareApiToken,
|
|
384
504
|
};
|
|
385
505
|
}
|
|
386
506
|
function getTotalSteps(options) {
|
|
387
507
|
let steps = 3; // content structure (includes workflows), bundle, config files
|
|
388
508
|
if (!options.skipGithub)
|
|
389
|
-
steps +=
|
|
509
|
+
steps += 4; // create repo, clone, git identity, commit/push
|
|
390
510
|
if (options.terraform)
|
|
391
511
|
steps += 1;
|
|
392
512
|
else if (!options.skipCloudflare)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frelio set-domain - 公開後のドメイン / URL 変更(Issue #24)
|
|
3
|
+
*
|
|
4
|
+
* config.json を正本として本番 URL / プレビュー URL / R2 公開 URL を更新し、
|
|
5
|
+
* 派生ファイル(wrangler.toml ×2 等)を一括再生成する薄いオーケストレーター。
|
|
6
|
+
* core/domain の changeDomain を呼ぶだけで、新規ロジックは持たない。
|
|
7
|
+
*/
|
|
8
|
+
type SetDomainOptions = {
|
|
9
|
+
productionUrl?: string;
|
|
10
|
+
previewUrl?: string;
|
|
11
|
+
stagingDomain?: string;
|
|
12
|
+
r2PublicUrl?: string;
|
|
13
|
+
regenStagingHash?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function setDomainCommand(options: SetDomainOptions): Promise<void>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frelio set-domain - 公開後のドメイン / URL 変更(Issue #24)
|
|
3
|
+
*
|
|
4
|
+
* config.json を正本として本番 URL / プレビュー URL / R2 公開 URL を更新し、
|
|
5
|
+
* 派生ファイル(wrangler.toml ×2 等)を一括再生成する薄いオーケストレーター。
|
|
6
|
+
* core/domain の changeDomain を呼ぶだけで、新規ロジックは持たない。
|
|
7
|
+
*/
|
|
8
|
+
import prompts from 'prompts';
|
|
9
|
+
import { log, logSuccess, logError } from '../lib/shell.js';
|
|
10
|
+
import { validateProductionUrl, validateR2PublicUrl } from '../lib/validators.js';
|
|
11
|
+
import { readConfig } from '../core/config.js';
|
|
12
|
+
import { changeDomain } from '../core/domain.js';
|
|
13
|
+
export async function setDomainCommand(options) {
|
|
14
|
+
log('');
|
|
15
|
+
log('🌐 ドメイン / URL の変更');
|
|
16
|
+
log('');
|
|
17
|
+
const projectDir = process.cwd();
|
|
18
|
+
// config.json の存在チェック
|
|
19
|
+
const configResult = readConfig(projectDir);
|
|
20
|
+
if (!configResult.success) {
|
|
21
|
+
logError(configResult.error);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (!configResult.data.config) {
|
|
25
|
+
logError('admin/config.json が見つかりません。Frelio プロジェクトのルートで実行してください。');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const current = configResult.data.config;
|
|
29
|
+
const hasFlags = options.productionUrl !== undefined ||
|
|
30
|
+
options.previewUrl !== undefined ||
|
|
31
|
+
options.stagingDomain !== undefined ||
|
|
32
|
+
options.r2PublicUrl !== undefined;
|
|
33
|
+
// フラグ検証(指定されたもののみ)
|
|
34
|
+
if (options.productionUrl !== undefined) {
|
|
35
|
+
const c = validateProductionUrl(options.productionUrl);
|
|
36
|
+
if (c !== true) {
|
|
37
|
+
logError(`--production-url: ${c}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (options.r2PublicUrl !== undefined) {
|
|
42
|
+
const c = validateR2PublicUrl(options.r2PublicUrl);
|
|
43
|
+
if (c !== true) {
|
|
44
|
+
logError(`--r2-public-url: ${c}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const isInteractive = process.stdin.isTTY === true;
|
|
49
|
+
const change = {};
|
|
50
|
+
if (hasFlags) {
|
|
51
|
+
if (options.productionUrl !== undefined)
|
|
52
|
+
change.productionUrl = options.productionUrl;
|
|
53
|
+
if (options.previewUrl !== undefined)
|
|
54
|
+
change.previewUrl = options.previewUrl;
|
|
55
|
+
if (options.stagingDomain !== undefined)
|
|
56
|
+
change.stagingDomain = options.stagingDomain;
|
|
57
|
+
if (options.r2PublicUrl !== undefined)
|
|
58
|
+
change.r2PublicUrl = options.r2PublicUrl;
|
|
59
|
+
if (options.regenStagingHash)
|
|
60
|
+
change.regenStagingHash = true;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (!isInteractive) {
|
|
64
|
+
logError('非対話モードでは少なくとも 1 つの URL フラグが必要です。');
|
|
65
|
+
logError('使用例: frelio set-domain --production-url https://example.com --r2-public-url https://files.example.com');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
log(' 現在の値(空欄のままで変更なし):');
|
|
69
|
+
log(` 本番 URL : ${current.productionUrl || '(未設定)'}`);
|
|
70
|
+
log(` プレビュー URL: ${current.previewUrl || '(未設定)'}`);
|
|
71
|
+
log(` R2 公開 URL : ${current.r2PublicUrl || '(未設定)'}`);
|
|
72
|
+
log('');
|
|
73
|
+
const res = await prompts([
|
|
74
|
+
{
|
|
75
|
+
type: 'text',
|
|
76
|
+
name: 'productionUrl',
|
|
77
|
+
message: '新しい本番 URL(変更しない場合は空欄):',
|
|
78
|
+
initial: current.productionUrl || '',
|
|
79
|
+
validate: (v) => validateProductionUrl(v),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
name: 'r2PublicUrl',
|
|
84
|
+
message: '新しい R2 公開 URL(変更しない場合は空欄):',
|
|
85
|
+
initial: current.r2PublicUrl || '',
|
|
86
|
+
validate: (v) => validateR2PublicUrl(v),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: (_, values) => values.productionUrl && values.productionUrl !== current.productionUrl
|
|
90
|
+
? 'confirm'
|
|
91
|
+
: null,
|
|
92
|
+
name: 'regenStagingHash',
|
|
93
|
+
message: 'ステージングのハッシュを再生成しますか?(既定: 保持してドメインのみ追従)',
|
|
94
|
+
initial: false,
|
|
95
|
+
},
|
|
96
|
+
], { onCancel: () => process.exit(0) });
|
|
97
|
+
// 入力されたもの かつ 現在値と異なるもののみ変更対象にする
|
|
98
|
+
if (res.productionUrl && res.productionUrl !== current.productionUrl) {
|
|
99
|
+
change.productionUrl = res.productionUrl;
|
|
100
|
+
}
|
|
101
|
+
if (res.r2PublicUrl && res.r2PublicUrl !== current.r2PublicUrl) {
|
|
102
|
+
change.r2PublicUrl = res.r2PublicUrl;
|
|
103
|
+
}
|
|
104
|
+
if (res.regenStagingHash)
|
|
105
|
+
change.regenStagingHash = true;
|
|
106
|
+
}
|
|
107
|
+
if (Object.keys(change).length === 0) {
|
|
108
|
+
log('変更がありません。終了します。');
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
const result = changeDomain(projectDir, change);
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
logError(result.error);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const { config, regeneratedPaths } = result.data;
|
|
117
|
+
log('');
|
|
118
|
+
logSuccess('config.json と派生ファイルを再生成しました');
|
|
119
|
+
log('');
|
|
120
|
+
log(' 更新後の値:');
|
|
121
|
+
log(` 本番 URL : ${config.productionUrl || '(未設定)'}`);
|
|
122
|
+
log(` プレビュー URL: ${config.previewUrl || '(未設定)'}`);
|
|
123
|
+
log(` R2 公開 URL : ${config.r2PublicUrl || '(未設定)'}`);
|
|
124
|
+
log('');
|
|
125
|
+
log(' 再生成したファイル:');
|
|
126
|
+
for (const p of regeneratedPaths) {
|
|
127
|
+
log(` - ${p}`);
|
|
128
|
+
}
|
|
129
|
+
log('');
|
|
130
|
+
log(' 次の手動作業:');
|
|
131
|
+
log(' 1. 差分を確認して develop へ commit / push');
|
|
132
|
+
log(' (wrangler.toml [vars] はデプロイ時に自動適用される / Issue #23)');
|
|
133
|
+
log(' 2. Cloudflare Pages にカスタムドメインを追加(本番 + プレビュー)');
|
|
134
|
+
log(' 3. R2 公開バケットのカスタムドメインを設定(独自ドメイン化した場合)');
|
|
135
|
+
log(' 4. GitHub OAuth App の Homepage / Callback URL を新ドメインに更新');
|
|
136
|
+
log(' 5. file-upload Worker を別運用している場合は再デプロイ:');
|
|
137
|
+
log(' cd workers/file-upload && npx wrangler deploy');
|
|
138
|
+
log(' 6. 本番反映は CMS の「直接デプロイ」フローで実行');
|
|
139
|
+
log(' 7. 必要に応じて旧ドメインからのリダイレクトと Cloudflare Access を再設定');
|
|
140
|
+
log('');
|
|
141
|
+
}
|
|
@@ -23,4 +23,4 @@ export type CloudflareSetupResult = {
|
|
|
23
23
|
}>;
|
|
24
24
|
secrets: OperationResult[];
|
|
25
25
|
};
|
|
26
|
-
export declare function setupCloudflareResources(config: ProjectConfig, githubClientSecret?: string): OperationResult<CloudflareSetupResult>;
|
|
26
|
+
export declare function setupCloudflareResources(config: ProjectConfig, githubClientSecret?: string, cloudflareApiToken?: string): OperationResult<CloudflareSetupResult>;
|
package/dist/core/cloudflare.js
CHANGED
|
@@ -47,7 +47,7 @@ export function setPagesSecret(projectName, secretName, secretValue) {
|
|
|
47
47
|
return fail(`${secretName} の設定に失敗しました(${projectName}): ${e.message}`, 'EXEC_FAILED');
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
export function setupCloudflareResources(config, githubClientSecret) {
|
|
50
|
+
export function setupCloudflareResources(config, githubClientSecret, cloudflareApiToken) {
|
|
51
51
|
const r2Bucket = createR2Bucket(config.r2BucketName);
|
|
52
52
|
const contentPages = createPagesProject(config.pagesProjectName, 'main');
|
|
53
53
|
const adminPages = createPagesProject(config.adminPagesProjectName, 'admin');
|
|
@@ -56,5 +56,11 @@ export function setupCloudflareResources(config, githubClientSecret) {
|
|
|
56
56
|
secrets.push(setPagesSecret(config.pagesProjectName, 'GITHUB_CLIENT_SECRET', githubClientSecret));
|
|
57
57
|
secrets.push(setPagesSecret(config.pagesProjectName, 'GITHUB_CLIENT_ID', config.githubClientId));
|
|
58
58
|
}
|
|
59
|
+
// デプロイ管理機能(Issue #27)の Cloudflare API トークン。
|
|
60
|
+
// functions/api/deployments は管理画面 Pages プロジェクト(deploy-admin.yml の配信先)で
|
|
61
|
+
// 実行されるため、トークンは adminPagesProjectName のシークレットに設定する。
|
|
62
|
+
if (cloudflareApiToken) {
|
|
63
|
+
secrets.push(setPagesSecret(config.adminPagesProjectName, 'CLOUDFLARE_API_TOKEN', cloudflareApiToken));
|
|
64
|
+
}
|
|
59
65
|
return ok({ r2Bucket, contentPages, adminPages, secrets });
|
|
60
66
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -101,7 +101,7 @@ export function buildConfig(params) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
const repoName = params.contentRepo.split('/')[1];
|
|
104
|
-
const stagingDomain = params.stagingDomain ?? generateStagingDomain(params.productionUrl || ''
|
|
104
|
+
const stagingDomain = params.stagingDomain ?? generateStagingDomain(params.productionUrl || '');
|
|
105
105
|
const previewUrl = stagingDomain ? `https://${stagingDomain}` : '';
|
|
106
106
|
const config = {
|
|
107
107
|
contentRepo: params.contentRepo,
|
|
@@ -115,6 +115,7 @@ export function buildConfig(params) {
|
|
|
115
115
|
adminPagesProjectName: `${repoName}-admin`,
|
|
116
116
|
ownerUsername: params.ownerUsername || '',
|
|
117
117
|
stagingDomain,
|
|
118
|
+
cloudflareAccountId: params.cloudflareAccountId || '',
|
|
118
119
|
};
|
|
119
120
|
return ok({ config });
|
|
120
121
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/domain — ドメイン / URL 変更(Issue #24)
|
|
3
|
+
*
|
|
4
|
+
* 公開後に独自ドメインを取得した等で本番 URL が変わったとき、
|
|
5
|
+
* config.json を正本として URL 系設定を更新し、派生ファイル
|
|
6
|
+
* (wrangler.toml ×2 等)を一括再生成する。
|
|
7
|
+
*
|
|
8
|
+
* 重要: on-disk の config.json は generateConfigJson 形状(fileUploadUrl /
|
|
9
|
+
* allowedOrigins を持ち stagingDomain は持たない)のため、ステージングの
|
|
10
|
+
* 推測困難なハッシュは previewUrl のホスト名から復元する。書き込みは
|
|
11
|
+
* regenerateAllConfigFiles(= generateConfigJson)に一本化し、config.json と
|
|
12
|
+
* wrangler.toml [vars] の値が乖離しないようにする。
|
|
13
|
+
*/
|
|
14
|
+
import { type ProjectConfig, type OperationResult } from './types.js';
|
|
15
|
+
/** ドメイン / URL の変更指定(指定したフィールドのみ更新) */
|
|
16
|
+
export type DomainChange = {
|
|
17
|
+
productionUrl?: string;
|
|
18
|
+
/** 明示指定時は逐語優先(productionUrl からの自動追従より優先) */
|
|
19
|
+
previewUrl?: string;
|
|
20
|
+
/** 明示指定時は逐語優先 */
|
|
21
|
+
stagingDomain?: string;
|
|
22
|
+
r2PublicUrl?: string;
|
|
23
|
+
siteTitle?: string;
|
|
24
|
+
/** productionUrl 変更時にステージングのハッシュを保持せず再生成する */
|
|
25
|
+
regenStagingHash?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* config.json の URL 系設定を更新し、派生ファイルを一括再生成する。
|
|
29
|
+
*
|
|
30
|
+
* - productionUrl 変更時、明示指定が無ければ既存 previewUrl のホスト名から
|
|
31
|
+
* ステージングのハッシュラベルを保持しつつ新ホストへ追従する。
|
|
32
|
+
* - allowedOrigins は productionUrl / previewUrl からの派生値として
|
|
33
|
+
* regenerateAllConfigFiles 内で自動的に再計算される。
|
|
34
|
+
*/
|
|
35
|
+
export declare function changeDomain(projectDir: string, change: DomainChange): OperationResult<{
|
|
36
|
+
config: ProjectConfig;
|
|
37
|
+
regeneratedPaths: string[];
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/domain — ドメイン / URL 変更(Issue #24)
|
|
3
|
+
*
|
|
4
|
+
* 公開後に独自ドメインを取得した等で本番 URL が変わったとき、
|
|
5
|
+
* config.json を正本として URL 系設定を更新し、派生ファイル
|
|
6
|
+
* (wrangler.toml ×2 等)を一括再生成する。
|
|
7
|
+
*
|
|
8
|
+
* 重要: on-disk の config.json は generateConfigJson 形状(fileUploadUrl /
|
|
9
|
+
* allowedOrigins を持ち stagingDomain は持たない)のため、ステージングの
|
|
10
|
+
* 推測困難なハッシュは previewUrl のホスト名から復元する。書き込みは
|
|
11
|
+
* regenerateAllConfigFiles(= generateConfigJson)に一本化し、config.json と
|
|
12
|
+
* wrangler.toml [vars] の値が乖離しないようにする。
|
|
13
|
+
*/
|
|
14
|
+
import { ok, fail, } from './types.js';
|
|
15
|
+
import { readConfig, validateConfig } from './config.js';
|
|
16
|
+
import { regenerateAllConfigFiles } from './file-generators.js';
|
|
17
|
+
import { generateStagingDomain, rehostStagingDomain } from '../lib/templates.js';
|
|
18
|
+
function safeHostname(url) {
|
|
19
|
+
try {
|
|
20
|
+
return new URL(url).hostname;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* config.json の URL 系設定を更新し、派生ファイルを一括再生成する。
|
|
28
|
+
*
|
|
29
|
+
* - productionUrl 変更時、明示指定が無ければ既存 previewUrl のホスト名から
|
|
30
|
+
* ステージングのハッシュラベルを保持しつつ新ホストへ追従する。
|
|
31
|
+
* - allowedOrigins は productionUrl / previewUrl からの派生値として
|
|
32
|
+
* regenerateAllConfigFiles 内で自動的に再計算される。
|
|
33
|
+
*/
|
|
34
|
+
export function changeDomain(projectDir, change) {
|
|
35
|
+
const read = readConfig(projectDir);
|
|
36
|
+
if (!read.success)
|
|
37
|
+
return read;
|
|
38
|
+
if (!read.data.config) {
|
|
39
|
+
return fail('admin/config.json が見つかりません。Frelio プロジェクトのルートで実行してください。', 'NOT_FOUND');
|
|
40
|
+
}
|
|
41
|
+
const existing = read.data.config;
|
|
42
|
+
const existingStaging = existing.previewUrl
|
|
43
|
+
? safeHostname(existing.previewUrl)
|
|
44
|
+
: '';
|
|
45
|
+
const merged = {
|
|
46
|
+
...existing,
|
|
47
|
+
ownerUsername: existing.ownerUsername ?? '',
|
|
48
|
+
stagingDomain: existing.stagingDomain ?? existingStaging,
|
|
49
|
+
};
|
|
50
|
+
if (change.siteTitle !== undefined)
|
|
51
|
+
merged.siteTitle = change.siteTitle;
|
|
52
|
+
if (change.r2PublicUrl !== undefined)
|
|
53
|
+
merged.r2PublicUrl = change.r2PublicUrl;
|
|
54
|
+
if (change.productionUrl !== undefined) {
|
|
55
|
+
merged.productionUrl = change.productionUrl;
|
|
56
|
+
const newStaging = change.regenStagingHash
|
|
57
|
+
? generateStagingDomain(change.productionUrl)
|
|
58
|
+
: rehostStagingDomain(existingStaging, change.productionUrl);
|
|
59
|
+
merged.stagingDomain = newStaging;
|
|
60
|
+
merged.previewUrl = newStaging ? `https://${newStaging}` : '';
|
|
61
|
+
}
|
|
62
|
+
// 明示指定は自動追従より優先(逐語)
|
|
63
|
+
if (change.stagingDomain !== undefined) {
|
|
64
|
+
merged.stagingDomain = change.stagingDomain;
|
|
65
|
+
merged.previewUrl = change.stagingDomain
|
|
66
|
+
? `https://${change.stagingDomain}`
|
|
67
|
+
: '';
|
|
68
|
+
}
|
|
69
|
+
if (change.previewUrl !== undefined) {
|
|
70
|
+
merged.previewUrl = change.previewUrl;
|
|
71
|
+
merged.stagingDomain = change.previewUrl ? safeHostname(change.previewUrl) : '';
|
|
72
|
+
}
|
|
73
|
+
// バリデーション(失敗時は何も書かない)
|
|
74
|
+
const validation = validateConfig(merged);
|
|
75
|
+
if (!validation.success)
|
|
76
|
+
return validation;
|
|
77
|
+
if (validation.data.errors.length > 0) {
|
|
78
|
+
const msg = validation.data.errors
|
|
79
|
+
.map((e) => `${e.field}: ${e.message}`)
|
|
80
|
+
.join('; ');
|
|
81
|
+
return fail(msg, 'VALIDATION_ERROR');
|
|
82
|
+
}
|
|
83
|
+
const regen = regenerateAllConfigFiles(projectDir, merged);
|
|
84
|
+
if (!regen.success)
|
|
85
|
+
return regen;
|
|
86
|
+
return ok({ config: merged, regeneratedPaths: regen.data.paths });
|
|
87
|
+
}
|
|
@@ -6,6 +6,20 @@ export declare function initialCommitAndBranches(projectDir: string, contentRepo
|
|
|
6
6
|
branches: string[];
|
|
7
7
|
defaultBranch: string;
|
|
8
8
|
}>;
|
|
9
|
+
/**
|
|
10
|
+
* commit author に使われる git identity を取得する。
|
|
11
|
+
* projectDir 内で実行するため、git が commit 時に解決する値(local→global→system)と一致する。
|
|
12
|
+
* 未設定の項目は空文字を返す。
|
|
13
|
+
*/
|
|
14
|
+
export declare function getGitIdentity(projectDir: string): {
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* git identity をリポジトリのローカルスコープに設定する(--global は付けない)。
|
|
20
|
+
* ユーザーのグローバル設定を変更せず、当該リポジトリの commit author のみを確定させる。
|
|
21
|
+
*/
|
|
22
|
+
export declare function setGitIdentity(projectDir: string, name: string, email: string): OperationResult<void>;
|
|
9
23
|
export declare function createBranch(projectDir: string, branchName: string, baseBranch: string): OperationResult<{
|
|
10
24
|
alreadyExisted: boolean;
|
|
11
25
|
}>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* core/git-operations — Git コミット・ブランチ・プッシュ操作
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import { ok, fail } from './types.js';
|
|
5
|
+
import { ok, okVoid, fail } from './types.js';
|
|
6
6
|
import { exec } from '../lib/shell.js';
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Initial commit + branch creation
|
|
@@ -30,6 +30,45 @@ export function initialCommitAndBranches(projectDir, contentRepo) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
|
+
// Git identity (user.name / user.email)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* commit author に使われる git identity を取得する。
|
|
37
|
+
* projectDir 内で実行するため、git が commit 時に解決する値(local→global→system)と一致する。
|
|
38
|
+
* 未設定の項目は空文字を返す。
|
|
39
|
+
*/
|
|
40
|
+
export function getGitIdentity(projectDir) {
|
|
41
|
+
let name = '';
|
|
42
|
+
let email = '';
|
|
43
|
+
try {
|
|
44
|
+
name = exec('git config user.name', { cwd: projectDir, silent: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// 未設定 → 空文字
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
email = exec('git config user.email', { cwd: projectDir, silent: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// 未設定 → 空文字
|
|
54
|
+
}
|
|
55
|
+
return { name, email };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* git identity をリポジトリのローカルスコープに設定する(--global は付けない)。
|
|
59
|
+
* ユーザーのグローバル設定を変更せず、当該リポジトリの commit author のみを確定させる。
|
|
60
|
+
*/
|
|
61
|
+
export function setGitIdentity(projectDir, name, email) {
|
|
62
|
+
try {
|
|
63
|
+
exec(`git config user.name "${name}"`, { cwd: projectDir, silent: true });
|
|
64
|
+
exec(`git config user.email "${email}"`, { cwd: projectDir, silent: true });
|
|
65
|
+
return okVoid();
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return fail(`git identity 設定失敗: ${e.message}`, 'EXEC_FAILED');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
33
72
|
// Single branch creation
|
|
34
73
|
// ---------------------------------------------------------------------------
|
|
35
74
|
export function createBranch(projectDir, branchName, baseBranch) {
|
package/dist/core/types.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { initCommand } from './commands/init.js';
|
|
4
4
|
import { updateCommand } from './commands/update.js';
|
|
5
5
|
import { addStagingCommand } from './commands/add-staging.js';
|
|
6
|
+
import { setDomainCommand } from './commands/set-domain.js';
|
|
6
7
|
const program = new Command();
|
|
7
8
|
program
|
|
8
9
|
.name('frelio')
|
|
@@ -19,10 +20,12 @@ program
|
|
|
19
20
|
.option('--production-url <url>', 'Production URL')
|
|
20
21
|
.option('--staging-domain <domain>', 'Staging domain')
|
|
21
22
|
.option('--r2-bucket-name <name>', 'R2 bucket name')
|
|
22
|
-
.option('--r2-public-url <url>', 'R2
|
|
23
|
+
.option('--r2-public-url <url>', 'R2 公開 URL(アップロードファイルの配信元・例: https://files.example.com)')
|
|
23
24
|
.option('--owner-username <user>', 'Admin GitHub username')
|
|
24
25
|
.option('--client-id <id>', 'GitHub OAuth Client ID')
|
|
25
26
|
.option('--client-secret <secret>', 'GitHub OAuth Client Secret')
|
|
27
|
+
.option('--cloudflare-account-id <id>', 'Cloudflare Account ID(デプロイ管理機能 / Issue #27)')
|
|
28
|
+
.option('--cloudflare-api-token <token>', 'Cloudflare API Token(Pages:Edit・デプロイ一覧/削除に使用)')
|
|
26
29
|
.action(initCommand);
|
|
27
30
|
program
|
|
28
31
|
.command('update')
|
|
@@ -31,7 +34,16 @@ program
|
|
|
31
34
|
.action(updateCommand);
|
|
32
35
|
program
|
|
33
36
|
.command('add-staging')
|
|
34
|
-
.description('Add a staging environment (creates branch
|
|
37
|
+
.description('Add a staging environment (creates a staging-* branch; deployed via GitHub Actions wrangler --branch)')
|
|
35
38
|
.option('--name <name>', 'Staging name (creates staging-{name} branch)')
|
|
36
39
|
.action(addStagingCommand);
|
|
40
|
+
program
|
|
41
|
+
.command('set-domain')
|
|
42
|
+
.description('Change production / preview / R2 URLs after init and regenerate config files (config.json + wrangler.toml)')
|
|
43
|
+
.option('--production-url <url>', 'New production URL')
|
|
44
|
+
.option('--preview-url <url>', 'New preview URL (overrides derived staging domain)')
|
|
45
|
+
.option('--staging-domain <domain>', 'New staging domain (verbatim)')
|
|
46
|
+
.option('--r2-public-url <url>', 'New R2 public URL')
|
|
47
|
+
.option('--regen-staging-hash', 'Regenerate the staging subdomain hash instead of preserving it')
|
|
48
|
+
.action(setDomainCommand);
|
|
37
49
|
program.parse();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 作成した SaaS リソースへのリンク生成
|
|
3
|
+
*
|
|
4
|
+
* init 完了時に「どこを開けば作成物を確認・設定変更できるか」を案内するための
|
|
5
|
+
* 純粋関数群。URL 文字列を組み立てるだけで副作用は持たない。
|
|
6
|
+
*/
|
|
7
|
+
import type { ProjectConfig } from './templates.js';
|
|
8
|
+
export type ResourceLink = {
|
|
9
|
+
label: string;
|
|
10
|
+
url: string;
|
|
11
|
+
};
|
|
12
|
+
export type ResourceLinkSection = {
|
|
13
|
+
title: string;
|
|
14
|
+
links: ResourceLink[];
|
|
15
|
+
};
|
|
16
|
+
export type ResourceLinkOptions = {
|
|
17
|
+
/** GitHub リポジトリを作成したか(--skip-github で false) */
|
|
18
|
+
github: boolean;
|
|
19
|
+
/** Cloudflare リソースを wrangler で作成したか(--skip-cloudflare / --terraform で false) */
|
|
20
|
+
cloudflare: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 作成済みリソースへのリンクをセクションごとに組み立てる。
|
|
24
|
+
* 作成していないリソース(skip 指定)はセクションごと省く。
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildResourceLinks(config: ProjectConfig, options: ResourceLinkOptions): ResourceLinkSection[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 作成した SaaS リソースへのリンク生成
|
|
3
|
+
*
|
|
4
|
+
* init 完了時に「どこを開けば作成物を確認・設定変更できるか」を案内するための
|
|
5
|
+
* 純粋関数群。URL 文字列を組み立てるだけで副作用は持たない。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Cloudflare ダッシュボードのディープリンク。
|
|
9
|
+
* `:account` はログイン中のアカウントに dash 側で解決されるため、
|
|
10
|
+
* Account ID を CLI 側で取得しなくてもリンクが機能する。
|
|
11
|
+
*/
|
|
12
|
+
function cloudflareDashUrl(path) {
|
|
13
|
+
return `https://dash.cloudflare.com/?to=/:account${path}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 作成済みリソースへのリンクをセクションごとに組み立てる。
|
|
17
|
+
* 作成していないリソース(skip 指定)はセクションごと省く。
|
|
18
|
+
*/
|
|
19
|
+
export function buildResourceLinks(config, options) {
|
|
20
|
+
const sections = [];
|
|
21
|
+
if (options.github) {
|
|
22
|
+
sections.push({
|
|
23
|
+
title: 'GitHub',
|
|
24
|
+
links: [
|
|
25
|
+
{ label: 'リポジトリ', url: `https://github.com/${config.contentRepo}` },
|
|
26
|
+
{ label: 'OAuth App 設定', url: 'https://github.com/settings/developers' },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (options.cloudflare) {
|
|
31
|
+
sections.push({
|
|
32
|
+
title: 'Cloudflare',
|
|
33
|
+
links: [
|
|
34
|
+
{
|
|
35
|
+
label: `コンテンツ配信 Pages (${config.pagesProjectName})`,
|
|
36
|
+
url: cloudflareDashUrl(`/pages/view/${config.pagesProjectName}`),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: `管理画面 Pages (${config.adminPagesProjectName})`,
|
|
40
|
+
url: cloudflareDashUrl(`/pages/view/${config.adminPagesProjectName}`),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: `R2 バケット (${config.r2BucketName})`,
|
|
44
|
+
url: cloudflareDashUrl(`/r2/default/buckets/${config.r2BucketName}`),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return sections;
|
|
50
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* {{variable}} 形式のプレースホルダーを値に置換する。
|
|
5
5
|
* GitHub Actions の ${{ expression }} とは衝突しない。
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type ProjectConfig } from './templates.js';
|
|
8
8
|
export type TemplateVariables = Record<string, string>;
|
|
9
9
|
/**
|
|
10
10
|
* テンプレート文字列内の {{variable}} を置換する
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* {{variable}} 形式のプレースホルダーを値に置換する。
|
|
5
5
|
* GitHub Actions の ${{ expression }} とは衝突しない。
|
|
6
6
|
*/
|
|
7
|
+
import { buildAllowedOrigins } from './templates.js';
|
|
7
8
|
/**
|
|
8
9
|
* テンプレート文字列内の {{variable}} を置換する
|
|
9
10
|
*/
|
|
@@ -28,5 +29,7 @@ export function projectConfigToVars(config) {
|
|
|
28
29
|
r2BucketName: config.r2BucketName,
|
|
29
30
|
r2PublicUrl: config.r2PublicUrl,
|
|
30
31
|
ownerUsername: config.ownerUsername,
|
|
32
|
+
allowedOrigins: buildAllowedOrigins(config),
|
|
33
|
+
cloudflareAccountId: config.cloudflareAccountId ?? '',
|
|
31
34
|
};
|
|
32
35
|
}
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -13,17 +13,51 @@ export type ProjectConfig = {
|
|
|
13
13
|
adminPagesProjectName: string;
|
|
14
14
|
ownerUsername: string;
|
|
15
15
|
stagingDomain: string;
|
|
16
|
+
cloudflareAccountId: string;
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
18
19
|
* ランダムな8文字のハッシュを生成(URL推測を困難にする)
|
|
19
20
|
*/
|
|
20
21
|
export declare function generateHash(): string;
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
-
* 例: example.com → staging-a3f9c2kp.example.com
|
|
23
|
+
* productionUrl からステージング用の推測困難なカスタムサブドメインを生成する。
|
|
24
|
+
* 例: https://example.com → staging-a3f9c2kp.example.com
|
|
25
|
+
*
|
|
26
|
+
* productionUrl が無い/無効な場合は空文字を返す(カスタムドメイン無し)。
|
|
27
|
+
* その場合プレビューは GitHub Actions の wrangler デプロイが生成する
|
|
28
|
+
* ブランチエイリアス `staging.<project>.pages.dev` で運用する。
|
|
29
|
+
* `*.pages.dev` の任意サブドメインは実在しないため、ここでは生成しない。
|
|
24
30
|
*/
|
|
25
|
-
export declare function generateStagingDomain(productionUrl: string
|
|
31
|
+
export declare function generateStagingDomain(productionUrl: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* 既存の staging サブドメインのハッシュ部分(`staging-<hash>` ラベル)を保持したまま、
|
|
34
|
+
* ホスト名だけ新しい productionUrl のホストへ差し替える。
|
|
35
|
+
* ドメイン移行時にプレビュー URL の推測困難なハッシュを変えずにドメインだけ追従させる。
|
|
36
|
+
*
|
|
37
|
+
* 既存 stagingDomain が空(init 時に本番 URL 未設定だった等)の場合は新規生成する。
|
|
38
|
+
* 新 productionUrl が無効なら空文字を返す(generateStagingDomain と同じ規約)。
|
|
39
|
+
*/
|
|
40
|
+
export declare function rehostStagingDomain(existingStagingDomain: string, newProductionUrl: string): string;
|
|
26
41
|
export declare function generateConfigJson(config: ProjectConfig): string;
|
|
42
|
+
/**
|
|
43
|
+
* CORS 許可オリジンの一覧を生成する(ローカル開発 + 本番/プレビュー)。
|
|
44
|
+
* 管理画面 functions/api と file-upload Worker の双方で使用する。
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildAllowedOrigins(config: ProjectConfig): string;
|
|
47
|
+
/**
|
|
48
|
+
* ルートの wrangler.toml(Cloudflare Pages 設定)を生成する。
|
|
49
|
+
*
|
|
50
|
+
* Issue #23: `pages_build_output_dir` が無いと `wrangler pages deploy` が
|
|
51
|
+
* 設定ファイルを丸ごと無視するため、R2 バインディング・環境変数がデプロイに
|
|
52
|
+
* 反映されず、ダッシュボードでの手動設定が必須になっていた。
|
|
53
|
+
*
|
|
54
|
+
* このファイルはコンテンツ配信・管理画面の両 Pages デプロイで共用する:
|
|
55
|
+
* - コンテンツ配信: `wrangler pages deploy public`(pages_build_output_dir と一致)
|
|
56
|
+
* - 管理画面: `wrangler pages deploy .admin-deploy`(コマンドライン引数が優先)
|
|
57
|
+
* デプロイ先ディレクトリと project 名はコマンドライン引数(`--project-name`)が
|
|
58
|
+
* 優先されるため、1 ファイルで両デプロイに R2 バインディング・環境変数を適用できる。
|
|
59
|
+
* いずれのデプロイ対象ディレクトリにも本ファイルは含まれない(公開されない)。
|
|
60
|
+
*/
|
|
27
61
|
export declare function generateWranglerToml(config: ProjectConfig): string;
|
|
28
62
|
export declare function generateWorkerWranglerToml(config: ProjectConfig): string;
|
|
29
63
|
export declare function generateRedirects(): string;
|
package/dist/lib/templates.js
CHANGED
|
@@ -17,19 +17,45 @@ export function generateHash() {
|
|
|
17
17
|
return hash;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
* 例: example.com → staging-a3f9c2kp.example.com
|
|
20
|
+
* productionUrl からステージング用の推測困難なカスタムサブドメインを生成する。
|
|
21
|
+
* 例: https://example.com → staging-a3f9c2kp.example.com
|
|
22
|
+
*
|
|
23
|
+
* productionUrl が無い/無効な場合は空文字を返す(カスタムドメイン無し)。
|
|
24
|
+
* その場合プレビューは GitHub Actions の wrangler デプロイが生成する
|
|
25
|
+
* ブランチエイリアス `staging.<project>.pages.dev` で運用する。
|
|
26
|
+
* `*.pages.dev` の任意サブドメインは実在しないため、ここでは生成しない。
|
|
22
27
|
*/
|
|
23
|
-
export function generateStagingDomain(productionUrl
|
|
24
|
-
const hash = generateHash();
|
|
28
|
+
export function generateStagingDomain(productionUrl) {
|
|
25
29
|
try {
|
|
26
30
|
const url = new URL(productionUrl);
|
|
27
|
-
return `staging-${
|
|
31
|
+
return `staging-${generateHash()}.${url.hostname}`;
|
|
28
32
|
}
|
|
29
33
|
catch {
|
|
30
|
-
return
|
|
34
|
+
return '';
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* 既存の staging サブドメインのハッシュ部分(`staging-<hash>` ラベル)を保持したまま、
|
|
39
|
+
* ホスト名だけ新しい productionUrl のホストへ差し替える。
|
|
40
|
+
* ドメイン移行時にプレビュー URL の推測困難なハッシュを変えずにドメインだけ追従させる。
|
|
41
|
+
*
|
|
42
|
+
* 既存 stagingDomain が空(init 時に本番 URL 未設定だった等)の場合は新規生成する。
|
|
43
|
+
* 新 productionUrl が無効なら空文字を返す(generateStagingDomain と同じ規約)。
|
|
44
|
+
*/
|
|
45
|
+
export function rehostStagingDomain(existingStagingDomain, newProductionUrl) {
|
|
46
|
+
if (!existingStagingDomain) {
|
|
47
|
+
return generateStagingDomain(newProductionUrl);
|
|
48
|
+
}
|
|
49
|
+
let hostname;
|
|
50
|
+
try {
|
|
51
|
+
hostname = new URL(newProductionUrl).hostname;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
const label = existingStagingDomain.split('.')[0]; // staging-<hash>
|
|
57
|
+
return `${label}.${hostname}`;
|
|
58
|
+
}
|
|
33
59
|
export function generateConfigJson(config) {
|
|
34
60
|
return JSON.stringify({
|
|
35
61
|
contentRepo: config.contentRepo,
|
|
@@ -42,27 +68,65 @@ export function generateConfigJson(config) {
|
|
|
42
68
|
adminPagesProjectName: config.adminPagesProjectName,
|
|
43
69
|
r2BucketName: config.r2BucketName,
|
|
44
70
|
r2PublicUrl: config.r2PublicUrl,
|
|
71
|
+
// allowedOrigins は productionUrl / previewUrl からの派生値(単一ソース / Issue #24)。
|
|
72
|
+
// wrangler.toml [vars] の ALLOWED_ORIGINS と同じ buildAllowedOrigins で導出し乖離を防ぐ。
|
|
73
|
+
allowedOrigins: buildAllowedOrigins(config),
|
|
74
|
+
// デプロイ管理機能(Issue #27)の表示用。account_id は非シークレットのため config.json に保持する。
|
|
75
|
+
cloudflareAccountId: config.cloudflareAccountId ?? '',
|
|
45
76
|
}, null, 2);
|
|
46
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* CORS 許可オリジンの一覧を生成する(ローカル開発 + 本番/プレビュー)。
|
|
80
|
+
* 管理画面 functions/api と file-upload Worker の双方で使用する。
|
|
81
|
+
*/
|
|
82
|
+
export function buildAllowedOrigins(config) {
|
|
83
|
+
return [
|
|
84
|
+
'http://localhost:5173',
|
|
85
|
+
'http://localhost:5174',
|
|
86
|
+
config.productionUrl,
|
|
87
|
+
config.previewUrl,
|
|
88
|
+
].filter(Boolean).join(',');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* ルートの wrangler.toml(Cloudflare Pages 設定)を生成する。
|
|
92
|
+
*
|
|
93
|
+
* Issue #23: `pages_build_output_dir` が無いと `wrangler pages deploy` が
|
|
94
|
+
* 設定ファイルを丸ごと無視するため、R2 バインディング・環境変数がデプロイに
|
|
95
|
+
* 反映されず、ダッシュボードでの手動設定が必須になっていた。
|
|
96
|
+
*
|
|
97
|
+
* このファイルはコンテンツ配信・管理画面の両 Pages デプロイで共用する:
|
|
98
|
+
* - コンテンツ配信: `wrangler pages deploy public`(pages_build_output_dir と一致)
|
|
99
|
+
* - 管理画面: `wrangler pages deploy .admin-deploy`(コマンドライン引数が優先)
|
|
100
|
+
* デプロイ先ディレクトリと project 名はコマンドライン引数(`--project-name`)が
|
|
101
|
+
* 優先されるため、1 ファイルで両デプロイに R2 バインディング・環境変数を適用できる。
|
|
102
|
+
* いずれのデプロイ対象ディレクトリにも本ファイルは含まれない(公開されない)。
|
|
103
|
+
*/
|
|
47
104
|
export function generateWranglerToml(config) {
|
|
48
|
-
return
|
|
105
|
+
return `# Cloudflare Pages 設定(コンテンツ配信・管理画面デプロイで共用 / Issue #23)
|
|
106
|
+
# pages_build_output_dir を指定することで wrangler pages deploy が本ファイルを
|
|
107
|
+
# 読み込み、R2 バインディングと環境変数をデプロイ時に自動適用する。
|
|
108
|
+
# デプロイ先ディレクトリ・project 名はコマンドライン引数が優先される。
|
|
109
|
+
name = "${config.pagesProjectName}"
|
|
110
|
+
pages_build_output_dir = "public"
|
|
49
111
|
compatibility_date = "2024-01-01"
|
|
50
112
|
|
|
51
113
|
[[r2_buckets]]
|
|
52
114
|
binding = "R2"
|
|
53
115
|
bucket_name = "${config.r2BucketName}"
|
|
54
116
|
|
|
117
|
+
# 環境変数(管理画面 functions/api が参照。コンテンツ配信側では未使用)
|
|
55
118
|
[vars]
|
|
56
119
|
R2_PUBLIC_URL = "${config.r2PublicUrl}"
|
|
120
|
+
ALLOWED_ORIGINS = "${buildAllowedOrigins(config)}"
|
|
121
|
+
CONTENT_REPO = "${config.contentRepo}"
|
|
122
|
+
# デプロイ管理機能(Issue #27): functions/api/deployments が Cloudflare Pages API を呼ぶのに使用。
|
|
123
|
+
# CLOUDFLARE_API_TOKEN はシークレットのため wrangler pages secret で管理画面プロジェクトに設定する(ここには置かない)。
|
|
124
|
+
CLOUDFLARE_ACCOUNT_ID = "${config.cloudflareAccountId ?? ''}"
|
|
125
|
+
PAGES_PROJECT_NAME = "${config.pagesProjectName}"
|
|
57
126
|
`;
|
|
58
127
|
}
|
|
59
128
|
export function generateWorkerWranglerToml(config) {
|
|
60
|
-
const allowedOrigins =
|
|
61
|
-
'http://localhost:5173',
|
|
62
|
-
'http://localhost:5174',
|
|
63
|
-
config.productionUrl,
|
|
64
|
-
config.previewUrl,
|
|
65
|
-
].filter(Boolean).join(',');
|
|
129
|
+
const allowedOrigins = buildAllowedOrigins(config);
|
|
66
130
|
return `name = "frelio-file-upload"
|
|
67
131
|
main = "src/index.ts"
|
|
68
132
|
compatibility_date = "2024-02-08"
|
|
@@ -223,7 +287,7 @@ variable "admin_domain" {
|
|
|
223
287
|
variable "staging_domain" {
|
|
224
288
|
type = string
|
|
225
289
|
default = ""
|
|
226
|
-
description = "
|
|
290
|
+
description = "ステージングカスタムドメイン(空ならブランチエイリアス staging.<project>.pages.dev で運用)"
|
|
227
291
|
}
|
|
228
292
|
`;
|
|
229
293
|
}
|
|
@@ -255,9 +319,13 @@ resource "cloudflare_pages_project" "admin" {
|
|
|
255
319
|
secrets = {
|
|
256
320
|
GITHUB_CLIENT_SECRET = var.github_client_secret
|
|
257
321
|
GITHUB_CLIENT_ID = var.github_client_id
|
|
322
|
+
# デプロイ管理機能(Issue #27): functions/api/deployments が Cloudflare Pages API を呼ぶのに使用
|
|
323
|
+
CLOUDFLARE_API_TOKEN = var.cloudflare_api_token
|
|
258
324
|
}
|
|
259
325
|
environment_variables = {
|
|
260
|
-
R2_PUBLIC_URL
|
|
326
|
+
R2_PUBLIC_URL = var.r2_public_url
|
|
327
|
+
CLOUDFLARE_ACCOUNT_ID = var.cloudflare_account_id
|
|
328
|
+
PAGES_PROJECT_NAME = var.pages_project_name
|
|
261
329
|
}
|
|
262
330
|
}
|
|
263
331
|
}
|
package/dist/lib/validators.d.ts
CHANGED
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function validateContentRepo(v: string): true | string;
|
|
6
6
|
export declare function validateR2PublicUrl(v: string): true | string;
|
|
7
|
+
export declare function validateProductionUrl(v: string): true | string;
|
|
7
8
|
export declare function validateStagingName(v: string): true | string;
|
|
8
9
|
export declare function validateRequired(v: string, label: string): true | string;
|
package/dist/lib/validators.js
CHANGED
|
@@ -6,7 +6,20 @@ export function validateContentRepo(v) {
|
|
|
6
6
|
return v.includes('/') || 'owner/repo 形式で入力してください';
|
|
7
7
|
}
|
|
8
8
|
export function validateR2PublicUrl(v) {
|
|
9
|
-
return v === '' ||
|
|
9
|
+
return (v === '' ||
|
|
10
|
+
v.startsWith('https://') ||
|
|
11
|
+
'https:// で始まる URL を入力してください(例: https://files.example.com)。\n空欄のままにすると後で設定できます。');
|
|
12
|
+
}
|
|
13
|
+
export function validateProductionUrl(v) {
|
|
14
|
+
if (v === '')
|
|
15
|
+
return true;
|
|
16
|
+
try {
|
|
17
|
+
new URL(v);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '有効な URL を入力してください(例: https://example.com)';
|
|
22
|
+
}
|
|
10
23
|
}
|
|
11
24
|
export function validateStagingName(v) {
|
|
12
25
|
if (!v)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-time/frelio-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "Frelio CMS setup CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc",
|
|
21
21
|
"dev": "tsc --watch",
|
|
22
|
+
"test": "vitest run",
|
|
22
23
|
"prepublishOnly": "npm run build"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/prompts": "^2.4.9",
|
|
31
32
|
"@types/node": "^22.0.0",
|
|
32
|
-
"typescript": "^5.7.0"
|
|
33
|
+
"typescript": "^5.7.0",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
33
35
|
}
|
|
34
36
|
}
|