@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.
- package/README.md +18 -14
- package/dist/commands/add-staging.d.ts +2 -3
- package/dist/commands/add-staging.js +38 -184
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +164 -373
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +11 -67
- package/dist/core/bundle.d.ts +14 -0
- package/dist/core/bundle.js +122 -0
- package/dist/core/cloudflare.d.ts +26 -0
- package/dist/core/cloudflare.js +60 -0
- package/dist/core/config.d.ts +26 -0
- package/dist/core/config.js +120 -0
- package/dist/core/content-structure.d.ts +13 -0
- package/dist/core/content-structure.js +13 -0
- package/dist/core/file-generators.d.ts +28 -0
- package/dist/core/file-generators.js +93 -0
- package/dist/core/git-operations.d.ts +15 -0
- package/dist/core/git-operations.js +78 -0
- package/dist/core/github.d.ts +16 -0
- package/dist/core/github.js +43 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +30 -0
- package/dist/core/prerequisites.d.ts +22 -0
- package/dist/core/prerequisites.js +107 -0
- package/dist/core/status.d.ts +18 -0
- package/dist/core/status.js +122 -0
- package/dist/core/template-scaffold.d.ts +14 -0
- package/dist/core/template-scaffold.js +99 -0
- package/dist/core/terraform.d.ts +7 -0
- package/dist/core/terraform.js +47 -0
- package/dist/core/types.d.ts +48 -0
- package/dist/core/types.js +21 -0
- package/dist/core/workflows.d.ts +11 -0
- package/dist/core/workflows.js +345 -0
- package/dist/index.js +2 -4
- package/dist/lib/github-release.d.ts +15 -0
- package/dist/lib/github-release.js +41 -0
- package/dist/lib/initial-content.js +87 -55
- package/dist/lib/template-renderer.d.ts +16 -0
- package/dist/lib/template-renderer.js +32 -0
- package/dist/lib/templates.d.ts +7 -7
- package/dist/lib/templates.js +311 -214
- package/package.json +2 -3
package/dist/commands/update.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* frelio update - CMS Admin バンドルを最新版に更新
|
|
3
|
+
*
|
|
4
|
+
* core/bundle の updateBundle を呼ぶ薄いオーケストレーター。
|
|
3
5
|
*/
|
|
4
6
|
import fs from 'node:fs';
|
|
5
7
|
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import { getLatestRelease, getRelease, downloadTarball, extractNpmTarball } from '../lib/npm-registry.js';
|
|
8
8
|
import { log, logSuccess, logError } from '../lib/shell.js';
|
|
9
|
+
import { updateBundle } from '../core/bundle.js';
|
|
9
10
|
export async function updateCommand(options) {
|
|
10
11
|
log('');
|
|
11
12
|
log('🔄 Frelio CMS Admin アップデート');
|
|
@@ -17,73 +18,16 @@ export async function updateCommand(options) {
|
|
|
17
18
|
logError('admin/ ディレクトリが見つかりません。Frelio プロジェクトのルートで実行してください。');
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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 { adminDir: srcAdminDir, functionsDir: srcFunctionsDir, workersDir: srcWorkersDir } = await extractNpmTarball(tarPath, tmpDir);
|
|
38
|
-
// admin/ を置き換え(dist/ の中身から functions/ を除く)
|
|
39
|
-
if (fs.existsSync(srcAdminDir)) {
|
|
40
|
-
fs.rmSync(adminDir, { recursive: true, force: true });
|
|
41
|
-
copyDir(srcAdminDir, adminDir, ['functions']);
|
|
42
|
-
logSuccess('admin/ を更新しました');
|
|
43
|
-
}
|
|
44
|
-
// functions/api/ を置き換え(functions/storage/ はユーザー管理なので保持)
|
|
45
|
-
const functionsApiDir = path.join(projectDir, 'functions', 'api');
|
|
46
|
-
if (fs.existsSync(path.join(srcFunctionsDir, 'api'))) {
|
|
47
|
-
if (fs.existsSync(functionsApiDir)) {
|
|
48
|
-
fs.rmSync(functionsApiDir, { recursive: true, force: true });
|
|
49
|
-
}
|
|
50
|
-
copyDir(path.join(srcFunctionsDir, 'api'), functionsApiDir);
|
|
51
|
-
logSuccess('functions/api/ を更新しました');
|
|
52
|
-
}
|
|
53
|
-
// workers/ を置き換え(Pages Functions が import する)
|
|
54
|
-
const workersDestDir = path.join(projectDir, 'workers');
|
|
55
|
-
if (fs.existsSync(srcWorkersDir)) {
|
|
56
|
-
if (fs.existsSync(workersDestDir)) {
|
|
57
|
-
fs.rmSync(workersDestDir, { recursive: true, force: true });
|
|
58
|
-
}
|
|
59
|
-
copyDir(srcWorkersDir, workersDestDir);
|
|
60
|
-
logSuccess('workers/ を更新しました');
|
|
61
|
-
}
|
|
62
|
-
// config.json を復元
|
|
63
|
-
if (configBackup) {
|
|
64
|
-
fs.writeFileSync(configPath, configBackup, 'utf-8');
|
|
65
|
-
logSuccess('config.json を復元しました');
|
|
66
|
-
}
|
|
21
|
+
log(' 📥 バンドルを取得・更新中...');
|
|
22
|
+
const result = await updateBundle(projectDir, options.version);
|
|
23
|
+
if (result.success) {
|
|
24
|
+
logSuccess('config.json を保持したままバンドルを更新しました');
|
|
67
25
|
log('');
|
|
68
|
-
log(`✅ ${
|
|
26
|
+
log(`✅ ${result.data.version} にアップデートしました!`);
|
|
69
27
|
log('');
|
|
70
28
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
function copyDir(src, dest, exclude = []) {
|
|
76
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
77
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
78
|
-
if (exclude.includes(entry.name))
|
|
79
|
-
continue;
|
|
80
|
-
const srcPath = path.join(src, entry.name);
|
|
81
|
-
const destPath = path.join(dest, entry.name);
|
|
82
|
-
if (entry.isDirectory()) {
|
|
83
|
-
copyDir(srcPath, destPath);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
fs.copyFileSync(srcPath, destPath);
|
|
87
|
-
}
|
|
29
|
+
else {
|
|
30
|
+
logError(result.error);
|
|
31
|
+
process.exit(1);
|
|
88
32
|
}
|
|
89
33
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/bundle — CMS Admin バンドルのダウンロード・展開・更新
|
|
3
|
+
*/
|
|
4
|
+
import { type OperationResult } from './types.js';
|
|
5
|
+
export declare function installBundle(projectDir: string, version?: string): Promise<OperationResult<{
|
|
6
|
+
version: string;
|
|
7
|
+
}>>;
|
|
8
|
+
export declare function updateBundle(projectDir: string, version?: string): Promise<OperationResult<{
|
|
9
|
+
version: string;
|
|
10
|
+
}>>;
|
|
11
|
+
export declare function getBundleVersion(projectDir: string): OperationResult<{
|
|
12
|
+
version: string | null;
|
|
13
|
+
installed: boolean;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/bundle — CMS Admin バンドルのダウンロード・展開・更新
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { ok, fail } from './types.js';
|
|
8
|
+
import { getLatestRelease, getRelease, downloadTarball, extractNpmTarball, } from '../lib/npm-registry.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Install (initial)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export async function installBundle(projectDir, version) {
|
|
13
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-'));
|
|
14
|
+
try {
|
|
15
|
+
const release = version ? await getRelease(version) : await getLatestRelease();
|
|
16
|
+
const tarPath = await downloadTarball(release, tmpDir);
|
|
17
|
+
const { adminDir, functionsDir, workersDir } = await extractNpmTarball(tarPath, tmpDir);
|
|
18
|
+
if (fs.existsSync(adminDir)) {
|
|
19
|
+
copyDir(adminDir, path.join(projectDir, 'admin'), ['functions']);
|
|
20
|
+
}
|
|
21
|
+
if (fs.existsSync(functionsDir)) {
|
|
22
|
+
copyDir(functionsDir, path.join(projectDir, 'functions'));
|
|
23
|
+
}
|
|
24
|
+
if (fs.existsSync(workersDir)) {
|
|
25
|
+
copyDir(workersDir, path.join(projectDir, 'workers'));
|
|
26
|
+
}
|
|
27
|
+
return ok({ version: release.tag_name });
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return fail(`バンドル展開失敗: ${e.message}`, 'EXEC_FAILED');
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Update (preserves config.json)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export async function updateBundle(projectDir, version) {
|
|
40
|
+
const adminDir = path.join(projectDir, 'admin');
|
|
41
|
+
if (!fs.existsSync(adminDir)) {
|
|
42
|
+
return fail('admin/ ディレクトリが見つかりません。', 'NOT_FOUND');
|
|
43
|
+
}
|
|
44
|
+
// config.json バックアップ
|
|
45
|
+
const configPath = path.join(adminDir, 'config.json');
|
|
46
|
+
let configBackup = null;
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
configBackup = fs.readFileSync(configPath, 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-update-'));
|
|
51
|
+
try {
|
|
52
|
+
const release = version ? await getRelease(version) : await getLatestRelease();
|
|
53
|
+
const tarPath = await downloadTarball(release, tmpDir);
|
|
54
|
+
const { adminDir: srcAdminDir, functionsDir: srcFunctionsDir, workersDir: srcWorkersDir, } = await extractNpmTarball(tarPath, tmpDir);
|
|
55
|
+
// admin/ 置き換え
|
|
56
|
+
if (fs.existsSync(srcAdminDir)) {
|
|
57
|
+
fs.rmSync(adminDir, { recursive: true, force: true });
|
|
58
|
+
copyDir(srcAdminDir, adminDir, ['functions']);
|
|
59
|
+
}
|
|
60
|
+
// functions/api/ 置き換え(functions/storage/ は保持)
|
|
61
|
+
const functionsApiDir = path.join(projectDir, 'functions', 'api');
|
|
62
|
+
if (fs.existsSync(path.join(srcFunctionsDir, 'api'))) {
|
|
63
|
+
if (fs.existsSync(functionsApiDir)) {
|
|
64
|
+
fs.rmSync(functionsApiDir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
copyDir(path.join(srcFunctionsDir, 'api'), functionsApiDir);
|
|
67
|
+
}
|
|
68
|
+
// workers/ 置き換え
|
|
69
|
+
const workersDestDir = path.join(projectDir, 'workers');
|
|
70
|
+
if (fs.existsSync(srcWorkersDir)) {
|
|
71
|
+
if (fs.existsSync(workersDestDir)) {
|
|
72
|
+
fs.rmSync(workersDestDir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
copyDir(srcWorkersDir, workersDestDir);
|
|
75
|
+
}
|
|
76
|
+
// config.json 復元
|
|
77
|
+
if (configBackup) {
|
|
78
|
+
fs.writeFileSync(configPath, configBackup, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
return ok({ version: release.tag_name });
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return fail(`バンドル更新失敗: ${e.message}`, 'EXEC_FAILED');
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Version check
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export function getBundleVersion(projectDir) {
|
|
93
|
+
const installed = fs.existsSync(path.join(projectDir, 'admin'));
|
|
94
|
+
const versionPath = path.join(projectDir, 'version.json');
|
|
95
|
+
let version = null;
|
|
96
|
+
try {
|
|
97
|
+
const raw = fs.readFileSync(versionPath, 'utf-8');
|
|
98
|
+
version = JSON.parse(raw).version ?? null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// version.json がない場合
|
|
102
|
+
}
|
|
103
|
+
return ok({ version, installed });
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Shared helper
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
function copyDir(src, dest, exclude = []) {
|
|
109
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
110
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
111
|
+
if (exclude.includes(entry.name))
|
|
112
|
+
continue;
|
|
113
|
+
const srcPath = path.join(src, entry.name);
|
|
114
|
+
const destPath = path.join(dest, entry.name);
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
copyDir(srcPath, destPath);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
fs.copyFileSync(srcPath, destPath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/cloudflare — Cloudflare リソース操作
|
|
3
|
+
*
|
|
4
|
+
* 各操作は冪等。「既に存在」は成功バリアントとして返す。
|
|
5
|
+
*/
|
|
6
|
+
import { type ProjectConfig, type OperationResult } from './types.js';
|
|
7
|
+
export declare function createR2Bucket(bucketName: string): OperationResult<{
|
|
8
|
+
alreadyExisted: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function createPagesProject(projectName: string, productionBranch: string): OperationResult<{
|
|
11
|
+
alreadyExisted: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function setPagesSecret(projectName: string, secretName: string, secretValue: string): OperationResult;
|
|
14
|
+
export type CloudflareSetupResult = {
|
|
15
|
+
r2Bucket: OperationResult<{
|
|
16
|
+
alreadyExisted: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
contentPages: OperationResult<{
|
|
19
|
+
alreadyExisted: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
adminPages: OperationResult<{
|
|
22
|
+
alreadyExisted: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
secrets: OperationResult[];
|
|
25
|
+
};
|
|
26
|
+
export declare function setupCloudflareResources(config: ProjectConfig, githubClientSecret?: string): OperationResult<CloudflareSetupResult>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/cloudflare — Cloudflare リソース操作
|
|
3
|
+
*
|
|
4
|
+
* 各操作は冪等。「既に存在」は成功バリアントとして返す。
|
|
5
|
+
*/
|
|
6
|
+
import { ok, okVoid, fail } from './types.js';
|
|
7
|
+
import { exec } from '../lib/shell.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Individual resource operations
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export function createR2Bucket(bucketName) {
|
|
12
|
+
try {
|
|
13
|
+
exec(`wrangler r2 bucket create ${bucketName}`, { silent: true });
|
|
14
|
+
return ok({ alreadyExisted: false });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const msg = error.message;
|
|
18
|
+
if (msg.includes('already exists')) {
|
|
19
|
+
return ok({ alreadyExisted: true }, true);
|
|
20
|
+
}
|
|
21
|
+
return fail(`R2 バケット作成失敗: ${msg}`, 'EXEC_FAILED');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function createPagesProject(projectName, productionBranch) {
|
|
25
|
+
try {
|
|
26
|
+
exec(`wrangler pages project create ${projectName} --production-branch ${productionBranch}`, {
|
|
27
|
+
silent: true,
|
|
28
|
+
});
|
|
29
|
+
return ok({ alreadyExisted: false });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const msg = error.message;
|
|
33
|
+
if (msg.includes('already exists') || msg.includes('A project with this name already exists')) {
|
|
34
|
+
return ok({ alreadyExisted: true }, true);
|
|
35
|
+
}
|
|
36
|
+
return fail(`Pages プロジェクト作成失敗: ${msg}`, 'EXEC_FAILED');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function setPagesSecret(projectName, secretName, secretValue) {
|
|
40
|
+
try {
|
|
41
|
+
exec(`echo "${secretValue}" | wrangler pages secret put ${secretName} --project-name ${projectName}`, {
|
|
42
|
+
silent: true,
|
|
43
|
+
});
|
|
44
|
+
return okVoid();
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return fail(`${secretName} の設定に失敗しました(${projectName}): ${e.message}`, 'EXEC_FAILED');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function setupCloudflareResources(config, githubClientSecret) {
|
|
51
|
+
const r2Bucket = createR2Bucket(config.r2BucketName);
|
|
52
|
+
const contentPages = createPagesProject(config.pagesProjectName, 'main');
|
|
53
|
+
const adminPages = createPagesProject(config.adminPagesProjectName, 'admin');
|
|
54
|
+
const secrets = [];
|
|
55
|
+
if (githubClientSecret) {
|
|
56
|
+
secrets.push(setPagesSecret(config.pagesProjectName, 'GITHUB_CLIENT_SECRET', githubClientSecret));
|
|
57
|
+
secrets.push(setPagesSecret(config.pagesProjectName, 'GITHUB_CLIENT_ID', config.githubClientId));
|
|
58
|
+
}
|
|
59
|
+
return ok({ r2Bucket, contentPages, adminPages, secrets });
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/config — config.json の読み書き・バリデーション・構築
|
|
3
|
+
*
|
|
4
|
+
* CMS 管理画面の GeneralSettings パターンに倣い、
|
|
5
|
+
* read-only フィールド(contentRepo, githubClientId)を保護しつつ部分更新できる。
|
|
6
|
+
*/
|
|
7
|
+
import { type ProjectConfig, type BuildConfigParams, type OperationResult } from './types.js';
|
|
8
|
+
export declare function readConfig(projectDir: string): OperationResult<{
|
|
9
|
+
config: ProjectConfig | null;
|
|
10
|
+
path: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function writeConfig(projectDir: string, config: ProjectConfig): OperationResult;
|
|
13
|
+
export type EditableConfigFields = Omit<ProjectConfig, 'contentRepo' | 'githubClientId'>;
|
|
14
|
+
export declare function updateConfig(projectDir: string, updates: Partial<EditableConfigFields>): OperationResult<{
|
|
15
|
+
config: ProjectConfig;
|
|
16
|
+
}>;
|
|
17
|
+
export type ConfigValidationError = {
|
|
18
|
+
field: string;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function validateConfig(config: Partial<ProjectConfig>): OperationResult<{
|
|
22
|
+
errors: ConfigValidationError[];
|
|
23
|
+
}>;
|
|
24
|
+
export declare function buildConfig(params: BuildConfigParams): OperationResult<{
|
|
25
|
+
config: ProjectConfig;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/config — config.json の読み書き・バリデーション・構築
|
|
3
|
+
*
|
|
4
|
+
* CMS 管理画面の GeneralSettings パターンに倣い、
|
|
5
|
+
* read-only フィールド(contentRepo, githubClientId)を保護しつつ部分更新できる。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { ok, okVoid, fail, } from './types.js';
|
|
10
|
+
import { generateStagingDomain } from '../lib/templates.js';
|
|
11
|
+
import { validateContentRepo, validateR2PublicUrl, } from '../lib/validators.js';
|
|
12
|
+
/** read-only フィールド(updateConfig で上書き不可) */
|
|
13
|
+
const READ_ONLY_FIELDS = ['contentRepo', 'githubClientId'];
|
|
14
|
+
/** config.json の相対パス */
|
|
15
|
+
const CONFIG_REL_PATH = path.join('admin', 'config.json');
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Read
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export function readConfig(projectDir) {
|
|
20
|
+
const configPath = path.join(projectDir, CONFIG_REL_PATH);
|
|
21
|
+
if (!fs.existsSync(configPath)) {
|
|
22
|
+
return ok({ config: null, path: configPath });
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
26
|
+
const config = JSON.parse(raw);
|
|
27
|
+
return ok({ config, path: configPath });
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return fail(`config.json の読み込みに失敗: ${e.message}`, 'EXEC_FAILED');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Write (full)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
export function writeConfig(projectDir, config) {
|
|
37
|
+
const configPath = path.join(projectDir, CONFIG_REL_PATH);
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
40
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
41
|
+
return okVoid();
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return fail(`config.json の書き込みに失敗: ${e.message}`, 'EXEC_FAILED');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function updateConfig(projectDir, updates) {
|
|
48
|
+
const readResult = readConfig(projectDir);
|
|
49
|
+
if (!readResult.success)
|
|
50
|
+
return readResult;
|
|
51
|
+
if (!readResult.data.config) {
|
|
52
|
+
return fail('config.json が存在しません。先に writeConfig で作成してください。', 'NOT_FOUND');
|
|
53
|
+
}
|
|
54
|
+
const existing = readResult.data.config;
|
|
55
|
+
// read-only フィールドが updates に含まれていたら除去(型で制限済みだが安全策)
|
|
56
|
+
const safeUpdates = { ...updates };
|
|
57
|
+
for (const key of READ_ONLY_FIELDS) {
|
|
58
|
+
delete safeUpdates[key];
|
|
59
|
+
}
|
|
60
|
+
const merged = { ...existing, ...safeUpdates };
|
|
61
|
+
const writeResult = writeConfig(projectDir, merged);
|
|
62
|
+
if (!writeResult.success)
|
|
63
|
+
return writeResult;
|
|
64
|
+
return ok({ config: merged });
|
|
65
|
+
}
|
|
66
|
+
export function validateConfig(config) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
if (config.contentRepo !== undefined) {
|
|
69
|
+
const check = validateContentRepo(config.contentRepo);
|
|
70
|
+
if (check !== true)
|
|
71
|
+
errors.push({ field: 'contentRepo', message: check });
|
|
72
|
+
}
|
|
73
|
+
if (config.r2PublicUrl !== undefined) {
|
|
74
|
+
const check = validateR2PublicUrl(config.r2PublicUrl);
|
|
75
|
+
if (check !== true)
|
|
76
|
+
errors.push({ field: 'r2PublicUrl', message: check });
|
|
77
|
+
}
|
|
78
|
+
if (config.productionUrl !== undefined && config.productionUrl !== '') {
|
|
79
|
+
try {
|
|
80
|
+
new URL(config.productionUrl);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
errors.push({ field: 'productionUrl', message: '有効な URL を入力してください' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return ok({ errors });
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Build (raw params → ProjectConfig with derived values)
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
export function buildConfig(params) {
|
|
92
|
+
// バリデーション
|
|
93
|
+
const repoCheck = validateContentRepo(params.contentRepo);
|
|
94
|
+
if (repoCheck !== true) {
|
|
95
|
+
return fail(`contentRepo: ${repoCheck}`, 'VALIDATION_ERROR');
|
|
96
|
+
}
|
|
97
|
+
if (params.r2PublicUrl) {
|
|
98
|
+
const urlCheck = validateR2PublicUrl(params.r2PublicUrl);
|
|
99
|
+
if (urlCheck !== true) {
|
|
100
|
+
return fail(`r2PublicUrl: ${urlCheck}`, 'VALIDATION_ERROR');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const repoName = params.contentRepo.split('/')[1];
|
|
104
|
+
const stagingDomain = params.stagingDomain ?? generateStagingDomain(params.productionUrl || '', repoName);
|
|
105
|
+
const previewUrl = stagingDomain ? `https://${stagingDomain}` : '';
|
|
106
|
+
const config = {
|
|
107
|
+
contentRepo: params.contentRepo,
|
|
108
|
+
githubClientId: params.githubClientId || '',
|
|
109
|
+
siteTitle: params.siteTitle || '',
|
|
110
|
+
productionUrl: params.productionUrl || '',
|
|
111
|
+
previewUrl,
|
|
112
|
+
r2BucketName: params.r2BucketName ?? `${repoName}-files`,
|
|
113
|
+
r2PublicUrl: params.r2PublicUrl || '',
|
|
114
|
+
pagesProjectName: repoName,
|
|
115
|
+
adminPagesProjectName: `${repoName}-admin`,
|
|
116
|
+
ownerUsername: params.ownerUsername || '',
|
|
117
|
+
stagingDomain,
|
|
118
|
+
};
|
|
119
|
+
return ok({ config });
|
|
120
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/content-structure — コンテンツリポジトリのディレクトリ・ファイル構造生成
|
|
3
|
+
*
|
|
4
|
+
* テンプレートからのスキャフォールディングに委譲する。
|
|
5
|
+
*/
|
|
6
|
+
import { type ProjectConfig, type OperationResult } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* テンプレートからコンテンツリポジトリの全構造を生成する。
|
|
9
|
+
* ワークフローファイル(.github/workflows/)もテンプレートに含まれる。
|
|
10
|
+
*/
|
|
11
|
+
export declare function createFullContentStructure(projectDir: string, config: ProjectConfig): Promise<OperationResult<{
|
|
12
|
+
files: string[];
|
|
13
|
+
}>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/content-structure — コンテンツリポジトリのディレクトリ・ファイル構造生成
|
|
3
|
+
*
|
|
4
|
+
* テンプレートからのスキャフォールディングに委譲する。
|
|
5
|
+
*/
|
|
6
|
+
import { scaffoldFromTemplate } from './template-scaffold.js';
|
|
7
|
+
/**
|
|
8
|
+
* テンプレートからコンテンツリポジトリの全構造を生成する。
|
|
9
|
+
* ワークフローファイル(.github/workflows/)もテンプレートに含まれる。
|
|
10
|
+
*/
|
|
11
|
+
export async function createFullContentStructure(projectDir, config) {
|
|
12
|
+
return scaffoldFromTemplate(projectDir, config);
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/file-generators — 個別ファイルの再生成
|
|
3
|
+
*
|
|
4
|
+
* config.json 変更後に特定のファイルだけ再生成する場合に使う。
|
|
5
|
+
* 既存ファイルを上書きする(冪等)。
|
|
6
|
+
*/
|
|
7
|
+
import { type ProjectConfig, type OperationResult } from './types.js';
|
|
8
|
+
export declare function generateAndWriteConfigJson(projectDir: string, config: ProjectConfig): OperationResult<{
|
|
9
|
+
path: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function generateAndWriteWranglerToml(projectDir: string, config: ProjectConfig): OperationResult<{
|
|
12
|
+
path: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function generateAndWriteRedirects(projectDir: string): OperationResult<{
|
|
15
|
+
path: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function generateAndWriteRoutesJson(projectDir: string): OperationResult<{
|
|
18
|
+
path: string;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function generateAndWritePublicRoutesJson(projectDir: string): OperationResult<{
|
|
21
|
+
path: string;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function generateAndWriteStorageFunction(projectDir: string): OperationResult<{
|
|
24
|
+
path: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function regenerateAllConfigFiles(projectDir: string, config: ProjectConfig): OperationResult<{
|
|
27
|
+
paths: string[];
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/file-generators — 個別ファイルの再生成
|
|
3
|
+
*
|
|
4
|
+
* config.json 変更後に特定のファイルだけ再生成する場合に使う。
|
|
5
|
+
* 既存ファイルを上書きする(冪等)。
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { ok, fail } from './types.js';
|
|
9
|
+
import { generateConfigJson, generateWranglerToml, generateRedirects, generateRoutesJson, generateStorageFunction, writeFile, } from '../lib/templates.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Individual file generators
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function generateAndWriteConfigJson(projectDir, config) {
|
|
14
|
+
try {
|
|
15
|
+
const filePath = path.join(projectDir, 'admin', 'config.json');
|
|
16
|
+
writeFile(filePath, generateConfigJson(config));
|
|
17
|
+
return ok({ path: filePath });
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return fail(`config.json 生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function generateAndWriteWranglerToml(projectDir, config) {
|
|
24
|
+
try {
|
|
25
|
+
const filePath = path.join(projectDir, 'wrangler.toml');
|
|
26
|
+
writeFile(filePath, generateWranglerToml(config));
|
|
27
|
+
return ok({ path: filePath });
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return fail(`wrangler.toml 生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function generateAndWriteRedirects(projectDir) {
|
|
34
|
+
try {
|
|
35
|
+
const filePath = path.join(projectDir, '_redirects');
|
|
36
|
+
writeFile(filePath, generateRedirects());
|
|
37
|
+
return ok({ path: filePath });
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return fail(`_redirects 生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function generateAndWriteRoutesJson(projectDir) {
|
|
44
|
+
try {
|
|
45
|
+
const filePath = path.join(projectDir, '_routes.json');
|
|
46
|
+
writeFile(filePath, generateRoutesJson());
|
|
47
|
+
return ok({ path: filePath });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return fail(`_routes.json 生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function generateAndWritePublicRoutesJson(projectDir) {
|
|
54
|
+
try {
|
|
55
|
+
const filePath = path.join(projectDir, 'public', '_routes.json');
|
|
56
|
+
const content = JSON.stringify({ version: 1, include: ['/storage/*'], exclude: [] }, null, 2);
|
|
57
|
+
writeFile(filePath, content);
|
|
58
|
+
return ok({ path: filePath });
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return fail(`public/_routes.json 生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function generateAndWriteStorageFunction(projectDir) {
|
|
65
|
+
try {
|
|
66
|
+
const filePath = path.join(projectDir, 'functions', 'storage', '[[path]].ts');
|
|
67
|
+
writeFile(filePath, generateStorageFunction());
|
|
68
|
+
return ok({ path: filePath });
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return fail(`storage function 生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Regenerate all config-derived files at once
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export function regenerateAllConfigFiles(projectDir, config) {
|
|
78
|
+
const paths = [];
|
|
79
|
+
const generators = [
|
|
80
|
+
() => generateAndWriteConfigJson(projectDir, config),
|
|
81
|
+
() => generateAndWriteWranglerToml(projectDir, config),
|
|
82
|
+
() => generateAndWriteRedirects(projectDir),
|
|
83
|
+
() => generateAndWriteRoutesJson(projectDir),
|
|
84
|
+
() => generateAndWritePublicRoutesJson(projectDir),
|
|
85
|
+
];
|
|
86
|
+
for (const gen of generators) {
|
|
87
|
+
const result = gen();
|
|
88
|
+
if (!result.success)
|
|
89
|
+
return result;
|
|
90
|
+
paths.push(result.data.path);
|
|
91
|
+
}
|
|
92
|
+
return ok({ paths });
|
|
93
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/git-operations — Git コミット・ブランチ・プッシュ操作
|
|
3
|
+
*/
|
|
4
|
+
import { type OperationResult } from './types.js';
|
|
5
|
+
export declare function initialCommitAndBranches(projectDir: string, contentRepo: string): OperationResult<{
|
|
6
|
+
branches: string[];
|
|
7
|
+
defaultBranch: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function createBranch(projectDir: string, branchName: string, baseBranch: string): OperationResult<{
|
|
10
|
+
alreadyExisted: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function isGitRepo(projectDir: string): boolean;
|
|
13
|
+
export declare function checkWorkflowBranchCoverage(projectDir: string, branchName: string): OperationResult<{
|
|
14
|
+
covered: boolean;
|
|
15
|
+
}>;
|