create-bw-app 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Scaffold a new BrightWeb app from either the `platform` or `site` starter.
4
4
 
5
+ The CLI can also update an existing generated platform app in place.
6
+
5
7
  ## Workspace usage
6
8
 
7
9
  From the BrightWeb platform repo root:
@@ -21,9 +23,29 @@ Once this package is published to npm:
21
23
  ```bash
22
24
  pnpm dlx create-bw-app
23
25
  pnpm dlx create-bw-app --template site
26
+ pnpm dlx create-bw-app update
24
27
  npm create bw-app@latest
25
28
  ```
26
29
 
30
+ ## Update existing apps
31
+
32
+ Run the updater from an existing generated app directory, or point it at one with `--target-dir`:
33
+
34
+ ```bash
35
+ pnpm dlx create-bw-app update
36
+ pnpm dlx create-bw-app update --dry-run
37
+ pnpm dlx create-bw-app update --refresh-starters
38
+ pnpm dlx create-bw-app update --target-dir ./apps/client-portal
39
+ ```
40
+
41
+ Current updater behavior:
42
+
43
+ - updates installed `@brightweblabs/*` packages only
44
+ - re-syncs managed BrightWeb config files such as `next.config.ts`, `config/modules.ts`, and `config/shell.ts`
45
+ - reports missing or drifted starter files and only rewrites them with `--refresh-starters`
46
+ - prints the follow-up install command unless `--install` is passed
47
+ - preserves unrelated third-party dependencies and app-owned product pages
48
+
27
49
  ## Template behavior
28
50
 
29
51
  - prompts for app type: `platform` or `site`
@@ -34,7 +56,7 @@ npm create bw-app@latest
34
56
  - platform apps include BrightWeb auth, shell wiring, and optional module starter surfaces
35
57
  - site apps include Next.js, Tailwind CSS v4, and local component primitives
36
58
  - writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
37
- - platform apps also write `.env.example`, generated config files, and module feature flags
59
+ - platform apps also write `.env.local`, `AGENTS.md`, `docs/ai/README.md`, and generated config files for brand and module state
38
60
  - supports repo-local `workspace:*` wiring and future published dependency wiring
39
61
 
40
62
  ## Workspace mode extras
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.8.0",
4
+ "version": "0.9.1",
5
5
  "type": "module",
6
6
  "bin": "bin/create-bw-app.mjs",
