@c-time/frelio-cli 1.4.9 → 1.4.11
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 +179 -3
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +71 -0
- package/dist/commands/fleet.d.ts +16 -0
- package/dist/commands/fleet.js +110 -0
- package/dist/commands/init.js +1 -0
- package/dist/commands/set-mode.d.ts +10 -0
- package/dist/commands/set-mode.js +41 -0
- package/dist/commands/update.d.ts +7 -2
- package/dist/commands/update.js +72 -19
- package/dist/core/config.js +1 -0
- package/dist/core/doctor.d.ts +36 -0
- package/dist/core/doctor.js +86 -0
- package/dist/core/file-generators.d.ts +3 -0
- package/dist/core/file-generators.js +13 -3
- package/dist/core/fleet.d.ts +62 -0
- package/dist/core/fleet.js +172 -0
- package/dist/core/migrations/index.d.ts +3 -0
- package/dist/core/migrations/index.js +2 -0
- package/dist/core/migrations/node20-to-node24.d.ts +15 -0
- package/dist/core/migrations/node20-to-node24.js +35 -0
- package/dist/core/migrations/registry.d.ts +11 -0
- package/dist/core/migrations/registry.js +18 -0
- package/dist/core/migrations/types.d.ts +55 -0
- package/dist/core/migrations/types.js +11 -0
- package/dist/core/site-mode.d.ts +23 -0
- package/dist/core/site-mode.js +37 -0
- package/dist/core/template-refresh.d.ts +41 -0
- package/dist/core/template-refresh.js +148 -0
- package/dist/core/template-scaffold.d.ts +11 -0
- package/dist/core/template-scaffold.js +12 -2
- package/dist/core/types.d.ts +2 -0
- package/dist/core/version-check.d.ts +31 -0
- package/dist/core/version-check.js +114 -0
- package/dist/index.js +41 -3
- package/dist/lib/npm-registry.d.ts +12 -0
- package/dist/lib/npm-registry.js +30 -0
- package/dist/lib/template-renderer.js +6 -1
- package/dist/lib/templates.d.ts +36 -0
- package/dist/lib/templates.js +171 -0
- package/package.json +2 -2
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/template-refresh — 既存プロジェクトのテンプレ再生成(#86)
|
|
3
|
+
*
|
|
4
|
+
* frelio doctor(#85)が「古い」と指摘した workflow / functions / wrangler を、
|
|
5
|
+
* 最新テンプレートで再生成する(Pull の受け皿)。
|
|
6
|
+
*
|
|
7
|
+
* - ワークフロー(.github/)は GitHub のテンプレートリポジトリから取得して .hbs を描画する。
|
|
8
|
+
* (init と同じ取得経路。`fetchTemplate` を共有)
|
|
9
|
+
* - wrangler.toml / functions/storage / _redirects / _routes.json は config から決定的に生成する
|
|
10
|
+
* (lib/templates.ts の生成関数を共有。set-domain と同一の正本)
|
|
11
|
+
*
|
|
12
|
+
* ユーザー改変を保護するため、既存ファイルと差分がある場合は競合モードに従う
|
|
13
|
+
* (既定 backup: <file>.frelio-bak に退避してから上書き)。
|
|
14
|
+
*
|
|
15
|
+
* frelio-data/・public/・admin/config.json・functions/api/・workers/file-upload/src は触らない。
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { ok, fail } from './types.js';
|
|
20
|
+
import { fetchTemplate, cleanupTemplate } from './template-scaffold.js';
|
|
21
|
+
import { renderTemplate, projectConfigToVars } from '../lib/template-renderer.js';
|
|
22
|
+
import { generateWranglerToml, generateWorkerWranglerToml, generateRedirects, generateRoutesJson, generatePublicRoutesJson, generateStorageFunction, generateGateMiddleware, } from '../lib/templates.js';
|
|
23
|
+
const DEFAULT_TARGETS = ['workflows', 'wrangler', 'functions', 'routing'];
|
|
24
|
+
function toPosix(p) {
|
|
25
|
+
return p.split(path.sep).join('/');
|
|
26
|
+
}
|
|
27
|
+
/** .github/ 配下を再帰的に走査して相対パス一覧を返す */
|
|
28
|
+
function walkGithub(dir, base) {
|
|
29
|
+
const out = [];
|
|
30
|
+
if (!fs.existsSync(dir))
|
|
31
|
+
return out;
|
|
32
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const full = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
out.push(...walkGithub(full, base));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
out.push(path.relative(base, full));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** GitHub テンプレートの .github/ 配下から workflow/dependabot などの生成計画を作る */
|
|
44
|
+
function planWorkflows(templateDir, config) {
|
|
45
|
+
const vars = projectConfigToVars(config);
|
|
46
|
+
const githubDir = path.join(templateDir, '.github');
|
|
47
|
+
const files = walkGithub(githubDir, templateDir);
|
|
48
|
+
const planned = [];
|
|
49
|
+
for (const relPath of files) {
|
|
50
|
+
const raw = fs.readFileSync(path.join(templateDir, relPath), 'utf-8');
|
|
51
|
+
const isHbs = relPath.endsWith('.hbs');
|
|
52
|
+
const outRel = isHbs ? relPath.slice(0, -4) : relPath;
|
|
53
|
+
const content = isHbs ? renderTemplate(raw, vars) : raw;
|
|
54
|
+
planned.push({ relPath: toPosix(outRel), content });
|
|
55
|
+
}
|
|
56
|
+
return planned;
|
|
57
|
+
}
|
|
58
|
+
/** config から決定的に生成するファイル群の計画を作る */
|
|
59
|
+
function planGenerated(config, targets) {
|
|
60
|
+
const planned = [];
|
|
61
|
+
if (targets.includes('wrangler')) {
|
|
62
|
+
planned.push({ relPath: 'wrangler.toml', content: generateWranglerToml(config) });
|
|
63
|
+
planned.push({
|
|
64
|
+
relPath: 'workers/file-upload/wrangler.toml',
|
|
65
|
+
content: generateWorkerWranglerToml(config),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (targets.includes('functions')) {
|
|
69
|
+
planned.push({
|
|
70
|
+
relPath: 'functions/storage/[[path]].ts',
|
|
71
|
+
content: generateStorageFunction(),
|
|
72
|
+
});
|
|
73
|
+
// 本番ゲート middleware(Issue #93)
|
|
74
|
+
planned.push({
|
|
75
|
+
relPath: 'functions/_middleware.ts',
|
|
76
|
+
content: generateGateMiddleware(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (targets.includes('routing')) {
|
|
80
|
+
planned.push({ relPath: '_redirects', content: generateRedirects() });
|
|
81
|
+
planned.push({ relPath: '_routes.json', content: generateRoutesJson() });
|
|
82
|
+
// コンテンツ配信側 _routes.json(include /* で全リクエストをゲート経由に / Issue #93)
|
|
83
|
+
planned.push({ relPath: 'public/_routes.json', content: generatePublicRoutesJson() });
|
|
84
|
+
}
|
|
85
|
+
return planned;
|
|
86
|
+
}
|
|
87
|
+
/** 1ファイルを競合モードに従って適用する */
|
|
88
|
+
function applyFile(projectDir, file, mode, result) {
|
|
89
|
+
const dest = path.join(projectDir, file.relPath);
|
|
90
|
+
const exists = fs.existsSync(dest);
|
|
91
|
+
if (exists) {
|
|
92
|
+
const current = fs.readFileSync(dest, 'utf-8');
|
|
93
|
+
if (current === file.content) {
|
|
94
|
+
result.unchanged.push(file.relPath);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// 差分あり
|
|
98
|
+
if (mode === 'skip') {
|
|
99
|
+
result.skipped.push(file.relPath);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (mode === 'backup') {
|
|
103
|
+
fs.writeFileSync(`${dest}.frelio-bak`, current, 'utf-8');
|
|
104
|
+
result.backedUp.push(file.relPath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
108
|
+
fs.writeFileSync(dest, file.content, 'utf-8');
|
|
109
|
+
result.written.push(file.relPath);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* テンプレートを再生成して既存プロジェクトに適用する。
|
|
113
|
+
*/
|
|
114
|
+
export async function refreshTemplates(projectDir, config, opts = {}) {
|
|
115
|
+
const targets = opts.targets ?? DEFAULT_TARGETS;
|
|
116
|
+
const mode = opts.mode ?? 'backup';
|
|
117
|
+
const result = { written: [], unchanged: [], skipped: [], backedUp: [] };
|
|
118
|
+
const needsTemplate = targets.includes('workflows');
|
|
119
|
+
let templateDir = opts.templateDir ?? null;
|
|
120
|
+
let ownTemplate = false;
|
|
121
|
+
try {
|
|
122
|
+
if (needsTemplate && !templateDir) {
|
|
123
|
+
try {
|
|
124
|
+
templateDir = await fetchTemplate();
|
|
125
|
+
ownTemplate = true;
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return fail(`テンプレートの取得に失敗しました: ${e.message}`, 'EXEC_FAILED');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const planned = [];
|
|
132
|
+
if (targets.includes('workflows') && templateDir) {
|
|
133
|
+
planned.push(...planWorkflows(templateDir, config));
|
|
134
|
+
}
|
|
135
|
+
planned.push(...planGenerated(config, targets));
|
|
136
|
+
for (const file of planned) {
|
|
137
|
+
applyFile(projectDir, file, mode, result);
|
|
138
|
+
}
|
|
139
|
+
return ok(result);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return fail(`テンプレート再生成に失敗しました: ${e.message}`, 'EXEC_FAILED');
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
if (ownTemplate && templateDir)
|
|
146
|
+
cleanupTemplate(templateDir);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
* .hbs ファイルは変数置換、それ以外はそのままコピーして出力先に展開する。
|
|
6
6
|
*/
|
|
7
7
|
import { type ProjectConfig, type OperationResult } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* GitHub から tarball をダウンロードして一時ディレクトリに展開する。
|
|
10
|
+
* 展開済みテンプレートのルートディレクトリ(owner-repo-sha/)の絶対パスを返す。
|
|
11
|
+
* 呼び出し側は使用後に `cleanupTemplate(dir)` で一時ディレクトリを削除する。
|
|
12
|
+
*/
|
|
13
|
+
export declare function fetchTemplate(): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* fetchTemplate が作成した一時ディレクトリを削除する。
|
|
16
|
+
* 引数は fetchTemplate の戻り値(展開ルート)。
|
|
17
|
+
*/
|
|
18
|
+
export declare function cleanupTemplate(templateDir: string): void;
|
|
8
19
|
/**
|
|
9
20
|
* テンプレートからコンテンツリポジトリを生成する。
|
|
10
21
|
* GitHub から c-time/frelio-content-template を取得して展開する。
|
|
@@ -14,9 +14,11 @@ import { renderTemplate, projectConfigToVars } from '../lib/template-renderer.js
|
|
|
14
14
|
const TEMPLATE_REPO = 'c-time/frelio-content-template';
|
|
15
15
|
const TEMPLATE_BRANCH = 'main';
|
|
16
16
|
/**
|
|
17
|
-
* GitHub から tarball
|
|
17
|
+
* GitHub から tarball をダウンロードして一時ディレクトリに展開する。
|
|
18
|
+
* 展開済みテンプレートのルートディレクトリ(owner-repo-sha/)の絶対パスを返す。
|
|
19
|
+
* 呼び出し側は使用後に `cleanupTemplate(dir)` で一時ディレクトリを削除する。
|
|
18
20
|
*/
|
|
19
|
-
async function fetchTemplate() {
|
|
21
|
+
export async function fetchTemplate() {
|
|
20
22
|
const url = `https://api.github.com/repos/${TEMPLATE_REPO}/tarball/${TEMPLATE_BRANCH}`;
|
|
21
23
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frelio-template-'));
|
|
22
24
|
const tarPath = path.join(tmpDir, 'template.tar.gz');
|
|
@@ -39,6 +41,14 @@ async function fetchTemplate() {
|
|
|
39
41
|
}
|
|
40
42
|
return path.join(extractDir, entries[0]);
|
|
41
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* fetchTemplate が作成した一時ディレクトリを削除する。
|
|
46
|
+
* 引数は fetchTemplate の戻り値(展開ルート)。
|
|
47
|
+
*/
|
|
48
|
+
export function cleanupTemplate(templateDir) {
|
|
49
|
+
const tmpDir = path.resolve(templateDir, '..', '..');
|
|
50
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
42
52
|
/**
|
|
43
53
|
* ディレクトリを再帰的に走査してファイル一覧を返す
|
|
44
54
|
*/
|
package/dist/core/types.d.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/version-check — CLI 起動時バージョンチェック(#87)
|
|
3
|
+
*
|
|
4
|
+
* 到達できない匿名ユーザーにも「古い・期限付き対応が要る」を実行経路で届ける。
|
|
5
|
+
* - npm の最新版を確認し、古ければナッジ表示(stderr)
|
|
6
|
+
* - 頻度制御(キャッシュ・TTL)/オフライン耐性(タイムアウト・失敗は無言)/opt-out
|
|
7
|
+
*/
|
|
8
|
+
/** キャッシュファイルのパス(~/.cache/frelio/update-check.json、失敗時は tmpdir) */
|
|
9
|
+
export declare function defaultCachePath(): string;
|
|
10
|
+
/**
|
|
11
|
+
* セムバー比較。a が b より新しければ正、古ければ負、同じなら 0。
|
|
12
|
+
* プレリリースタグは無視して数値部のみ比較する。
|
|
13
|
+
*/
|
|
14
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
15
|
+
export type VersionCheckOptions = {
|
|
16
|
+
cachePath?: string;
|
|
17
|
+
/** テスト用に現在時刻を注入 */
|
|
18
|
+
now?: number;
|
|
19
|
+
/** テスト用に取得関数を差し替え */
|
|
20
|
+
fetchLatest?: (pkg: string, signal?: AbortSignal) => Promise<string>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 最新版を確認し、現在版より新しければナッジ文字列を返す。
|
|
24
|
+
* チェック不要・最新・失敗時は null。副作用はキャッシュ更新のみ(表示しない)。
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkForUpdate(currentVersion: string, opts?: VersionCheckOptions): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* 起動時に最新版チェックを行い、必要ならナッジを stderr に表示する。
|
|
29
|
+
* 失敗しても呼び出し元の動作には影響しない。
|
|
30
|
+
*/
|
|
31
|
+
export declare function maybeNotifyUpdate(currentVersion: string, opts?: VersionCheckOptions): Promise<void>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/version-check — CLI 起動時バージョンチェック(#87)
|
|
3
|
+
*
|
|
4
|
+
* 到達できない匿名ユーザーにも「古い・期限付き対応が要る」を実行経路で届ける。
|
|
5
|
+
* - npm の最新版を確認し、古ければナッジ表示(stderr)
|
|
6
|
+
* - 頻度制御(キャッシュ・TTL)/オフライン耐性(タイムアウト・失敗は無言)/opt-out
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { getLatestNpmVersion } from '../lib/npm-registry.js';
|
|
12
|
+
const PACKAGE_NAME = '@c-time/frelio-cli';
|
|
13
|
+
const ENV_OPT_OUT = 'FRELIO_NO_UPDATE_CHECK';
|
|
14
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 1日
|
|
15
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
16
|
+
/** キャッシュファイルのパス(~/.cache/frelio/update-check.json、失敗時は tmpdir) */
|
|
17
|
+
export function defaultCachePath() {
|
|
18
|
+
const base = process.env.XDG_CACHE_HOME ||
|
|
19
|
+
(process.platform === 'win32' && process.env.LOCALAPPDATA) ||
|
|
20
|
+
path.join(os.homedir(), '.cache');
|
|
21
|
+
return path.join(base, 'frelio', 'update-check.json');
|
|
22
|
+
}
|
|
23
|
+
function readCache(cachePath) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeCache(cachePath, data) {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
34
|
+
fs.writeFileSync(cachePath, JSON.stringify(data), 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// キャッシュ書き込み失敗は致命的でない
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* セムバー比較。a が b より新しければ正、古ければ負、同じなら 0。
|
|
42
|
+
* プレリリースタグは無視して数値部のみ比較する。
|
|
43
|
+
*/
|
|
44
|
+
export function compareSemver(a, b) {
|
|
45
|
+
const norm = (v) => v
|
|
46
|
+
.replace(/^v/, '')
|
|
47
|
+
.split('-')[0]
|
|
48
|
+
.split('.')
|
|
49
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
50
|
+
const pa = norm(a);
|
|
51
|
+
const pb = norm(b);
|
|
52
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
53
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
54
|
+
if (d !== 0)
|
|
55
|
+
return d > 0 ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 最新版を確認し、現在版より新しければナッジ文字列を返す。
|
|
61
|
+
* チェック不要・最新・失敗時は null。副作用はキャッシュ更新のみ(表示しない)。
|
|
62
|
+
*/
|
|
63
|
+
export async function checkForUpdate(currentVersion, opts = {}) {
|
|
64
|
+
// opt-out(明示無効化 or CI)
|
|
65
|
+
if (process.env[ENV_OPT_OUT] || process.env.CI)
|
|
66
|
+
return null;
|
|
67
|
+
const now = opts.now ?? Date.now();
|
|
68
|
+
const cachePath = opts.cachePath ?? defaultCachePath();
|
|
69
|
+
const cache = readCache(cachePath);
|
|
70
|
+
let latest = null;
|
|
71
|
+
if (cache && now - cache.lastCheck < TTL_MS) {
|
|
72
|
+
// TTL 内: ネットワークは叩かずキャッシュの値で判定
|
|
73
|
+
latest = cache.latest;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const fetchLatest = opts.fetchLatest ?? getLatestNpmVersion;
|
|
77
|
+
try {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
80
|
+
try {
|
|
81
|
+
latest = await fetchLatest(PACKAGE_NAME, controller.signal);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
writeCache(cachePath, { lastCheck: now, latest });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// オフライン等は無言で諦める
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (latest && compareSemver(latest, currentVersion) > 0) {
|
|
94
|
+
return (`新しい ${PACKAGE_NAME} が公開されています: ${currentVersion} → ${latest}\n` +
|
|
95
|
+
` 更新: npm i -g ${PACKAGE_NAME}@latest / 実行のみ: npx ${PACKAGE_NAME}@latest <command>\n` +
|
|
96
|
+
` (このチェックは ${ENV_OPT_OUT}=1 で無効化できます)`);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 起動時に最新版チェックを行い、必要ならナッジを stderr に表示する。
|
|
102
|
+
* 失敗しても呼び出し元の動作には影響しない。
|
|
103
|
+
*/
|
|
104
|
+
export async function maybeNotifyUpdate(currentVersion, opts = {}) {
|
|
105
|
+
try {
|
|
106
|
+
const message = await checkForUpdate(currentVersion, opts);
|
|
107
|
+
if (message) {
|
|
108
|
+
console.error(`\n⚠ ${message}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// 何があっても本処理を妨げない
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
2
5
|
import { Command } from 'commander';
|
|
3
6
|
import { initCommand } from './commands/init.js';
|
|
4
7
|
import { updateCommand } from './commands/update.js';
|
|
5
8
|
import { addStagingCommand } from './commands/add-staging.js';
|
|
6
9
|
import { setDomainCommand } from './commands/set-domain.js';
|
|
10
|
+
import { setModeCommand } from './commands/set-mode.js';
|
|
11
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
12
|
+
import { fleetCommand } from './commands/fleet.js';
|
|
13
|
+
import { maybeNotifyUpdate } from './core/version-check.js';
|
|
14
|
+
// バージョンは package.json から読む(ハードコードしない)
|
|
15
|
+
const pkg = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf-8'));
|
|
7
16
|
const program = new Command();
|
|
8
17
|
program
|
|
9
18
|
.name('frelio')
|
|
10
19
|
.description('Frelio CMS setup and management CLI')
|
|
11
|
-
.version(
|
|
20
|
+
.version(pkg.version);
|
|
12
21
|
program
|
|
13
22
|
.command('init')
|
|
14
23
|
.description('Initialize a new Frelio CMS project (creates repo, deploys admin, sets up R2)')
|
|
@@ -29,9 +38,17 @@ program
|
|
|
29
38
|
.action(initCommand);
|
|
30
39
|
program
|
|
31
40
|
.command('update')
|
|
32
|
-
.description('Update CMS admin bundle
|
|
41
|
+
.description('Update CMS admin bundle and regenerate templates (workflows / functions / wrangler)')
|
|
33
42
|
.option('--version <version>', 'Specific version to install (default: latest)')
|
|
43
|
+
.option('--bundle-only', 'Update only the CMS admin bundle (skip template regeneration)')
|
|
44
|
+
.option('--templates-only', 'Regenerate only templates (skip CMS admin bundle)')
|
|
45
|
+
.option('--force', 'Overwrite changed files without creating .frelio-bak backups')
|
|
34
46
|
.action(updateCommand);
|
|
47
|
+
program
|
|
48
|
+
.command('doctor')
|
|
49
|
+
.description('生成物を診断し、期限付き廃止や未適用のテンプレ更新を検出する')
|
|
50
|
+
.option('--ci', '対応が必要な指摘があれば非ゼロの終了コードで終了する')
|
|
51
|
+
.action(doctorCommand);
|
|
35
52
|
program
|
|
36
53
|
.command('add-staging')
|
|
37
54
|
.description('Add a staging environment (creates a staging-* branch; deployed via GitHub Actions wrangler --branch)')
|
|
@@ -46,4 +63,25 @@ program
|
|
|
46
63
|
.option('--r2-public-url <url>', 'New R2 public URL')
|
|
47
64
|
.option('--regen-staging-hash', 'Regenerate the staging subdomain hash instead of preserving it')
|
|
48
65
|
.action(setDomainCommand);
|
|
49
|
-
program
|
|
66
|
+
program
|
|
67
|
+
.command('fleet')
|
|
68
|
+
.description('Scan managed content repos and report bundle versions + pending migrations (operator / #89)')
|
|
69
|
+
.option('--org <org>', 'GitHub org/user to enumerate (filters to frelio repos via version.json)')
|
|
70
|
+
.option('--repos <repos>', 'Comma-separated owner/repo list')
|
|
71
|
+
.option('--repo-list <file>', 'File with one owner/repo per line (# comments allowed)')
|
|
72
|
+
.option('--json', 'Output as JSON')
|
|
73
|
+
.option('--ci', 'Exit non-zero if any actionable (critical/warning) migration is pending')
|
|
74
|
+
.action(fleetCommand);
|
|
75
|
+
program
|
|
76
|
+
.command('set-mode <mode>')
|
|
77
|
+
.description('Set the production gate mode (live | prelaunch | maintenance | closed) and regenerate config + edge middleware (#93)')
|
|
78
|
+
.action(setModeCommand);
|
|
79
|
+
async function main() {
|
|
80
|
+
await program.parseAsync();
|
|
81
|
+
// コマンド完了後に最新版チェック(best-effort、本処理には影響しない)
|
|
82
|
+
await maybeNotifyUpdate(pkg.version);
|
|
83
|
+
}
|
|
84
|
+
main().catch((e) => {
|
|
85
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
@@ -5,6 +5,18 @@ export type Release = {
|
|
|
5
5
|
tag_name: string;
|
|
6
6
|
tarballUrl: string;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* 任意の npm パッケージの最新バージョン(dist-tags.latest)を取得する。
|
|
10
|
+
* 起動時バージョンチェック(#87)など、tarball を要しない用途に使う。
|
|
11
|
+
* `signal` でタイムアウト/中断を渡せる。
|
|
12
|
+
*/
|
|
13
|
+
export declare function getLatestNpmVersion(packageName: string, signal?: AbortSignal): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* 任意 npm パッケージの直近 1 か月のダウンロード数を取得する(#89)。
|
|
16
|
+
* 匿名層の粗い母数把握に使う。失敗(オフライン等)は null。
|
|
17
|
+
* `fetchImpl` はテスト用に差し替え可能。
|
|
18
|
+
*/
|
|
19
|
+
export declare function getMonthlyDownloads(packageName: string, fetchImpl?: typeof fetch): Promise<number | null>;
|
|
8
20
|
export declare function getLatestRelease(): Promise<Release>;
|
|
9
21
|
export declare function getRelease(version: string): Promise<Release>;
|
|
10
22
|
export declare function downloadTarball(release: Release, destDir: string): Promise<string>;
|
package/dist/lib/npm-registry.js
CHANGED
|
@@ -7,6 +7,36 @@ import { Readable } from 'node:stream';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
const PACKAGE_NAME = '@c-time/frelio-cms';
|
|
9
9
|
const REGISTRY = 'https://registry.npmjs.org';
|
|
10
|
+
/**
|
|
11
|
+
* 任意の npm パッケージの最新バージョン(dist-tags.latest)を取得する。
|
|
12
|
+
* 起動時バージョンチェック(#87)など、tarball を要しない用途に使う。
|
|
13
|
+
* `signal` でタイムアウト/中断を渡せる。
|
|
14
|
+
*/
|
|
15
|
+
export async function getLatestNpmVersion(packageName, signal) {
|
|
16
|
+
const res = await fetch(`${REGISTRY}/${packageName}/latest`, { signal });
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
throw new Error(`Failed to fetch latest version of ${packageName}: ${res.status}`);
|
|
19
|
+
}
|
|
20
|
+
const info = (await res.json());
|
|
21
|
+
return info.version;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 任意 npm パッケージの直近 1 か月のダウンロード数を取得する(#89)。
|
|
25
|
+
* 匿名層の粗い母数把握に使う。失敗(オフライン等)は null。
|
|
26
|
+
* `fetchImpl` はテスト用に差し替え可能。
|
|
27
|
+
*/
|
|
28
|
+
export async function getMonthlyDownloads(packageName, fetchImpl = fetch) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetchImpl(`https://api.npmjs.org/downloads/point/last-month/${packageName}`);
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
return null;
|
|
33
|
+
const data = (await res.json());
|
|
34
|
+
return typeof data.downloads === 'number' ? data.downloads : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
10
40
|
export async function getLatestRelease() {
|
|
11
41
|
const res = await fetch(`${REGISTRY}/${PACKAGE_NAME}/latest`);
|
|
12
42
|
if (!res.ok) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* {{variable}} 形式のプレースホルダーを値に置換する。
|
|
5
5
|
* GitHub Actions の ${{ expression }} とは衝突しない。
|
|
6
6
|
*/
|
|
7
|
-
import { buildAllowedOrigins } from './templates.js';
|
|
7
|
+
import { buildAllowedOrigins, resolveSiteMode, deriveCanonicalHost, deriveContentPagesDev, } from './templates.js';
|
|
8
8
|
/**
|
|
9
9
|
* テンプレート文字列内の {{variable}} を置換する
|
|
10
10
|
*/
|
|
@@ -31,5 +31,10 @@ export function projectConfigToVars(config) {
|
|
|
31
31
|
ownerUsername: config.ownerUsername,
|
|
32
32
|
allowedOrigins: buildAllowedOrigins(config),
|
|
33
33
|
cloudflareAccountId: config.cloudflareAccountId ?? '',
|
|
34
|
+
saveTimezone: config.saveTimezone || '+09:00',
|
|
35
|
+
// 本番ゲート(Issue #93)
|
|
36
|
+
siteMode: resolveSiteMode(config),
|
|
37
|
+
canonicalHost: deriveCanonicalHost(config),
|
|
38
|
+
contentPagesDev: deriveContentPagesDev(config),
|
|
34
39
|
};
|
|
35
40
|
}
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -14,7 +14,23 @@ export type ProjectConfig = {
|
|
|
14
14
|
ownerUsername: string;
|
|
15
15
|
stagingDomain: string;
|
|
16
16
|
cloudflareAccountId: string;
|
|
17
|
+
/** 保存タイムゾーン(ISO 8601 オフセット表記、例 "+09:00")。issue #92 */
|
|
18
|
+
saveTimezone: string;
|
|
19
|
+
/**
|
|
20
|
+
* 本番ゲートのサイト公開状態(Issue #93)。未指定は 'live'(通常配信)。
|
|
21
|
+
* コンテンツ配信の functions/_middleware.ts がエッジで参照する。
|
|
22
|
+
*/
|
|
23
|
+
siteMode?: SiteMode;
|
|
17
24
|
};
|
|
25
|
+
/** 本番ゲートのサイト公開状態(Issue #93) */
|
|
26
|
+
export declare const SITE_MODES: readonly ["live", "prelaunch", "maintenance", "closed"];
|
|
27
|
+
export type SiteMode = (typeof SITE_MODES)[number];
|
|
28
|
+
/** config.siteMode を正規化する(未知値・未指定は 'live') */
|
|
29
|
+
export declare function resolveSiteMode(config: ProjectConfig): SiteMode;
|
|
30
|
+
/** productionUrl から正規化先ホスト(独自ドメイン)を導出する。無効/空なら空文字。 */
|
|
31
|
+
export declare function deriveCanonicalHost(config: ProjectConfig): string;
|
|
32
|
+
/** コンテンツ配信 Pages の本番 pages.dev ホストを導出する(例: mysite.pages.dev)。 */
|
|
33
|
+
export declare function deriveContentPagesDev(config: ProjectConfig): string;
|
|
18
34
|
/**
|
|
19
35
|
* ランダムな8文字のハッシュを生成(URL推測を困難にする)
|
|
20
36
|
*/
|
|
@@ -62,6 +78,26 @@ export declare function generateWranglerToml(config: ProjectConfig): string;
|
|
|
62
78
|
export declare function generateWorkerWranglerToml(config: ProjectConfig): string;
|
|
63
79
|
export declare function generateRedirects(): string;
|
|
64
80
|
export declare function generateRoutesJson(): string;
|
|
81
|
+
/**
|
|
82
|
+
* コンテンツ配信側(public/)の _routes.json を生成する。
|
|
83
|
+
*
|
|
84
|
+
* 本番ゲート(Issue #93)の functions/_middleware.ts に全リクエストを通すため
|
|
85
|
+
* include を `/*` にする。これにより静的ページもエッジ middleware を経由する
|
|
86
|
+
* (live モードでは即 next() で素通り)。
|
|
87
|
+
*/
|
|
88
|
+
export declare function generatePublicRoutesJson(): string;
|
|
89
|
+
/**
|
|
90
|
+
* 本番ゲート middleware(Issue #93)を生成する。
|
|
91
|
+
*
|
|
92
|
+
* 注意: この functions/ は管理画面 Pages にも配られる共有資産のため、
|
|
93
|
+
* ホスト判定で「コンテンツのホスト」のみを対象にし、admin / staging /
|
|
94
|
+
* localhost / 不明ホストは素通り(next)して CMS を保護する。
|
|
95
|
+
*
|
|
96
|
+
* テンプレート実ファイル
|
|
97
|
+
* (templates/content-repo/functions/_middleware.ts)と同一文字列を返す。
|
|
98
|
+
* 乖離は sync テストで検出する。
|
|
99
|
+
*/
|
|
100
|
+
export declare function generateGateMiddleware(): string;
|
|
65
101
|
export declare function generateStorageFunction(): string;
|
|
66
102
|
export declare function generateTerraformProviders(): string;
|
|
67
103
|
export declare function generateTerraformVariables(config: ProjectConfig): string;
|