@c-time/frelio-cli 0.1.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.
@@ -0,0 +1,532 @@
1
+ /**
2
+ * frelio init - 新規プロジェクトの対話式セットアップ
3
+ */
4
+ import prompts from 'prompts';
5
+ import fs from 'node:fs';
6
+ 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 } from '../lib/github-release.js';
10
+ import { generateInitialContent } from '../lib/initial-content.js';
11
+ import { generateConfigJson, generateWranglerToml, generateUsersIndex, generateVersionJson, generateRedirects, generateRoutesJson, generateStorageFunction, generateViteConfig, generatePackageJson, generateTsConfig, generateStagingDomain, writeFile, ensureDir, } from '../lib/templates.js';
12
+ export async function initCommand(options) {
13
+ log('');
14
+ log('🚀 Frelio CMS プロジェクトセットアップ');
15
+ log('');
16
+ // 前提チェック
17
+ log('🔍 前提チェック...');
18
+ const checks = checkPrerequisites(options);
19
+ if (!checks.ok) {
20
+ process.exit(1);
21
+ }
22
+ log('');
23
+ // 対話式プロンプト
24
+ log('📝 プロジェクト設定:');
25
+ const config = await promptConfig(options);
26
+ if (!config) {
27
+ log('セットアップをキャンセルしました。');
28
+ process.exit(0);
29
+ }
30
+ log('');
31
+ // OAuth App 案内
32
+ log('🔑 GitHub OAuth App:');
33
+ if (!config.githubClientId) {
34
+ log(' ⚠ OAuth App は GitHub の Web UI で作成が必要です。');
35
+ log(' → https://github.com/settings/developers');
36
+ log(' → New OAuth App:');
37
+ log(` - Application name: ${config.siteTitle || config.pagesProjectName} CMS`);
38
+ log(` - Homepage URL: https://${config.pagesProjectName}.pages.dev`);
39
+ log(` - Callback URL: https://${config.pagesProjectName}.pages.dev/api/auth/callback`);
40
+ log('');
41
+ const oauthResponse = await prompts([
42
+ {
43
+ type: 'text',
44
+ name: 'clientId',
45
+ message: 'OAuth Client ID を入力:',
46
+ validate: (v) => v.length > 0 || 'Client ID は必須です',
47
+ },
48
+ {
49
+ type: 'password',
50
+ name: 'clientSecret',
51
+ message: 'OAuth Client Secret を入力:',
52
+ validate: (v) => v.length > 0 || 'Client Secret は必須です',
53
+ },
54
+ ]);
55
+ if (!oauthResponse.clientId) {
56
+ log('セットアップをキャンセルしました。');
57
+ process.exit(0);
58
+ }
59
+ config.githubClientId = oauthResponse.clientId;
60
+ config.githubClientSecret =
61
+ oauthResponse.clientSecret;
62
+ }
63
+ log('');
64
+ // セットアップ実行
65
+ const totalSteps = getTotalSteps(options);
66
+ let step = 0;
67
+ // GitHub リポジトリ作成
68
+ if (!options.skipGithub) {
69
+ step++;
70
+ logStep(step, totalSteps, 'GitHub リポジトリ作成...');
71
+ try {
72
+ exec(`gh repo create ${config.contentRepo} --private --confirm`, { silent: true });
73
+ logSuccess('リポジトリ作成完了');
74
+ }
75
+ catch (error) {
76
+ const msg = error.message;
77
+ if (msg.includes('already exists')) {
78
+ logSuccess('リポジトリは既に存在します');
79
+ }
80
+ else {
81
+ logError(`リポジトリ作成失敗: ${msg}`);
82
+ process.exit(1);
83
+ }
84
+ }
85
+ }
86
+ // 作業ディレクトリ作成
87
+ const repoName = config.contentRepo.split('/')[1];
88
+ const projectDir = path.resolve(process.cwd(), repoName);
89
+ if (!options.skipGithub) {
90
+ step++;
91
+ logStep(step, totalSteps, 'リポジトリをクローン...');
92
+ try {
93
+ exec(`gh repo clone ${config.contentRepo} ${repoName}`, { silent: true });
94
+ logSuccess('クローン完了');
95
+ }
96
+ catch (error) {
97
+ if (fs.existsSync(projectDir)) {
98
+ logSuccess('ディレクトリは既に存在します');
99
+ }
100
+ else {
101
+ logError(`クローン失敗: ${error.message}`);
102
+ process.exit(1);
103
+ }
104
+ }
105
+ }
106
+ else {
107
+ if (!fs.existsSync(projectDir)) {
108
+ fs.mkdirSync(projectDir, { recursive: true });
109
+ }
110
+ }
111
+ // コンテンツリポジトリ初期構造
112
+ step++;
113
+ logStep(step, totalSteps, 'コンテンツリポジトリ初期構造作成...');
114
+ createContentStructure(projectDir, config);
115
+ generateInitialContent(projectDir);
116
+ logSuccess('初期構造作成完了(デモサイトテンプレート付き)');
117
+ // GitHub Actions ワークフロー
118
+ step++;
119
+ logStep(step, totalSteps, 'GitHub Actions ワークフロー配置...');
120
+ copyWorkflows(projectDir);
121
+ logSuccess('ワークフロー配置完了');
122
+ // CMS Admin バンドル展開
123
+ step++;
124
+ logStep(step, totalSteps, 'CMS Admin バンドルをダウンロード・展開...');
125
+ try {
126
+ await extractBundle(projectDir);
127
+ logSuccess('バンドル展開完了');
128
+ }
129
+ catch (error) {
130
+ logError(`バンドル展開失敗: ${error.message}`);
131
+ log(' ℹ リリースが公開されていない場合は、手動でバンドルを展開してください。');
132
+ log(' scripts/build-distributable.sh を実行してから dist-release/ の内容をコピーします。');
133
+ }
134
+ // 設定ファイル生成
135
+ step++;
136
+ logStep(step, totalSteps, '設定ファイル生成...');
137
+ writeFile(path.join(projectDir, 'admin', 'config.json'), generateConfigJson(config));
138
+ writeFile(path.join(projectDir, 'wrangler.toml'), generateWranglerToml(config));
139
+ writeFile(path.join(projectDir, '_redirects'), generateRedirects());
140
+ writeFile(path.join(projectDir, '_routes.json'), generateRoutesJson());
141
+ // staging Pages 用(build output: public/)
142
+ writeFile(path.join(projectDir, 'public', '_routes.json'), JSON.stringify({ version: 1, include: ['/storage/*'], exclude: [] }, null, 2));
143
+ logSuccess('設定ファイル生成完了');
144
+ // Cloudflare セットアップ
145
+ if (!options.skipCloudflare) {
146
+ step++;
147
+ logStep(step, totalSteps, 'Cloudflare セットアップ...');
148
+ try {
149
+ exec(`wrangler r2 bucket create ${config.r2BucketName}`, { silent: true });
150
+ logSuccess(`R2 バケット "${config.r2BucketName}" 作成完了`);
151
+ }
152
+ catch (error) {
153
+ const msg = error.message;
154
+ if (msg.includes('already exists')) {
155
+ logSuccess(`R2 バケット "${config.r2BucketName}" は既に存在します`);
156
+ }
157
+ else {
158
+ logError(`R2 バケット作成失敗: ${msg}`);
159
+ }
160
+ }
161
+ try {
162
+ exec(`wrangler pages project create ${config.pagesProjectName} --production-branch main`, {
163
+ silent: true,
164
+ });
165
+ logSuccess(`Pages プロジェクト "${config.pagesProjectName}" 作成完了`);
166
+ }
167
+ catch (error) {
168
+ const msg = error.message;
169
+ if (msg.includes('already exists') || msg.includes('A project with this name already exists')) {
170
+ logSuccess(`Pages プロジェクト "${config.pagesProjectName}" は既に存在します`);
171
+ }
172
+ else {
173
+ logError(`Pages プロジェクト作成失敗: ${msg}`);
174
+ }
175
+ }
176
+ // ステージング用 Pages プロジェクト作成
177
+ const stagingProjectName = `${config.pagesProjectName}-staging`;
178
+ try {
179
+ exec(`wrangler pages project create ${stagingProjectName} --production-branch staging`, {
180
+ silent: true,
181
+ });
182
+ logSuccess(`Pages プロジェクト "${stagingProjectName}"(ステージング)作成完了`);
183
+ }
184
+ catch (error) {
185
+ const msg = error.message;
186
+ if (msg.includes('already exists') || msg.includes('A project with this name already exists')) {
187
+ logSuccess(`Pages プロジェクト "${stagingProjectName}" は既に存在します`);
188
+ }
189
+ else {
190
+ logError(`ステージング Pages プロジェクト作成失敗: ${msg}`);
191
+ }
192
+ }
193
+ // シークレット設定
194
+ const secret = config.githubClientSecret;
195
+ if (secret) {
196
+ try {
197
+ exec(`echo "${secret}" | wrangler pages secret put GITHUB_CLIENT_SECRET --project-name ${config.pagesProjectName}`, { silent: true });
198
+ logSuccess('GITHUB_CLIENT_SECRET 設定完了');
199
+ }
200
+ catch {
201
+ logError('GITHUB_CLIENT_SECRET の設定に失敗しました。CF ダッシュボードから手動設定してください。');
202
+ }
203
+ try {
204
+ exec(`echo "${config.githubClientId}" | wrangler pages secret put GITHUB_CLIENT_ID --project-name ${config.pagesProjectName}`, { silent: true });
205
+ logSuccess('GITHUB_CLIENT_ID 設定完了');
206
+ }
207
+ catch {
208
+ logError('GITHUB_CLIENT_ID の設定に失敗しました。CF ダッシュボードから手動設定してください。');
209
+ }
210
+ }
211
+ }
212
+ // ブランチ構造とプッシュ
213
+ if (!options.skipGithub) {
214
+ step++;
215
+ logStep(step, totalSteps, '初回コミット & ブランチ作成...');
216
+ try {
217
+ exec('git add -A', { cwd: projectDir, silent: true });
218
+ exec('git commit -m "Initial Frelio CMS setup"', { cwd: projectDir, silent: true });
219
+ // develop, staging ブランチ作成
220
+ exec('git branch develop', { cwd: projectDir, silent: true });
221
+ exec('git branch staging', { cwd: projectDir, silent: true });
222
+ // プッシュ
223
+ exec('git push -u origin main', { cwd: projectDir, silent: true });
224
+ exec('git push -u origin develop', { cwd: projectDir, silent: true });
225
+ exec('git push -u origin staging', { cwd: projectDir, silent: true });
226
+ // デフォルトブランチを develop に変更
227
+ exec(`gh repo edit ${config.contentRepo} --default-branch develop`, { silent: true });
228
+ logSuccess('ブランチ作成・プッシュ完了');
229
+ }
230
+ catch (error) {
231
+ logError(`Git 操作失敗: ${error.message}`);
232
+ }
233
+ }
234
+ // 完了
235
+ log('');
236
+ log('✅ セットアップ完了!');
237
+ log('');
238
+ log(` 管理画面: https://${config.pagesProjectName}.pages.dev/admin/`);
239
+ log('');
240
+ log(' 残りの手動作業:');
241
+ log(` 1. 本番 Pages(${config.pagesProjectName})にリポジトリを接続`);
242
+ log(` 2. ステージング Pages(${config.pagesProjectName}-staging)にリポジトリを接続`);
243
+ if (config.stagingDomain) {
244
+ log(` 3. ステージング Pages にカスタムドメインを設定: ${config.stagingDomain}`);
245
+ }
246
+ log(` ${config.stagingDomain ? '4' : '3'}. ステージングのアクセス制限を設定(Cloudflare Access 推奨)`);
247
+ log('');
248
+ }
249
+ function checkPrerequisites(options) {
250
+ let ok = true;
251
+ if (!options.skipGithub) {
252
+ if (commandExists('gh')) {
253
+ logSuccess('gh CLI (GitHub)');
254
+ try {
255
+ exec('gh auth status', { silent: true });
256
+ logSuccess('gh auth status (ログイン済み)');
257
+ }
258
+ catch {
259
+ logError('gh にログインしていません。`gh auth login` を実行してください。');
260
+ ok = false;
261
+ }
262
+ }
263
+ else {
264
+ logError('gh CLI が見つかりません。https://cli.github.com/ からインストールしてください。');
265
+ ok = false;
266
+ }
267
+ }
268
+ if (!options.skipCloudflare) {
269
+ if (commandExists('wrangler')) {
270
+ logSuccess('wrangler CLI (Cloudflare)');
271
+ try {
272
+ exec('wrangler whoami', { silent: true });
273
+ logSuccess('wrangler whoami (ログイン済み)');
274
+ }
275
+ catch {
276
+ logError('wrangler にログインしていません。`wrangler login` を実行してください。');
277
+ ok = false;
278
+ }
279
+ }
280
+ else {
281
+ logError('wrangler CLI が見つかりません。`npm i -g wrangler` でインストールしてください。');
282
+ ok = false;
283
+ }
284
+ }
285
+ if (commandExists('git')) {
286
+ logSuccess('git');
287
+ }
288
+ else {
289
+ logError('git が見つかりません。');
290
+ ok = false;
291
+ }
292
+ return { ok };
293
+ }
294
+ async function promptConfig(options) {
295
+ const response = await prompts([
296
+ {
297
+ type: 'text',
298
+ name: 'contentRepo',
299
+ message: 'リポジトリ名 (owner/repo):',
300
+ validate: (v) => v.includes('/') || 'owner/repo 形式で入力してください',
301
+ },
302
+ {
303
+ type: 'text',
304
+ name: 'siteTitle',
305
+ message: 'サイトタイトル:',
306
+ initial: '',
307
+ },
308
+ {
309
+ type: 'text',
310
+ name: 'productionUrl',
311
+ message: '本番 URL (optional):',
312
+ initial: '',
313
+ },
314
+ {
315
+ type: 'text',
316
+ name: 'stagingDomain',
317
+ message: 'ステージングのドメイン(推測困難なハッシュ付き推奨):',
318
+ initial: (_prev, values) => {
319
+ const repo = values.contentRepo?.split('/')[1] || 'site';
320
+ return generateStagingDomain(values.productionUrl || '', repo);
321
+ },
322
+ },
323
+ {
324
+ type: 'text',
325
+ name: 'r2BucketName',
326
+ message: 'R2 バケット名:',
327
+ initial: (prev, values) => {
328
+ const repo = values.contentRepo?.split('/')[1] || 'site';
329
+ return `${repo}-files`;
330
+ },
331
+ },
332
+ {
333
+ type: 'text',
334
+ name: 'r2PublicUrl',
335
+ message: 'R2 公開 URL:',
336
+ initial: '',
337
+ validate: (v) => v === '' || v.startsWith('https://') || 'https:// で始まる URL を入力してください',
338
+ },
339
+ {
340
+ type: 'text',
341
+ name: 'ownerUsername',
342
+ message: '管理者の GitHub ユーザー名:',
343
+ initial: () => {
344
+ try {
345
+ return exec('gh api user -q .login', { silent: true });
346
+ }
347
+ catch {
348
+ return '';
349
+ }
350
+ },
351
+ },
352
+ ], { onCancel: () => process.exit(0) });
353
+ if (!response.contentRepo)
354
+ return null;
355
+ const repoName = response.contentRepo.split('/')[1];
356
+ const stagingDomain = response.stagingDomain || '';
357
+ const previewUrl = stagingDomain ? `https://${stagingDomain}` : '';
358
+ return {
359
+ contentRepo: response.contentRepo,
360
+ githubClientId: '',
361
+ siteTitle: response.siteTitle || '',
362
+ productionUrl: response.productionUrl || '',
363
+ previewUrl,
364
+ r2BucketName: response.r2BucketName || `${repoName}-files`,
365
+ r2PublicUrl: response.r2PublicUrl || '',
366
+ pagesProjectName: repoName,
367
+ ownerUsername: response.ownerUsername || '',
368
+ stagingDomain,
369
+ };
370
+ }
371
+ function createContentStructure(projectDir, config) {
372
+ // frelio-data ディレクトリ構造
373
+ const dirs = [
374
+ 'frelio-data/site/content_types',
375
+ 'frelio-data/site/contents/published',
376
+ 'frelio-data/site/contents/private',
377
+ 'frelio-data/site/templates/assets/scss',
378
+ 'frelio-data/site/templates/assets/ts',
379
+ 'frelio-data/site/data/data-json',
380
+ 'frelio-data/admin/metadata',
381
+ 'frelio-data/admin/users',
382
+ 'frelio-data/admin/recipes',
383
+ 'public',
384
+ ];
385
+ for (const dir of dirs) {
386
+ ensureDir(path.join(projectDir, dir));
387
+ }
388
+ // メタデータファイル
389
+ writeFile(path.join(projectDir, 'frelio-data/admin/users/_index.json'), generateUsersIndex(config));
390
+ // version.json
391
+ writeFile(path.join(projectDir, 'version.json'), generateVersionJson());
392
+ // vite.config.ts + tsconfig.json + package.json
393
+ writeFile(path.join(projectDir, 'vite.config.ts'), generateViteConfig());
394
+ writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
395
+ writeFile(path.join(projectDir, 'package.json'), generatePackageJson(config));
396
+ // R2 ファイル配信用 Pages Function (/storage/*)
397
+ writeFile(path.join(projectDir, 'functions', 'storage', '[[path]].ts'), generateStorageFunction());
398
+ // .gitignore
399
+ const gitignore = [
400
+ 'node_modules/',
401
+ '.wrangler/',
402
+ '.dev.vars',
403
+ '',
404
+ ].join('\n');
405
+ writeFile(path.join(projectDir, '.gitignore'), gitignore);
406
+ }
407
+ function copyWorkflows(projectDir) {
408
+ const workflowsDir = path.join(projectDir, '.github', 'workflows');
409
+ fs.mkdirSync(workflowsDir, { recursive: true });
410
+ // build-staging.yml
411
+ writeFile(path.join(workflowsDir, 'build-staging.yml'), `name: Build Staging
412
+ on:
413
+ push:
414
+ branches: [staging, 'staging-*']
415
+
416
+ permissions:
417
+ contents: write
418
+
419
+ jobs:
420
+ build:
421
+ runs-on: ubuntu-latest
422
+ steps:
423
+ - uses: actions/checkout@v4
424
+ - uses: actions/setup-node@v4
425
+ with:
426
+ node-version: 20
427
+ - run: echo "TODO: Add SSG build steps"
428
+ `);
429
+ // promote-production.yml
430
+ writeFile(path.join(workflowsDir, 'promote-production.yml'), `name: Promote to Production
431
+ on:
432
+ push:
433
+ branches: [main]
434
+
435
+ permissions:
436
+ contents: write
437
+
438
+ jobs:
439
+ tag:
440
+ runs-on: ubuntu-latest
441
+ steps:
442
+ - uses: actions/checkout@v4
443
+ with:
444
+ fetch-depth: 0
445
+ - name: Create deploy tag
446
+ run: |
447
+ VERSION=$(cat version.json | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).version))")
448
+ EXISTING=\$(git tag -l "d\${VERSION}.*" | wc -l)
449
+ NEXT=\$((EXISTING + 1))
450
+ TAG="d\${VERSION}.\${NEXT}"
451
+ git tag "\$TAG"
452
+ git push origin "\$TAG"
453
+ `);
454
+ // direct-deploy.yml
455
+ writeFile(path.join(workflowsDir, 'direct-deploy.yml'), `name: Direct Deploy
456
+ on:
457
+ workflow_dispatch:
458
+
459
+ permissions:
460
+ contents: write
461
+
462
+ jobs:
463
+ deploy:
464
+ runs-on: ubuntu-latest
465
+ steps:
466
+ - uses: actions/checkout@v4
467
+ with:
468
+ fetch-depth: 0
469
+ - name: Merge develop to staging
470
+ run: |
471
+ git config user.name "github-actions[bot]"
472
+ git config user.email "github-actions[bot]@users.noreply.github.com"
473
+ git checkout staging
474
+ git merge origin/develop --no-edit
475
+ git push origin staging
476
+ - name: Fast-forward main
477
+ run: |
478
+ git checkout main
479
+ git merge staging --ff-only
480
+ git push origin main
481
+ `);
482
+ }
483
+ async function extractBundle(projectDir) {
484
+ const release = await getLatestRelease();
485
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-'));
486
+ const tarPath = await downloadTarball(release, tmpDir);
487
+ // tar で展開
488
+ const { extract } = await import('tar');
489
+ await extract({ file: tarPath, cwd: tmpDir });
490
+ // 展開されたディレクトリを特定
491
+ const entries = fs.readdirSync(tmpDir).filter((e) => e.startsWith('frelio-cms-'));
492
+ const bundleDir = entries.find((e) => fs.statSync(path.join(tmpDir, e)).isDirectory());
493
+ if (!bundleDir) {
494
+ throw new Error('Bundle directory not found in tarball');
495
+ }
496
+ const srcDir = path.join(tmpDir, bundleDir);
497
+ // admin/ をコピー
498
+ if (fs.existsSync(path.join(srcDir, 'admin'))) {
499
+ copyDir(path.join(srcDir, 'admin'), path.join(projectDir, 'admin'));
500
+ }
501
+ // functions/ をコピー
502
+ if (fs.existsSync(path.join(srcDir, 'functions'))) {
503
+ copyDir(path.join(srcDir, 'functions'), path.join(projectDir, 'functions'));
504
+ }
505
+ // workers/ をコピー(Pages Functions が import する)
506
+ if (fs.existsSync(path.join(srcDir, 'workers'))) {
507
+ copyDir(path.join(srcDir, 'workers'), path.join(projectDir, 'workers'));
508
+ }
509
+ // クリーンアップ
510
+ fs.rmSync(tmpDir, { recursive: true, force: true });
511
+ }
512
+ function copyDir(src, dest) {
513
+ fs.mkdirSync(dest, { recursive: true });
514
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
515
+ const srcPath = path.join(src, entry.name);
516
+ const destPath = path.join(dest, entry.name);
517
+ if (entry.isDirectory()) {
518
+ copyDir(srcPath, destPath);
519
+ }
520
+ else {
521
+ fs.copyFileSync(srcPath, destPath);
522
+ }
523
+ }
524
+ }
525
+ function getTotalSteps(options) {
526
+ let steps = 4; // content structure, workflows, bundle, config files
527
+ if (!options.skipGithub)
528
+ steps += 3; // create repo, clone, commit/push
529
+ if (!options.skipCloudflare)
530
+ steps += 1; // R2 + Pages
531
+ return steps;
532
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * frelio update - CMS Admin バンドルを最新版に更新
3
+ */
4
+ type UpdateOptions = {
5
+ version?: string;
6
+ };
7
+ export declare function updateCommand(options: UpdateOptions): Promise<void>;
8
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * frelio update - CMS Admin バンドルを最新版に更新
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { getLatestRelease, getRelease, downloadTarball } from '../lib/github-release.js';
8
+ import { log, logSuccess, logError } from '../lib/shell.js';
9
+ export async function updateCommand(options) {
10
+ log('');
11
+ log('🔄 Frelio CMS Admin アップデート');
12
+ log('');
13
+ const projectDir = process.cwd();
14
+ // admin/ の存在チェック
15
+ const adminDir = path.join(projectDir, 'admin');
16
+ if (!fs.existsSync(adminDir)) {
17
+ logError('admin/ ディレクトリが見つかりません。Frelio プロジェクトのルートで実行してください。');
18
+ process.exit(1);
19
+ }
20
+ // config.json のバックアップ
21
+ const configPath = path.join(adminDir, 'config.json');
22
+ let configBackup = null;
23
+ if (fs.existsSync(configPath)) {
24
+ configBackup = fs.readFileSync(configPath, 'utf-8');
25
+ logSuccess('config.json をバックアップしました');
26
+ }
27
+ // リリース取得
28
+ log(' 📥 リリースを取得中...');
29
+ const release = options.version
30
+ ? await getRelease(options.version)
31
+ : await getLatestRelease();
32
+ log(` → ${release.tag_name}`);
33
+ // tarball ダウンロード & 展開
34
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-update-'));
35
+ try {
36
+ const tarPath = await downloadTarball(release, tmpDir);
37
+ const { extract } = await import('tar');
38
+ await extract({ file: tarPath, cwd: tmpDir });
39
+ // 展開されたディレクトリを特定
40
+ const entries = fs.readdirSync(tmpDir).filter((e) => e.startsWith('frelio-cms-'));
41
+ const bundleDir = entries.find((e) => fs.statSync(path.join(tmpDir, e)).isDirectory());
42
+ if (!bundleDir) {
43
+ throw new Error('Bundle directory not found in tarball');
44
+ }
45
+ const srcDir = path.join(tmpDir, bundleDir);
46
+ // admin/ を置き換え
47
+ if (fs.existsSync(path.join(srcDir, 'admin'))) {
48
+ fs.rmSync(adminDir, { recursive: true, force: true });
49
+ copyDir(path.join(srcDir, 'admin'), adminDir);
50
+ logSuccess('admin/ を更新しました');
51
+ }
52
+ // functions/api/ を置き換え(functions/storage/ はユーザー管理なので保持)
53
+ const functionsApiDir = path.join(projectDir, 'functions', 'api');
54
+ if (fs.existsSync(path.join(srcDir, 'functions', 'api'))) {
55
+ if (fs.existsSync(functionsApiDir)) {
56
+ fs.rmSync(functionsApiDir, { recursive: true, force: true });
57
+ }
58
+ copyDir(path.join(srcDir, 'functions', 'api'), functionsApiDir);
59
+ logSuccess('functions/api/ を更新しました');
60
+ }
61
+ // workers/ を置き換え(Pages Functions が import する)
62
+ const workersDir = path.join(projectDir, 'workers');
63
+ if (fs.existsSync(path.join(srcDir, 'workers'))) {
64
+ if (fs.existsSync(workersDir)) {
65
+ fs.rmSync(workersDir, { recursive: true, force: true });
66
+ }
67
+ copyDir(path.join(srcDir, 'workers'), workersDir);
68
+ logSuccess('workers/ を更新しました');
69
+ }
70
+ // config.json を復元
71
+ if (configBackup) {
72
+ fs.writeFileSync(configPath, configBackup, 'utf-8');
73
+ logSuccess('config.json を復元しました');
74
+ }
75
+ log('');
76
+ log(`✅ ${release.tag_name} にアップデートしました!`);
77
+ log('');
78
+ }
79
+ finally {
80
+ fs.rmSync(tmpDir, { recursive: true, force: true });
81
+ }
82
+ }
83
+ function copyDir(src, dest) {
84
+ fs.mkdirSync(dest, { recursive: true });
85
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
86
+ const srcPath = path.join(src, entry.name);
87
+ const destPath = path.join(dest, entry.name);
88
+ if (entry.isDirectory()) {
89
+ copyDir(srcPath, destPath);
90
+ }
91
+ else {
92
+ fs.copyFileSync(srcPath, destPath);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { initCommand } from './commands/init.js';
4
+ import { updateCommand } from './commands/update.js';
5
+ import { addStagingCommand } from './commands/add-staging.js';
6
+ const program = new Command();
7
+ program
8
+ .name('frelio')
9
+ .description('Frelio CMS setup and management CLI')
10
+ .version('0.1.0');
11
+ program
12
+ .command('init')
13
+ .description('Initialize a new Frelio CMS project (creates repo, deploys admin, sets up R2)')
14
+ .option('--skip-github', 'Skip GitHub repository creation')
15
+ .option('--skip-cloudflare', 'Skip Cloudflare setup (R2, Pages)')
16
+ .action(initCommand);
17
+ program
18
+ .command('update')
19
+ .description('Update CMS admin bundle to the latest version')
20
+ .option('--version <version>', 'Specific version to install (default: latest)')
21
+ .action(updateCommand);
22
+ program
23
+ .command('add-staging')
24
+ .description('Add a staging environment (preview branch + Cloudflare Pages project)')
25
+ .option('--name <name>', 'Staging name (creates staging-{name} branch)')
26
+ .option('--skip-cloudflare', 'Skip Cloudflare Pages project creation')
27
+ .action(addStagingCommand);
28
+ program.parse();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * GitHub Release からの tarball ダウンロード
3
+ */
4
+ type ReleaseAsset = {
5
+ name: string;
6
+ browser_download_url: string;
7
+ };
8
+ type Release = {
9
+ tag_name: string;
10
+ assets: ReleaseAsset[];
11
+ };
12
+ export declare function getLatestRelease(): Promise<Release>;
13
+ export declare function getRelease(version: string): Promise<Release>;
14
+ export declare function downloadTarball(release: Release, destDir: string): Promise<string>;
15
+ export {};