7
7
  "files": [
package/src/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { HELP_TEXT } from "./constants.mjs";
2
2
  import { createBrightwebClientApp } from "./generator.mjs";
3
+ import { updateBrightwebApp } from "./update.mjs";
3
4
 
4
5
  function toCamelCase(flagName) {
5
6
  return flagName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
@@ -56,7 +57,8 @@ function parseArgv(argv) {
56
57
  }
57
58
 
58
59
  export async function runCreateBwAppCli(argv = process.argv.slice(2), runtimeOptions = {}) {
59
- const argvOptions = parseArgv(argv);
60
+ const isUpdateCommand = argv[0] === "update";
61
+ const argvOptions = parseArgv(isUpdateCommand ? argv.slice(1) : argv);
60
62
 
61
63
  if (argvOptions.help) {
62
64
  process.stdout.write(`${HELP_TEXT}\n`);
@@ -64,6 +66,11 @@ export async function runCreateBwAppCli(argv = process.argv.slice(2), runtimeOpt
64
66
  }
65
67
 
66
68
  try {
69
+ if (isUpdateCommand) {
70
+ await updateBrightwebApp(argvOptions, runtimeOptions);
71
+ return;
72
+ }
73
+
67
74
  await createBrightwebClientApp(argvOptions, runtimeOptions);
68
75
  } catch (error) {
69
76
  const message = error instanceof Error ? error.message : "Unknown error";
package/src/constants.mjs CHANGED
@@ -20,21 +20,18 @@ export const SELECTABLE_MODULES = [
20
20
  label: "CRM",
21
21
  packageName: "@brightweblabs/module-crm",
22
22
  templateFolder: "crm",
23
- envKey: "NEXT_PUBLIC_ENABLE_CRM",
24
23
  },
25
24
  {
26
25
  key: "projects",
27
26
  label: "Projects",
28
27
  packageName: "@brightweblabs/module-projects",
29
28
  templateFolder: "projects",
30
- envKey: "NEXT_PUBLIC_ENABLE_PROJECTS",
31
29
  },
32
30
  {
33
31
  key: "admin",
34
32
  label: "Admin",
35
33
  packageName: "@brightweblabs/module-admin",
36
34
  templateFolder: "admin",
37
- envKey: "NEXT_PUBLIC_ENABLE_ADMIN",
38
35
  },
39
36
  ];
40
37
 
@@ -45,6 +42,29 @@ export const CORE_PACKAGES = [
45
42
  "@brightweblabs/ui",
46
43
  ];
47
44
 
45
+ export const BRIGHTWEB_PACKAGE_NAMES = [
46
+ ...CORE_PACKAGES,
47
+ ...SELECTABLE_MODULES.map((moduleDefinition) => moduleDefinition.packageName),
48
+ ];
49
+
50
+ export const MODULE_STARTER_FILES = {
51
+ admin: [
52
+ "app/api/admin/users/route.ts",
53
+ "app/api/admin/users/roles/route.ts",
54
+ "app/playground/admin/page.tsx",
55
+ ],
56
+ crm: [
57
+ "app/api/crm/contacts/route.ts",
58
+ "app/api/crm/organizations/route.ts",
59
+ "app/api/crm/owners/route.ts",
60
+ "app/api/crm/stats/route.ts",
61
+ "app/playground/crm/page.tsx",
62
+ ],
63
+ projects: [
64
+ "app/playground/projects/page.tsx",
65
+ ],
66
+ };
67
+
48
68
  export const APP_DEPENDENCY_DEFAULTS = {
49
69
  "@brightweblabs/app-shell": "^0.1.1",
50
70
  "@brightweblabs/core-auth": "^0.1.1",
@@ -96,8 +116,9 @@ export const DEFAULTS = {
96
116
  export const HELP_TEXT = `
97
117
  Usage:
98
118
  create-bw-app [options]
119
+ create-bw-app update [options]
99
120
 
100
- Options:
121
+ Scaffold options:
101
122
  --template <platform|site> Scaffold a platform app or a standalone site
102
123
  --name, --slug <name> Project name and default directory name
103
124
  --modules <list> Comma-separated modules: crm,projects,admin
@@ -110,4 +131,12 @@ Options:
110
131
  --yes Accept defaults for any missing optional prompt
111
132
  --dry-run Print planned actions without writing files
112
133
  --help Show this help message
134
+
135
+ Update options:
136
+ --target-dir <path> Existing app directory to update (defaults to cwd)
137
+ --workspace-root <path> BrightWeb workspace root for workspace:* apps
138
+ --package-manager <name> Override package manager: pnpm, npm, yarn, or bun
139
+ --install Run install after writing package changes
140
+ --refresh-starters Rewrite starter route files from the latest template
141
+ --dry-run Print the update plan without writing files
113
142
  `.trim();
package/src/generator.mjs CHANGED
@@ -16,8 +16,8 @@ import {
16
16
  TEMPLATE_OPTIONS,
17
17
  } from "./constants.mjs";
18
18
 
19
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
- const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
19
+ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ export const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
21
21
  const TEMPLATE_KEY_SET = new Set(TEMPLATE_OPTIONS.map((templateOption) => templateOption.key));
22
22
  const DEFAULT_DB_MODULE_REGISTRY = {
23
23
  modules: {
@@ -77,7 +77,7 @@ function parseTemplateInput(rawValue) {
77
77
  throw new Error(`Unknown template: ${rawValue}`);
78
78
  }
79
79
 
80
- function detectPackageManager(explicitManager) {
80
+ export function detectPackageManager(explicitManager) {
81
81
  if (explicitManager) return explicitManager;
82
82
 
83
83
  const userAgent = process.env.npm_config_user_agent || "";
@@ -88,13 +88,13 @@ function detectPackageManager(explicitManager) {
88
88
  return "pnpm";
89
89
  }
90
90
 
91
- function sortObjectKeys(inputObject) {
91
+ export function sortObjectKeys(inputObject) {
92
92
  return Object.fromEntries(
93
93
  Object.entries(inputObject).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)),
94
94
  );
95
95
  }
96
96
 
97
- async function pathExists(targetPath) {
97
+ export async function pathExists(targetPath) {
98
98
  try {
99
99
  await fs.access(targetPath);
100
100
  return true;
@@ -103,7 +103,7 @@ async function pathExists(targetPath) {
103
103
  }
104
104
  }
105
105
 
106
- async function readJsonIfPresent(filePath) {
106
+ export async function readJsonIfPresent(filePath) {
107
107
  if (!(await pathExists(filePath))) {
108
108
  return null;
109
109
  }
@@ -111,7 +111,7 @@ async function readJsonIfPresent(filePath) {
111
111
  return JSON.parse(await fs.readFile(filePath, "utf8"));
112
112
  }
113
113
 
114
- async function getDbModuleRegistry(workspaceRoot) {
114
+ export async function getDbModuleRegistry(workspaceRoot) {
115
115
  if (!workspaceRoot) {
116
116
  return DEFAULT_DB_MODULE_REGISTRY;
117
117
  }
@@ -171,7 +171,7 @@ function getModuleLabel(moduleKey) {
171
171
  return titleizeSlug(moduleKey);
172
172
  }
173
173
 
174
- function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
174
+ export function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
175
175
  if (!workspaceMode) {
176
176
  return {
177
177
  selectedLabels: getSelectedModuleLabels(selectedModules),
@@ -218,7 +218,7 @@ function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
218
218
  };
219
219
  }
220
220
 
221
- async function getVersionMap(workspaceRoot) {
221
+ export async function getVersionMap(workspaceRoot) {
222
222
  const versionMap = {
223
223
  ...APP_DEPENDENCY_DEFAULTS,
224
224
  ...APP_DEV_DEPENDENCY_DEFAULTS,
@@ -256,13 +256,6 @@ async function getVersionMap(workspaceRoot) {
256
256
  return versionMap;
257
257
  }
258
258
 
259
- function createModuleFlags(selectedModules) {
260
- const selected = new Set(selectedModules);
261
- return Object.fromEntries(
262
- SELECTABLE_MODULES.map((moduleDefinition) => [moduleDefinition.key, selected.has(moduleDefinition.key)]),
263
- );
264
- }
265
-
266
259
  function createDerivedBrandValues(slug) {
267
260
  const projectName = titleizeSlug(slug);
268
261
 
@@ -276,7 +269,94 @@ function createDerivedBrandValues(slug) {
276
269
  };
277
270
  }
278
271
 
279
- function createEnvFileContent({ slug, brandValues, moduleFlags }) {
272
+ function createPlatformBrandConfigFile({ slug, brandValues }) {
273
+ return [
274
+ "export type StarterBrandConfig = {",
275
+ " companyName: string;",
276
+ " productName: string;",
277
+ " slug: string;",
278
+ " tagline: string;",
279
+ " contactEmail: string;",
280
+ " supportEmail: string;",
281
+ " primaryHex: string;",
282
+ "};",
283
+ "",
284
+ "export const starterBrandConfig: StarterBrandConfig = {",
285
+ ` companyName: ${JSON.stringify(brandValues.companyName)},`,
286
+ ` productName: ${JSON.stringify(brandValues.productName)},`,
287
+ ` slug: ${JSON.stringify(slug)},`,
288
+ ` tagline: ${JSON.stringify(brandValues.tagline)},`,
289
+ ` contactEmail: ${JSON.stringify(brandValues.contactEmail)},`,
290
+ ` supportEmail: ${JSON.stringify(brandValues.supportEmail)},`,
291
+ ` primaryHex: ${JSON.stringify(brandValues.primaryHex)},`,
292
+ "};",
293
+ "",
294
+ ].join("\n");
295
+ }
296
+
297
+ export function createPlatformModulesConfigFile(selectedModules) {
298
+ const selected = new Set(selectedModules);
299
+
300
+ return [
301
+ 'export type StarterModuleKey = "core-auth" | "crm" | "projects" | "admin";',
302
+ "",
303
+ "export type StarterModuleConfig = {",
304
+ " key: StarterModuleKey;",
305
+ " label: string;",
306
+ " description: string;",
307
+ " enabled: boolean;",
308
+ " packageName: string;",
309
+ " playgroundHref?: string;",
310
+ ' placement: "core" | "primary" | "admin";',
311
+ "};",
312
+ "",
313
+ "export const starterModuleConfig: StarterModuleConfig[] = [",
314
+ " {",
315
+ ' key: "core-auth",',
316
+ ' label: "Core Auth",',
317
+ ' description: "Login, reset-password, callback URLs, and shared auth validation utilities.",',
318
+ " enabled: true,",
319
+ ' packageName: "@brightweblabs/core-auth",',
320
+ ' playgroundHref: "/playground/auth",',
321
+ ' placement: "core",',
322
+ " },",
323
+ " {",
324
+ ' key: "crm",',
325
+ ' label: "CRM",',
326
+ ' description: "Contacts, marketing audience, and CRM server/data layer.",',
327
+ ` enabled: ${String(selected.has("crm"))},`,
328
+ ' packageName: "@brightweblabs/module-crm",',
329
+ ' playgroundHref: "/playground/crm",',
330
+ ' placement: "primary",',
331
+ " },",
332
+ " {",
333
+ ' key: "projects",',
334
+ ' label: "Projects",',
335
+ ' description: "Project portfolio, detail routes, and work-management server logic.",',
336
+ ` enabled: ${String(selected.has("projects"))},`,
337
+ ' packageName: "@brightweblabs/module-projects",',
338
+ ' playgroundHref: "/playground/projects",',
339
+ ' placement: "primary",',
340
+ " },",
341
+ " {",
342
+ ' key: "admin",',
343
+ ' label: "Admin",',
344
+ ' description: "User role governance, admin tools, and access-control surfaces.",',
345
+ ` enabled: ${String(selected.has("admin"))},`,
346
+ ' packageName: "@brightweblabs/module-admin",',
347
+ ' playgroundHref: "/playground/admin",',
348
+ ' placement: "admin",',
349
+ " },",
350
+ "];",
351
+ "",
352
+ "export function getEnabledStarterModules() {",
353
+ " return starterModuleConfig.filter((moduleConfig) => moduleConfig.enabled);",
354
+ "}",
355
+ "",
356
+ ].join("\n");
357
+ }
358
+
359
+ function createEnvFileContent() {
280
360
  return [
281
361
  "NEXT_PUBLIC_APP_URL=http://localhost:3000",
282
362
  "NEXT_PUBLIC_SUPABASE_URL=",
@@ -284,18 +364,6 @@ function createEnvFileContent({ slug, brandValues, moduleFlags }) {
284
364
  "SUPABASE_SERVICE_ROLE_KEY=",
285
365
  "RESEND_API_KEY=",
286
366
  "",
287
- `NEXT_PUBLIC_CLIENT_COMPANY_NAME=${brandValues.companyName}`,
288
- `NEXT_PUBLIC_CLIENT_PRODUCT_NAME=${brandValues.productName}`,
289
- `NEXT_PUBLIC_CLIENT_SLUG=${slug}`,
290
- `NEXT_PUBLIC_CLIENT_TAGLINE=${brandValues.tagline}`,
291
- `NEXT_PUBLIC_CLIENT_CONTACT_EMAIL=${brandValues.contactEmail}`,
292
- `NEXT_PUBLIC_CLIENT_SUPPORT_EMAIL=${brandValues.supportEmail}`,
293
- `NEXT_PUBLIC_CLIENT_PRIMARY_HEX=${brandValues.primaryHex}`,
294
- "",
295
- `NEXT_PUBLIC_ENABLE_CRM=${String(moduleFlags.crm)}`,
296
- `NEXT_PUBLIC_ENABLE_PROJECTS=${String(moduleFlags.projects)}`,
297
- `NEXT_PUBLIC_ENABLE_ADMIN=${String(moduleFlags.admin)}`,
298
- "",
299
367
  ].join("\n");
300
368
  }
301
369
 
@@ -333,9 +401,8 @@ function createGitignore() {
333
401
  "yarn-error.log*",
334
402
  ".pnpm-debug.log*",
335
403
  "",
336
- "# env files (can opt-in for committing if needed)",
404
+ "# env files",
337
405
  ".env*",
338
- "!.env.example",
339
406
  "",
340
407
  "# vercel",
341
408
  ".vercel",
@@ -363,12 +430,12 @@ function createPlatformReadme({
363
430
 
364
431
  const localSteps = workspaceMode
365
432
  ? [
366
- "1. Review `.env.example`, then copy it to `.env.local` and fill in real service credentials.",
433
+ "1. Review `.env.local` and fill in real service credentials.",
367
434
  "2. Run `pnpm install` from the BrightWeb workspace root.",
368
435
  `3. Run \`pnpm --filter ${slug} dev\`.`,
369
436
  ]
370
437
  : [
371
- "1. Review `.env.example`, then copy it to `.env.local` and fill in real service credentials.",
438
+ "1. Review `.env.local` and fill in real service credentials.",
372
439
  `2. Run \`${packageManager} install\`.`,
373
440
  `3. Run \`${packageManager} dev\`.`,
374
441
  ];
@@ -448,7 +515,7 @@ function createSiteReadme({ slug, workspaceMode, packageManager }) {
448
515
  ].join("\n");
449
516
  }
450
517
 
451
- function createPackageJson({
518
+ export function createPackageJson({
452
519
  slug,
453
520
  dependencyMode,
454
521
  selectedModules,
@@ -528,7 +595,7 @@ function createPackageJson({
528
595
  };
529
596
  }
530
597
 
531
- function createNextConfig({ template, selectedModules }) {
598
+ export function createNextConfig({ template, selectedModules }) {
532
599
  if (template === "site") {
533
600
  return [
534
601
  'import type { NextConfig } from "next";',
@@ -562,7 +629,7 @@ function createNextConfig({ template, selectedModules }) {
562
629
  ].join("\n");
563
630
  }
564
631
 
565
- function createShellConfig(selectedModules) {
632
+ export function createShellConfig(selectedModules) {
566
633
  const importLines = [];
567
634
  const registrationLines = [];
568
635
 
@@ -678,7 +745,7 @@ async function copyDirectory(sourceDir, targetDir) {
678
745
  await fs.cp(sourceDir, targetDir, { recursive: true });
679
746
  }
680
747
 
681
- async function ensureDirectory(targetDir) {
748
+ export async function ensureDirectory(targetDir) {
682
749
  await fs.mkdir(targetDir, { recursive: true });
683
750
  }
684
751
 
@@ -726,7 +793,7 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
726
793
  );
727
794
  }
728
795
 
729
- async function runInstall(command, cwd) {
796
+ export async function runInstall(command, cwd) {
730
797
  return new Promise((resolve, reject) => {
731
798
  const child = spawn(command, ["install"], {
732
799
  cwd,
@@ -871,7 +938,6 @@ async function scaffoldPlatformProject({
871
938
  answers,
872
939
  dbInstallPlan,
873
940
  }) {
874
- const moduleFlags = createModuleFlags(selectedModules);
875
941
  const brandValues = createDerivedBrandValues(answers.slug);
876
942
  const baseTemplateDir = path.join(TEMPLATE_ROOT, "base");
877
943
 
@@ -899,11 +965,16 @@ async function scaffoldPlatformProject({
899
965
  )}\n`,
900
966
  );
901
967
  await fs.writeFile(path.join(targetDir, "next.config.ts"), createNextConfig({ template: "platform", selectedModules }));
968
+ await fs.writeFile(
969
+ path.join(targetDir, "config", "brand.ts"),
970
+ createPlatformBrandConfigFile({ slug: answers.slug, brandValues }),
971
+ );
972
+ await fs.writeFile(path.join(targetDir, "config", "modules.ts"), createPlatformModulesConfigFile(selectedModules));
902
973
  await fs.writeFile(path.join(targetDir, "config", "shell.ts"), createShellConfig(selectedModules));
903
974
 
904
- const envFileContent = createEnvFileContent({ slug: answers.slug, brandValues, moduleFlags });
975
+ const envFileContent = createEnvFileContent();
905
976
 
906
- await fs.writeFile(path.join(targetDir, ".env.example"), envFileContent);
977
+ await fs.writeFile(path.join(targetDir, ".env.local"), envFileContent);
907
978
  await fs.writeFile(path.join(targetDir, ".gitignore"), createGitignore());
908
979
  await fs.writeFile(
909
980
  path.join(targetDir, "README.md"),
@@ -1069,7 +1140,8 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1069
1140
 
1070
1141
  if (install) {
1071
1142
  const installCwd = workspaceMode ? workspaceRoot : targetDir;
1072
- await runInstall(packageManager, installCwd);
1143
+ const installRunner = runtimeOptions.installRunner || runInstall;
1144
+ await installRunner(packageManager, installCwd);
1073
1145
  }
1074
1146
 
1075
1147
  printCompletionMessage({
package/src/update.mjs ADDED
@@ -0,0 +1,481 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stdout as output } from "node:process";
4
+ import {
5
+ BRIGHTWEB_PACKAGE_NAMES,
6
+ CLI_DISPLAY_NAME,
7
+ MODULE_STARTER_FILES,
8
+ SELECTABLE_MODULES,
9
+ } from "./constants.mjs";
10
+ import {
11
+ TEMPLATE_ROOT,
12
+ createDbInstallPlan,
13
+ createNextConfig,
14
+ createPackageJson,
15
+ createPlatformModulesConfigFile,
16
+ createShellConfig,
17
+ detectPackageManager,
18
+ getDbModuleRegistry,
19
+ getVersionMap,
20
+ pathExists,
21
+ readJsonIfPresent,
22
+ runInstall,
23
+ } from "./generator.mjs";
24
+
25
+ const MANAGED_PLATFORM_FILES = [
26
+ "next.config.ts",
27
+ path.join("config", "modules.ts"),
28
+ path.join("config", "shell.ts"),
29
+ ];
30
+
31
+ function resolveUpdateTargetDirectory(runtimeOptions, argvOptions) {
32
+ if (runtimeOptions.targetDir || argvOptions.targetDir) {
33
+ return path.resolve(runtimeOptions.targetDir || argvOptions.targetDir);
34
+ }
35
+
36
+ return process.cwd();
37
+ }
38
+
39
+ async function findWorkspaceRoot(startDir) {
40
+ let currentDir = path.resolve(startDir);
41
+
42
+ while (true) {
43
+ const registryPath = path.join(currentDir, "supabase", "module-registry.json");
44
+ const cliPackagePath = path.join(currentDir, "packages", "create-bw-app", "package.json");
45
+
46
+ if ((await pathExists(registryPath)) && (await pathExists(cliPackagePath))) {
47
+ return currentDir;
48
+ }
49
+
50
+ const parentDir = path.dirname(currentDir);
51
+ if (parentDir === currentDir) {
52
+ return null;
53
+ }
54
+
55
+ currentDir = parentDir;
56
+ }
57
+ }
58
+
59
+ function collectInstalledBrightwebPackages(manifest) {
60
+ const installed = new Map();
61
+
62
+ for (const section of ["dependencies", "devDependencies"]) {
63
+ const sectionManifest = manifest[section] || {};
64
+ for (const [packageName, version] of Object.entries(sectionManifest)) {
65
+ if (!packageName.startsWith("@brightweblabs/")) continue;
66
+ installed.set(packageName, { section, version });
67
+ }
68
+ }
69
+
70
+ return installed;
71
+ }
72
+
73
+ function parseConfiguredModules(content) {
74
+ const enabledModules = [];
75
+
76
+ for (const moduleDefinition of SELECTABLE_MODULES) {
77
+ const matcher = new RegExp(`key:\\s*"${moduleDefinition.key}"[\\s\\S]*?enabled:\\s*(true|false)`);
78
+ const match = content.match(matcher);
79
+ if (match?.[1] === "true") {
80
+ enabledModules.push(moduleDefinition.key);
81
+ }
82
+ }
83
+
84
+ return enabledModules;
85
+ }
86
+
87
+ async function detectTemplate(targetDir, installedBrightwebPackages) {
88
+ if (await pathExists(path.join(targetDir, "config", "modules.ts"))) {
89
+ return "platform";
90
+ }
91
+
92
+ return installedBrightwebPackages.size > 0 ? "platform" : "site";
93
+ }
94
+
95
+ function detectDependencyMode(installedBrightwebPackages) {
96
+ for (const { version } of installedBrightwebPackages.values()) {
97
+ if (typeof version === "string" && version.startsWith("workspace:")) {
98
+ return "workspace";
99
+ }
100
+ }
101
+
102
+ return "published";
103
+ }
104
+
105
+ function detectInstalledModules(installedBrightwebPackages) {
106
+ return SELECTABLE_MODULES
107
+ .filter((moduleDefinition) => installedBrightwebPackages.has(moduleDefinition.packageName))
108
+ .map((moduleDefinition) => moduleDefinition.key);
109
+ }
110
+
111
+ function getCanonicalBrightwebVersions({ manifest, template, dependencyMode, installedModules, versionMap }) {
112
+ const canonicalManifest = createPackageJson({
113
+ slug: manifest.name || path.basename(process.cwd()),
114
+ dependencyMode,
115
+ selectedModules: installedModules,
116
+ versionMap,
117
+ template,
118
+ });
119
+
120
+ const versions = {};
121
+
122
+ for (const section of ["dependencies", "devDependencies"]) {
123
+ for (const [packageName, version] of Object.entries(canonicalManifest[section] || {})) {
124
+ if (BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) {
125
+ versions[packageName] = version;
126
+ }
127
+ }
128
+ }
129
+
130
+ return versions;
131
+ }
132
+
133
+ function mergeManagedPackageUpdates({ manifest, targetVersions, installedBrightwebPackages }) {
134
+ let changed = false;
135
+ const nextManifest = {
136
+ ...manifest,
137
+ dependencies: manifest.dependencies ? { ...manifest.dependencies } : undefined,
138
+ devDependencies: manifest.devDependencies ? { ...manifest.devDependencies } : undefined,
139
+ };
140
+ const packageUpdates = [];
141
+
142
+ for (const [packageName, details] of installedBrightwebPackages.entries()) {
143
+ if (!BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) continue;
144
+ const targetVersion = targetVersions[packageName];
145
+ if (!targetVersion) continue;
146
+
147
+ const currentSection = nextManifest[details.section] || {};
148
+ if (currentSection[packageName] === targetVersion) continue;
149
+
150
+ currentSection[packageName] = targetVersion;
151
+ nextManifest[details.section] = currentSection;
152
+ packageUpdates.push({
153
+ packageName,
154
+ from: details.version,
155
+ to: targetVersion,
156
+ section: details.section,
157
+ });
158
+ changed = true;
159
+ }
160
+
161
+ return {
162
+ changed,
163
+ packageUpdates,
164
+ content: `${JSON.stringify(nextManifest, null, 2)}\n`,
165
+ };
166
+ }
167
+
168
+ async function getStarterFileStatus(targetDir, installedModules) {
169
+ const starterFiles = [];
170
+
171
+ for (const moduleKey of installedModules) {
172
+ const templateFolder = SELECTABLE_MODULES.find((moduleDefinition) => moduleDefinition.key === moduleKey)?.templateFolder;
173
+ if (!templateFolder) continue;
174
+
175
+ for (const relativePath of MODULE_STARTER_FILES[moduleKey] || []) {
176
+ const sourcePath = path.join(TEMPLATE_ROOT, "modules", templateFolder, relativePath);
177
+ const targetPath = path.join(targetDir, relativePath);
178
+ const exists = await pathExists(targetPath);
179
+
180
+ if (!exists) {
181
+ starterFiles.push({
182
+ moduleKey,
183
+ relativePath,
184
+ sourcePath,
185
+ targetPath,
186
+ status: "missing",
187
+ });
188
+ continue;
189
+ }
190
+
191
+ const [sourceContent, targetContent] = await Promise.all([
192
+ fs.readFile(sourcePath, "utf8"),
193
+ fs.readFile(targetPath, "utf8"),
194
+ ]);
195
+
196
+ starterFiles.push({
197
+ moduleKey,
198
+ relativePath,
199
+ sourcePath,
200
+ targetPath,
201
+ status: sourceContent === targetContent ? "current" : "drifted",
202
+ });
203
+ }
204
+ }
205
+
206
+ return starterFiles;
207
+ }
208
+
209
+ async function detectModulesConfigMismatch(targetDir, installedModules) {
210
+ const modulesConfigPath = path.join(targetDir, "config", "modules.ts");
211
+ if (!(await pathExists(modulesConfigPath))) {
212
+ return null;
213
+ }
214
+
215
+ const content = await fs.readFile(modulesConfigPath, "utf8");
216
+ const configuredModules = parseConfiguredModules(content);
217
+ const installed = new Set(installedModules);
218
+ const configured = new Set(configuredModules);
219
+
220
+ const mismatch = installedModules.length !== configuredModules.length
221
+ || installedModules.some((moduleKey) => !configured.has(moduleKey))
222
+ || configuredModules.some((moduleKey) => !installed.has(moduleKey));
223
+
224
+ if (!mismatch) {
225
+ return null;
226
+ }
227
+
228
+ return {
229
+ installedModules,
230
+ configuredModules,
231
+ };
232
+ }
233
+
234
+ function renderInstallCommand({ packageManager, dependencyMode, targetDir, workspaceRoot }) {
235
+ if (dependencyMode === "workspace" && workspaceRoot) {
236
+ return `cd ${workspaceRoot} && ${packageManager} install`;
237
+ }
238
+
239
+ return `cd ${targetDir} && ${packageManager} install`;
240
+ }
241
+
242
+ function renderPlanSummary(plan, options = {}) {
243
+ const lines = [
244
+ `${CLI_DISPLAY_NAME} update`,
245
+ "",
246
+ `Detected app type: ${plan.template}`,
247
+ `Dependency mode: ${plan.dependencyMode}`,
248
+ `Installed BrightWeb packages: ${plan.installedBrightwebPackages.length > 0 ? plan.installedBrightwebPackages.join(", ") : "none"}`,
249
+ `Installed modules: ${plan.installedModules.length > 0 ? plan.installedModules.join(", ") : "none"}`,
250
+ ];
251
+
252
+ if (plan.modulesConfigMismatch) {
253
+ lines.push(
254
+ `Config mismatch: installed modules (${plan.modulesConfigMismatch.installedModules.join(", ") || "none"}) differ from config/modules.ts (${plan.modulesConfigMismatch.configuredModules.join(", ") || "none"}). Using installed packages as source of truth.`,
255
+ );
256
+ }
257
+
258
+ lines.push("");
259
+
260
+ if (plan.packageUpdates.length > 0) {
261
+ lines.push("Packages to update:");
262
+ for (const update of plan.packageUpdates) {
263
+ lines.push(`- ${update.packageName}: ${update.from} -> ${update.to}`);
264
+ }
265
+ } else {
266
+ lines.push("Packages to update: none");
267
+ }
268
+
269
+ lines.push("");
270
+
271
+ if (plan.configFilesToWrite.length > 0) {
272
+ lines.push("Config files to rewrite:");
273
+ for (const relativePath of plan.configFilesToWrite) {
274
+ lines.push(`- ${relativePath}`);
275
+ }
276
+ } else {
277
+ lines.push("Config files to rewrite: none");
278
+ }
279
+
280
+ lines.push("");
281
+
282
+ if (plan.starterFilesMissing.length > 0 || plan.starterFilesDrifted.length > 0) {
283
+ lines.push("Starter file status:");
284
+ for (const relativePath of plan.starterFilesMissing) {
285
+ lines.push(`- missing: ${relativePath}`);
286
+ }
287
+ for (const relativePath of plan.starterFilesDrifted) {
288
+ lines.push(`- drifted: ${relativePath}`);
289
+ }
290
+ } else {
291
+ lines.push("Starter file status: all current");
292
+ }
293
+
294
+ lines.push("");
295
+
296
+ if (plan.dbInstallPlan.resolvedOrder.length > 0) {
297
+ lines.push(`Resolved database stack: ${plan.dbInstallPlan.resolvedOrder.join(" -> ")}`);
298
+ }
299
+
300
+ if (plan.dbInstallPlan.notes.length > 0) {
301
+ lines.push("Database notes:");
302
+ for (const note of plan.dbInstallPlan.notes) {
303
+ lines.push(`- ${note}`);
304
+ }
305
+ }
306
+
307
+ if (options.installCommand) {
308
+ lines.push("", `Next install command: ${options.installCommand}`);
309
+ }
310
+
311
+ return `${lines.join("\n")}\n`;
312
+ }
313
+
314
+ export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptions = {}) {
315
+ const targetDir = resolveUpdateTargetDirectory(runtimeOptions, argvOptions);
316
+ const packageJsonPath = path.join(targetDir, "package.json");
317
+ const manifest = await readJsonIfPresent(packageJsonPath);
318
+
319
+ if (!manifest) {
320
+ throw new Error(`Target directory does not contain package.json: ${targetDir}`);
321
+ }
322
+
323
+ const installedBrightwebPackagesMap = collectInstalledBrightwebPackages(manifest);
324
+ const template = await detectTemplate(targetDir, installedBrightwebPackagesMap);
325
+ const dependencyMode = detectDependencyMode(installedBrightwebPackagesMap);
326
+ const workspaceRoot = runtimeOptions.workspaceRoot
327
+ ? path.resolve(runtimeOptions.workspaceRoot)
328
+ : argvOptions.workspaceRoot
329
+ ? path.resolve(argvOptions.workspaceRoot)
330
+ : dependencyMode === "workspace"
331
+ ? await findWorkspaceRoot(targetDir)
332
+ : null;
333
+ const packageManager = detectPackageManager(argvOptions.packageManager || runtimeOptions.packageManager);
334
+ const installedModules = detectInstalledModules(installedBrightwebPackagesMap);
335
+ const versionMap = await getVersionMap(workspaceRoot);
336
+ const dbRegistry = await getDbModuleRegistry(workspaceRoot);
337
+ const dbInstallPlan = template === "platform"
338
+ ? createDbInstallPlan({
339
+ selectedModules: installedModules,
340
+ workspaceMode: true,
341
+ registry: dbRegistry,
342
+ })
343
+ : {
344
+ selectedLabels: [],
345
+ resolvedOrder: [],
346
+ notes: [],
347
+ };
348
+ const canonicalVersions = getCanonicalBrightwebVersions({
349
+ manifest,
350
+ template,
351
+ dependencyMode,
352
+ installedModules,
353
+ versionMap,
354
+ });
355
+ const packageJsonUpdate = mergeManagedPackageUpdates({
356
+ manifest,
357
+ targetVersions: canonicalVersions,
358
+ installedBrightwebPackages: installedBrightwebPackagesMap,
359
+ });
360
+
361
+ const fileWrites = [];
362
+ if (packageJsonUpdate.changed) {
363
+ fileWrites.push({
364
+ relativePath: "package.json",
365
+ targetPath: packageJsonPath,
366
+ content: packageJsonUpdate.content,
367
+ type: "config",
368
+ });
369
+ }
370
+
371
+ if (template === "platform") {
372
+ const canonicalConfigFiles = {
373
+ "next.config.ts": createNextConfig({ template: "platform", selectedModules: installedModules }),
374
+ [path.join("config", "modules.ts")]: createPlatformModulesConfigFile(installedModules),
375
+ [path.join("config", "shell.ts")]: createShellConfig(installedModules),
376
+ };
377
+
378
+ for (const relativePath of MANAGED_PLATFORM_FILES) {
379
+ const targetPath = path.join(targetDir, relativePath);
380
+ const currentContent = (await pathExists(targetPath)) ? await fs.readFile(targetPath, "utf8") : null;
381
+ const nextContent = canonicalConfigFiles[relativePath];
382
+
383
+ if (currentContent !== nextContent) {
384
+ fileWrites.push({
385
+ relativePath,
386
+ targetPath,
387
+ content: nextContent,
388
+ type: "config",
389
+ });
390
+ }
391
+ }
392
+ }
393
+
394
+ const starterFiles = template === "platform"
395
+ ? await getStarterFileStatus(targetDir, installedModules)
396
+ : [];
397
+ const starterFilesMissing = starterFiles.filter((entry) => entry.status === "missing");
398
+ const starterFilesDrifted = starterFiles.filter((entry) => entry.status === "drifted");
399
+
400
+ if (argvOptions.refreshStarters) {
401
+ for (const entry of starterFiles.filter((candidate) => candidate.status !== "current")) {
402
+ fileWrites.push({
403
+ relativePath: entry.relativePath,
404
+ targetPath: entry.targetPath,
405
+ content: await fs.readFile(entry.sourcePath, "utf8"),
406
+ type: "starter",
407
+ });
408
+ }
409
+ }
410
+
411
+ const modulesConfigMismatch = template === "platform"
412
+ ? await detectModulesConfigMismatch(targetDir, installedModules)
413
+ : null;
414
+
415
+ return {
416
+ targetDir,
417
+ workspaceRoot,
418
+ template,
419
+ dependencyMode,
420
+ packageManager,
421
+ manifest,
422
+ installedModules,
423
+ installedBrightwebPackages: Array.from(installedBrightwebPackagesMap.keys()).sort(),
424
+ packageUpdates: packageJsonUpdate.packageUpdates,
425
+ configFilesToWrite: fileWrites.filter((entry) => entry.type === "config").map((entry) => entry.relativePath),
426
+ starterFilesMissing: starterFilesMissing.map((entry) => entry.relativePath),
427
+ starterFilesDrifted: starterFilesDrifted.map((entry) => entry.relativePath),
428
+ starterFilesToRefresh: fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath),
429
+ dbInstallPlan,
430
+ modulesConfigMismatch,
431
+ fileWrites,
432
+ };
433
+ }
434
+
435
+ export async function updateBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
436
+ const plan = await buildBrightwebAppUpdatePlan(argvOptions, runtimeOptions);
437
+ const installCommand = renderInstallCommand({
438
+ packageManager: plan.packageManager,
439
+ dependencyMode: plan.dependencyMode,
440
+ targetDir: plan.targetDir,
441
+ workspaceRoot: plan.workspaceRoot,
442
+ });
443
+
444
+ output.write(renderPlanSummary(plan, { installCommand }));
445
+
446
+ if (argvOptions.dryRun) {
447
+ return {
448
+ dryRun: true,
449
+ plan,
450
+ installCommand,
451
+ };
452
+ }
453
+
454
+ for (const fileWrite of plan.fileWrites) {
455
+ await fs.mkdir(path.dirname(fileWrite.targetPath), { recursive: true });
456
+ await fs.writeFile(fileWrite.targetPath, fileWrite.content, "utf8");
457
+ }
458
+
459
+ const packageJsonChanged = plan.fileWrites.some((entry) => entry.relativePath === "package.json");
460
+ if (argvOptions.install && packageJsonChanged) {
461
+ const installRunner = runtimeOptions.installRunner || runInstall;
462
+ const installCwd = plan.dependencyMode === "workspace" && plan.workspaceRoot ? plan.workspaceRoot : plan.targetDir;
463
+ await installRunner(plan.packageManager, installCwd);
464
+ }
465
+
466
+ if (!argvOptions.install && packageJsonChanged) {
467
+ output.write(`Run \`${installCommand}\` to install updated package versions.\n`);
468
+ }
469
+
470
+ if (plan.fileWrites.length === 0) {
471
+ output.write("No managed changes were required.\n");
472
+ } else {
473
+ output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"}.\n`);
474
+ }
475
+
476
+ return {
477
+ dryRun: false,
478
+ plan,
479
+ installCommand,
480
+ };
481
+ }
@@ -0,0 +1,26 @@
1
+ # AGENTS.md
2
+
3
+ This generated project is a BrightWeb platform starter. Use this file as the local entrypoint for AI agents working inside the app.
4
+
5
+ ## Start here
6
+
7
+ - `README.md`: local setup commands and starter routes.
8
+ - `docs/ai/README.md`: app-specific routing guide for agents.
9
+ - `config/brand.ts`: client identity, naming, and contact defaults.
10
+ - `config/modules.ts`: selected module set and runtime enablement.
11
+ - `config/client.ts`: starter-facing derived state used by the home page and setup surfaces.
12
+ - `.env.local`: runtime service values for local development.
13
+
14
+ ## Working rules
15
+
16
+ - Treat `/bootstrap`, `/preview/app-shell`, and `/playground/*` as starter validation surfaces. They are app-owned and can be removed after setup if links and references are cleaned up too.
17
+ - Check `config/modules.ts` before assuming CRM, Projects, or Admin routes exist.
18
+ - Prefer composing app-level routes and config before forking logic from `@brightweblabs/*` packages.
19
+ - Keep edits local to this app unless the change is intentionally shared across multiple BrightWeb projects.
20
+
21
+ ## First validation pass
22
+
23
+ 1. Run the local dev server from this project or workspace.
24
+ 2. Open `/`, `/bootstrap`, `/preview/app-shell`, and `/playground/auth`.
25
+ 3. If optional modules are enabled, open the matching `/playground/*` route for each one.
26
+ 4. Confirm `.env.local` contains real service values before debugging runtime behavior.
@@ -115,7 +115,7 @@ export default function HomePage() {
115
115
  <li>`config/brand.ts` for client identity and contact details.</li>
116
116
  <li>`config/modules.ts` for enabled platform modules.</li>
117
117
  <li>`config/env.ts` for infra requirements and readiness checks.</li>
118
- <li>`.env.example` for starter defaults; copy it to `.env.local` for per-client secrets and flags.</li>
118
+ <li>`.env.local` for per-client service credentials and local runtime overrides.</li>
119
119
  </ul>
120
120
  </div>
121
121
  </article>
@@ -69,7 +69,7 @@ export function getStarterBootstrapChecklist() {
69
69
  {
70
70
  label: "Create per-client environment variables",
71
71
  done: config.envReadiness.allReady,
72
- detail: "Copy `.env.example` to `.env.local` and fill the real values.",
72
+ detail: "Fill `.env.local` with the real service values for this client.",
73
73
  },
74
74
  ],
75
75
  };
@@ -9,13 +9,11 @@ export type StarterBrandConfig = {
9
9
  };
10
10
 
11
11
  export const starterBrandConfig: StarterBrandConfig = {
12
- companyName: process.env.NEXT_PUBLIC_CLIENT_COMPANY_NAME?.trim() || "Starter Client",
13
- productName: process.env.NEXT_PUBLIC_CLIENT_PRODUCT_NAME?.trim() || "Operations Platform",
14
- slug: process.env.NEXT_PUBLIC_CLIENT_SLUG?.trim() || "starter-client",
15
- tagline:
16
- process.env.NEXT_PUBLIC_CLIENT_TAGLINE?.trim()
17
- || "A configurable Brightweb starter app for shipping new client instances without rebuilding the platform.",
18
- contactEmail: process.env.NEXT_PUBLIC_CLIENT_CONTACT_EMAIL?.trim() || "hello@example.com",
19
- supportEmail: process.env.NEXT_PUBLIC_CLIENT_SUPPORT_EMAIL?.trim() || "support@example.com",
20
- primaryHex: process.env.NEXT_PUBLIC_CLIENT_PRIMARY_HEX?.trim() || "#1f7a45",
12
+ companyName: "Starter Client",
13
+ productName: "Operations Platform",
14
+ slug: "starter-client",
15
+ tagline: "A configurable Brightweb starter app for shipping new client instances without rebuilding the platform.",
16
+ contactEmail: "hello@example.com",
17
+ supportEmail: "support@example.com",
18
+ primaryHex: "#1f7a45",
21
19
  };
@@ -10,14 +10,6 @@ export type StarterModuleConfig = {
10
10
  placement: "core" | "primary" | "admin";
11
11
  };
12
12
 
13
- function envFlag(value: string | undefined, defaultValue: boolean) {
14
- if (typeof value !== "string") return defaultValue;
15
- const normalized = value.trim().toLowerCase();
16
- if (["1", "true", "yes", "on"].includes(normalized)) return true;
17
- if (["0", "false", "no", "off"].includes(normalized)) return false;
18
- return defaultValue;
19
- }
20
-
21
13
  export const starterModuleConfig: StarterModuleConfig[] = [
22
14
  {
23
15
  key: "core-auth",
@@ -32,7 +24,7 @@ export const starterModuleConfig: StarterModuleConfig[] = [
32
24
  key: "crm",
33
25
  label: "CRM",
34
26
  description: "Contacts, marketing audience, and CRM server/data layer.",
35
- enabled: envFlag(process.env.NEXT_PUBLIC_ENABLE_CRM, true),
27
+ enabled: true,
36
28
  packageName: "@brightweblabs/module-crm",
37
29
  playgroundHref: "/playground/crm",
38
30
  placement: "primary",
@@ -41,7 +33,7 @@ export const starterModuleConfig: StarterModuleConfig[] = [
41
33
  key: "projects",
42
34
  label: "Projects",
43
35
  description: "Project portfolio, detail routes, and work-management server logic.",
44
- enabled: envFlag(process.env.NEXT_PUBLIC_ENABLE_PROJECTS, true),
36
+ enabled: true,
45
37
  packageName: "@brightweblabs/module-projects",
46
38
  playgroundHref: "/playground/projects",
47
39
  placement: "primary",
@@ -50,7 +42,7 @@ export const starterModuleConfig: StarterModuleConfig[] = [
50
42
  key: "admin",
51
43
  label: "Admin",
52
44
  description: "User role governance, admin tools, and access-control surfaces.",
53
- enabled: envFlag(process.env.NEXT_PUBLIC_ENABLE_ADMIN, true),
45
+ enabled: true,
54
46
  packageName: "@brightweblabs/module-admin",
55
47
  playgroundHref: "/playground/admin",
56
48
  placement: "admin",
@@ -0,0 +1,44 @@
1
+ # Agent Guide
2
+
3
+ This file is the local routing guide for AI agents working inside a generated BrightWeb platform app.
4
+
5
+ It is intentionally app-scoped. It explains the generated project you are in, not the maintainer-only BrightWeb monorepo internals.
6
+
7
+ ## Project shape
8
+
9
+ This app is a normal Next.js App Router project with BrightWeb runtime wiring layered on top.
10
+
11
+ - `app/`: route tree, layouts, pages, starter previews, and playground routes.
12
+ - `config/`: generated app configuration for brand, env readiness, enabled modules, bootstrap content, and shell registration.
13
+ - `public/brand/`: starter logos used by the shell lockups.
14
+ - `.env.local`: local service configuration for Supabase, Resend, and runtime URLs.
15
+
16
+ ## Fast routing map
17
+
18
+ - `README.md`: first-run setup steps.
19
+ - `config/brand.ts`: client name, product name, support inboxes, and brand color.
20
+ - `config/modules.ts`: module metadata and enablement flags for CRM, Projects, and Admin.
21
+ - `config/client.ts`: aggregated state consumed by starter pages.
22
+ - `config/bootstrap.ts`: bootstrap checklist content for `/bootstrap`.
23
+ - `config/shell.ts`: app-shell registration and navigation wiring.
24
+ - `app/page.tsx`: starter landing page for the generated app.
25
+ - `app/bootstrap/page.tsx`: setup checklist surface.
26
+ - `app/preview/app-shell/page.tsx`: shell preview validation route.
27
+ - `app/playground/auth/page.tsx`: auth validation route.
28
+ - `app/playground/*`: optional module playgrounds when those modules were selected at scaffold time.
29
+
30
+ ## Editing strategy
31
+
32
+ - Change client identity first in `config/brand.ts`.
33
+ - Check module presence in `config/modules.ts` before editing or creating module-specific routes.
34
+ - Use `config/shell.ts` when navigation or toolbar behavior needs to change.
35
+ - Use `config/bootstrap.ts` and `config/client.ts` when the setup checklist or readiness messaging is wrong.
36
+ - Keep starter validation routes until the real product routes replace their purpose.
37
+
38
+ ## Validation checklist
39
+
40
+ 1. Confirm `.env.local` is populated with real values.
41
+ 2. Run the app locally.
42
+ 3. Validate `/`, `/bootstrap`, `/preview/app-shell`, and `/playground/auth`.
43
+ 4. Validate the playground route for each enabled optional module.
44
+ 5. If a starter route is removed, also remove any links or config references that still point to it.
@@ -0,0 +1,6 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ export async function GET(request: Request) {
4
+ const { handleCrmContactsGetRequest } = await import("@brightweblabs/module-crm");
5
+ return handleCrmContactsGetRequest(request);
6
+ }
@@ -0,0 +1,6 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ export async function GET(request: Request) {
4
+ const { handleCrmOrganizationsGetRequest } = await import("@brightweblabs/module-crm");
5
+ return handleCrmOrganizationsGetRequest(request);
6
+ }
@@ -0,0 +1,6 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ export async function GET(request: Request) {
4
+ const { handleCrmOwnersGetRequest } = await import("@brightweblabs/module-crm");
5
+ return handleCrmOwnersGetRequest(request);
6
+ }
@@ -0,0 +1,6 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ export async function GET(request: Request) {
4
+ const { handleCrmStatsGetRequest } = await import("@brightweblabs/module-crm");
5
+ return handleCrmStatsGetRequest(request);
6
+ }