@mezzanine-stack/create-mezzanine 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.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # create-mezzanine
2
+
3
+ Mezzanine Platform のプロジェクト生成・運用補助 CLI です。
4
+
5
+ ## 提供コマンド
6
+
7
+ - `create-starter`
8
+ - `contact-env`
9
+ - `manifest-init`
10
+ - `tenant-add`
11
+ - `tenant-list`
12
+
13
+ ## 例
14
+
15
+ ```bash
16
+ npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare
17
+ npx create-mezzanine contact-env --output ./contact-vars.example.toml
18
+ npx create-mezzanine manifest-init --tenant acme --output ./customer-manifest.yaml
19
+ ```
20
+
21
+ ## 用途
22
+
23
+ - `create-starter`: `apps/site-starter-astro` と必要最小限 package をコピーして standalone リポジトリを生成
24
+ - `contact-env`: contact service 向け `wrangler.toml [vars]` テンプレートを出力
25
+ - `manifest-init`: managed ops 用 `customer-manifest.yaml` テンプレートを出力
26
+ - `tenant-add` / `tenant-list`: マルチテナント運用向け KV 設定補助
27
+
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-mezzanine — Mezzanine Platform プロジェクト生成 CLI
4
+ *
5
+ * Phase 5: contact service の導入補助サブコマンドを提供します。
6
+ * Phase 6: managed ops 用の manifest-init サブコマンドを追加しました。
7
+ * Phase 6 (multi-tenant): KV テナント管理の tenant-add / tenant-list を追加しました。
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;GAMG"}
package/dist/cli.js ADDED
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-mezzanine — Mezzanine Platform プロジェクト生成 CLI
4
+ *
5
+ * Phase 5: contact service の導入補助サブコマンドを提供します。
6
+ * Phase 6: managed ops 用の manifest-init サブコマンドを追加しました。
7
+ * Phase 6 (multi-tenant): KV テナント管理の tenant-add / tenant-list を追加しました。
8
+ */
9
+ const [, , command, ...args] = process.argv;
10
+ async function main() {
11
+ switch (command) {
12
+ case "create-starter":
13
+ await createStarterProject(args);
14
+ break;
15
+ case "contact-env":
16
+ await printContactEnvTemplate(args);
17
+ break;
18
+ case "manifest-init":
19
+ await printManifestTemplate(args);
20
+ break;
21
+ case "tenant-add":
22
+ await printTenantConfig(args);
23
+ break;
24
+ case "tenant-list":
25
+ await listTenants(args);
26
+ break;
27
+ case "help":
28
+ case "--help":
29
+ case "-h":
30
+ case undefined:
31
+ printHelp();
32
+ break;
33
+ default:
34
+ console.error(`Unknown command: ${command}`);
35
+ printHelp();
36
+ process.exit(1);
37
+ }
38
+ }
39
+ main().catch((err) => {
40
+ console.error(err);
41
+ process.exit(1);
42
+ });
43
+ function printHelp() {
44
+ console.log(`
45
+ create-mezzanine — Mezzanine Platform CLI
46
+
47
+ Usage:
48
+ create-mezzanine <command> [options]
49
+
50
+ Commands:
51
+ create-starter [--output <dir>] [--hosting <cloudflare|vercel|netlify|static>] [--name <project-name>] [--smoke-test]
52
+ starter + 必要最小限 package をコピーして、単体で動く顧客サイト repo を生成する
53
+ --smoke-test 指定時は生成直後に pnpm install / pnpm build を実行して検証する
54
+
55
+ contact-env [--output <path>]
56
+ contact-service-cloudflare 用の wrangler.toml [vars] テンプレートを出力する
57
+
58
+ manifest-init [--tenant <id>] [--output <path>]
59
+ managed ops 用の customer manifest テンプレートを出力する
60
+
61
+ tenant-add --origin <url> --to <email> --from <email> --success <url>
62
+ [--tenant <id>] [--subject <prefix>] [--failure <url>]
63
+ [--output <path>]
64
+ KV (TENANT_CONFIG_KV) に投入する顧客設定 JSON を出力する
65
+ Auth KV (AUTH_ALLOWED_KV) への origin 登録コマンドも案内する
66
+
67
+ tenant-list --namespace-id <id>
68
+ wrangler CLI 経由で TENANT_CONFIG_KV のテナント一覧を表示する
69
+
70
+ help
71
+ このヘルプを表示する
72
+
73
+ Examples:
74
+ npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare
75
+ npx create-mezzanine create-starter --output ../acme-site --hosting static --name acme-site
76
+ npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare --smoke-test
77
+ npx create-mezzanine contact-env
78
+ npx create-mezzanine manifest-init --tenant my-client --output customers/my-client/manifest.yaml
79
+ npx create-mezzanine tenant-add \\
80
+ --origin https://acme.com \\
81
+ --to owner@acme.com \\
82
+ --from noreply@your-domain.com \\
83
+ --success https://acme.com/contact/thanks \\
84
+ --output tenant-acme.json
85
+ npx create-mezzanine tenant-list --namespace-id xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
86
+ `);
87
+ }
88
+ /**
89
+ * create-starter:
90
+ * `apps/site-starter-astro` と依存 package の最小セットをコピーし、
91
+ * 顧客向けの standalone リポジトリを生成する。
92
+ */
93
+ async function createStarterProject(args) {
94
+ const get = (flag) => {
95
+ const i = args.indexOf(flag);
96
+ return i !== -1 ? args[i + 1] : undefined;
97
+ };
98
+ const has = (flag) => args.includes(flag);
99
+ const outputArg = get("--output");
100
+ if (!outputArg) {
101
+ console.error("Error: --output は必須です。");
102
+ console.error(" 例: npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare");
103
+ process.exit(1);
104
+ }
105
+ const hosting = (get("--hosting") ?? "cloudflare").toLowerCase();
106
+ if (!["cloudflare", "vercel", "netlify", "static"].includes(hosting)) {
107
+ console.error(`Error: --hosting は cloudflare|vercel|netlify|static のいずれかを指定してください。受け取った値: ${hosting}`);
108
+ process.exit(1);
109
+ }
110
+ const projectName = get("--name") ?? "mezzanine-customer-site";
111
+ const smokeTest = has("--smoke-test");
112
+ const fs = await import("node:fs");
113
+ const path = await import("node:path");
114
+ const { fileURLToPath } = await import("node:url");
115
+ const thisFile = fileURLToPath(import.meta.url);
116
+ const repoRoot = path.resolve(path.dirname(thisFile), "../../..");
117
+ const outDir = path.resolve(process.cwd(), outputArg);
118
+ if (fs.existsSync(outDir) && fs.readdirSync(outDir).length > 0) {
119
+ console.error(`Error: 出力先ディレクトリが空ではありません: ${outDir}`);
120
+ process.exit(1);
121
+ }
122
+ fs.mkdirSync(outDir, { recursive: true });
123
+ const appSrc = path.join(repoRoot, "apps/site-starter-astro");
124
+ const packageNames = [
125
+ "tokens",
126
+ "css",
127
+ "ui",
128
+ "content-schema",
129
+ "astro-renderer",
130
+ "cms-core",
131
+ "git-provider-github",
132
+ ];
133
+ const copyFiltered = (src, dest) => {
134
+ fs.cpSync(src, dest, {
135
+ recursive: true,
136
+ filter: (entry) => {
137
+ const normalized = entry.replace(/\\/g, "/");
138
+ if (normalized.endsWith("/node_modules") ||
139
+ normalized.includes("/node_modules/") ||
140
+ normalized.endsWith("/dist") ||
141
+ normalized.includes("/dist/") ||
142
+ normalized.endsWith("/.astro") ||
143
+ normalized.includes("/.astro/") ||
144
+ normalized.endsWith("/.git") ||
145
+ normalized.includes("/.git/")) {
146
+ return false;
147
+ }
148
+ return true;
149
+ },
150
+ });
151
+ };
152
+ // apps/site-starter-astro
153
+ const appDest = path.join(outDir, "apps/site-starter-astro");
154
+ fs.mkdirSync(path.dirname(appDest), { recursive: true });
155
+ copyFiltered(appSrc, appDest);
156
+ // 必要 package のみコピー
157
+ for (const name of packageNames) {
158
+ const src = path.join(repoRoot, "packages", name);
159
+ const dest = path.join(outDir, "packages", name);
160
+ copyFiltered(src, dest);
161
+ }
162
+ // ルート設定ファイルを生成
163
+ fs.writeFileSync(path.join(outDir, "package.json"), JSON.stringify({
164
+ name: projectName,
165
+ private: true,
166
+ version: "0.1.0",
167
+ packageManager: "pnpm@10.28.2",
168
+ scripts: {
169
+ dev: "pnpm --filter @mezzanine/site-starter-astro dev",
170
+ build: "pnpm --filter @mezzanine/site-starter-astro build",
171
+ preview: "pnpm --filter @mezzanine/site-starter-astro preview",
172
+ typecheck: "pnpm -r typecheck",
173
+ },
174
+ }, null, 2) + "\n", "utf-8");
175
+ fs.writeFileSync(path.join(outDir, "pnpm-workspace.yaml"), 'packages:\n - "apps/*"\n - "packages/*"\n', "utf-8");
176
+ const tsconfigBaseSrc = path.join(repoRoot, "tsconfig.base.json");
177
+ if (fs.existsSync(tsconfigBaseSrc)) {
178
+ fs.copyFileSync(tsconfigBaseSrc, path.join(outDir, "tsconfig.base.json"));
179
+ }
180
+ fs.writeFileSync(path.join(outDir, ".gitignore"), "node_modules\n.pnpm-store\n.astro\ndist\n.DS_Store\n", "utf-8");
181
+ writeHostingFiles(fs, path, outDir, hosting);
182
+ writeStarterReadme(fs, path, outDir, hosting);
183
+ if (smokeTest) {
184
+ await runStarterSmokeTest(outDir);
185
+ }
186
+ console.log(`Created starter project: ${outDir}`);
187
+ console.log(`Hosting preset: ${hosting}`);
188
+ console.log("");
189
+ console.log("Next steps:");
190
+ console.log(` cd ${outDir}`);
191
+ console.log(" pnpm install");
192
+ console.log(" pnpm dev");
193
+ }
194
+ async function runStarterSmokeTest(outDir) {
195
+ const { spawnSync } = await import("node:child_process");
196
+ console.log("Running smoke test...");
197
+ console.log(" 1) pnpm install");
198
+ const install = spawnSync("pnpm", ["install"], {
199
+ cwd: outDir,
200
+ stdio: "inherit",
201
+ env: process.env,
202
+ });
203
+ if (install.error) {
204
+ console.error("Smoke test failed: pnpm install を実行できませんでした。");
205
+ console.error(install.error.message);
206
+ process.exit(1);
207
+ }
208
+ if (install.status !== 0) {
209
+ console.error(`Smoke test failed: pnpm install が失敗しました (exit=${install.status ?? 1})`);
210
+ process.exit(install.status ?? 1);
211
+ }
212
+ console.log(" 2) pnpm build");
213
+ const build = spawnSync("pnpm", ["build"], {
214
+ cwd: outDir,
215
+ stdio: "inherit",
216
+ env: process.env,
217
+ });
218
+ if (build.error) {
219
+ console.error("Smoke test failed: pnpm build を実行できませんでした。");
220
+ console.error(build.error.message);
221
+ process.exit(1);
222
+ }
223
+ if (build.status !== 0) {
224
+ console.error(`Smoke test failed: pnpm build が失敗しました (exit=${build.status ?? 1})`);
225
+ process.exit(build.status ?? 1);
226
+ }
227
+ console.log("Smoke test passed.");
228
+ }
229
+ function writeHostingFiles(fs, path, outDir, hosting) {
230
+ const appDir = path.join(outDir, "apps/site-starter-astro");
231
+ if (hosting === "vercel") {
232
+ fs.writeFileSync(path.join(appDir, "vercel.json"), JSON.stringify({
233
+ framework: "astro",
234
+ buildCommand: "pnpm --filter @mezzanine/site-starter-astro build",
235
+ outputDirectory: "dist",
236
+ }, null, 2) + "\n", "utf-8");
237
+ return;
238
+ }
239
+ if (hosting === "netlify") {
240
+ fs.writeFileSync(path.join(appDir, "netlify.toml"), `[build]
241
+ base = "apps/site-starter-astro"
242
+ publish = "dist"
243
+ command = "pnpm build"
244
+ `, "utf-8");
245
+ return;
246
+ }
247
+ if (hosting === "cloudflare") {
248
+ const publicDir = path.join(appDir, "public");
249
+ fs.mkdirSync(publicDir, { recursive: true });
250
+ fs.writeFileSync(path.join(publicDir, "_headers"), `/*
251
+ X-Frame-Options: SAMEORIGIN
252
+ X-Content-Type-Options: nosniff
253
+ `, "utf-8");
254
+ return;
255
+ }
256
+ if (hosting === "static") {
257
+ fs.writeFileSync(path.join(outDir, "HOSTING.md"), `# Hosting Guide (Static)
258
+
259
+ ## Build
260
+
261
+ \`\`\`bash
262
+ pnpm install
263
+ pnpm build
264
+ \`\`\`
265
+
266
+ Static output is generated under:
267
+
268
+ \`apps/site-starter-astro/dist\`
269
+
270
+ Upload this directory to your static hosting platform (S3 + CDN, Nginx, etc).
271
+ `, "utf-8");
272
+ }
273
+ }
274
+ function writeStarterReadme(fs, path, outDir, hosting) {
275
+ fs.writeFileSync(path.join(outDir, "README.md"), `# Mezzanine Starter (Generated)
276
+
277
+ このプロジェクトは \`create-mezzanine create-starter\` で生成されました。
278
+
279
+ ## Hosting preset
280
+
281
+ \`${hosting}\`
282
+
283
+ ## Quick start
284
+
285
+ \`\`\`bash
286
+ pnpm install
287
+ pnpm dev
288
+ \`\`\`
289
+
290
+ ## CMS
291
+
292
+ - サイトと同一ドメインで \`/admin\` を提供します
293
+ - 認証は \`PUBLIC_AUTH_BROKER_URL\` を設定して利用します
294
+
295
+ ## Commands
296
+
297
+ - \`pnpm dev\`
298
+ - \`pnpm build\`
299
+ - \`pnpm preview\`
300
+ - \`pnpm typecheck\`
301
+ `, "utf-8");
302
+ }
303
+ /**
304
+ * tenant-add: KV に投入する顧客設定 JSON を生成して出力する。
305
+ *
306
+ * 使い方:
307
+ * npx create-mezzanine tenant-add \
308
+ * --origin https://acme.com \
309
+ * --to owner@acme.com \
310
+ * --from noreply@your-domain.com \
311
+ * --success https://acme.com/contact/thanks \
312
+ * [--tenant acme] [--subject "[acme お問い合わせ]"] [--failure https://acme.com/contact?error=1] \
313
+ * [--output tenant-acme.json]
314
+ *
315
+ * 出力後は以下のコマンドで KV に投入する:
316
+ * wrangler kv key put --binding=TENANT_CONFIG_KV "tenant:<origin>" "$(cat <output>)"
317
+ * wrangler kv key put --binding=AUTH_ALLOWED_KV "allowed-origin:<origin>" "1"
318
+ */
319
+ async function printTenantConfig(args) {
320
+ const get = (flag) => {
321
+ const i = args.indexOf(flag);
322
+ return i !== -1 ? args[i + 1] : undefined;
323
+ };
324
+ const origin = get("--origin");
325
+ const toEmail = get("--to");
326
+ const fromEmail = get("--from");
327
+ const successUrl = get("--success");
328
+ if (!origin || !toEmail || !fromEmail || !successUrl) {
329
+ console.error("Error: --origin, --to, --from, --success はすべて必須です。");
330
+ console.error(" 例: npx create-mezzanine tenant-add --origin https://acme.com --to owner@acme.com --from noreply@your-domain.com --success https://acme.com/contact/thanks");
331
+ process.exit(1);
332
+ }
333
+ const tenantId = get("--tenant") ?? origin.replace(/https?:\/\//, "").replace(/[^a-z0-9-]/gi, "-");
334
+ const subjectPrefix = get("--subject");
335
+ const failureUrl = get("--failure");
336
+ const outputPath = get("--output");
337
+ const config = {
338
+ tenantId,
339
+ toEmails: [toEmail],
340
+ fromEmail,
341
+ ...(subjectPrefix ? { subjectPrefix } : {}),
342
+ successRedirectUrl: successUrl,
343
+ ...(failureUrl ? { failureRedirectUrl: failureUrl } : {}),
344
+ uniformResponse: false,
345
+ enableHoneypot: true,
346
+ captchaRequired: true,
347
+ ratePerMinute: 5,
348
+ ratePer10Minutes: 20,
349
+ };
350
+ const json = JSON.stringify(config, null, 2);
351
+ if (outputPath) {
352
+ const fs = await import("node:fs");
353
+ fs.writeFileSync(outputPath, json, "utf-8");
354
+ console.log(`Written: ${outputPath}`);
355
+ }
356
+ else {
357
+ process.stdout.write(json + "\n");
358
+ }
359
+ const kvKey = `tenant:${origin}`;
360
+ const authKey = `allowed-origin:${origin}`;
361
+ const src = outputPath ? `"$(cat ${outputPath})"` : `'${json.replace(/'/g, "'\\''")}'`;
362
+ console.error(`
363
+ Next steps — TENANT_CONFIG_KV に登録する:
364
+ wrangler kv key put --binding=TENANT_CONFIG_KV "${kvKey}" ${src}
365
+
366
+ Next steps — AUTH_ALLOWED_KV に登録する:
367
+ wrangler kv key put --binding=AUTH_ALLOWED_KV "${authKey}" "1"
368
+ `);
369
+ }
370
+ /**
371
+ * tenant-list: wrangler CLI 経由で TENANT_CONFIG_KV のテナント一覧を表示する。
372
+ *
373
+ * 使い方:
374
+ * npx create-mezzanine tenant-list --namespace-id <KV_NAMESPACE_ID>
375
+ *
376
+ * wrangler CLI が PATH に存在する必要があります。
377
+ */
378
+ async function listTenants(args) {
379
+ const nsIndex = args.indexOf("--namespace-id");
380
+ const namespaceId = nsIndex !== -1 ? args[nsIndex + 1] : undefined;
381
+ if (!namespaceId) {
382
+ console.error("Error: --namespace-id は必須です。");
383
+ console.error(" 例: npx create-mezzanine tenant-list --namespace-id xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
384
+ console.error("\n namespace ID は以下のコマンドで確認できます:");
385
+ console.error(" wrangler kv namespace list");
386
+ process.exit(1);
387
+ }
388
+ const { spawnSync } = await import("node:child_process");
389
+ const result = spawnSync("wrangler", ["kv", "key", "list", "--namespace-id", namespaceId, "--prefix", "tenant:"], { encoding: "utf-8" });
390
+ if (result.error) {
391
+ console.error("wrangler の実行に失敗しました。wrangler がインストールされているか確認してください。");
392
+ console.error(result.error.message);
393
+ process.exit(1);
394
+ }
395
+ if (result.status !== 0) {
396
+ console.error(result.stderr ?? "wrangler がエラーを返しました。");
397
+ process.exit(result.status ?? 1);
398
+ }
399
+ let keys = [];
400
+ try {
401
+ keys = JSON.parse(result.stdout);
402
+ }
403
+ catch {
404
+ process.stdout.write(result.stdout);
405
+ return;
406
+ }
407
+ if (keys.length === 0) {
408
+ console.log("登録済みのテナントはありません。");
409
+ console.log(" npx create-mezzanine tenant-add でテナントを追加してください。");
410
+ return;
411
+ }
412
+ console.log(`登録済みテナント (${keys.length} 件):`);
413
+ for (const key of keys) {
414
+ const origin = key.name.replace(/^tenant:/, "");
415
+ console.log(` ${origin}`);
416
+ }
417
+ }
418
+ async function printManifestTemplate(args) {
419
+ const tenantIndex = args.indexOf("--tenant");
420
+ const tenantId = tenantIndex !== -1 ? args[tenantIndex + 1] : "REPLACE_WITH_TENANT_ID";
421
+ const outputIndex = args.indexOf("--output");
422
+ const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : undefined;
423
+ const template = `# Mezzanine Managed Ops — Customer Manifest
424
+ # このファイルは private 運用 repo の customers/<tenant-id>/manifest.yaml として管理します。
425
+ # 顧客固有のシークレット実体はここに置かず、secretRefs にキー名のみ記録してください。
426
+ #
427
+ # 生成コマンド: npx create-mezzanine manifest-init --tenant ${tenantId}
428
+ # 参照: docs/adr/0004-phase6-managed-ops-design.md
429
+
430
+ # --- 識別情報 ---
431
+ tenantId: "${tenantId}"
432
+ schemaVersion: "1"
433
+
434
+ # --- 顧客サイト ---
435
+ site:
436
+ repository: "https://github.com/YOUR_ORG/${tenantId}-site"
437
+ defaultBranch: "main"
438
+ hosting: "cloudflare-pages"
439
+ domains:
440
+ production: "https://your-site.com"
441
+ staging: "https://staging.your-site.com"
442
+
443
+ # --- 利用モジュール ---
444
+ modules:
445
+ cmsAdmin: true
446
+ authBroker: true
447
+ contactService: true
448
+
449
+ # --- Platform バージョン ---
450
+ platform:
451
+ # @mezzanine/* packages の使用バージョン (semver)
452
+ packageVersion: "0.1.0"
453
+ # site-starter-astro の起点バージョン (git tag または commit SHA)
454
+ starterBaseline: "v0.1.0"
455
+ # このファイルの manifest schema バージョン
456
+ manifestSchemaVersion: "1"
457
+
458
+ # --- リリースポリシー ---
459
+ # stable : smoke test 通過後、順次適用候補にする
460
+ # pilot : 先行検証環境で確認してから stable 顧客へ展開する
461
+ # pinned : 明示 opt-in なしに更新しない
462
+ releasePolicy: "stable"
463
+
464
+ # --- シークレット参照 (実体は secret manager に登録し、ここにはキー名のみ記録) ---
465
+ secretRefs:
466
+ # Cloudflare Pages / Workers の secret 名
467
+ resendApiKey: "RESEND_API_KEY"
468
+ turnstileSecretKey: "TURNSTILE_SECRET_KEY"
469
+ # GitHub OAuth broker の secret 名
470
+ githubClientId: "GITHUB_CLIENT_ID"
471
+ githubClientSecret: "GITHUB_CLIENT_SECRET"
472
+
473
+ # --- 監視・通知 ---
474
+ observability:
475
+ uptimeChecks:
476
+ - url: "https://your-site.com"
477
+ name: "site-top"
478
+ - url: "https://your-site.com/admin"
479
+ name: "cms-admin"
480
+ - url: "https://auth.your-domain.com/api/auth"
481
+ name: "auth-broker"
482
+ - url: "https://contact.your-domain.com"
483
+ name: "contact-service"
484
+ notifyTo: "ops@your-domain.com"
485
+ alertThresholdMs: 3000
486
+
487
+ # --- 運用メモ ---
488
+ notes: |
489
+ 初回導入日: YYYY-MM-DD
490
+ 担当者: YOUR_NAME
491
+ 特記事項: なし
492
+ `;
493
+ if (outputPath) {
494
+ const fs = await import("node:fs");
495
+ const path = await import("node:path");
496
+ const dir = path.dirname(outputPath);
497
+ if (dir !== ".") {
498
+ fs.mkdirSync(dir, { recursive: true });
499
+ }
500
+ fs.writeFileSync(outputPath, template, "utf-8");
501
+ console.log(`Written: ${outputPath}`);
502
+ console.log(`\nNext steps:`);
503
+ console.log(` 1. ${outputPath} の各値を実際の設定に置き換える`);
504
+ console.log(` 2. secretRefs のキー名を secret manager に登録する`);
505
+ console.log(` 3. customers/${tenantId}/environments/production.yaml を作成して環境差分を記録する`);
506
+ console.log(` 4. smoke test(公開 URL / /admin / auth popup / contact 送信)を実施する`);
507
+ }
508
+ else {
509
+ process.stdout.write(template);
510
+ }
511
+ }
512
+ async function printContactEnvTemplate(args) {
513
+ const outputIndex = args.indexOf("--output");
514
+ const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : undefined;
515
+ const template = `# contact-service-cloudflare — wrangler.toml [vars] テンプレート
516
+ # 各値を実際の設定に置き換えてください。
517
+
518
+ name = "contact-service-cloudflare"
519
+ main = "src/worker.ts"
520
+ compatibility_date = "2024-09-23"
521
+ compatibility_flags = ["nodejs_compat"]
522
+
523
+ [[kv_namespaces]]
524
+ binding = "RATE_LIMIT_KV"
525
+ id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"
526
+
527
+ [vars]
528
+ # 許可する Origin (カンマ区切り)
529
+ ALLOWED_ORIGINS = "https://your-site.com"
530
+
531
+ # 成功・失敗時のリダイレクト先
532
+ SUCCESS_REDIRECT_URL = "https://your-site.com/contact/thanks"
533
+ FAILURE_REDIRECT_URL = "https://your-site.com/contact?error=1"
534
+
535
+ # 通知メールの設定
536
+ TO_EMAIL = "you@example.com"
537
+ FROM_EMAIL = "noreply@your-domain.com"
538
+ SUBJECT_PREFIX = "[Contact]"
539
+
540
+ # レスポンス方針
541
+ # true: 失敗も SUCCESS_REDIRECT_URL にリダイレクト(失敗を隠す)
542
+ # false: FAILURE_REDIRECT_URL または JSON エラーを返す
543
+ UNIFORM_RESPONSE = "false"
544
+
545
+ # スパム防御
546
+ ENABLE_HONEYPOT = "true"
547
+ RATE_PER_MINUTE = "5"
548
+ RATE_PER_10_MINUTES = "20"
549
+
550
+ # JSON ボディを許可するか(通常 false で OK)
551
+ ALLOW_JSON_BODY = "false"
552
+
553
+ # シークレット: wrangler secret put で設定してください
554
+ # wrangler secret put RESEND_API_KEY
555
+ # wrangler secret put TURNSTILE_SECRET_KEY
556
+ `;
557
+ if (outputPath) {
558
+ const fs = await import("node:fs");
559
+ fs.writeFileSync(outputPath, template, "utf-8");
560
+ console.log(`Written: ${outputPath}`);
561
+ console.log(`Next steps:`);
562
+ console.log(` 1. Edit ${outputPath} with your actual values`);
563
+ console.log(` 2. wrangler kv namespace create "RATE_LIMIT_KV" — copy the id`);
564
+ console.log(` 3. wrangler secret put RESEND_API_KEY`);
565
+ console.log(` 4. wrangler secret put TURNSTILE_SECRET_KEY`);
566
+ console.log(` 5. wrangler deploy`);
567
+ }
568
+ else {
569
+ process.stdout.write(template);
570
+ }
571
+ }
572
+ export {};
573
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@mezzanine-stack/create-mezzanine",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "license": "MIT",
9
+ "type": "module",
10
+ "bin": {
11
+ "create-mezzanine": "./dist/cli.js"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^20.0.0",
15
+ "typescript": "^5.7.3"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc --noEmit false",
19
+ "typecheck": "tsc --noEmit"
20
+ }
21
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,678 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-mezzanine — Mezzanine Platform プロジェクト生成 CLI
4
+ *
5
+ * Phase 5: contact service の導入補助サブコマンドを提供します。
6
+ * Phase 6: managed ops 用の manifest-init サブコマンドを追加しました。
7
+ * Phase 6 (multi-tenant): KV テナント管理の tenant-add / tenant-list を追加しました。
8
+ */
9
+
10
+ const [, , command, ...args] = process.argv;
11
+
12
+ async function main(): Promise<void> {
13
+ switch (command) {
14
+ case "create-starter":
15
+ await createStarterProject(args);
16
+ break;
17
+ case "contact-env":
18
+ await printContactEnvTemplate(args);
19
+ break;
20
+ case "manifest-init":
21
+ await printManifestTemplate(args);
22
+ break;
23
+ case "tenant-add":
24
+ await printTenantConfig(args);
25
+ break;
26
+ case "tenant-list":
27
+ await listTenants(args);
28
+ break;
29
+ case "help":
30
+ case "--help":
31
+ case "-h":
32
+ case undefined:
33
+ printHelp();
34
+ break;
35
+ default:
36
+ console.error(`Unknown command: ${command}`);
37
+ printHelp();
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ main().catch((err: unknown) => {
43
+ console.error(err);
44
+ process.exit(1);
45
+ });
46
+
47
+ function printHelp(): void {
48
+ console.log(`
49
+ create-mezzanine — Mezzanine Platform CLI
50
+
51
+ Usage:
52
+ create-mezzanine <command> [options]
53
+
54
+ Commands:
55
+ create-starter [--output <dir>] [--hosting <cloudflare|vercel|netlify|static>] [--name <project-name>] [--smoke-test]
56
+ starter + 必要最小限 package をコピーして、単体で動く顧客サイト repo を生成する
57
+ --smoke-test 指定時は生成直後に pnpm install / pnpm build を実行して検証する
58
+
59
+ contact-env [--output <path>]
60
+ contact-service-cloudflare 用の wrangler.toml [vars] テンプレートを出力する
61
+
62
+ manifest-init [--tenant <id>] [--output <path>]
63
+ managed ops 用の customer manifest テンプレートを出力する
64
+
65
+ tenant-add --origin <url> --to <email> --from <email> --success <url>
66
+ [--tenant <id>] [--subject <prefix>] [--failure <url>]
67
+ [--output <path>]
68
+ KV (TENANT_CONFIG_KV) に投入する顧客設定 JSON を出力する
69
+ Auth KV (AUTH_ALLOWED_KV) への origin 登録コマンドも案内する
70
+
71
+ tenant-list --namespace-id <id>
72
+ wrangler CLI 経由で TENANT_CONFIG_KV のテナント一覧を表示する
73
+
74
+ help
75
+ このヘルプを表示する
76
+
77
+ Examples:
78
+ npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare
79
+ npx create-mezzanine create-starter --output ../acme-site --hosting static --name acme-site
80
+ npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare --smoke-test
81
+ npx create-mezzanine contact-env
82
+ npx create-mezzanine manifest-init --tenant my-client --output customers/my-client/manifest.yaml
83
+ npx create-mezzanine tenant-add \\
84
+ --origin https://acme.com \\
85
+ --to owner@acme.com \\
86
+ --from noreply@your-domain.com \\
87
+ --success https://acme.com/contact/thanks \\
88
+ --output tenant-acme.json
89
+ npx create-mezzanine tenant-list --namespace-id xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
90
+ `);
91
+ }
92
+
93
+ /**
94
+ * create-starter:
95
+ * `apps/site-starter-astro` と依存 package の最小セットをコピーし、
96
+ * 顧客向けの standalone リポジトリを生成する。
97
+ */
98
+ async function createStarterProject(args: string[]): Promise<void> {
99
+ const get = (flag: string): string | undefined => {
100
+ const i = args.indexOf(flag);
101
+ return i !== -1 ? args[i + 1] : undefined;
102
+ };
103
+ const has = (flag: string): boolean => args.includes(flag);
104
+
105
+ const outputArg = get("--output");
106
+ if (!outputArg) {
107
+ console.error("Error: --output は必須です。");
108
+ console.error(" 例: npx create-mezzanine create-starter --output ../acme-site --hosting cloudflare");
109
+ process.exit(1);
110
+ }
111
+
112
+ const hosting = (get("--hosting") ?? "cloudflare").toLowerCase();
113
+ if (!["cloudflare", "vercel", "netlify", "static"].includes(hosting)) {
114
+ console.error(`Error: --hosting は cloudflare|vercel|netlify|static のいずれかを指定してください。受け取った値: ${hosting}`);
115
+ process.exit(1);
116
+ }
117
+
118
+ const projectName = get("--name") ?? "mezzanine-customer-site";
119
+ const smokeTest = has("--smoke-test");
120
+
121
+ const fs = await import("node:fs");
122
+ const path = await import("node:path");
123
+ const { fileURLToPath } = await import("node:url");
124
+
125
+ const thisFile = fileURLToPath(import.meta.url);
126
+ const repoRoot = path.resolve(path.dirname(thisFile), "../../..");
127
+ const outDir = path.resolve(process.cwd(), outputArg);
128
+
129
+ if (fs.existsSync(outDir) && fs.readdirSync(outDir).length > 0) {
130
+ console.error(`Error: 出力先ディレクトリが空ではありません: ${outDir}`);
131
+ process.exit(1);
132
+ }
133
+ fs.mkdirSync(outDir, { recursive: true });
134
+
135
+ const appSrc = path.join(repoRoot, "apps/site-starter-astro");
136
+ const packageNames = [
137
+ "tokens",
138
+ "css",
139
+ "ui",
140
+ "content-schema",
141
+ "astro-renderer",
142
+ "cms-core",
143
+ "git-provider-github",
144
+ ];
145
+
146
+ const copyFiltered = (src: string, dest: string): void => {
147
+ fs.cpSync(src, dest, {
148
+ recursive: true,
149
+ filter: (entry) => {
150
+ const normalized = entry.replace(/\\/g, "/");
151
+ if (
152
+ normalized.endsWith("/node_modules") ||
153
+ normalized.includes("/node_modules/") ||
154
+ normalized.endsWith("/dist") ||
155
+ normalized.includes("/dist/") ||
156
+ normalized.endsWith("/.astro") ||
157
+ normalized.includes("/.astro/") ||
158
+ normalized.endsWith("/.git") ||
159
+ normalized.includes("/.git/")
160
+ ) {
161
+ return false;
162
+ }
163
+ return true;
164
+ },
165
+ });
166
+ };
167
+
168
+ // apps/site-starter-astro
169
+ const appDest = path.join(outDir, "apps/site-starter-astro");
170
+ fs.mkdirSync(path.dirname(appDest), { recursive: true });
171
+ copyFiltered(appSrc, appDest);
172
+
173
+ // 必要 package のみコピー
174
+ for (const name of packageNames) {
175
+ const src = path.join(repoRoot, "packages", name);
176
+ const dest = path.join(outDir, "packages", name);
177
+ copyFiltered(src, dest);
178
+ }
179
+
180
+ // ルート設定ファイルを生成
181
+ fs.writeFileSync(
182
+ path.join(outDir, "package.json"),
183
+ JSON.stringify(
184
+ {
185
+ name: projectName,
186
+ private: true,
187
+ version: "0.1.0",
188
+ packageManager: "pnpm@10.28.2",
189
+ scripts: {
190
+ dev: "pnpm --filter @mezzanine-stack/site-starter-astro dev",
191
+ build: "pnpm --filter @mezzanine-stack/site-starter-astro build",
192
+ preview: "pnpm --filter @mezzanine-stack/site-starter-astro preview",
193
+ typecheck: "pnpm -r typecheck",
194
+ },
195
+ },
196
+ null,
197
+ 2,
198
+ ) + "\n",
199
+ "utf-8",
200
+ );
201
+
202
+ fs.writeFileSync(
203
+ path.join(outDir, "pnpm-workspace.yaml"),
204
+ 'packages:\n - "apps/*"\n - "packages/*"\n',
205
+ "utf-8",
206
+ );
207
+
208
+ const tsconfigBaseSrc = path.join(repoRoot, "tsconfig.base.json");
209
+ if (fs.existsSync(tsconfigBaseSrc)) {
210
+ fs.copyFileSync(tsconfigBaseSrc, path.join(outDir, "tsconfig.base.json"));
211
+ }
212
+
213
+ fs.writeFileSync(
214
+ path.join(outDir, ".gitignore"),
215
+ "node_modules\n.pnpm-store\n.astro\ndist\n.DS_Store\n",
216
+ "utf-8",
217
+ );
218
+
219
+ writeHostingFiles(fs, path, outDir, hosting);
220
+ writeStarterReadme(fs, path, outDir, hosting);
221
+
222
+ if (smokeTest) {
223
+ await runStarterSmokeTest(outDir);
224
+ }
225
+
226
+ console.log(`Created starter project: ${outDir}`);
227
+ console.log(`Hosting preset: ${hosting}`);
228
+ console.log("");
229
+ console.log("Next steps:");
230
+ console.log(` cd ${outDir}`);
231
+ console.log(" pnpm install");
232
+ console.log(" pnpm dev");
233
+ }
234
+
235
+ async function runStarterSmokeTest(outDir: string): Promise<void> {
236
+ const { spawnSync } = await import("node:child_process");
237
+
238
+ console.log("Running smoke test...");
239
+ console.log(" 1) pnpm install");
240
+ const install = spawnSync("pnpm", ["install"], {
241
+ cwd: outDir,
242
+ stdio: "inherit",
243
+ env: process.env,
244
+ });
245
+ if (install.error) {
246
+ console.error("Smoke test failed: pnpm install を実行できませんでした。");
247
+ console.error(install.error.message);
248
+ process.exit(1);
249
+ }
250
+ if (install.status !== 0) {
251
+ console.error(`Smoke test failed: pnpm install が失敗しました (exit=${install.status ?? 1})`);
252
+ process.exit(install.status ?? 1);
253
+ }
254
+
255
+ console.log(" 2) pnpm build");
256
+ const build = spawnSync("pnpm", ["build"], {
257
+ cwd: outDir,
258
+ stdio: "inherit",
259
+ env: process.env,
260
+ });
261
+ if (build.error) {
262
+ console.error("Smoke test failed: pnpm build を実行できませんでした。");
263
+ console.error(build.error.message);
264
+ process.exit(1);
265
+ }
266
+ if (build.status !== 0) {
267
+ console.error(`Smoke test failed: pnpm build が失敗しました (exit=${build.status ?? 1})`);
268
+ process.exit(build.status ?? 1);
269
+ }
270
+
271
+ console.log("Smoke test passed.");
272
+ }
273
+
274
+ function writeHostingFiles(
275
+ fs: typeof import("node:fs"),
276
+ path: typeof import("node:path"),
277
+ outDir: string,
278
+ hosting: string,
279
+ ): void {
280
+ const appDir = path.join(outDir, "apps/site-starter-astro");
281
+
282
+ if (hosting === "vercel") {
283
+ fs.writeFileSync(
284
+ path.join(appDir, "vercel.json"),
285
+ JSON.stringify(
286
+ {
287
+ framework: "astro",
288
+ buildCommand: "pnpm --filter @mezzanine-stack/site-starter-astro build",
289
+ outputDirectory: "dist",
290
+ },
291
+ null,
292
+ 2,
293
+ ) + "\n",
294
+ "utf-8",
295
+ );
296
+ return;
297
+ }
298
+
299
+ if (hosting === "netlify") {
300
+ fs.writeFileSync(
301
+ path.join(appDir, "netlify.toml"),
302
+ `[build]
303
+ base = "apps/site-starter-astro"
304
+ publish = "dist"
305
+ command = "pnpm build"
306
+ `,
307
+ "utf-8",
308
+ );
309
+ return;
310
+ }
311
+
312
+ if (hosting === "cloudflare") {
313
+ const publicDir = path.join(appDir, "public");
314
+ fs.mkdirSync(publicDir, { recursive: true });
315
+ fs.writeFileSync(
316
+ path.join(publicDir, "_headers"),
317
+ `/*
318
+ X-Frame-Options: SAMEORIGIN
319
+ X-Content-Type-Options: nosniff
320
+ `,
321
+ "utf-8",
322
+ );
323
+ return;
324
+ }
325
+
326
+ if (hosting === "static") {
327
+ fs.writeFileSync(
328
+ path.join(outDir, "HOSTING.md"),
329
+ `# Hosting Guide (Static)
330
+
331
+ ## Build
332
+
333
+ \`\`\`bash
334
+ pnpm install
335
+ pnpm build
336
+ \`\`\`
337
+
338
+ Static output is generated under:
339
+
340
+ \`apps/site-starter-astro/dist\`
341
+
342
+ Upload this directory to your static hosting platform (S3 + CDN, Nginx, etc).
343
+ `,
344
+ "utf-8",
345
+ );
346
+ }
347
+ }
348
+
349
+ function writeStarterReadme(
350
+ fs: typeof import("node:fs"),
351
+ path: typeof import("node:path"),
352
+ outDir: string,
353
+ hosting: string,
354
+ ): void {
355
+ fs.writeFileSync(
356
+ path.join(outDir, "README.md"),
357
+ `# Mezzanine Starter (Generated)
358
+
359
+ このプロジェクトは \`create-mezzanine create-starter\` で生成されました。
360
+
361
+ ## Hosting preset
362
+
363
+ \`${hosting}\`
364
+
365
+ ## Quick start
366
+
367
+ \`\`\`bash
368
+ pnpm install
369
+ pnpm dev
370
+ \`\`\`
371
+
372
+ ## CMS
373
+
374
+ - サイトと同一ドメインで \`/admin\` を提供します
375
+ - 認証は \`PUBLIC_AUTH_BROKER_URL\` を設定して利用します
376
+
377
+ ## Commands
378
+
379
+ - \`pnpm dev\`
380
+ - \`pnpm build\`
381
+ - \`pnpm preview\`
382
+ - \`pnpm typecheck\`
383
+ `,
384
+ "utf-8",
385
+ );
386
+ }
387
+
388
+ /**
389
+ * tenant-add: KV に投入する顧客設定 JSON を生成して出力する。
390
+ *
391
+ * 使い方:
392
+ * npx create-mezzanine tenant-add \
393
+ * --origin https://acme.com \
394
+ * --to owner@acme.com \
395
+ * --from noreply@your-domain.com \
396
+ * --success https://acme.com/contact/thanks \
397
+ * [--tenant acme] [--subject "[acme お問い合わせ]"] [--failure https://acme.com/contact?error=1] \
398
+ * [--output tenant-acme.json]
399
+ *
400
+ * 出力後は以下のコマンドで KV に投入する:
401
+ * wrangler kv key put --binding=TENANT_CONFIG_KV "tenant:<origin>" "$(cat <output>)"
402
+ * wrangler kv key put --binding=AUTH_ALLOWED_KV "allowed-origin:<origin>" "1"
403
+ */
404
+ async function printTenantConfig(args: string[]): Promise<void> {
405
+ const get = (flag: string): string | undefined => {
406
+ const i = args.indexOf(flag);
407
+ return i !== -1 ? args[i + 1] : undefined;
408
+ };
409
+
410
+ const origin = get("--origin");
411
+ const toEmail = get("--to");
412
+ const fromEmail = get("--from");
413
+ const successUrl = get("--success");
414
+
415
+ if (!origin || !toEmail || !fromEmail || !successUrl) {
416
+ console.error("Error: --origin, --to, --from, --success はすべて必須です。");
417
+ console.error(" 例: npx create-mezzanine tenant-add --origin https://acme.com --to owner@acme.com --from noreply@your-domain.com --success https://acme.com/contact/thanks");
418
+ process.exit(1);
419
+ }
420
+
421
+ const tenantId = get("--tenant") ?? origin.replace(/https?:\/\//, "").replace(/[^a-z0-9-]/gi, "-");
422
+ const subjectPrefix = get("--subject");
423
+ const failureUrl = get("--failure");
424
+ const outputPath = get("--output");
425
+
426
+ const config = {
427
+ tenantId,
428
+ toEmails: [toEmail],
429
+ fromEmail,
430
+ ...(subjectPrefix ? { subjectPrefix } : {}),
431
+ successRedirectUrl: successUrl,
432
+ ...(failureUrl ? { failureRedirectUrl: failureUrl } : {}),
433
+ uniformResponse: false,
434
+ enableHoneypot: true,
435
+ captchaRequired: true,
436
+ ratePerMinute: 5,
437
+ ratePer10Minutes: 20,
438
+ };
439
+
440
+ const json = JSON.stringify(config, null, 2);
441
+
442
+ if (outputPath) {
443
+ const fs = await import("node:fs");
444
+ fs.writeFileSync(outputPath, json, "utf-8");
445
+ console.log(`Written: ${outputPath}`);
446
+ } else {
447
+ process.stdout.write(json + "\n");
448
+ }
449
+
450
+ const kvKey = `tenant:${origin}`;
451
+ const authKey = `allowed-origin:${origin}`;
452
+ const src = outputPath ? `"$(cat ${outputPath})"` : `'${json.replace(/'/g, "'\\''")}'`;
453
+
454
+ console.error(`
455
+ Next steps — TENANT_CONFIG_KV に登録する:
456
+ wrangler kv key put --binding=TENANT_CONFIG_KV "${kvKey}" ${src}
457
+
458
+ Next steps — AUTH_ALLOWED_KV に登録する:
459
+ wrangler kv key put --binding=AUTH_ALLOWED_KV "${authKey}" "1"
460
+ `);
461
+ }
462
+
463
+ /**
464
+ * tenant-list: wrangler CLI 経由で TENANT_CONFIG_KV のテナント一覧を表示する。
465
+ *
466
+ * 使い方:
467
+ * npx create-mezzanine tenant-list --namespace-id <KV_NAMESPACE_ID>
468
+ *
469
+ * wrangler CLI が PATH に存在する必要があります。
470
+ */
471
+ async function listTenants(args: string[]): Promise<void> {
472
+ const nsIndex = args.indexOf("--namespace-id");
473
+ const namespaceId = nsIndex !== -1 ? args[nsIndex + 1] : undefined;
474
+
475
+ if (!namespaceId) {
476
+ console.error("Error: --namespace-id は必須です。");
477
+ console.error(" 例: npx create-mezzanine tenant-list --namespace-id xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
478
+ console.error("\n namespace ID は以下のコマンドで確認できます:");
479
+ console.error(" wrangler kv namespace list");
480
+ process.exit(1);
481
+ }
482
+
483
+ const { spawnSync } = await import("node:child_process");
484
+ const result = spawnSync(
485
+ "wrangler",
486
+ ["kv", "key", "list", "--namespace-id", namespaceId, "--prefix", "tenant:"],
487
+ { encoding: "utf-8" },
488
+ );
489
+
490
+ if (result.error) {
491
+ console.error("wrangler の実行に失敗しました。wrangler がインストールされているか確認してください。");
492
+ console.error(result.error.message);
493
+ process.exit(1);
494
+ }
495
+ if (result.status !== 0) {
496
+ console.error(result.stderr ?? "wrangler がエラーを返しました。");
497
+ process.exit(result.status ?? 1);
498
+ }
499
+
500
+ let keys: Array<{ name: string }> = [];
501
+ try {
502
+ keys = JSON.parse(result.stdout) as Array<{ name: string }>;
503
+ } catch {
504
+ process.stdout.write(result.stdout);
505
+ return;
506
+ }
507
+
508
+ if (keys.length === 0) {
509
+ console.log("登録済みのテナントはありません。");
510
+ console.log(" npx create-mezzanine tenant-add でテナントを追加してください。");
511
+ return;
512
+ }
513
+
514
+ console.log(`登録済みテナント (${keys.length} 件):`);
515
+ for (const key of keys) {
516
+ const origin = key.name.replace(/^tenant:/, "");
517
+ console.log(` ${origin}`);
518
+ }
519
+ }
520
+
521
+ async function printManifestTemplate(args: string[]): Promise<void> {
522
+ const tenantIndex = args.indexOf("--tenant");
523
+ const tenantId = tenantIndex !== -1 ? args[tenantIndex + 1] : "REPLACE_WITH_TENANT_ID";
524
+
525
+ const outputIndex = args.indexOf("--output");
526
+ const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : undefined;
527
+
528
+ const template = `# Mezzanine Managed Ops — Customer Manifest
529
+ # このファイルは private 運用 repo の customers/<tenant-id>/manifest.yaml として管理します。
530
+ # 顧客固有のシークレット実体はここに置かず、secretRefs にキー名のみ記録してください。
531
+ #
532
+ # 生成コマンド: npx create-mezzanine manifest-init --tenant ${tenantId}
533
+ # 参照: docs/adr/0004-phase6-managed-ops-design.md
534
+
535
+ # --- 識別情報 ---
536
+ tenantId: "${tenantId}"
537
+ schemaVersion: "1"
538
+
539
+ # --- 顧客サイト ---
540
+ site:
541
+ repository: "https://github.com/YOUR_ORG/${tenantId}-site"
542
+ defaultBranch: "main"
543
+ hosting: "cloudflare-pages"
544
+ domains:
545
+ production: "https://your-site.com"
546
+ staging: "https://staging.your-site.com"
547
+
548
+ # --- 利用モジュール ---
549
+ modules:
550
+ cmsAdmin: true
551
+ authBroker: true
552
+ contactService: true
553
+
554
+ # --- Platform バージョン ---
555
+ platform:
556
+ # @mezzanine-stack/* packages の使用バージョン (semver)
557
+ packageVersion: "0.1.0"
558
+ # site-starter-astro の起点バージョン (git tag または commit SHA)
559
+ starterBaseline: "v0.1.0"
560
+ # このファイルの manifest schema バージョン
561
+ manifestSchemaVersion: "1"
562
+
563
+ # --- リリースポリシー ---
564
+ # stable : smoke test 通過後、順次適用候補にする
565
+ # pilot : 先行検証環境で確認してから stable 顧客へ展開する
566
+ # pinned : 明示 opt-in なしに更新しない
567
+ releasePolicy: "stable"
568
+
569
+ # --- シークレット参照 (実体は secret manager に登録し、ここにはキー名のみ記録) ---
570
+ secretRefs:
571
+ # Cloudflare Pages / Workers の secret 名
572
+ resendApiKey: "RESEND_API_KEY"
573
+ turnstileSecretKey: "TURNSTILE_SECRET_KEY"
574
+ # GitHub OAuth broker の secret 名
575
+ githubClientId: "GITHUB_CLIENT_ID"
576
+ githubClientSecret: "GITHUB_CLIENT_SECRET"
577
+
578
+ # --- 監視・通知 ---
579
+ observability:
580
+ uptimeChecks:
581
+ - url: "https://your-site.com"
582
+ name: "site-top"
583
+ - url: "https://your-site.com/admin"
584
+ name: "cms-admin"
585
+ - url: "https://auth.your-domain.com/api/auth"
586
+ name: "auth-broker"
587
+ - url: "https://contact.your-domain.com"
588
+ name: "contact-service"
589
+ notifyTo: "ops@your-domain.com"
590
+ alertThresholdMs: 3000
591
+
592
+ # --- 運用メモ ---
593
+ notes: |
594
+ 初回導入日: YYYY-MM-DD
595
+ 担当者: YOUR_NAME
596
+ 特記事項: なし
597
+ `;
598
+
599
+ if (outputPath) {
600
+ const fs = await import("node:fs");
601
+ const path = await import("node:path");
602
+ const dir = path.dirname(outputPath);
603
+ if (dir !== ".") {
604
+ fs.mkdirSync(dir, { recursive: true });
605
+ }
606
+ fs.writeFileSync(outputPath, template, "utf-8");
607
+ console.log(`Written: ${outputPath}`);
608
+ console.log(`\nNext steps:`);
609
+ console.log(` 1. ${outputPath} の各値を実際の設定に置き換える`);
610
+ console.log(` 2. secretRefs のキー名を secret manager に登録する`);
611
+ console.log(` 3. customers/${tenantId}/environments/production.yaml を作成して環境差分を記録する`);
612
+ console.log(` 4. smoke test(公開 URL / /admin / auth popup / contact 送信)を実施する`);
613
+ } else {
614
+ process.stdout.write(template);
615
+ }
616
+ }
617
+
618
+ async function printContactEnvTemplate(args: string[]): Promise<void> {
619
+ const outputIndex = args.indexOf("--output");
620
+ const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : undefined;
621
+
622
+ const template = `# contact-service-cloudflare — wrangler.toml [vars] テンプレート
623
+ # 各値を実際の設定に置き換えてください。
624
+
625
+ name = "contact-service-cloudflare"
626
+ main = "src/worker.ts"
627
+ compatibility_date = "2024-09-23"
628
+ compatibility_flags = ["nodejs_compat"]
629
+
630
+ [[kv_namespaces]]
631
+ binding = "RATE_LIMIT_KV"
632
+ id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"
633
+
634
+ [vars]
635
+ # 許可する Origin (カンマ区切り)
636
+ ALLOWED_ORIGINS = "https://your-site.com"
637
+
638
+ # 成功・失敗時のリダイレクト先
639
+ SUCCESS_REDIRECT_URL = "https://your-site.com/contact/thanks"
640
+ FAILURE_REDIRECT_URL = "https://your-site.com/contact?error=1"
641
+
642
+ # 通知メールの設定
643
+ TO_EMAIL = "you@example.com"
644
+ FROM_EMAIL = "noreply@your-domain.com"
645
+ SUBJECT_PREFIX = "[Contact]"
646
+
647
+ # レスポンス方針
648
+ # true: 失敗も SUCCESS_REDIRECT_URL にリダイレクト(失敗を隠す)
649
+ # false: FAILURE_REDIRECT_URL または JSON エラーを返す
650
+ UNIFORM_RESPONSE = "false"
651
+
652
+ # スパム防御
653
+ ENABLE_HONEYPOT = "true"
654
+ RATE_PER_MINUTE = "5"
655
+ RATE_PER_10_MINUTES = "20"
656
+
657
+ # JSON ボディを許可するか(通常 false で OK)
658
+ ALLOW_JSON_BODY = "false"
659
+
660
+ # シークレット: wrangler secret put で設定してください
661
+ # wrangler secret put RESEND_API_KEY
662
+ # wrangler secret put TURNSTILE_SECRET_KEY
663
+ `;
664
+
665
+ if (outputPath) {
666
+ const fs = await import("node:fs");
667
+ fs.writeFileSync(outputPath, template, "utf-8");
668
+ console.log(`Written: ${outputPath}`);
669
+ console.log(`Next steps:`);
670
+ console.log(` 1. Edit ${outputPath} with your actual values`);
671
+ console.log(` 2. wrangler kv namespace create "RATE_LIMIT_KV" — copy the id`);
672
+ console.log(` 3. wrangler secret put RESEND_API_KEY`);
673
+ console.log(` 4. wrangler secret put TURNSTILE_SECRET_KEY`);
674
+ console.log(` 5. wrangler deploy`);
675
+ } else {
676
+ process.stdout.write(template);
677
+ }
678
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "lib": ["ES2022"],
7
+ "types": ["node"],
8
+ "noEmit": true
9
+ },
10
+ "include": ["src"]
11
+ }