create-apollo-monorepo 0.9.0 → 0.9.2

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.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/index.mjs +301 -44
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -6,15 +6,15 @@ mounted as a **git submodule** backend (read-only — pull updates only).
6
6
  ## Usage
7
7
 
8
8
  ```bash
9
- npx create-apollo-monorepo thamc-new
9
+ npx create-apollo-monorepo my-site
10
10
  ```
11
11
 
12
12
  With flags:
13
13
 
14
14
  ```bash
15
- npx create-apollo-monorepo thamc-new \
16
- --frontend-name "@thamc/frontend" \
17
- --db "postgresql://user:pass@localhost:5432/thamc" \
15
+ npx create-apollo-monorepo my-site \
16
+ --frontend-name "@my-site/frontend" \
17
+ --db "postgresql://user:pass@localhost:5432/my-site" \
18
18
  --url "http://localhost:3000" \
19
19
  --locale th
20
20
  ```
@@ -22,9 +22,9 @@ npx create-apollo-monorepo thamc-new \
22
22
  ## Result
23
23
 
24
24
  ```
25
- thamc-new/
25
+ my-site/
26
26
  ├── apps/
27
- │ ├── frontend/ ← @thamc/frontend (Next.js skeleton)
27
+ │ ├── frontend/ ← @my-site/frontend (Next.js skeleton)
28
28
  │ └── backend/ ← git submodule → apollo-cms
29
29
  ├── package.json ← root workspace
30
30
  ├── pnpm-workspace.yaml
@@ -89,7 +89,7 @@ subdomains (e.g. `cms.example.com` + `example.com`).
89
89
  ## After install
90
90
 
91
91
  ```bash
92
- cd thamc-new
92
+ cd my-site
93
93
  pnpm backend:setup # push schema + seed apollo-cms
94
94
  pnpm dev # frontend :3001 + backend :3000 in parallel
95
95
  ```
package/index.mjs CHANGED
@@ -29,6 +29,13 @@ const FRONTEND_PATH = "apps/frontend";
29
29
  const PROXY_PATH = "apps/proxy";
30
30
  const MIN_NODE_MAJOR = 20;
31
31
 
32
+ // Default dev ports. Kept in sync with scripts/with-env.mjs and the proxy
33
+ // template. .env.local is the runtime source of truth — these are the
34
+ // installer-side fallbacks for computing initial URL defaults.
35
+ const DEFAULT_PROXY_PORT = 3030;
36
+ const DEFAULT_FRONTEND_PORT = 3001;
37
+ const DEFAULT_BACKEND_PORT = 3002;
38
+
32
39
  const COLORS = {
33
40
  reset: "\x1b[0m",
34
41
  bold: "\x1b[1m",
@@ -46,9 +53,9 @@ ${COLORS.bold}Usage:${COLORS.reset}
46
53
  npx create-apollo-monorepo <directory> [flags]
47
54
 
48
55
  ${COLORS.bold}Examples:${COLORS.reset}
49
- npx create-apollo-monorepo thamc-new
50
- npx create-apollo-monorepo thamc-new --frontend-name "@thamc/frontend"
51
- npx create-apollo-monorepo thamc-new --backend-branch develop --skip-install
56
+ npx create-apollo-monorepo my-site
57
+ npx create-apollo-monorepo my-site --frontend-name "@my-site/frontend"
58
+ npx create-apollo-monorepo my-site --backend-branch develop --skip-install
52
59
 
53
60
  ${COLORS.bold}Flags:${COLORS.reset}
54
61
  --frontend-name <name> Frontend package name (default: "@<dir>/frontend")
@@ -283,7 +290,14 @@ async function gatherEnv(flags, { singleOrigin }) {
283
290
  let dbUrl = flags.db;
284
291
  // In single-origin mode the frontend is the public entry → default :3001.
285
292
  // In separate-origins mode the backend is the public CMS URL → default :3000.
286
- const defaultSiteUrl = singleOrigin ? "http://localhost:3001" : "http://localhost:3000";
293
+ // Public origin = frontend in single-origin mode (frontend rewrites /admin/*,
294
+ // /api/*, /uploads/* to the backend), backend in separate-origins mode.
295
+ // For `pnpm dev:rp` switch this to http://localhost:${PROXY_PORT} in .env.local.
296
+ // Derived from the PORT constants so changing a default updates both the
297
+ // .env.local seed and the prompt's suggested value.
298
+ const defaultSiteUrl = singleOrigin
299
+ ? `http://localhost:${DEFAULT_FRONTEND_PORT}`
300
+ : `http://localhost:${DEFAULT_BACKEND_PORT}`;
287
301
  let siteUrl = flags.url ?? defaultSiteUrl;
288
302
  let locale = flags.locale ?? "en";
