@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/git-operations — Git コミット・ブランチ・プッシュ操作
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { ok, fail } from './types.js';
|
|
6
|
+
import { exec } from '../lib/shell.js';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Initial commit + branch creation
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export function initialCommitAndBranches(projectDir, contentRepo) {
|
|
11
|
+
try {
|
|
12
|
+
exec('git add -A', { cwd: projectDir, silent: true });
|
|
13
|
+
exec('git commit -m "Initial Frelio CMS setup"', { cwd: projectDir, silent: true });
|
|
14
|
+
// develop, admin, staging ブランチ作成
|
|
15
|
+
const branches = ['develop', 'admin', 'staging'];
|
|
16
|
+
for (const branch of branches) {
|
|
17
|
+
exec(`git branch ${branch}`, { cwd: projectDir, silent: true });
|
|
18
|
+
}
|
|
19
|
+
// プッシュ
|
|
20
|
+
exec('git push -u origin main', { cwd: projectDir, silent: true });
|
|
21
|
+
for (const branch of branches) {
|
|
22
|
+
exec(`git push -u origin ${branch}`, { cwd: projectDir, silent: true });
|
|
23
|
+
}
|
|
24
|
+
// デフォルトブランチを develop に変更
|
|
25
|
+
exec(`gh repo edit ${contentRepo} --default-branch develop`, { silent: true });
|
|
26
|
+
return ok({ branches: ['main', ...branches], defaultBranch: 'develop' });
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return fail(`Git 操作失敗: ${e.message}`, 'EXEC_FAILED');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Single branch creation
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export function createBranch(projectDir, branchName, baseBranch) {
|
|
36
|
+
try {
|
|
37
|
+
exec('git fetch origin', { cwd: projectDir, silent: true });
|
|
38
|
+
// リモートに既にあるか確認
|
|
39
|
+
try {
|
|
40
|
+
exec(`git rev-parse --verify origin/${branchName}`, { cwd: projectDir, silent: true });
|
|
41
|
+
return ok({ alreadyExisted: true }, true);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// 存在しない → 作成
|
|
45
|
+
}
|
|
46
|
+
exec(`git branch ${branchName} ${baseBranch}`, { cwd: projectDir, silent: true });
|
|
47
|
+
exec(`git push -u origin ${branchName}`, { cwd: projectDir, silent: true });
|
|
48
|
+
return ok({ alreadyExisted: false });
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return fail(`ブランチ作成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Check
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
export function isGitRepo(projectDir) {
|
|
58
|
+
try {
|
|
59
|
+
exec('git rev-parse --is-inside-work-tree', { cwd: projectDir, silent: true });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Workflow branch check
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
export function checkWorkflowBranchCoverage(projectDir, branchName) {
|
|
70
|
+
try {
|
|
71
|
+
const workflow = fs.readFileSync(`${projectDir}/.github/workflows/build-staging.yml`, 'utf-8');
|
|
72
|
+
const covered = workflow.includes('staging-*') || workflow.includes(branchName);
|
|
73
|
+
return ok({ covered });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return fail('build-staging.yml が見つかりません。', 'NOT_FOUND');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/github — GitHub リポジトリ操作
|
|
3
|
+
*
|
|
4
|
+
* 各操作は冪等。「既に存在」は成功バリアントとして返す。
|
|
5
|
+
*/
|
|
6
|
+
import { type OperationResult } from './types.js';
|
|
7
|
+
export declare function createRepo(contentRepo: string): OperationResult<{
|
|
8
|
+
alreadyExisted: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function cloneRepo(contentRepo: string, projectDir: string): OperationResult<{
|
|
11
|
+
path: string;
|
|
12
|
+
alreadyExisted: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function getAuthenticatedUser(): OperationResult<{
|
|
15
|
+
username: string;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/github — GitHub リポジトリ操作
|
|
3
|
+
*
|
|
4
|
+
* 各操作は冪等。「既に存在」は成功バリアントとして返す。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { ok, fail } from './types.js';
|
|
8
|
+
import { exec } from '../lib/shell.js';
|
|
9
|
+
export function createRepo(contentRepo) {
|
|
10
|
+
try {
|
|
11
|
+
exec(`gh repo create ${contentRepo} --private --confirm`, { silent: true });
|
|
12
|
+
return ok({ alreadyExisted: false });
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const msg = error.message;
|
|
16
|
+
if (msg.includes('already exists')) {
|
|
17
|
+
return ok({ alreadyExisted: true }, true);
|
|
18
|
+
}
|
|
19
|
+
return fail(`リポジトリ作成失敗: ${msg}`, 'EXEC_FAILED');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function cloneRepo(contentRepo, projectDir) {
|
|
23
|
+
try {
|
|
24
|
+
const repoName = contentRepo.split('/')[1];
|
|
25
|
+
exec(`gh repo clone ${contentRepo} ${repoName}`, { silent: true });
|
|
26
|
+
return ok({ path: projectDir, alreadyExisted: false });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (fs.existsSync(projectDir)) {
|
|
30
|
+
return ok({ path: projectDir, alreadyExisted: true }, true);
|
|
31
|
+
}
|
|
32
|
+
return fail(`クローン失敗: ${error.message}`, 'EXEC_FAILED');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function getAuthenticatedUser() {
|
|
36
|
+
try {
|
|
37
|
+
const username = exec('gh api user -q .login', { silent: true });
|
|
38
|
+
return ok({ username });
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return fail(`ユーザー情報取得失敗: ${e.message}`, 'AUTH_REQUIRED');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ — AI エージェント向け関数群の一括エクスポート
|
|
3
|
+
*
|
|
4
|
+
* AI エージェントはこのモジュールから全関数にアクセスできる。
|
|
5
|
+
* 各関数は OperationResult<T> を返し、prompts/process.exit/console.log を含まない。
|
|
6
|
+
*/
|
|
7
|
+
export type { OperationResult, OperationSuccess, OperationFailure, ErrorCode, ProjectConfig, ProjectStatus, BuildConfigParams, } from './types.js';
|
|
8
|
+
export { ok, okVoid, fail } from './types.js';
|
|
9
|
+
export { inspectProject, isGitRepo, hasContentStructure, hasAdminBundle, hasValidConfig, listBranches, getRemoteUrl, } from './status.js';
|
|
10
|
+
export { readConfig, writeConfig, updateConfig, validateConfig, buildConfig, } from './config.js';
|
|
11
|
+
export type { EditableConfigFields, ConfigValidationError } from './config.js';
|
|
12
|
+
export { checkGit, checkGhCli, checkWrangler, checkPrerequisitesFor, } from './prerequisites.js';
|
|
13
|
+
export type { PrerequisiteTarget } from './prerequisites.js';
|
|
14
|
+
export { createRepo, cloneRepo, getAuthenticatedUser } from './github.js';
|
|
15
|
+
export { createR2Bucket, createPagesProject, setPagesSecret, setupCloudflareResources, } from './cloudflare.js';
|
|
16
|
+
export type { CloudflareSetupResult } from './cloudflare.js';
|
|
17
|
+
export { createFullContentStructure } from './content-structure.js';
|
|
18
|
+
export { scaffoldFromTemplate } from './template-scaffold.js';
|
|
19
|
+
export { generateAndWriteConfigJson, generateAndWriteWranglerToml, generateAndWriteRedirects, generateAndWriteRoutesJson, generateAndWritePublicRoutesJson, generateAndWriteStorageFunction, regenerateAllConfigFiles, } from './file-generators.js';
|
|
20
|
+
export { generateTerraformFiles } from './terraform.js';
|
|
21
|
+
export { installBundle, updateBundle, getBundleVersion } from './bundle.js';
|
|
22
|
+
export { initialCommitAndBranches, createBranch, checkWorkflowBranchCoverage, } from './git-operations.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ — AI エージェント向け関数群の一括エクスポート
|
|
3
|
+
*
|
|
4
|
+
* AI エージェントはこのモジュールから全関数にアクセスできる。
|
|
5
|
+
* 各関数は OperationResult<T> を返し、prompts/process.exit/console.log を含まない。
|
|
6
|
+
*/
|
|
7
|
+
export { ok, okVoid, fail } from './types.js';
|
|
8
|
+
// Status — AI の最初のエントリーポイント
|
|
9
|
+
export { inspectProject, isGitRepo, hasContentStructure, hasAdminBundle, hasValidConfig, listBranches, getRemoteUrl, } from './status.js';
|
|
10
|
+
// Note: isGitRepo is also available from git-operations.ts but status.ts is the canonical export
|
|
11
|
+
// Config — config.json CRUD
|
|
12
|
+
export { readConfig, writeConfig, updateConfig, validateConfig, buildConfig, } from './config.js';
|
|
13
|
+
// Prerequisites — ツール別チェック
|
|
14
|
+
export { checkGit, checkGhCli, checkWrangler, checkPrerequisitesFor, } from './prerequisites.js';
|
|
15
|
+
// GitHub
|
|
16
|
+
export { createRepo, cloneRepo, getAuthenticatedUser } from './github.js';
|
|
17
|
+
// Cloudflare
|
|
18
|
+
export { createR2Bucket, createPagesProject, setPagesSecret, setupCloudflareResources, } from './cloudflare.js';
|
|
19
|
+
// Content structure
|
|
20
|
+
export { createFullContentStructure } from './content-structure.js';
|
|
21
|
+
// Template scaffold
|
|
22
|
+
export { scaffoldFromTemplate } from './template-scaffold.js';
|
|
23
|
+
// File generators
|
|
24
|
+
export { generateAndWriteConfigJson, generateAndWriteWranglerToml, generateAndWriteRedirects, generateAndWriteRoutesJson, generateAndWritePublicRoutesJson, generateAndWriteStorageFunction, regenerateAllConfigFiles, } from './file-generators.js';
|
|
25
|
+
// Terraform
|
|
26
|
+
export { generateTerraformFiles } from './terraform.js';
|
|
27
|
+
// Bundle
|
|
28
|
+
export { installBundle, updateBundle, getBundleVersion } from './bundle.js';
|
|
29
|
+
// Git operations
|
|
30
|
+
export { initialCommitAndBranches, createBranch, checkWorkflowBranchCoverage, } from './git-operations.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/prerequisites — ツール別の前提チェック
|
|
3
|
+
*
|
|
4
|
+
* 各ツールを個別にチェックし、構造化された結果を返す。
|
|
5
|
+
*/
|
|
6
|
+
import { type OperationResult } from './types.js';
|
|
7
|
+
export declare function checkGit(): OperationResult<{
|
|
8
|
+
version: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function checkGhCli(): OperationResult<{
|
|
11
|
+
version: string;
|
|
12
|
+
authenticated: boolean;
|
|
13
|
+
username: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function checkWrangler(): OperationResult<{
|
|
16
|
+
version: string;
|
|
17
|
+
authenticated: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
export type PrerequisiteTarget = 'git' | 'github' | 'cloudflare';
|
|
20
|
+
export declare function checkPrerequisitesFor(targets: PrerequisiteTarget[]): OperationResult<{
|
|
21
|
+
results: Record<string, OperationResult<Record<string, unknown>>>;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/prerequisites — ツール別の前提チェック
|
|
3
|
+
*
|
|
4
|
+
* 各ツールを個別にチェックし、構造化された結果を返す。
|
|
5
|
+
*/
|
|
6
|
+
import { ok, fail } from './types.js';
|
|
7
|
+
import { exec, commandExists } from '../lib/shell.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Individual tool checks
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export function checkGit() {
|
|
12
|
+
if (!commandExists('git')) {
|
|
13
|
+
return fail('git が見つかりません。', 'COMMAND_MISSING');
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const version = exec('git --version', { silent: true });
|
|
17
|
+
return ok({ version });
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return fail(`git バージョン取得失敗: ${e.message}`, 'EXEC_FAILED');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function checkGhCli() {
|
|
24
|
+
if (!commandExists('gh')) {
|
|
25
|
+
return fail('gh CLI が見つかりません。https://cli.github.com/ からインストールしてください。', 'COMMAND_MISSING');
|
|
26
|
+
}
|
|
27
|
+
let version;
|
|
28
|
+
try {
|
|
29
|
+
version = exec('gh --version', { silent: true }).split('\n')[0];
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return fail(`gh バージョン取得失敗: ${e.message}`, 'EXEC_FAILED');
|
|
33
|
+
}
|
|
34
|
+
let authenticated = false;
|
|
35
|
+
let username = '';
|
|
36
|
+
try {
|
|
37
|
+
exec('gh auth status', { silent: true });
|
|
38
|
+
authenticated = true;
|
|
39
|
+
try {
|
|
40
|
+
username = exec('gh api user -q .login', { silent: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// username 取得失敗は致命的でない
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// 未認証
|
|
48
|
+
}
|
|
49
|
+
if (!authenticated) {
|
|
50
|
+
return fail('gh にログインしていません。`gh auth login` を実行してください。', 'AUTH_REQUIRED');
|
|
51
|
+
}
|
|
52
|
+
return ok({ version, authenticated, username });
|
|
53
|
+
}
|
|
54
|
+
export function checkWrangler() {
|
|
55
|
+
if (!commandExists('wrangler')) {
|
|
56
|
+
return fail('wrangler CLI が見つかりません。`npm i -g wrangler` でインストールしてください。', 'COMMAND_MISSING');
|
|
57
|
+
}
|
|
58
|
+
let version;
|
|
59
|
+
try {
|
|
60
|
+
version = exec('wrangler --version', { silent: true });
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return fail(`wrangler バージョン取得失敗: ${e.message}`, 'EXEC_FAILED');
|
|
64
|
+
}
|
|
65
|
+
let authenticated = false;
|
|
66
|
+
try {
|
|
67
|
+
exec('wrangler whoami', { silent: true });
|
|
68
|
+
authenticated = true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// 未認証
|
|
72
|
+
}
|
|
73
|
+
if (!authenticated) {
|
|
74
|
+
return fail('wrangler にログインしていません。`wrangler login` を実行してください。', 'AUTH_REQUIRED');
|
|
75
|
+
}
|
|
76
|
+
return ok({ version, authenticated });
|
|
77
|
+
}
|
|
78
|
+
export function checkPrerequisitesFor(targets) {
|
|
79
|
+
const results = {};
|
|
80
|
+
let allOk = true;
|
|
81
|
+
if (targets.includes('git')) {
|
|
82
|
+
const r = checkGit();
|
|
83
|
+
results.git = r;
|
|
84
|
+
if (!r.success)
|
|
85
|
+
allOk = false;
|
|
86
|
+
}
|
|
87
|
+
if (targets.includes('github')) {
|
|
88
|
+
const r = checkGhCli();
|
|
89
|
+
results.github = r;
|
|
90
|
+
if (!r.success)
|
|
91
|
+
allOk = false;
|
|
92
|
+
}
|
|
93
|
+
if (targets.includes('cloudflare')) {
|
|
94
|
+
const r = checkWrangler();
|
|
95
|
+
results.cloudflare = r;
|
|
96
|
+
if (!r.success)
|
|
97
|
+
allOk = false;
|
|
98
|
+
}
|
|
99
|
+
if (!allOk) {
|
|
100
|
+
const errors = Object.entries(results)
|
|
101
|
+
.filter(([, r]) => !r.success)
|
|
102
|
+
.map(([name, r]) => `${name}: ${r.error}`)
|
|
103
|
+
.join('; ');
|
|
104
|
+
return fail(errors, 'COMMAND_MISSING');
|
|
105
|
+
}
|
|
106
|
+
return ok({ results });
|
|
107
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/status — プロジェクト状態の検査
|
|
3
|
+
*
|
|
4
|
+
* AI エージェントが最初に呼ぶ関数。
|
|
5
|
+
* プロジェクトの現在の状態を構造的に返す。
|
|
6
|
+
*/
|
|
7
|
+
import type { ProjectConfig, ProjectStatus, OperationResult } from './types.js';
|
|
8
|
+
import type { ConfigValidationError } from './config.js';
|
|
9
|
+
export declare function inspectProject(projectDir: string): ProjectStatus;
|
|
10
|
+
export declare function isGitRepo(projectDir: string): boolean;
|
|
11
|
+
export declare function hasContentStructure(projectDir: string): boolean;
|
|
12
|
+
export declare function hasAdminBundle(projectDir: string): boolean;
|
|
13
|
+
export declare function hasValidConfig(projectDir: string): OperationResult<{
|
|
14
|
+
config: ProjectConfig | null;
|
|
15
|
+
errors: ConfigValidationError[];
|
|
16
|
+
}>;
|
|
17
|
+
export declare function listBranches(projectDir: string): string[];
|
|
18
|
+
export declare function getRemoteUrl(projectDir: string): string | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/status — プロジェクト状態の検査
|
|
3
|
+
*
|
|
4
|
+
* AI エージェントが最初に呼ぶ関数。
|
|
5
|
+
* プロジェクトの現在の状態を構造的に返す。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { ok } from './types.js';
|
|
10
|
+
import { exec, commandExists } from '../lib/shell.js';
|
|
11
|
+
import { readConfig, validateConfig } from './config.js';
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Full inspection
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export function inspectProject(projectDir) {
|
|
16
|
+
const config = readConfigSafe(projectDir);
|
|
17
|
+
const branches = listBranches(projectDir);
|
|
18
|
+
return {
|
|
19
|
+
projectDir,
|
|
20
|
+
hasGitRepo: isGitRepo(projectDir),
|
|
21
|
+
hasContentStructure: hasContentStructure(projectDir),
|
|
22
|
+
hasAdminBundle: hasAdminBundle(projectDir),
|
|
23
|
+
hasConfigJson: config !== null,
|
|
24
|
+
hasWranglerToml: fs.existsSync(path.join(projectDir, 'wrangler.toml')),
|
|
25
|
+
hasWorkflows: fs.existsSync(path.join(projectDir, '.github', 'workflows', 'build-staging.yml')),
|
|
26
|
+
hasTerraform: fs.existsSync(path.join(projectDir, 'terraform', 'main.tf')),
|
|
27
|
+
config,
|
|
28
|
+
branches,
|
|
29
|
+
remoteUrl: getRemoteUrl(projectDir),
|
|
30
|
+
missingPrerequisites: detectMissingPrerequisites(),
|
|
31
|
+
bundleVersion: getBundleVersionFromPackage(projectDir),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Individual checks
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
export function isGitRepo(projectDir) {
|
|
38
|
+
try {
|
|
39
|
+
exec('git rev-parse --is-inside-work-tree', { cwd: projectDir, silent: true });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function hasContentStructure(projectDir) {
|
|
47
|
+
return (fs.existsSync(path.join(projectDir, 'frelio-data', 'site', 'content_types')) &&
|
|
48
|
+
fs.existsSync(path.join(projectDir, 'frelio-data', 'admin', 'structure')));
|
|
49
|
+
}
|
|
50
|
+
export function hasAdminBundle(projectDir) {
|
|
51
|
+
return (fs.existsSync(path.join(projectDir, 'admin', 'index.html')) ||
|
|
52
|
+
fs.existsSync(path.join(projectDir, 'admin')));
|
|
53
|
+
}
|
|
54
|
+
export function hasValidConfig(projectDir) {
|
|
55
|
+
const readResult = readConfig(projectDir);
|
|
56
|
+
if (!readResult.success)
|
|
57
|
+
return readResult;
|
|
58
|
+
const config = readResult.data.config;
|
|
59
|
+
if (!config) {
|
|
60
|
+
return ok({ config: null, errors: [{ field: 'config.json', message: 'ファイルが存在しません' }] });
|
|
61
|
+
}
|
|
62
|
+
const validateResult = validateConfig(config);
|
|
63
|
+
if (!validateResult.success)
|
|
64
|
+
return validateResult;
|
|
65
|
+
return ok({ config, errors: validateResult.data.errors });
|
|
66
|
+
}
|
|
67
|
+
export function listBranches(projectDir) {
|
|
68
|
+
if (!isGitRepo(projectDir))
|
|
69
|
+
return [];
|
|
70
|
+
try {
|
|
71
|
+
const output = exec('git branch -a --format="%(refname:short)"', {
|
|
72
|
+
cwd: projectDir,
|
|
73
|
+
silent: true,
|
|
74
|
+
});
|
|
75
|
+
return output
|
|
76
|
+
.split('\n')
|
|
77
|
+
.map((b) => b.trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function getRemoteUrl(projectDir) {
|
|
85
|
+
if (!isGitRepo(projectDir))
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
return exec('git remote get-url origin', { cwd: projectDir, silent: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Internal helpers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
function readConfigSafe(projectDir) {
|
|
98
|
+
const result = readConfig(projectDir);
|
|
99
|
+
if (!result.success)
|
|
100
|
+
return null;
|
|
101
|
+
return result.data.config;
|
|
102
|
+
}
|
|
103
|
+
function detectMissingPrerequisites() {
|
|
104
|
+
const missing = [];
|
|
105
|
+
if (!commandExists('git'))
|
|
106
|
+
missing.push('git');
|
|
107
|
+
if (!commandExists('gh'))
|
|
108
|
+
missing.push('gh');
|
|
109
|
+
if (!commandExists('wrangler'))
|
|
110
|
+
missing.push('wrangler');
|
|
111
|
+
return missing;
|
|
112
|
+
}
|
|
113
|
+
function getBundleVersionFromPackage(projectDir) {
|
|
114
|
+
const versionPath = path.join(projectDir, 'version.json');
|
|
115
|
+
try {
|
|
116
|
+
const raw = fs.readFileSync(versionPath, 'utf-8');
|
|
117
|
+
return JSON.parse(raw).version ?? null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/template-scaffold — テンプレートからのコンテンツリポジトリ生成
|
|
3
|
+
*
|
|
4
|
+
* GitHub から c-time/frelio-content-template の tarball をダウンロードし、
|
|
5
|
+
* .hbs ファイルは変数置換、それ以外はそのままコピーして出力先に展開する。
|
|
6
|
+
*/
|
|
7
|
+
import { type ProjectConfig, type OperationResult } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* テンプレートからコンテンツリポジトリを生成する。
|
|
10
|
+
* GitHub から c-time/frelio-content-template を取得して展開する。
|
|
11
|
+
*/
|
|
12
|
+
export declare function scaffoldFromTemplate(projectDir: string, config: ProjectConfig): Promise<OperationResult<{
|
|
13
|
+
files: string[];
|
|
14
|
+
}>>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/template-scaffold — テンプレートからのコンテンツリポジトリ生成
|
|
3
|
+
*
|
|
4
|
+
* GitHub から c-time/frelio-content-template の tarball をダウンロードし、
|
|
5
|
+
* .hbs ファイルは変数置換、それ以外はそのままコピーして出力先に展開する。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { pipeline } from 'node:stream/promises';
|
|
11
|
+
import { extract } from 'tar';
|
|
12
|
+
import { ok, fail } from './types.js';
|
|
13
|
+
import { renderTemplate, projectConfigToVars } from '../lib/template-renderer.js';
|
|
14
|
+
const TEMPLATE_REPO = 'c-time/frelio-content-template';
|
|
15
|
+
const TEMPLATE_BRANCH = 'main';
|
|
16
|
+
/**
|
|
17
|
+
* GitHub から tarball をダウンロードして一時ディレクトリに展開する
|
|
18
|
+
*/
|
|
19
|
+
async function fetchTemplate() {
|
|
20
|
+
const url = `https://api.github.com/repos/${TEMPLATE_REPO}/tarball/${TEMPLATE_BRANCH}`;
|
|
21
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-template-'));
|
|
22
|
+
const tarPath = path.join(tmpDir, 'template.tar.gz');
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
25
|
+
redirect: 'follow',
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok || !response.body) {
|
|
28
|
+
throw new Error(`GitHub API responded with ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
const dest = fs.createWriteStream(tarPath);
|
|
31
|
+
await pipeline(response.body, dest);
|
|
32
|
+
const extractDir = path.join(tmpDir, 'extracted');
|
|
33
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
34
|
+
await extract({ file: tarPath, cwd: extractDir });
|
|
35
|
+
// GitHub の tarball は owner-repo-sha/ というプレフィックスディレクトリに入る
|
|
36
|
+
const entries = fs.readdirSync(extractDir);
|
|
37
|
+
if (entries.length !== 1) {
|
|
38
|
+
throw new Error(`Unexpected tarball structure: ${entries.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
return path.join(extractDir, entries[0]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* ディレクトリを再帰的に走査してファイル一覧を返す
|
|
44
|
+
*/
|
|
45
|
+
function walkDir(dir, base = dir) {
|
|
46
|
+
const files = [];
|
|
47
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
const fullPath = path.join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
files.push(...walkDir(fullPath, base));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
files.push(path.relative(base, fullPath));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
/** コピー対象外のファイル */
|
|
59
|
+
const SKIP_FILES = new Set(['template.config.json', 'README.md']);
|
|
60
|
+
/**
|
|
61
|
+
* テンプレートからコンテンツリポジトリを生成する。
|
|
62
|
+
* GitHub から c-time/frelio-content-template を取得して展開する。
|
|
63
|
+
*/
|
|
64
|
+
export async function scaffoldFromTemplate(projectDir, config) {
|
|
65
|
+
let templateDir;
|
|
66
|
+
try {
|
|
67
|
+
templateDir = await fetchTemplate();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return fail(`テンプレートの取得に失敗しました: ${e.message}`, 'EXEC_FAILED');
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const vars = projectConfigToVars(config);
|
|
74
|
+
const templateFiles = walkDir(templateDir);
|
|
75
|
+
const outputFiles = [];
|
|
76
|
+
for (const relPath of templateFiles) {
|
|
77
|
+
if (SKIP_FILES.has(relPath))
|
|
78
|
+
continue;
|
|
79
|
+
const srcPath = path.join(templateDir, relPath);
|
|
80
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
81
|
+
const isHbs = relPath.endsWith('.hbs');
|
|
82
|
+
const outputRelPath = isHbs ? relPath.slice(0, -4) : relPath;
|
|
83
|
+
const outputContent = isHbs ? renderTemplate(content, vars) : content;
|
|
84
|
+
const outputPath = path.join(projectDir, outputRelPath);
|
|
85
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
86
|
+
fs.writeFileSync(outputPath, outputContent, 'utf-8');
|
|
87
|
+
outputFiles.push(outputRelPath);
|
|
88
|
+
}
|
|
89
|
+
return ok({ files: outputFiles });
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return fail(`テンプレート展開失敗: ${e.message}`, 'EXEC_FAILED');
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
// 一時ディレクトリを削除
|
|
96
|
+
const tmpDir = path.resolve(templateDir, '..', '..');
|
|
97
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/terraform — Terraform ファイル生成
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { ok, fail } from './types.js';
|
|
7
|
+
import { generateTerraformProviders, generateTerraformVariables, generateTerraformMain, generateTerraformOutputs, generateTerraformTfvarsExample, generateTerraformReadme, writeFile, } from '../lib/templates.js';
|
|
8
|
+
export function generateTerraformFiles(projectDir, config) {
|
|
9
|
+
const terraformDir = path.join(projectDir, 'terraform');
|
|
10
|
+
const files = [];
|
|
11
|
+
try {
|
|
12
|
+
const writes = [
|
|
13
|
+
['providers.tf', generateTerraformProviders()],
|
|
14
|
+
['variables.tf', generateTerraformVariables(config)],
|
|
15
|
+
['main.tf', generateTerraformMain(config)],
|
|
16
|
+
['outputs.tf', generateTerraformOutputs()],
|
|
17
|
+
['terraform.tfvars.example', generateTerraformTfvarsExample(config)],
|
|
18
|
+
['README.md', generateTerraformReadme()],
|
|
19
|
+
];
|
|
20
|
+
for (const [name, content] of writes) {
|
|
21
|
+
const filePath = path.join(terraformDir, name);
|
|
22
|
+
writeFile(filePath, content);
|
|
23
|
+
files.push(filePath);
|
|
24
|
+
}
|
|
25
|
+
// .gitignore に Terraform エントリ追加
|
|
26
|
+
appendTerraformGitignore(projectDir);
|
|
27
|
+
return ok({ files });
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return fail(`Terraform ファイル生成失敗: ${e.message}`, 'EXEC_FAILED');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function appendTerraformGitignore(projectDir) {
|
|
34
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
35
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
36
|
+
const terraformIgnore = [
|
|
37
|
+
'',
|
|
38
|
+
'# Terraform',
|
|
39
|
+
'terraform/.terraform/',
|
|
40
|
+
'terraform/terraform.tfvars',
|
|
41
|
+
'terraform/*.tfstate',
|
|
42
|
+
'terraform/*.tfstate.backup',
|
|
43
|
+
].join('\n');
|
|
44
|
+
if (!existing.includes('terraform/.terraform/')) {
|
|
45
|
+
fs.appendFileSync(gitignorePath, terraformIgnore + '\n', 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
}
|