create-apollo-monorepo 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.
Files changed (3) hide show
  1. package/README.md +65 -0
  2. package/index.mjs +583 -0
  3. package/package.json +26 -0
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # create-apollo-monorepo
2
+
3
+ Scaffold a pnpm monorepo where your custom frontend lives alongside Apollo CMS
4
+ mounted as a **git submodule** backend (read-only — pull updates only).
5
+
6
+ ## Usage
7
+
8
+ ```bash
9
+ npx create-apollo-monorepo thamc-new
10
+ ```
11
+
12
+ With flags:
13
+
14
+ ```bash
15
+ npx create-apollo-monorepo thamc-new \
16
+ --frontend-name "@thamc/frontend" \
17
+ --db "postgresql://user:pass@localhost:5432/thamc" \
18
+ --url "http://localhost:3000" \
19
+ --locale th
20
+ ```
21
+
22
+ ## Result
23
+
24
+ ```
25
+ thamc-new/
26
+ ├── apps/
27
+ │ ├── frontend/ ← @thamc/frontend (Next.js skeleton)
28
+ │ └── backend/ ← git submodule → apollo-cms
29
+ ├── package.json ← root workspace
30
+ ├── pnpm-workspace.yaml
31
+ ├── .env.local ← shared dev env
32
+ ├── .gitmodules ← submodule config
33
+ └── .gitignore
34
+ ```
35
+
36
+ ## Flags
37
+
38
+ | Flag | Default | Description |
39
+ | -------------------------- | --------------------------------------- | ------------------------------------ |
40
+ | `--frontend-name <name>` | `@<dir>/frontend` | Frontend `package.json` name |
41
+ | `--backend-url <url>` | `https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git` | Submodule git URL |
42
+ | `--backend-branch <name>` | `main` | Submodule branch to track |
43
+ | `-d, --db <url>` | _(prompted)_ | `DATABASE_URL` for backend |
44
+ | `-u, --url <url>` | `http://localhost:3000` | `NEXT_PUBLIC_SITE_URL` |
45
+ | `-l, --locale <code>` | `en` | `NEXT_PUBLIC_DEFAULT_LOCALE` |
46
+ | `--skip-install` | off | Don't run `pnpm install` |
47
+ | `--skip-submodule` | off | Don't add the git submodule |
48
+ | `-h, --help` | — | Show help |
49
+
50
+ ## After install
51
+
52
+ ```bash
53
+ cd thamc-new
54
+ pnpm backend:setup # push schema + seed apollo-cms
55
+ pnpm dev # frontend :3001 + backend :3000 in parallel
56
+ ```
57
+
58
+ ## Updating the backend
59
+
60
+ ```bash
61
+ pnpm backend:update # git submodule update --remote --merge apps/backend
62
+ ```
63
+
64
+ Don't edit files inside `apps/backend` — open issues / PRs against the
65
+ `apollo-cms` repository upstream.
package/index.mjs ADDED
@@ -0,0 +1,583 @@
1
+ #!/usr/bin/env node
2
+
3
+ // create-apollo-monorepo — Zero-dependency scaffolder for an Apollo CMS monorepo
4
+ // Usage: npx create-apollo-monorepo <directory> [flags]
5
+ //
6
+ // Produces:
7
+ // <directory>/
8
+ // ├── apps/
9
+ // │ ├── frontend/ ← your app (pnpm workspace member)
10
+ // │ └── backend/ ← git submodule → apollo-cms
11
+ // ├── package.json
12
+ // ├── pnpm-workspace.yaml
13
+ // ├── .env.local
14
+ // ├── .gitmodules
15
+ // └── .gitignore
16
+
17
+ import { execSync } from "node:child_process";
18
+ import { randomBytes } from "node:crypto";
19
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
20
+ import { resolve, basename } from "node:path";
21
+ import { createInterface } from "node:readline";
22
+
23
+ // ─── Constants ───────────────────────────────────────────────────────────────
24
+
25
+ const BACKEND_REPO_URL = "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git";
26
+ const BACKEND_BRANCH = "main";
27
+ const BACKEND_PATH = "apps/backend";
28
+ const FRONTEND_PATH = "apps/frontend";
29
+ const MIN_NODE_MAJOR = 20;
30
+
31
+ const COLORS = {
32
+ reset: "\x1b[0m",
33
+ bold: "\x1b[1m",
34
+ dim: "\x1b[2m",
35
+ red: "\x1b[31m",
36
+ green: "\x1b[32m",
37
+ yellow: "\x1b[33m",
38
+ cyan: "\x1b[36m",
39
+ };
40
+
41
+ const HELP_TEXT = `
42
+ ${COLORS.bold}create-apollo-monorepo${COLORS.reset} — Scaffold a monorepo with Apollo CMS as a git submodule backend
43
+
44
+ ${COLORS.bold}Usage:${COLORS.reset}
45
+ npx create-apollo-monorepo <directory> [flags]
46
+
47
+ ${COLORS.bold}Examples:${COLORS.reset}
48
+ npx create-apollo-monorepo thamc-new
49
+ npx create-apollo-monorepo thamc-new --frontend-name "@thamc/frontend"
50
+ npx create-apollo-monorepo thamc-new --backend-branch develop --skip-install
51
+
52
+ ${COLORS.bold}Flags:${COLORS.reset}
53
+ --frontend-name <name> Frontend package name (default: "@<dir>/frontend")
54
+ --backend-url <url> Backend git URL (default: ${BACKEND_REPO_URL})
55
+ --backend-branch <name> Backend branch to track (default: ${BACKEND_BRANCH})
56
+ -d, --db <url> DATABASE_URL for backend
57
+ -u, --url <url> NEXT_PUBLIC_SITE_URL (default: http://localhost:3000)
58
+ -l, --locale <code> NEXT_PUBLIC_DEFAULT_LOCALE (default: en)
59
+ --skip-install Skip dependency installation
60
+ --skip-submodule Skip git submodule add (you'll add it later)
61
+ -h, --help Show this help message
62
+ `;
63
+
64
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
65
+
66
+ function log(msg) {
67
+ console.log(msg);
68
+ }
69
+
70
+ function step(num, msg) {
71
+ log(`\n${COLORS.cyan}[${num}]${COLORS.reset} ${msg}`);
72
+ }
73
+
74
+ function success(msg) {
75
+ log(` ${COLORS.green}✓${COLORS.reset} ${msg}`);
76
+ }
77
+
78
+ function warn(msg) {
79
+ log(` ${COLORS.yellow}⚠${COLORS.reset} ${msg}`);
80
+ }
81
+
82
+ function fatal(msg) {
83
+ console.error(`\n${COLORS.red}Error:${COLORS.reset} ${msg}\n`);
84
+ process.exit(1);
85
+ }
86
+
87
+ function run(cmd, opts = {}) {
88
+ return execSync(cmd, { stdio: "inherit", ...opts });
89
+ }
90
+
91
+ function runSilent(cmd, opts = {}) {
92
+ try {
93
+ return execSync(cmd, { stdio: "pipe", ...opts }).toString().trim();
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ function commandExists(cmd) {
100
+ return runSilent(process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`) !== null;
101
+ }
102
+
103
+ // ─── Arg Parsing ─────────────────────────────────────────────────────────────
104
+
105
+ function parseArgs(argv) {
106
+ const args = argv.slice(2);
107
+ const flags = {
108
+ directory: null,
109
+ frontendName: null,
110
+ backendUrl: BACKEND_REPO_URL,
111
+ backendBranch: BACKEND_BRANCH,
112
+ db: null,
113
+ url: null,
114
+ locale: null,
115
+ skipInstall: false,
116
+ skipSubmodule: false,
117
+ help: false,
118
+ };
119
+
120
+ let i = 0;
121
+ while (i < args.length) {
122
+ const arg = args[i];
123
+ switch (arg) {
124
+ case "-h":
125
+ case "--help":
126
+ flags.help = true;
127
+ break;
128
+ case "--frontend-name":
129
+ flags.frontendName = args[++i];
130
+ break;
131
+ case "--backend-url":
132
+ flags.backendUrl = args[++i];
133
+ break;
134
+ case "--backend-branch":
135
+ flags.backendBranch = args[++i];
136
+ break;
137
+ case "-d":
138
+ case "--db":
139
+ flags.db = args[++i];
140
+ break;
141
+ case "-u":
142
+ case "--url":
143
+ flags.url = args[++i];
144
+ break;
145
+ case "-l":
146
+ case "--locale":
147
+ flags.locale = args[++i];
148
+ break;
149
+ case "--skip-install":
150
+ flags.skipInstall = true;
151
+ break;
152
+ case "--skip-submodule":
153
+ flags.skipSubmodule = true;
154
+ break;
155
+ default:
156
+ if (arg.startsWith("-")) {
157
+ fatal(`Unknown flag: ${arg}\nRun with --help for usage.`);
158
+ }
159
+ if (flags.directory) {
160
+ fatal(`Unexpected argument: ${arg}\nOnly one directory name is allowed.`);
161
+ }
162
+ flags.directory = arg;
163
+ }
164
+ i++;
165
+ }
166
+
167
+ return flags;
168
+ }
169
+
170
+ // ─── Validation ──────────────────────────────────────────────────────────────
171
+
172
+ function isValidDbUrl(url) {
173
+ return /^postgres(ql)?:\/\/.+/.test(url);
174
+ }
175
+
176
+ function isValidUrl(url) {
177
+ return /^https?:\/\/.+/.test(url);
178
+ }
179
+
180
+ function isValidLocale(code) {
181
+ return /^[a-z]{2,5}$/i.test(code);
182
+ }
183
+
184
+ // ─── Pre-flight ──────────────────────────────────────────────────────────────
185
+
186
+ function preflight(flags) {
187
+ step(1, "Pre-flight checks");
188
+
189
+ const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
190
+ if (nodeMajor < MIN_NODE_MAJOR) {
191
+ fatal(
192
+ `Node.js ${MIN_NODE_MAJOR}+ is required (found ${process.versions.node}).\n Install: https://nodejs.org/`,
193
+ );
194
+ }
195
+ success(`Node.js ${process.versions.node}`);
196
+
197
+ if (!commandExists("git")) {
198
+ fatal("git is required but not found.\n Install: https://git-scm.com/");
199
+ }
200
+ success("git found");
201
+
202
+ if (!commandExists("pnpm")) {
203
+ warn("pnpm not found — install with: npm i -g pnpm (required for workspaces)");
204
+ } else {
205
+ success("pnpm found");
206
+ }
207
+
208
+ const targetDir = resolve(flags.directory);
209
+ if (existsSync(targetDir)) {
210
+ fatal(`Directory already exists: ${targetDir}\n Choose a different name or remove it first.`);
211
+ }
212
+ success(`Target: ${targetDir}`);
213
+
214
+ return targetDir;
215
+ }
216
+
217
+ // ─── Prompts ─────────────────────────────────────────────────────────────────
218
+
219
+ function createPrompt() {
220
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
221
+ const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
222
+ const close = () => rl.close();
223
+ return { ask, close };
224
+ }
225
+
226
+ async function gatherEnv(flags) {
227
+ let dbUrl = flags.db;
228
+ let siteUrl = flags.url ?? "http://localhost:3000";
229
+ let locale = flags.locale ?? "en";
230
+
231
+ if (flags.db && !isValidDbUrl(flags.db)) fatal("--db must start with postgresql:// or postgres://");
232
+ if (flags.url && !isValidUrl(flags.url)) fatal("--url must start with http:// or https://");
233
+ if (flags.locale && !isValidLocale(flags.locale)) fatal("--locale must be a 2-5 character code (e.g., en, th)");
234
+
235
+ const needsPrompt = !dbUrl || !flags.url || !flags.locale;
236
+ if (!needsPrompt) return { dbUrl, siteUrl, locale };
237
+
238
+ const { ask, close } = createPrompt();
239
+
240
+ if (!dbUrl) {
241
+ log("");
242
+ while (!dbUrl) {
243
+ const ans = await ask(
244
+ ` ${COLORS.bold}DATABASE_URL${COLORS.reset} ${COLORS.dim}(postgresql://user:pass@host:5432/dbname)${COLORS.reset}\n > `,
245
+ );
246
+ if (isValidDbUrl(ans)) dbUrl = ans;
247
+ else warn("Must start with postgresql:// or postgres://");
248
+ }
249
+ }
250
+
251
+ if (!flags.url) {
252
+ const ans = await ask(
253
+ `\n ${COLORS.bold}NEXT_PUBLIC_SITE_URL${COLORS.reset} ${COLORS.dim}[${siteUrl}]${COLORS.reset}\n > `,
254
+ );
255
+ if (ans) {
256
+ if (isValidUrl(ans)) siteUrl = ans;
257
+ else warn(`Invalid URL, using default: ${siteUrl}`);
258
+ }
259
+ }
260
+
261
+ if (!flags.locale) {
262
+ const ans = await ask(
263
+ `\n ${COLORS.bold}Default locale${COLORS.reset} ${COLORS.dim}[${locale}]${COLORS.reset}\n > `,
264
+ );
265
+ if (ans) {
266
+ if (isValidLocale(ans)) locale = ans;
267
+ else warn(`Invalid locale code, using default: ${locale}`);
268
+ }
269
+ }
270
+
271
+ close();
272
+ return { dbUrl, siteUrl, locale };
273
+ }
274
+
275
+ // ─── Scaffolding ─────────────────────────────────────────────────────────────
276
+
277
+ function writeRootPackageJson(targetDir, dirName) {
278
+ const pkg = {
279
+ name: dirName,
280
+ version: "0.0.0",
281
+ private: true,
282
+ description: `${dirName} monorepo (frontend + apollo-cms backend submodule)`,
283
+ scripts: {
284
+ dev: "pnpm -r --parallel dev",
285
+ "dev:frontend": "pnpm --filter ./apps/frontend dev",
286
+ "dev:backend": "pnpm --filter ./apps/backend dev",
287
+ build: "pnpm -r build",
288
+ lint: "pnpm -r lint",
289
+ typecheck: "pnpm -r typecheck",
290
+ "backend:update": "git submodule update --remote --merge apps/backend",
291
+ "backend:setup": "pnpm --filter ./apps/backend setup",
292
+ },
293
+ engines: { node: ">=20", pnpm: ">=9" },
294
+ packageManager: "pnpm@9.0.0",
295
+ };
296
+ writeFileSync(resolve(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
297
+ }
298
+
299
+ function writePnpmWorkspace(targetDir) {
300
+ writeFileSync(
301
+ resolve(targetDir, "pnpm-workspace.yaml"),
302
+ "packages:\n - 'apps/*'\n",
303
+ );
304
+ }
305
+
306
+ function writeRootGitignore(targetDir) {
307
+ const ignore = [
308
+ "node_modules",
309
+ ".pnpm-store",
310
+ ".turbo",
311
+ ".next",
312
+ "dist",
313
+ "build",
314
+ ".env",
315
+ ".env.local",
316
+ ".env*.local",
317
+ "*.log",
318
+ ".DS_Store",
319
+ "",
320
+ ].join("\n");
321
+ writeFileSync(resolve(targetDir, ".gitignore"), ignore);
322
+ }
323
+
324
+ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret }) {
325
+ const lines = [
326
+ "# Shared dev env — backend reads these via apps/backend/.env.local symlink/copy",
327
+ `DATABASE_URL=${dbUrl}`,
328
+ `APOLLO_SECRET=${authSecret}`,
329
+ `BETTER_AUTH_SECRET=${authSecret}`,
330
+ `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
331
+ `NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
332
+ "",
333
+ "# Frontend",
334
+ `NEXT_PUBLIC_BACKEND_URL=${siteUrl}`,
335
+ "",
336
+ ].join("\n");
337
+ writeFileSync(resolve(targetDir, ".env.local"), lines);
338
+ }
339
+
340
+ function writeFrontendApp(targetDir, frontendName, siteUrl) {
341
+ const dir = resolve(targetDir, FRONTEND_PATH);
342
+ mkdirSync(dir, { recursive: true });
343
+ mkdirSync(resolve(dir, "src/app"), { recursive: true });
344
+ mkdirSync(resolve(dir, "public"), { recursive: true });
345
+
346
+ const pkg = {
347
+ name: frontendName,
348
+ version: "0.0.0",
349
+ private: true,
350
+ scripts: {
351
+ dev: "next dev -p 3001",
352
+ build: "next build",
353
+ start: "next start -p 3001",
354
+ lint: "next lint",
355
+ typecheck: "tsc --noEmit",
356
+ },
357
+ dependencies: {
358
+ next: "^16.0.0",
359
+ react: "^19.0.0",
360
+ "react-dom": "^19.0.0",
361
+ },
362
+ devDependencies: {
363
+ "@types/node": "^22.0.0",
364
+ "@types/react": "^19.0.0",
365
+ "@types/react-dom": "^19.0.0",
366
+ typescript: "^5.6.0",
367
+ },
368
+ };
369
+ writeFileSync(resolve(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
370
+
371
+ writeFileSync(
372
+ resolve(dir, "tsconfig.json"),
373
+ JSON.stringify(
374
+ {
375
+ compilerOptions: {
376
+ target: "ES2022",
377
+ lib: ["dom", "dom.iterable", "esnext"],
378
+ allowJs: true,
379
+ skipLibCheck: true,
380
+ strict: true,
381
+ noEmit: true,
382
+ esModuleInterop: true,
383
+ module: "esnext",
384
+ moduleResolution: "bundler",
385
+ resolveJsonModule: true,
386
+ isolatedModules: true,
387
+ jsx: "preserve",
388
+ incremental: true,
389
+ plugins: [{ name: "next" }],
390
+ paths: { "@/*": ["./src/*"] },
391
+ },
392
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
393
+ exclude: ["node_modules"],
394
+ },
395
+ null,
396
+ 2,
397
+ ) + "\n",
398
+ );
399
+
400
+ writeFileSync(
401
+ resolve(dir, "next.config.ts"),
402
+ `import type { NextConfig } from "next";\n\nconst config: NextConfig = {\n reactStrictMode: true,\n};\n\nexport default config;\n`,
403
+ );
404
+
405
+ writeFileSync(
406
+ resolve(dir, ".env.local.example"),
407
+ `NEXT_PUBLIC_BACKEND_URL=${siteUrl}\n`,
408
+ );
409
+
410
+ writeFileSync(
411
+ resolve(dir, "src/app/layout.tsx"),
412
+ `export const metadata = { title: "Frontend", description: "Apollo CMS frontend" };\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n`,
413
+ );
414
+
415
+ writeFileSync(
416
+ resolve(dir, "src/app/page.tsx"),
417
+ `export default function Page() {\n return (\n <main style={{ padding: 40, fontFamily: "system-ui" }}>\n <h1>Frontend</h1>\n <p>Backend: <code>{process.env.NEXT_PUBLIC_BACKEND_URL}</code></p>\n </main>\n );\n}\n`,
418
+ );
419
+
420
+ writeFileSync(
421
+ resolve(dir, ".gitignore"),
422
+ ["node_modules", ".next", "dist", ".env.local", ""].join("\n"),
423
+ );
424
+ }
425
+
426
+ function writeReadme(targetDir, dirName, frontendName) {
427
+ const readme = `# ${dirName}
428
+
429
+ Monorepo with a custom frontend and Apollo CMS as a git submodule backend.
430
+
431
+ ## Layout
432
+
433
+ \`\`\`
434
+ ${dirName}/
435
+ ├── apps/
436
+ │ ├── frontend/ ← ${frontendName}
437
+ │ └── backend/ ← git submodule → apollo-cms (read-only, pull updates)
438
+ ├── package.json
439
+ └── pnpm-workspace.yaml
440
+ \`\`\`
441
+
442
+ ## Quick start
443
+
444
+ \`\`\`bash
445
+ pnpm install
446
+ pnpm backend:setup # push schema + seed apollo-cms
447
+ pnpm dev # runs frontend (3001) + backend (3000) in parallel
448
+ \`\`\`
449
+
450
+ ## Updating the backend
451
+
452
+ Apollo CMS is tracked as a git submodule. Pull the latest:
453
+
454
+ \`\`\`bash
455
+ pnpm backend:update
456
+ \`\`\`
457
+
458
+ Do **not** edit files inside \`apps/backend\`. Open issues / PRs in the
459
+ \`apollo-cms\` repository upstream.
460
+
461
+ ## Environment variables
462
+
463
+ Shared dev env lives in the root \`.env.local\`. The backend reads its own
464
+ \`apps/backend/.env.local\` (already populated by the installer).
465
+ `;
466
+ writeFileSync(resolve(targetDir, "README.md"), readme);
467
+ }
468
+
469
+ // ─── Main ────────────────────────────────────────────────────────────────────
470
+
471
+ async function main() {
472
+ const flags = parseArgs(process.argv);
473
+
474
+ if (flags.help) {
475
+ log(HELP_TEXT);
476
+ process.exit(0);
477
+ }
478
+
479
+ if (!flags.directory) {
480
+ log(HELP_TEXT);
481
+ fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo thamc-new");
482
+ }
483
+
484
+ log(`\n${COLORS.bold}${COLORS.cyan} Apollo CMS Monorepo Installer${COLORS.reset}\n`);
485
+
486
+ // ── Step 1: Pre-flight ──
487
+ const targetDir = preflight(flags);
488
+ const dirName = basename(targetDir);
489
+ const frontendName = flags.frontendName ?? `@${dirName}/frontend`;
490
+
491
+ // ── Step 2: Gather env ──
492
+ step(2, "Configuring environment");
493
+ const { dbUrl, siteUrl, locale } = await gatherEnv(flags);
494
+ const authSecret = randomBytes(48).toString("base64");
495
+ success(`Frontend pkg name: ${frontendName}`);
496
+
497
+ // ── Step 3: Scaffold root ──
498
+ step(3, "Scaffolding monorepo root");
499
+ mkdirSync(targetDir, { recursive: true });
500
+ mkdirSync(resolve(targetDir, "apps"), { recursive: true });
501
+ writeRootPackageJson(targetDir, dirName);
502
+ writePnpmWorkspace(targetDir);
503
+ writeRootGitignore(targetDir);
504
+ writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret });
505
+ writeReadme(targetDir, dirName, frontendName);
506
+ success("package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md");
507
+
508
+ // ── Step 4: git init ──
509
+ step(4, "Initializing git");
510
+ run("git init -b main", { cwd: targetDir, stdio: "pipe" });
511
+ success("git repo initialized");
512
+
513
+ // ── Step 5: Frontend skeleton ──
514
+ step(5, "Creating frontend app");
515
+ writeFrontendApp(targetDir, frontendName, siteUrl);
516
+ success(`apps/frontend (${frontendName})`);
517
+
518
+ // ── Step 6: Backend submodule ──
519
+ if (flags.skipSubmodule) {
520
+ step(6, "Skipping git submodule (--skip-submodule)");
521
+ warn(
522
+ `Add it later:\n cd ${dirName}\n git submodule add -b ${flags.backendBranch} ${flags.backendUrl} ${BACKEND_PATH}`,
523
+ );
524
+ } else {
525
+ step(6, `Adding apollo-cms as git submodule (${BACKEND_PATH})`);
526
+ try {
527
+ run(
528
+ `git submodule add -b ${flags.backendBranch} ${flags.backendUrl} ${BACKEND_PATH}`,
529
+ { cwd: targetDir },
530
+ );
531
+ run(`git submodule update --init --recursive`, { cwd: targetDir });
532
+ success("submodule added");
533
+ } catch {
534
+ fatal(
535
+ `Failed to add submodule.\n Check the URL and your network, then run:\n cd ${dirName} && git submodule add -b ${flags.backendBranch} ${flags.backendUrl} ${BACKEND_PATH}`,
536
+ );
537
+ }
538
+
539
+ // Mirror env to backend
540
+ const backendEnv = [
541
+ `DATABASE_URL=${dbUrl}`,
542
+ `APOLLO_SECRET=${authSecret}`,
543
+ `BETTER_AUTH_SECRET=${authSecret}`,
544
+ `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
545
+ `NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
546
+ "",
547
+ ].join("\n");
548
+ writeFileSync(resolve(targetDir, BACKEND_PATH, ".env.local"), backendEnv);
549
+ success("apps/backend/.env.local");
550
+ }
551
+
552
+ // ── Step 7: Install ──
553
+ if (flags.skipInstall) {
554
+ step(7, "Skipping dependency installation (--skip-install)");
555
+ warn(`Run later:\n cd ${dirName} && pnpm install`);
556
+ } else if (!commandExists("pnpm")) {
557
+ step(7, "Skipping dependency installation (pnpm not installed)");
558
+ warn(`Install pnpm and run:\n npm i -g pnpm && cd ${dirName} && pnpm install`);
559
+ } else {
560
+ step(7, "Installing dependencies (pnpm)");
561
+ try {
562
+ run("pnpm install", { cwd: targetDir });
563
+ success("dependencies installed");
564
+ } catch {
565
+ warn(`Install failed — run manually: cd ${dirName} && pnpm install`);
566
+ }
567
+ }
568
+
569
+ // ── Done ──
570
+ log(`
571
+ ${COLORS.green}${COLORS.bold} Monorepo created!${COLORS.reset}
572
+
573
+ ${COLORS.bold}cd${COLORS.reset} ${dirName}
574
+ ${COLORS.bold}pnpm backend:setup${COLORS.reset} ${COLORS.dim}# push schema + seed apollo-cms${COLORS.reset}
575
+ ${COLORS.bold}pnpm dev${COLORS.reset} ${COLORS.dim}# frontend :3001 + backend :3000${COLORS.reset}
576
+
577
+ ${COLORS.dim}APOLLO_SECRET=${authSecret}${COLORS.reset}
578
+ `);
579
+ }
580
+
581
+ main().catch((err) => {
582
+ fatal(err.message ?? String(err));
583
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "create-apollo-monorepo",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a monorepo with a frontend app and Apollo CMS as a git submodule backend",
5
+ "bin": "./index.mjs",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "keywords": [
11
+ "apollo",
12
+ "cms",
13
+ "monorepo",
14
+ "submodule",
15
+ "pnpm",
16
+ "workspace",
17
+ "create",
18
+ "installer"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git",
23
+ "directory": "installer-monorepo"
24
+ },
25
+ "license": "MIT"
26
+ }