289
303
 
@@ -355,16 +369,17 @@ function writeRootPackageJson(targetDir, dirName) {
355
369
  // rebuild and the backend's plugin loader picks up the fresh
356
370
  // dist/server.mjs on next request.
357
371
  dev:
358
- "pnpm predev:setup && concurrently -k -n FE,BE,PL -c blue,magenta,yellow \"pnpm --filter ./apps/frontend dev\" \"pnpm --filter ./apps/backend exec next dev\" \"pnpm cms-plugins:dev\"",
372
+ "pnpm predev:setup && concurrently -k -n FE,BE,PL -c blue,magenta,yellow \"pnpm dev:frontend\" \"pnpm dev:backend\" \"pnpm cms-plugins:dev\"",
359
373
  // Optional single-origin dev: same as `pnpm dev` but adds a Node.js
360
- // reverse proxy on :3030 that fronts FE :3001 + BE :3000. Use it when
361
- // you want one URL, shared cookies, and no CORS — without nginx.
374
+ // reverse proxy on :3030 (PROXY_PORT) that fronts FE :3001 (FRONTEND_PORT)
375
+ // + BE :3002 (BACKEND_PORT). Ports come from .env.local; override there.
362
376
  "dev:rp":
363
- "pnpm predev:setup && concurrently -k -n FE,BE,PL,PX -c blue,magenta,yellow,cyan \"pnpm --filter ./apps/frontend dev\" \"pnpm --filter ./apps/backend exec next dev\" \"pnpm cms-plugins:dev\" \"pnpm dev:proxy\"",
364
- "dev:proxy": "node apps/proxy/server.mjs",
365
- "dev:frontend": "pnpm --filter ./apps/frontend dev",
377
+ "pnpm predev:setup && concurrently -k -n FE,BE,PL,PX -c blue,magenta,yellow,cyan \"pnpm dev:frontend\" \"pnpm dev:backend\" \"pnpm cms-plugins:dev\" \"pnpm dev:proxy\"",
378
+ "dev:proxy": "node scripts/with-env.mjs node apps/proxy/server.mjs",
379
+ "dev:frontend":
380
+ "node scripts/with-env.mjs --port=FRONTEND_PORT pnpm --filter ./apps/frontend exec next dev",
366
381
  "dev:backend":
367
- "pnpm predev:setup && pnpm --filter ./apps/backend exec next dev",
382
+ "pnpm predev:setup && node scripts/with-env.mjs --port=BACKEND_PORT pnpm --filter ./apps/backend exec next dev",
368
383
  "dev:cron": "pnpm --filter ./apps/backend exec bun scripts/dev-cron.ts",
369
384
  // Build pipeline: cms-plugins → apollo-cms's own plugins → backend → frontend.
370
385
  // `prebuild` runs first (npm/pnpm convention) and fails fast if the
@@ -375,17 +390,19 @@ function writeRootPackageJson(targetDir, dirName) {
375
390
  "build:backend":
376
391
  "pnpm cms-plugins:build && pnpm --filter ./apps/backend exec bun run plugins:build && pnpm --filter ./apps/backend build",
377
392
  "build:frontend": "pnpm --filter ./apps/frontend build",
378
- // Production start. Runs FE :3001 + BE :3000 in parallel via
379
- // \`next start\`. Run \`pnpm backend:upgrade\` BEFORE this — start does
380
- // NOT run migrations on boot (zero-downtime restart safety).
393
+ // Production start. Runs FE :FRONTEND_PORT + BE :BACKEND_PORT (defaults
394
+ // 3001 + 3002 from .env.local). Run \`pnpm backend:upgrade\` BEFORE this —
395
+ // start does NOT run migrations on boot (zero-downtime restart safety).
381
396
  start:
382
- "concurrently -k -n FE,BE -c blue,magenta \"pnpm --filter ./apps/frontend start\" \"pnpm --filter ./apps/backend exec next start\"",
383
- // Single-origin production: FE + BE + Node reverse proxy on :3030.
397
+ "concurrently -k -n FE,BE -c blue,magenta \"pnpm start:frontend\" \"pnpm start:backend\"",
398
+ // Single-origin production: FE + BE + Node reverse proxy on :PROXY_PORT.
384
399
  "start:rp":
385
- "concurrently -k -n FE,BE,PX -c blue,magenta,cyan \"pnpm --filter ./apps/frontend start\" \"pnpm --filter ./apps/backend exec next start\" \"pnpm start:proxy\"",
386
- "start:proxy": "NODE_ENV=production node apps/proxy/server.mjs",
387
- "start:frontend": "pnpm --filter ./apps/frontend start",
388
- "start:backend": "pnpm --filter ./apps/backend exec next start",
400
+ "concurrently -k -n FE,BE,PX -c blue,magenta,cyan \"pnpm start:frontend\" \"pnpm start:backend\" \"pnpm start:proxy\"",
401
+ "start:proxy": "NODE_ENV=production node scripts/with-env.mjs node apps/proxy/server.mjs",
402
+ "start:frontend":
403
+ "node scripts/with-env.mjs --port=FRONTEND_PORT pnpm --filter ./apps/frontend exec next start",
404
+ "start:backend":
405
+ "node scripts/with-env.mjs --port=BACKEND_PORT pnpm --filter ./apps/backend exec next start",
389
406
  // Self-hosted scheduler fallback. Vercel deploys use Vercel Cron via
390
407
  // apps/backend/vercel.json — skip this on Vercel. For Docker / VPS
391
408
  // you can either run \`pnpm start:cron\` alongside \`pnpm start\` (PM2
@@ -399,7 +416,13 @@ function writeRootPackageJson(targetDir, dirName) {
399
416
  "cms-plugin:new": "node scripts/new-cms-plugin.mjs",
400
417
  lint: "pnpm -r lint",
401
418
  typecheck: "pnpm -r typecheck",
402
- "backend:update": "git submodule update --remote --merge apps/backend",
419
+ // Stash any local changes inside the submodule so the fast-forward
420
+ // merge can't conflict, then update, then run `pnpm install` at the
421
+ // root so workspace deps pick up any package.json changes pulled in
422
+ // from the new apollo-cms commit. The stash is restored if it was
423
+ // created; otherwise this is a no-op.
424
+ "backend:update":
425
+ "node -e \"const {execSync:e}=require('child_process');const r=s=>e(s,{stdio:'inherit'});const has=e('git -C apps/backend status --porcelain',{encoding:'utf8'}).trim().length>0;if(has)r('git -C apps/backend stash push -u -m backend-update-auto');r('git submodule update --remote --merge apps/backend');r('pnpm install');if(has){try{r('git -C apps/backend stash pop');}catch(_){console.error('[backend:update] stash pop had conflicts — resolve manually with: git -C apps/backend stash list');}}\"",
403
426
  // `pnpm setup` is pnpm's CLI bootstrap built-in — must use `run` to
404
427
  // forward to the workspace's setup script (apollo-cms's db:push + db:seed).
405
428
  "backend:setup": "pnpm --filter ./apps/backend run setup",
@@ -539,10 +562,43 @@ function writeProxyApp(targetDir, dirName, adminPrefix) {
539
562
 
540
563
  import http from "node:http";
541
564
  import net from "node:net";
565
+ import { readFileSync, existsSync } from "node:fs";
566
+ import { resolve, dirname } from "node:path";
567
+ import { fileURLToPath } from "node:url";
568
+
569
+ // Load root .env.local so the proxy honors PROXY_PORT/FRONTEND_PORT/BACKEND_PORT
570
+ // alongside the frontend & backend. Process env still wins (12-factor friendly).
571
+ (function loadRootEnv() {
572
+ try {
573
+ const here = dirname(fileURLToPath(import.meta.url));
574
+ const envPath = resolve(here, "../../.env.local");
575
+ if (!existsSync(envPath)) return;
576
+ for (const raw of readFileSync(envPath, "utf8").split(/\\r?\\n/)) {
577
+ const line = raw.trim();
578
+ if (!line || line.startsWith("#")) continue;
579
+ const eq = line.indexOf("=");
580
+ if (eq < 0) continue;
581
+ const key = line.slice(0, eq).trim();
582
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;
583
+ if (process.env[key] !== undefined) continue;
584
+ let val = line.slice(eq + 1).trim();
585
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
586
+ val = val.slice(1, -1);
587
+ }
588
+ process.env[key] = val;
589
+ }
590
+ } catch {
591
+ // Best-effort. The proxy still runs on its hardcoded fallbacks.
592
+ }
593
+ })();
542
594
 
543
- const PORT = Number(process.env.PORT ?? 3030);
544
- const BACKEND = parseTarget(process.env.BACKEND ?? "http://127.0.0.1:3000");
545
- const FRONTEND = parseTarget(process.env.FRONTEND ?? "http://127.0.0.1:3001");
595
+ // Backward-compatible: PROXY_PORT/FRONTEND_PORT/BACKEND_PORT are the new names;
596
+ // PORT/FRONTEND/BACKEND are still honored if explicitly set in process.env.
597
+ const PORT = Number(process.env.PROXY_PORT ?? process.env.PORT ?? 3030);
598
+ const BACKEND_HOST = process.env.BACKEND ?? \`http://127.0.0.1:\${process.env.BACKEND_PORT ?? 3002}\`;
599
+ const FRONTEND_HOST = process.env.FRONTEND ?? \`http://127.0.0.1:\${process.env.FRONTEND_PORT ?? 3001}\`;
600
+ const BACKEND = parseTarget(BACKEND_HOST);
601
+ const FRONTEND = parseTarget(FRONTEND_HOST);
546
602
 
547
603
  const ADMIN_PREFIX = process.env.ADMIN_PREFIX ?? "${prefix}";
548
604
  // Pipe-separated list of /api/<segment> paths that route to the backend.
@@ -824,6 +880,10 @@ The Node proxy is intended for dev and small self-hosted deploys.
824
880
  }
825
881
 
826
882
  function writeRootGitignore(targetDir) {
883
+ // NOTE: .env.local is intentionally NOT ignored — it holds shared dev port
884
+ // defaults (PROXY_PORT/FRONTEND_PORT/BACKEND_PORT) and other non-secret
885
+ // workspace knobs that should travel with the repo. Keep real secrets in
886
+ // .env (ignored) or apps/backend/.env.local (ignored by the submodule).
827
887
  const ignore = [
828
888
  "node_modules",
829
889
  ".pnpm-store",
@@ -832,8 +892,6 @@ function writeRootGitignore(targetDir) {
832
892
  "dist",
833
893
  "build",
834
894
  ".env",
835
- ".env.local",
836
- ".env*.local",
837
895
  "*.log",
838
896
  ".DS_Store",
839
897
  "",
@@ -845,12 +903,12 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
845
903
  const header = adminPrefix
846
904
  ? [
847
905
  "# Single-origin monorepo dev env",
848
- "# Public origin = frontend (apps/frontend on :3001). Frontend rewrites /admin/*,",
849
- "# /api/*, /uploads/*, and the asset prefix to the backend (apps/backend on :3000).",
906
+ `# Public origin = frontend (apps/frontend on :${DEFAULT_FRONTEND_PORT}). Frontend rewrites /admin/*,`,
907
+ `# /api/*, /uploads/*, and the asset prefix to the backend (apps/backend on :${DEFAULT_BACKEND_PORT}).`,
850
908
  ]
851
909
  : [
852
910
  "# Separate-origins monorepo dev env",
853
- "# Backend runs at http://localhost:3000, frontend at http://localhost:3001.",
911
+ `# Backend runs at http://localhost:${DEFAULT_BACKEND_PORT}, frontend at http://localhost:${DEFAULT_FRONTEND_PORT}.`,
854
912
  "# To switch to single-origin: set APOLLO_ASSET_PREFIX (default: /admin) and",
855
913
  "# BACKEND_INTERNAL_URL, then add rewrites() to apps/frontend/next.config.ts.",
856
914
  ];
@@ -858,10 +916,25 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
858
916
  const lines = [
859
917
  ...header,
860
918
  "",
919
+ "# ── Dev server ports ─────────────────────────────────────────────",
920
+ "# Single source of truth for local ports. Consumed by:",
921
+ "# • apps/proxy/server.mjs (PROXY_PORT, FRONTEND_PORT, BACKEND_PORT)",
922
+ "# • scripts/with-env.mjs (maps FRONTEND_PORT/BACKEND_PORT → PORT",
923
+ "# when launching apps/frontend & apps/backend,",
924
+ "# and derives NEXT_PUBLIC_SITE_URL /",
925
+ "# BACKEND_INTERNAL_URL when those are unset)",
926
+ "# Override any of these; the proxy/frontend/backend will follow.",
927
+ `PROXY_PORT=${DEFAULT_PROXY_PORT}`,
928
+ `FRONTEND_PORT=${DEFAULT_FRONTEND_PORT}`,
929
+ `BACKEND_PORT=${DEFAULT_BACKEND_PORT}`,
930
+ "",
861
931
  "# ── Shared (consumed by both apps) ───────────────────────────────",
862
932
  `DATABASE_URL=${dbUrl}`,
863
933
  `APOLLO_SECRET=${authSecret}`,
864
934
  `CRON_SECRET=${cronSecret}`,
935
+ "# Public origin. In dev, leave blank to auto-derive from FRONTEND_PORT",
936
+ "# (single-origin) or BACKEND_PORT (separate-origins) via scripts/with-env.mjs.",
937
+ "# In prod set to the real domain (e.g. https://cms.example.com).",
865
938
  `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
866
939
  `NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
867
940
  "",
@@ -878,6 +951,9 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
878
951
  `APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins`,
879
952
  "",
880
953
  "# ── Frontend (apps/frontend) ─────────────────────────────────────",
954
+ "# Where the frontend's rewrites point internally. In dev, leave blank",
955
+ "# to auto-derive http://127.0.0.1:${BACKEND_PORT} via scripts/with-env.mjs.",
956
+ "# In prod set to a private hostname (e.g. http://backend.internal:3000).",
881
957
  `BACKEND_INTERNAL_URL=${backendInternalUrl}`,
882
958
  "",
883
959
  );
@@ -907,9 +983,12 @@ function writeFrontendApp(targetDir, frontendName, siteUrl, adminPrefix, backend
907
983
  version: "0.0.0",
908
984
  private: true,
909
985
  scripts: {
910
- dev: "next dev -p 3001",
986
+ // No -p flag: Next.js reads PORT from env. The repo-root scripts use
987
+ // scripts/with-env.mjs --port=FRONTEND_PORT to inject the right value
988
+ // from .env.local (default 3001).
989
+ dev: "next dev",
911
990
  build: "next build",
912
- start: "next start -p 3001",
991
+ start: "next start",
913
992
  lint: "next lint",
914
993
  typecheck: "tsc --noEmit",
915
994
  },
@@ -1039,7 +1118,8 @@ export default config;
1039
1118
 
1040
1119
  writeFileSync(
1041
1120
  resolve(dir, ".gitignore"),
1042
- ["node_modules", ".next", "dist", ".env.local", ""].join("\n"),
1121
+ // .env.local is intentionally committed — it only contains dev port hints.
1122
+ ["node_modules", ".next", "dist", ""].join("\n"),
1043
1123
  );
1044
1124
 
1045
1125
  // Vercel project config for the frontend. Skip cron + region pinning is
@@ -1267,6 +1347,88 @@ console.log(\`Run: pnpm install && pnpm cms-plugins:build && pnpm dev:backend\`)
1267
1347
  writeFileSync(resolve(scriptsDir, "new-cms-plugin.mjs"), script);
1268
1348
  }
1269
1349
 
1350
+ // Small launcher that loads root .env.local, applies port defaults
1351
+ // (PROXY_PORT=3030 / FRONTEND_PORT=3001 / BACKEND_PORT=3002), optionally maps
1352
+ // one of them onto PORT (Next.js reads PORT), and execs the rest of argv.
1353
+ //
1354
+ // Usage:
1355
+ // node scripts/with-env.mjs [--port=VAR_NAME] <command> [args...]
1356
+ function writeWithEnvScript(targetDir) {
1357
+ const scriptsDir = resolve(targetDir, "scripts");
1358
+ mkdirSync(scriptsDir, { recursive: true });
1359
+ const script = `#!/usr/bin/env node
1360
+ // Loads <repo-root>/.env.local so frontend / backend / proxy all share one
1361
+ // source of truth for ports. Process env still wins.
1362
+ //
1363
+ // Usage:
1364
+ // node scripts/with-env.mjs [--port=VAR_NAME] <command> [args...]
1365
+ // --port=FRONTEND_PORT copies process.env.FRONTEND_PORT onto PORT before exec,
1366
+ // so \`next dev\` and \`next start\` pick up the right port without --port flags.
1367
+
1368
+ import { existsSync, readFileSync } from "node:fs";
1369
+ import { spawn } from "node:child_process";
1370
+ import { dirname, resolve } from "node:path";
1371
+ import { fileURLToPath } from "node:url";
1372
+
1373
+ const here = dirname(fileURLToPath(import.meta.url));
1374
+ const envPath = resolve(here, "../.env.local");
1375
+
1376
+ if (existsSync(envPath)) {
1377
+ for (const raw of readFileSync(envPath, "utf8").split(/\\r?\\n/)) {
1378
+ const line = raw.trim();
1379
+ if (!line || line.startsWith("#")) continue;
1380
+ const eq = line.indexOf("=");
1381
+ if (eq < 0) continue;
1382
+ const k = line.slice(0, eq).trim();
1383
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(k)) continue;
1384
+ if (process.env[k] !== undefined) continue;
1385
+ let v = line.slice(eq + 1).trim();
1386
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
1387
+ v = v.slice(1, -1);
1388
+ }
1389
+ process.env[k] = v;
1390
+ }
1391
+ }
1392
+
1393
+ // Fallbacks — keep in sync with apps/proxy/server.mjs and PM2 config.
1394
+ process.env.PROXY_PORT ||= "3030";
1395
+ process.env.FRONTEND_PORT ||= "3001";
1396
+ process.env.BACKEND_PORT ||= "3002";
1397
+
1398
+ // Derive URL vars from PORTs when unset. Dev-friendly: edit a port and
1399
+ // everything follows. Prod-friendly: set these explicitly to real hostnames
1400
+ // (e.g. https://cms.example.com, http://backend.internal:3000) and the
1401
+ // derivation is bypassed.
1402
+ //
1403
+ // NEXT_PUBLIC_SITE_URL is the public origin — defaults to the frontend port
1404
+ // (frontend rewrites /admin, /api, /uploads to the backend). For \`pnpm dev:rp\`
1405
+ // set it to http://localhost:\${PROXY_PORT} so Better Auth / OAuth callbacks
1406
+ // land on the unified origin.
1407
+ process.env.NEXT_PUBLIC_SITE_URL ||= \`http://localhost:\${process.env.FRONTEND_PORT}\`;
1408
+ process.env.BACKEND_INTERNAL_URL ||= \`http://127.0.0.1:\${process.env.BACKEND_PORT}\`;
1409
+
1410
+ const args = process.argv.slice(2);
1411
+ if (args[0]?.startsWith("--port=")) {
1412
+ const name = args.shift().slice(7);
1413
+ if (process.env[name]) process.env.PORT = process.env[name];
1414
+ }
1415
+
1416
+ if (!args.length) {
1417
+ console.error("Usage: node scripts/with-env.mjs [--port=VAR] <command> [args...]");
1418
+ process.exit(2);
1419
+ }
1420
+
1421
+ const [cmd, ...rest] = args;
1422
+ const child = spawn(cmd, rest, {
1423
+ stdio: "inherit",
1424
+ env: process.env,
1425
+ shell: process.platform === "win32",
1426
+ });
1427
+ child.on("exit", (code, sig) => process.exit(sig ? 1 : code ?? 0));
1428
+ `;
1429
+ writeFileSync(resolve(scriptsDir, "with-env.mjs"), script);
1430
+ }
1431
+
1270
1432
  // Pre-build env check. Fails the build before \`next build\` runs if backend
1271
1433
  // required vars are missing, so users don't sit through a 30s build only to
1272
1434
  // hit a runtime error on first request.
@@ -1343,7 +1505,7 @@ module.exports = {
1343
1505
  cwd: "./apps/frontend",
1344
1506
  script: "node_modules/next/dist/bin/next",
1345
1507
  args: "start",
1346
- env: { NODE_ENV: "production", PORT: 3001 },
1508
+ env: { NODE_ENV: "production", PORT: Number(process.env.FRONTEND_PORT) || 3001 },
1347
1509
  max_memory_restart: "1G",
1348
1510
  autorestart: true,
1349
1511
  },
@@ -1352,7 +1514,7 @@ module.exports = {
1352
1514
  cwd: "./apps/backend",
1353
1515
  script: "node_modules/next/dist/bin/next",
1354
1516
  args: "start",
1355
- env: { NODE_ENV: "production", PORT: 3000 },
1517
+ env: { NODE_ENV: "production", PORT: Number(process.env.BACKEND_PORT) || 3002 },
1356
1518
  max_memory_restart: "1G",
1357
1519
  autorestart: true,
1358
1520
  },
@@ -1361,9 +1523,9 @@ module.exports = {
1361
1523
  script: "./apps/proxy/server.mjs",
1362
1524
  env: {
1363
1525
  NODE_ENV: "production",
1364
- PORT: 3030,
1365
- BACKEND: "http://127.0.0.1:3000",
1366
- FRONTEND: "http://127.0.0.1:3001",
1526
+ PROXY_PORT: Number(process.env.PROXY_PORT) || 3030,
1527
+ FRONTEND_PORT: Number(process.env.FRONTEND_PORT) || 3001,
1528
+ BACKEND_PORT: Number(process.env.BACKEND_PORT) || 3002,
1367
1529
  ${singleOrigin ? `ADMIN_PREFIX: "${adminPrefix}",` : ""}
1368
1530
  },
1369
1531
  max_memory_restart: "256M",
@@ -1383,6 +1545,100 @@ module.exports = {
1383
1545
  writeFileSync(resolve(targetDir, "ecosystem.config.cjs"), config);
1384
1546
  }
1385
1547
 
1548
+ function writeClaudeMd(targetDir, adminPrefix) {
1549
+ const singleOrigin = !!adminPrefix;
1550
+ const prefix = adminPrefix || "/admin";
1551
+ const singleOriginSection = singleOrigin
1552
+ ? `## Single-origin routing
1553
+
1554
+ Frontend is the public entry point. It rewrites these paths to the backend (do **not** create matching routes in the frontend):
1555
+
1556
+ - \`/admin/*\`, \`/uploads/*\`
1557
+ - \`/api/auth/*\`, \`/api/v1/*\`, \`/api/email/*\`, \`/api/cron\`, \`/api/health\`, \`/api/mcp\`, \`/api/admin/*\`, \`/api/editing-presence/*\`
1558
+ - \`/_next/static\` under \`APOLLO_ASSET_PREFIX=${prefix}\` (backend chunks)
1559
+
1560
+ Custom frontend APIs must be namespaced (e.g. \`/api/internal/*\`).
1561
+
1562
+ To disable single-origin: remove \`APOLLO_ASSET_PREFIX\` from \`apps/backend/.env.local\` and delete the \`rewrites()\` block in \`apps/frontend/next.config.ts\`.
1563
+
1564
+ Known gotcha: backend's \`/_next/image\` is not rewritten — custom admin code using \`<Image>\` on \`/uploads/*\` will 404. Use plain \`<img>\` or \`unoptimized\`.
1565
+ `
1566
+ : `## Routing model — separate origins
1567
+
1568
+ Frontend and backend run on independent origins. No rewrites; talk to the backend over its public URL.
1569
+ `;
1570
+
1571
+ const content = `# CLAUDE.md
1572
+
1573
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
1574
+
1575
+ ## Repo shape
1576
+
1577
+ pnpm workspace monorepo. Three workspaces under \`apps/\`:
1578
+
1579
+ - \`apps/frontend\` — public Next.js site${singleOrigin ? " (the public origin in single-origin mode)" : ""}.
1580
+ - \`apps/backend\` — **git submodule** pointing at \`apollo-cms\`. Treat as read-only; do not edit files in here. Open PRs upstream.
1581
+ - \`apps/cms-plugins/<slug>\` — project-specific Apollo CMS plugins. Loaded by the backend via \`APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins\`. Each plugin is built with esbuild to \`dist/server.mjs\` before the backend builds.
1582
+ - \`apps/proxy/server.mjs\` — zero-dep Node reverse proxy used for single-origin dev and self-hosted deploys.
1583
+
1584
+ Built-in plugins in \`apps/backend/plugins/\` always win on slug collisions with \`apps/cms-plugins/\`.
1585
+
1586
+ ${singleOriginSection}
1587
+ ## Common commands
1588
+
1589
+ \`\`\`bash
1590
+ pnpm install
1591
+ pnpm backend:setup # first-time: db:push + db:seed
1592
+ pnpm backend:upgrade # release-time: db:push + replay src/upgrades/0.1.*.ts + seed
1593
+ pnpm backend:update # fast-forward the apollo-cms submodule
1594
+
1595
+ pnpm dev # FE + BE + plugin watcher (parallel)
1596
+ pnpm dev:rp # same + reverse proxy on :3030 (single origin, shared cookies)
1597
+ pnpm dev:frontend # FE only
1598
+ pnpm dev:backend # BE only (runs predev:setup to build plugins first)
1599
+
1600
+ pnpm build # cms-plugins → backend plugins → backend → frontend
1601
+ pnpm build:backend
1602
+ pnpm build:frontend
1603
+
1604
+ pnpm start # FE + BE
1605
+ pnpm start:rp # FE + BE + proxy
1606
+
1607
+ pnpm lint # recursive
1608
+ pnpm typecheck # recursive
1609
+
1610
+ pnpm cms-plugin:new <slug> # scaffold a new plugin from example-plugin
1611
+ \`\`\`
1612
+
1613
+ \`pnpm dev\` and \`pnpm build\` both run \`cms-plugins:build\` first (via \`predev:setup\` / build script chain) — plugins must compile to \`dist/server.mjs\` before the backend resolves them in production. In dev under Bun the loader also accepts \`index.ts\` directly.
1614
+
1615
+ Release flow on a server: \`pnpm install && pnpm backend:upgrade && pnpm build && pm2 reload all\`. \`pnpm start\` does **not** run migrations.
1616
+
1617
+ ## Ports
1618
+
1619
+ Configured in \`ecosystem.config.cjs\` (PM2) and consumed by \`apps/proxy/server.mjs\` via env:
1620
+
1621
+ | Service | Port |
1622
+ | -------- | ---- |
1623
+ | proxy | 3030 |
1624
+ | backend | 3000 |
1625
+ | frontend | 3001 |
1626
+
1627
+ Proxy reads \`PORT\`, \`BACKEND\`, \`FRONTEND\`, \`ADMIN_PREFIX\` from env. When using the proxy locally, set \`NEXT_PUBLIC_SITE_URL=http://localhost:3030\` so Better Auth / OAuth callbacks land on the unified origin.
1628
+
1629
+ ## Deploy
1630
+
1631
+ - Self-hosted: build on a runner, rsync to the server, then \`pm2 startOrReload ecosystem.config.cjs --update-env\`. If the backend submodule is private, the deploy runner needs a token with read access.
1632
+ - Vercel: two projects per repo (\`apps/backend\` and \`apps/frontend\` root dirs). Backend's Build Command must copy \`apps/cms-plugins/*/dist\` into \`apps/backend/plugins/\` before \`next build\` because Turbopack rejects \`outputFileTracingIncludes\` globs above the project root. "Include all submodules" must be ON in both projects.
1633
+
1634
+ ## Submodule discipline
1635
+
1636
+ - Do not edit files under \`apps/backend\` — they belong to the upstream \`apollo-cms\` repo.
1637
+ - After \`pnpm backend:update\`, run \`pnpm install\` (in case package.json changed) then \`pnpm backend:upgrade\` (replays data migrations that \`backend:setup\` skips).
1638
+ `;
1639
+ writeFileSync(resolve(targetDir, "CLAUDE.md"), content);
1640
+ }
1641
+
1386
1642
  function writeReadme(targetDir, dirName, frontendName, adminPrefix) {
1387
1643
  const singleOrigin = !!adminPrefix;
1388
1644
 
@@ -1511,14 +1767,13 @@ folder + \`plugin.json#name\`.
1511
1767
  Apollo CMS is tracked as a git submodule. Full upgrade flow:
1512
1768
 
1513
1769
  \`\`\`bash
1514
- pnpm backend:update # fast-forward apps/backend to the latest apollo-cms commit
1515
- pnpm install # in case the submodule changed package.json
1770
+ pnpm backend:update # auto-stash, fast-forward apps/backend, run pnpm install
1516
1771
  pnpm backend:upgrade # drizzle push + replay version migrations + seed
1517
1772
  \`\`\`
1518
1773
 
1519
1774
  | Script | What it does |
1520
1775
  | --- | --- |
1521
- | \`pnpm backend:update\` | \`git submodule update --remote --merge apps/backend\` code only |
1776
+ | \`pnpm backend:update\` | Auto-stashes local submodule changes, fast-forwards \`apps/backend\`, runs \`pnpm install\` so new backend deps are picked up, then restores the stash. |
1522
1777
  | \`pnpm backend:setup\` | First-time bootstrap: \`db:push\` + \`db:seed\` only |
1523
1778
  | \`pnpm backend:upgrade\` | Full pipeline: \`db:push\` + replay \`src/upgrades/0.1.*.ts\` data migrations + \`db:seed\`. **Use this** after \`backend:update\` — \`backend:setup\` skips the data-migration phase. |
1524
1779
 
@@ -1659,7 +1914,7 @@ async function main() {
1659
1914
 
1660
1915
  if (!flags.directory) {
1661
1916
  log(HELP_TEXT);
1662
- fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo thamc-new");
1917
+ fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo my-site");
1663
1918
  }
1664
1919
 
1665
1920
  log(`\n${COLORS.bold}${COLORS.cyan} Apollo CMS Monorepo Installer${COLORS.reset}\n`);
@@ -1678,7 +1933,7 @@ async function main() {
1678
1933
  // Required by apollo-cms's /api/cron route and scripts/dev-cron.ts in dev.
1679
1934
  // Without it the dev cron loop hits 403 "CRON_SECRET not configured".
1680
1935
  const cronSecret = randomBytes(24).toString("hex");
1681
- const backendInternalUrl = "http://localhost:3000";
1936
+ const backendInternalUrl = `http://localhost:${DEFAULT_BACKEND_PORT}`;
1682
1937
  success(`Frontend pkg name: ${frontendName}`);
1683
1938
  success(`Admin prefix: ${adminPrefix || "(disabled — separate origins)"}`);
1684
1939
 
@@ -1691,14 +1946,16 @@ async function main() {
1691
1946
  writeRootGitignore(targetDir);
1692
1947
  writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl });
1693
1948
  writeReadme(targetDir, dirName, frontendName, adminPrefix);
1949
+ writeClaudeMd(targetDir, adminPrefix);
1694
1950
  if (adminPrefix) writeNginxSample(targetDir, adminPrefix);
1695
1951
  writeProxyApp(targetDir, dirName, adminPrefix);
1696
1952
  writeCheckEnvScript(targetDir);
1953
+ writeWithEnvScript(targetDir);
1697
1954
  writePm2Config(targetDir, dirName, adminPrefix);
1698
1955
  success(
1699
- `package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md${
1956
+ `package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md, CLAUDE.md${
1700
1957
  adminPrefix ? ", nginx.conf.sample" : ""
1701
- }, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs`,
1958
+ }, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs, scripts/with-env.mjs`,
1702
1959
  );
1703
1960
 
1704
1961
  // ── Step 4: git init ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-apollo-monorepo",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Scaffold a monorepo with a frontend app and Apollo CMS as a git submodule backend (single-origin via Next.js rewrites + assetPrefix)",
5
5
  "bin": {
6
6
  "create-apollo-monorepo": "index.mjs"
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git",
24
+ "url": "git+https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git",
25
25
  "directory": "installer-monorepo"
26
26
  },
27
27
  "license": "MIT"