create-bw-app 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.
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,8 @@ 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.local`, `AGENTS.md`, `docs/ai/README.md`, and generated config files for brand and module state
59
+ - platform apps also write `.env.local`, `AGENTS.md`, `docs/ai/README.md`, `docs/ai/examples.md`, `docs/ai/app-context.json`, and generated config files for brand and module state
60
+ - site apps also write `AGENTS.md`, `docs/ai/README.md`, `docs/ai/examples.md`, and `docs/ai/app-context.json` for app-local AI handoff
38
61
  - supports repo-local `workspace:*` wiring and future published dependency wiring
39
62
 
40
63
  ## 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.9.0",
4
+ "version": "0.9.2",
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
@@ -42,6 +42,29 @@ export const CORE_PACKAGES = [
42
42
  "@brightweblabs/ui",
43
43
  ];
44
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
+
45
68
  export const APP_DEPENDENCY_DEFAULTS = {
46
69
  "@brightweblabs/app-shell": "^0.1.1",
47
70
  "@brightweblabs/core-auth": "^0.1.1",
@@ -93,8 +116,9 @@ export const DEFAULTS = {
93
116
  export const HELP_TEXT = `
94
117
  Usage:
95
118
  create-bw-app [options]
119
+ create-bw-app update [options]
96
120
 
97
- Options:
121
+ Scaffold options:
98
122
  --template <platform|site> Scaffold a platform app or a standalone site
99
123
  --name, --slug <name> Project name and default directory name
100
124
  --modules <list> Comma-separated modules: crm,projects,admin
@@ -107,4 +131,12 @@ Options:
107
131
  --yes Accept defaults for any missing optional prompt
108
132
  --dry-run Print planned actions without writing files
109
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
110
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,
@@ -294,7 +294,7 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
294
294
  ].join("\n");
295
295
  }
296
296
 
297
- function createPlatformModulesConfigFile(selectedModules) {
297
+ export function createPlatformModulesConfigFile(selectedModules) {
298
298
  const selected = new Set(selectedModules);
299
299
 
300
300
  return [
@@ -414,6 +414,20 @@ function createGitignore() {
414
414
  ].join("\n");
415
415
  }
416
416
 
417
+ function getPlatformStarterRoutes(selectedModules) {
418
+ return [
419
+ "/",
420
+ "/bootstrap",
421
+ "/preview/app-shell",
422
+ "/playground/auth",
423
+ ...selectedModules.map((moduleKey) => `/playground/${moduleKey}`),
424
+ ];
425
+ }
426
+
427
+ function getSiteStarterRoutes() {
428
+ return ["/"];
429
+ }
430
+
417
431
  function createPlatformReadme({
418
432
  slug,
419
433
  selectedModules,
@@ -471,11 +485,14 @@ function createPlatformReadme({
471
485
  : []),
472
486
  "## Starter routes",
473
487
  "",
474
- "- `/`",
475
- "- `/bootstrap`",
476
- "- `/preview/app-shell`",
477
- "- `/playground/auth`",
478
- ...selectedModules.map((moduleKey) => `- /playground/${moduleKey}`),
488
+ ...getPlatformStarterRoutes(selectedModules).map((route) => `- \`${route}\``),
489
+ "",
490
+ "## AI handoff",
491
+ "",
492
+ "- `AGENTS.md`",
493
+ "- `docs/ai/README.md`",
494
+ "- `docs/ai/examples.md`",
495
+ "- `docs/ai/app-context.json`",
479
496
  "",
480
497
  ].join("\n");
481
498
  }
@@ -508,14 +525,144 @@ function createSiteReadme({ slug, workspaceMode, packageManager }) {
508
525
  "",
509
526
  "## Starter surfaces",
510
527
  "",
511
- "- `/`",
528
+ ...getSiteStarterRoutes().map((route) => `- \`${route}\``),
512
529
  "",
513
530
  "Edit `config/site.ts` to change the site name, copy, and public links.",
514
531
  "",
532
+ "## AI handoff",
533
+ "",
534
+ "- `AGENTS.md`",
535
+ "- `docs/ai/README.md`",
536
+ "- `docs/ai/examples.md`",
537
+ "- `docs/ai/app-context.json`",
538
+ "",
515
539
  ].join("\n");
516
540
  }
517
541
 
518
- function createPackageJson({
542
+ export function createAppContextFile({
543
+ slug,
544
+ template,
545
+ selectedModules = [],
546
+ dbInstallPlan = { resolvedOrder: [] },
547
+ }) {
548
+ if (template === "site") {
549
+ return `${JSON.stringify(
550
+ {
551
+ schemaVersion: 1,
552
+ template: "site",
553
+ app: {
554
+ slug,
555
+ name: titleizeSlug(slug),
556
+ },
557
+ docs: {
558
+ agentGuide: "AGENTS.md",
559
+ routingGuide: "docs/ai/README.md",
560
+ setupGuide: "README.md",
561
+ examples: "docs/ai/examples.md",
562
+ },
563
+ paths: {
564
+ readFirst: [
565
+ "AGENTS.md",
566
+ "docs/ai/README.md",
567
+ "README.md",
568
+ "config/site.ts",
569
+ "app/page.tsx",
570
+ "app/globals.css",
571
+ ],
572
+ appRoutesRoot: "app",
573
+ configRoot: "config",
574
+ componentsRoot: "components",
575
+ uiComponentsRoot: "components/ui",
576
+ libRoot: "lib",
577
+ },
578
+ starterRoutes: getSiteStarterRoutes(),
579
+ ownership: {
580
+ appOwned: [
581
+ "app/**",
582
+ "components/**",
583
+ "config/**",
584
+ "docs/ai/**",
585
+ "lib/**",
586
+ "public/**",
587
+ "AGENTS.md",
588
+ "README.md",
589
+ ],
590
+ packageOwned: [],
591
+ },
592
+ agentRules: {
593
+ editConfigBeforeCopyTweaks: true,
594
+ keepUiPrimitiveChangesLocal: true,
595
+ treatStarterHomeAsAppOwned: true,
596
+ },
597
+ },
598
+ null,
599
+ 2,
600
+ )}\n`;
601
+ }
602
+
603
+ return `${JSON.stringify(
604
+ {
605
+ schemaVersion: 1,
606
+ template: "platform",
607
+ app: {
608
+ slug,
609
+ name: titleizeSlug(slug),
610
+ },
611
+ modules: {
612
+ enabled: selectedModules,
613
+ resolvedDatabaseStack: dbInstallPlan.resolvedOrder,
614
+ },
615
+ docs: {
616
+ agentGuide: "AGENTS.md",
617
+ routingGuide: "docs/ai/README.md",
618
+ setupGuide: "README.md",
619
+ examples: "docs/ai/examples.md",
620
+ },
621
+ paths: {
622
+ readFirst: [
623
+ "AGENTS.md",
624
+ "docs/ai/README.md",
625
+ "README.md",
626
+ "config/brand.ts",
627
+ "config/modules.ts",
628
+ "config/client.ts",
629
+ "config/bootstrap.ts",
630
+ "config/shell.ts",
631
+ ".env.local",
632
+ ],
633
+ appRoutesRoot: "app",
634
+ configRoot: "config",
635
+ brandAssetsRoot: "public/brand",
636
+ },
637
+ starterRoutes: getPlatformStarterRoutes(selectedModules),
638
+ ownership: {
639
+ appOwned: [
640
+ "app/**",
641
+ "config/**",
642
+ "docs/ai/**",
643
+ "public/brand/**",
644
+ "AGENTS.md",
645
+ "README.md",
646
+ ],
647
+ packageOwned: [
648
+ ...CORE_PACKAGES,
649
+ ...SELECTABLE_MODULES
650
+ .filter((moduleDefinition) => selectedModules.includes(moduleDefinition.key))
651
+ .map((moduleDefinition) => moduleDefinition.packageName),
652
+ ],
653
+ },
654
+ agentRules: {
655
+ checkModulesBeforeEditing: true,
656
+ preferAppLevelCompositionOverPackageForks: true,
657
+ treatStarterRoutesAsRemovable: true,
658
+ },
659
+ },
660
+ null,
661
+ 2,
662
+ )}\n`;
663
+ }
664
+
665
+ export function createPackageJson({
519
666
  slug,
520
667
  dependencyMode,
521
668
  selectedModules,
@@ -595,7 +742,7 @@ function createPackageJson({
595
742
  };
596
743
  }
597
744
 
598
- function createNextConfig({ template, selectedModules }) {
745
+ export function createNextConfig({ template, selectedModules }) {
599
746
  if (template === "site") {
600
747
  return [
601
748
  'import type { NextConfig } from "next";',
@@ -629,7 +776,7 @@ function createNextConfig({ template, selectedModules }) {
629
776
  ].join("\n");
630
777
  }
631
778
 
632
- function createShellConfig(selectedModules) {
779
+ export function createShellConfig(selectedModules) {
633
780
  const importLines = [];
634
781
  const registrationLines = [];
635
782
 
@@ -745,7 +892,7 @@ async function copyDirectory(sourceDir, targetDir) {
745
892
  await fs.cp(sourceDir, targetDir, { recursive: true });
746
893
  }
747
894
 
748
- async function ensureDirectory(targetDir) {
895
+ export async function ensureDirectory(targetDir) {
749
896
  await fs.mkdir(targetDir, { recursive: true });
750
897
  }
751
898
 
@@ -793,7 +940,7 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
793
940
  );
794
941
  }
795
942
 
796
- async function runInstall(command, cwd) {
943
+ export async function runInstall(command, cwd) {
797
944
  return new Promise((resolve, reject) => {
798
945
  const child = spawn(command, ["install"], {
799
946
  cwd,
@@ -971,6 +1118,15 @@ async function scaffoldPlatformProject({
971
1118
  );
972
1119
  await fs.writeFile(path.join(targetDir, "config", "modules.ts"), createPlatformModulesConfigFile(selectedModules));
973
1120
  await fs.writeFile(path.join(targetDir, "config", "shell.ts"), createShellConfig(selectedModules));
1121
+ await fs.writeFile(
1122
+ path.join(targetDir, "docs", "ai", "app-context.json"),
1123
+ createAppContextFile({
1124
+ slug: answers.slug,
1125
+ template: "platform",
1126
+ selectedModules,
1127
+ dbInstallPlan,
1128
+ }),
1129
+ );
974
1130
 
975
1131
  const envFileContent = createEnvFileContent();
976
1132
 
@@ -1007,6 +1163,7 @@ async function scaffoldSiteProject({
1007
1163
  await ensureDirectory(path.dirname(targetDir));
1008
1164
  await copyDirectory(baseTemplateDir, targetDir);
1009
1165
  await ensureDirectory(path.join(targetDir, "config"));
1166
+ await ensureDirectory(path.join(targetDir, "docs", "ai"));
1010
1167
 
1011
1168
  await fs.writeFile(
1012
1169
  path.join(targetDir, "package.json"),
@@ -1025,6 +1182,13 @@ async function scaffoldSiteProject({
1025
1182
  await fs.writeFile(path.join(targetDir, "next.config.ts"), createNextConfig({ template: "site", selectedModules: [] }));
1026
1183
  await fs.writeFile(path.join(targetDir, ".gitignore"), createGitignore());
1027
1184
  await fs.writeFile(path.join(targetDir, "config", "site.ts"), createSiteConfigFile(answers.slug));
1185
+ await fs.writeFile(
1186
+ path.join(targetDir, "docs", "ai", "app-context.json"),
1187
+ createAppContextFile({
1188
+ slug: answers.slug,
1189
+ template: "site",
1190
+ }),
1191
+ );
1028
1192
  await fs.writeFile(
1029
1193
  path.join(targetDir, "README.md"),
1030
1194
  createSiteReadme({
@@ -1140,7 +1304,8 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1140
1304
 
1141
1305
  if (install) {
1142
1306
  const installCwd = workspaceMode ? workspaceRoot : targetDir;
1143
- await runInstall(packageManager, installCwd);
1307
+ const installRunner = runtimeOptions.installRunner || runInstall;
1308
+ await installRunner(packageManager, installCwd);
1144
1309
  }
1145
1310
 
1146
1311
  printCompletionMessage({
package/src/update.mjs ADDED
@@ -0,0 +1,515 @@
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
+ createAppContextFile,
13
+ createDbInstallPlan,
14
+ createNextConfig,
15
+ createPackageJson,
16
+ createPlatformModulesConfigFile,
17
+ createShellConfig,
18
+ detectPackageManager,
19
+ getDbModuleRegistry,
20
+ getVersionMap,
21
+ pathExists,
22
+ readJsonIfPresent,
23
+ runInstall,
24
+ } from "./generator.mjs";
25
+
26
+ const MANAGED_PLATFORM_FILES = [
27
+ "next.config.ts",
28
+ path.join("config", "modules.ts"),
29
+ path.join("config", "shell.ts"),
30
+ path.join("docs", "ai", "app-context.json"),
31
+ ];
32
+
33
+ const MANAGED_SITE_FILES = [
34
+ path.join("docs", "ai", "app-context.json"),
35
+ ];
36
+
37
+ function resolveUpdateTargetDirectory(runtimeOptions, argvOptions) {
38
+ if (runtimeOptions.targetDir || argvOptions.targetDir) {
39
+ return path.resolve(runtimeOptions.targetDir || argvOptions.targetDir);
40
+ }
41
+
42
+ return process.cwd();
43
+ }
44
+
45
+ async function findWorkspaceRoot(startDir) {
46
+ let currentDir = path.resolve(startDir);
47
+
48
+ while (true) {
49
+ const registryPath = path.join(currentDir, "supabase", "module-registry.json");
50
+ const cliPackagePath = path.join(currentDir, "packages", "create-bw-app", "package.json");
51
+
52
+ if ((await pathExists(registryPath)) && (await pathExists(cliPackagePath))) {
53
+ return currentDir;
54
+ }
55
+
56
+ const parentDir = path.dirname(currentDir);
57
+ if (parentDir === currentDir) {
58
+ return null;
59
+ }
60
+
61
+ currentDir = parentDir;
62
+ }
63
+ }
64
+
65
+ function collectInstalledBrightwebPackages(manifest) {
66
+ const installed = new Map();
67
+
68
+ for (const section of ["dependencies", "devDependencies"]) {
69
+ const sectionManifest = manifest[section] || {};
70
+ for (const [packageName, version] of Object.entries(sectionManifest)) {
71
+ if (!packageName.startsWith("@brightweblabs/")) continue;
72
+ installed.set(packageName, { section, version });
73
+ }
74
+ }
75
+
76
+ return installed;
77
+ }
78
+
79
+ function parseConfiguredModules(content) {
80
+ const enabledModules = [];
81
+
82
+ for (const moduleDefinition of SELECTABLE_MODULES) {
83
+ const matcher = new RegExp(`key:\\s*"${moduleDefinition.key}"[\\s\\S]*?enabled:\\s*(true|false)`);
84
+ const match = content.match(matcher);
85
+ if (match?.[1] === "true") {
86
+ enabledModules.push(moduleDefinition.key);
87
+ }
88
+ }
89
+
90
+ return enabledModules;
91
+ }
92
+
93
+ async function detectTemplate(targetDir, installedBrightwebPackages) {
94
+ if (await pathExists(path.join(targetDir, "config", "modules.ts"))) {
95
+ return "platform";
96
+ }
97
+
98
+ return installedBrightwebPackages.size > 0 ? "platform" : "site";
99
+ }
100
+
101
+ function detectDependencyMode(installedBrightwebPackages) {
102
+ for (const { version } of installedBrightwebPackages.values()) {
103
+ if (typeof version === "string" && version.startsWith("workspace:")) {
104
+ return "workspace";
105
+ }
106
+ }
107
+
108
+ return "published";
109
+ }
110
+
111
+ function detectInstalledModules(installedBrightwebPackages) {
112
+ return SELECTABLE_MODULES
113
+ .filter((moduleDefinition) => installedBrightwebPackages.has(moduleDefinition.packageName))
114
+ .map((moduleDefinition) => moduleDefinition.key);
115
+ }
116
+
117
+ function getCanonicalBrightwebVersions({ manifest, template, dependencyMode, installedModules, versionMap }) {
118
+ const canonicalManifest = createPackageJson({
119
+ slug: manifest.name || path.basename(process.cwd()),
120
+ dependencyMode,
121
+ selectedModules: installedModules,
122
+ versionMap,
123
+ template,
124
+ });
125
+
126
+ const versions = {};
127
+
128
+ for (const section of ["dependencies", "devDependencies"]) {
129
+ for (const [packageName, version] of Object.entries(canonicalManifest[section] || {})) {
130
+ if (BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) {
131
+ versions[packageName] = version;
132
+ }
133
+ }
134
+ }
135
+
136
+ return versions;
137
+ }
138
+
139
+ function mergeManagedPackageUpdates({ manifest, targetVersions, installedBrightwebPackages }) {
140
+ let changed = false;
141
+ const nextManifest = {
142
+ ...manifest,
143
+ dependencies: manifest.dependencies ? { ...manifest.dependencies } : undefined,
144
+ devDependencies: manifest.devDependencies ? { ...manifest.devDependencies } : undefined,
145
+ };
146
+ const packageUpdates = [];
147
+
148
+ for (const [packageName, details] of installedBrightwebPackages.entries()) {
149
+ if (!BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) continue;
150
+ const targetVersion = targetVersions[packageName];
151
+ if (!targetVersion) continue;
152
+
153
+ const currentSection = nextManifest[details.section] || {};
154
+ if (currentSection[packageName] === targetVersion) continue;
155
+
156
+ currentSection[packageName] = targetVersion;
157
+ nextManifest[details.section] = currentSection;
158
+ packageUpdates.push({
159
+ packageName,
160
+ from: details.version,
161
+ to: targetVersion,
162
+ section: details.section,
163
+ });
164
+ changed = true;
165
+ }
166
+
167
+ return {
168
+ changed,
169
+ packageUpdates,
170
+ content: `${JSON.stringify(nextManifest, null, 2)}\n`,
171
+ };
172
+ }
173
+
174
+ async function getStarterFileStatus(targetDir, installedModules) {
175
+ const starterFiles = [];
176
+
177
+ for (const moduleKey of installedModules) {
178
+ const templateFolder = SELECTABLE_MODULES.find((moduleDefinition) => moduleDefinition.key === moduleKey)?.templateFolder;
179
+ if (!templateFolder) continue;
180
+
181
+ for (const relativePath of MODULE_STARTER_FILES[moduleKey] || []) {
182
+ const sourcePath = path.join(TEMPLATE_ROOT, "modules", templateFolder, relativePath);
183
+ const targetPath = path.join(targetDir, relativePath);
184
+ const exists = await pathExists(targetPath);
185
+
186
+ if (!exists) {
187
+ starterFiles.push({
188
+ moduleKey,
189
+ relativePath,
190
+ sourcePath,
191
+ targetPath,
192
+ status: "missing",
193
+ });
194
+ continue;
195
+ }
196
+
197
+ const [sourceContent, targetContent] = await Promise.all([
198
+ fs.readFile(sourcePath, "utf8"),
199
+ fs.readFile(targetPath, "utf8"),
200
+ ]);
201
+
202
+ starterFiles.push({
203
+ moduleKey,
204
+ relativePath,
205
+ sourcePath,
206
+ targetPath,
207
+ status: sourceContent === targetContent ? "current" : "drifted",
208
+ });
209
+ }
210
+ }
211
+
212
+ return starterFiles;
213
+ }
214
+
215
+ async function detectModulesConfigMismatch(targetDir, installedModules) {
216
+ const modulesConfigPath = path.join(targetDir, "config", "modules.ts");
217
+ if (!(await pathExists(modulesConfigPath))) {
218
+ return null;
219
+ }
220
+
221
+ const content = await fs.readFile(modulesConfigPath, "utf8");
222
+ const configuredModules = parseConfiguredModules(content);
223
+ const installed = new Set(installedModules);
224
+ const configured = new Set(configuredModules);
225
+
226
+ const mismatch = installedModules.length !== configuredModules.length
227
+ || installedModules.some((moduleKey) => !configured.has(moduleKey))
228
+ || configuredModules.some((moduleKey) => !installed.has(moduleKey));
229
+
230
+ if (!mismatch) {
231
+ return null;
232
+ }
233
+
234
+ return {
235
+ installedModules,
236
+ configuredModules,
237
+ };
238
+ }
239
+
240
+ function renderInstallCommand({ packageManager, dependencyMode, targetDir, workspaceRoot }) {
241
+ if (dependencyMode === "workspace" && workspaceRoot) {
242
+ return `cd ${workspaceRoot} && ${packageManager} install`;
243
+ }
244
+
245
+ return `cd ${targetDir} && ${packageManager} install`;
246
+ }
247
+
248
+ function renderPlanSummary(plan, options = {}) {
249
+ const lines = [
250
+ `${CLI_DISPLAY_NAME} update`,
251
+ "",
252
+ `Detected app type: ${plan.template}`,
253
+ `Dependency mode: ${plan.dependencyMode}`,
254
+ `Installed BrightWeb packages: ${plan.installedBrightwebPackages.length > 0 ? plan.installedBrightwebPackages.join(", ") : "none"}`,
255
+ `Installed modules: ${plan.installedModules.length > 0 ? plan.installedModules.join(", ") : "none"}`,
256
+ ];
257
+
258
+ if (plan.modulesConfigMismatch) {
259
+ lines.push(
260
+ `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.`,
261
+ );
262
+ }
263
+
264
+ lines.push("");
265
+
266
+ if (plan.packageUpdates.length > 0) {
267
+ lines.push("Packages to update:");
268
+ for (const update of plan.packageUpdates) {
269
+ lines.push(`- ${update.packageName}: ${update.from} -> ${update.to}`);
270
+ }
271
+ } else {
272
+ lines.push("Packages to update: none");
273
+ }
274
+
275
+ lines.push("");
276
+
277
+ if (plan.configFilesToWrite.length > 0) {
278
+ lines.push("Config files to rewrite:");
279
+ for (const relativePath of plan.configFilesToWrite) {
280
+ lines.push(`- ${relativePath}`);
281
+ }
282
+ } else {
283
+ lines.push("Config files to rewrite: none");
284
+ }
285
+
286
+ lines.push("");
287
+
288
+ if (plan.starterFilesMissing.length > 0 || plan.starterFilesDrifted.length > 0) {
289
+ lines.push("Starter file status:");
290
+ for (const relativePath of plan.starterFilesMissing) {
291
+ lines.push(`- missing: ${relativePath}`);
292
+ }
293
+ for (const relativePath of plan.starterFilesDrifted) {
294
+ lines.push(`- drifted: ${relativePath}`);
295
+ }
296
+ } else {
297
+ lines.push("Starter file status: all current");
298
+ }
299
+
300
+ lines.push("");
301
+
302
+ if (plan.dbInstallPlan.resolvedOrder.length > 0) {
303
+ lines.push(`Resolved database stack: ${plan.dbInstallPlan.resolvedOrder.join(" -> ")}`);
304
+ }
305
+
306
+ if (plan.dbInstallPlan.notes.length > 0) {
307
+ lines.push("Database notes:");
308
+ for (const note of plan.dbInstallPlan.notes) {
309
+ lines.push(`- ${note}`);
310
+ }
311
+ }
312
+
313
+ if (options.installCommand) {
314
+ lines.push("", `Next install command: ${options.installCommand}`);
315
+ }
316
+
317
+ return `${lines.join("\n")}\n`;
318
+ }
319
+
320
+ export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptions = {}) {
321
+ const targetDir = resolveUpdateTargetDirectory(runtimeOptions, argvOptions);
322
+ const packageJsonPath = path.join(targetDir, "package.json");
323
+ const manifest = await readJsonIfPresent(packageJsonPath);
324
+
325
+ if (!manifest) {
326
+ throw new Error(`Target directory does not contain package.json: ${targetDir}`);
327
+ }
328
+
329
+ const installedBrightwebPackagesMap = collectInstalledBrightwebPackages(manifest);
330
+ const template = await detectTemplate(targetDir, installedBrightwebPackagesMap);
331
+ const dependencyMode = detectDependencyMode(installedBrightwebPackagesMap);
332
+ const workspaceRoot = runtimeOptions.workspaceRoot
333
+ ? path.resolve(runtimeOptions.workspaceRoot)
334
+ : argvOptions.workspaceRoot
335
+ ? path.resolve(argvOptions.workspaceRoot)
336
+ : dependencyMode === "workspace"
337
+ ? await findWorkspaceRoot(targetDir)
338
+ : null;
339
+ const packageManager = detectPackageManager(argvOptions.packageManager || runtimeOptions.packageManager);
340
+ const installedModules = detectInstalledModules(installedBrightwebPackagesMap);
341
+ const versionMap = await getVersionMap(workspaceRoot);
342
+ const dbRegistry = await getDbModuleRegistry(workspaceRoot);
343
+ const dbInstallPlan = template === "platform"
344
+ ? createDbInstallPlan({
345
+ selectedModules: installedModules,
346
+ workspaceMode: dependencyMode === "workspace",
347
+ registry: dbRegistry,
348
+ })
349
+ : {
350
+ selectedLabels: [],
351
+ resolvedOrder: [],
352
+ notes: [],
353
+ };
354
+ const canonicalVersions = getCanonicalBrightwebVersions({
355
+ manifest,
356
+ template,
357
+ dependencyMode,
358
+ installedModules,
359
+ versionMap,
360
+ });
361
+ const packageJsonUpdate = mergeManagedPackageUpdates({
362
+ manifest,
363
+ targetVersions: canonicalVersions,
364
+ installedBrightwebPackages: installedBrightwebPackagesMap,
365
+ });
366
+
367
+ const fileWrites = [];
368
+ if (packageJsonUpdate.changed) {
369
+ fileWrites.push({
370
+ relativePath: "package.json",
371
+ targetPath: packageJsonPath,
372
+ content: packageJsonUpdate.content,
373
+ type: "config",
374
+ });
375
+ }
376
+
377
+ if (template === "platform") {
378
+ const canonicalConfigFiles = {
379
+ "next.config.ts": createNextConfig({ template: "platform", selectedModules: installedModules }),
380
+ [path.join("config", "modules.ts")]: createPlatformModulesConfigFile(installedModules),
381
+ [path.join("config", "shell.ts")]: createShellConfig(installedModules),
382
+ [path.join("docs", "ai", "app-context.json")]: createAppContextFile({
383
+ slug: manifest.name || path.basename(targetDir),
384
+ template: "platform",
385
+ selectedModules: installedModules,
386
+ dbInstallPlan,
387
+ }),
388
+ };
389
+
390
+ for (const relativePath of MANAGED_PLATFORM_FILES) {
391
+ const targetPath = path.join(targetDir, relativePath);
392
+ const currentContent = (await pathExists(targetPath)) ? await fs.readFile(targetPath, "utf8") : null;
393
+ const nextContent = canonicalConfigFiles[relativePath];
394
+
395
+ if (currentContent !== nextContent) {
396
+ fileWrites.push({
397
+ relativePath,
398
+ targetPath,
399
+ content: nextContent,
400
+ type: "config",
401
+ });
402
+ }
403
+ }
404
+ } else {
405
+ const canonicalConfigFiles = {
406
+ [path.join("docs", "ai", "app-context.json")]: createAppContextFile({
407
+ slug: manifest.name || path.basename(targetDir),
408
+ template: "site",
409
+ }),
410
+ };
411
+
412
+ for (const relativePath of MANAGED_SITE_FILES) {
413
+ const targetPath = path.join(targetDir, relativePath);
414
+ const currentContent = (await pathExists(targetPath)) ? await fs.readFile(targetPath, "utf8") : null;
415
+ const nextContent = canonicalConfigFiles[relativePath];
416
+
417
+ if (currentContent !== nextContent) {
418
+ fileWrites.push({
419
+ relativePath,
420
+ targetPath,
421
+ content: nextContent,
422
+ type: "config",
423
+ });
424
+ }
425
+ }
426
+ }
427
+
428
+ const starterFiles = template === "platform"
429
+ ? await getStarterFileStatus(targetDir, installedModules)
430
+ : [];
431
+ const starterFilesMissing = starterFiles.filter((entry) => entry.status === "missing");
432
+ const starterFilesDrifted = starterFiles.filter((entry) => entry.status === "drifted");
433
+
434
+ if (argvOptions.refreshStarters) {
435
+ for (const entry of starterFiles.filter((candidate) => candidate.status !== "current")) {
436
+ fileWrites.push({
437
+ relativePath: entry.relativePath,
438
+ targetPath: entry.targetPath,
439
+ content: await fs.readFile(entry.sourcePath, "utf8"),
440
+ type: "starter",
441
+ });
442
+ }
443
+ }
444
+
445
+ const modulesConfigMismatch = template === "platform"
446
+ ? await detectModulesConfigMismatch(targetDir, installedModules)
447
+ : null;
448
+
449
+ return {
450
+ targetDir,
451
+ workspaceRoot,
452
+ template,
453
+ dependencyMode,
454
+ packageManager,
455
+ manifest,
456
+ installedModules,
457
+ installedBrightwebPackages: Array.from(installedBrightwebPackagesMap.keys()).sort(),
458
+ packageUpdates: packageJsonUpdate.packageUpdates,
459
+ configFilesToWrite: fileWrites.filter((entry) => entry.type === "config").map((entry) => entry.relativePath),
460
+ starterFilesMissing: starterFilesMissing.map((entry) => entry.relativePath),
461
+ starterFilesDrifted: starterFilesDrifted.map((entry) => entry.relativePath),
462
+ starterFilesToRefresh: fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath),
463
+ dbInstallPlan,
464
+ modulesConfigMismatch,
465
+ fileWrites,
466
+ };
467
+ }
468
+
469
+ export async function updateBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
470
+ const plan = await buildBrightwebAppUpdatePlan(argvOptions, runtimeOptions);
471
+ const installCommand = renderInstallCommand({
472
+ packageManager: plan.packageManager,
473
+ dependencyMode: plan.dependencyMode,
474
+ targetDir: plan.targetDir,
475
+ workspaceRoot: plan.workspaceRoot,
476
+ });
477
+
478
+ output.write(renderPlanSummary(plan, { installCommand }));
479
+
480
+ if (argvOptions.dryRun) {
481
+ return {
482
+ dryRun: true,
483
+ plan,
484
+ installCommand,
485
+ };
486
+ }
487
+
488
+ for (const fileWrite of plan.fileWrites) {
489
+ await fs.mkdir(path.dirname(fileWrite.targetPath), { recursive: true });
490
+ await fs.writeFile(fileWrite.targetPath, fileWrite.content, "utf8");
491
+ }
492
+
493
+ const packageJsonChanged = plan.fileWrites.some((entry) => entry.relativePath === "package.json");
494
+ if (argvOptions.install && packageJsonChanged) {
495
+ const installRunner = runtimeOptions.installRunner || runInstall;
496
+ const installCwd = plan.dependencyMode === "workspace" && plan.workspaceRoot ? plan.workspaceRoot : plan.targetDir;
497
+ await installRunner(plan.packageManager, installCwd);
498
+ }
499
+
500
+ if (!argvOptions.install && packageJsonChanged) {
501
+ output.write(`Run \`${installCommand}\` to install updated package versions.\n`);
502
+ }
503
+
504
+ if (plan.fileWrites.length === 0) {
505
+ output.write("No managed changes were required.\n");
506
+ } else {
507
+ output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"}.\n`);
508
+ }
509
+
510
+ return {
511
+ dryRun: false,
512
+ plan,
513
+ installCommand,
514
+ };
515
+ }
@@ -6,6 +6,8 @@ This generated project is a BrightWeb platform starter. Use this file as the loc
6
6
 
7
7
  - `README.md`: local setup commands and starter routes.
8
8
  - `docs/ai/README.md`: app-specific routing guide for agents.
9
+ - `docs/ai/examples.md`: common setup and customization flows.
10
+ - `docs/ai/app-context.json`: machine-readable app summary for quick discovery.
9
11
  - `config/brand.ts`: client identity, naming, and contact defaults.
10
12
  - `config/modules.ts`: selected module set and runtime enablement.
11
13
  - `config/client.ts`: starter-facing derived state used by the home page and setup surfaces.
@@ -15,6 +15,8 @@ This app is a normal Next.js App Router project with BrightWeb runtime wiring la
15
15
 
16
16
  ## Fast routing map
17
17
 
18
+ - `docs/ai/app-context.json`: machine-readable summary of this app's template, starter routes, and first-read files.
19
+ - `docs/ai/examples.md`: common setup and customization workflows.
18
20
  - `README.md`: first-run setup steps.
19
21
  - `config/brand.ts`: client name, product name, support inboxes, and brand color.
20
22
  - `config/modules.ts`: module metadata and enablement flags for CRM, Projects, and Admin.
@@ -0,0 +1,38 @@
1
+ # Agent Examples
2
+
3
+ Use these workflows after reading `AGENTS.md`, `docs/ai/README.md`, and `docs/ai/app-context.json`.
4
+
5
+ ## First local setup
6
+
7
+ Goal: get the generated starter running with real credentials.
8
+
9
+ - Review `.env.local` and replace placeholder values.
10
+ - Review `config/brand.ts` and confirm client identity.
11
+ - Review `config/modules.ts` before touching module routes.
12
+ - Run the local dev server for this app or workspace.
13
+ - Validate `/`, `/bootstrap`, `/preview/app-shell`, and `/playground/auth`.
14
+ - Validate `/playground/crm`, `/playground/projects`, and `/playground/admin` only when those modules are enabled.
15
+
16
+ ## Change brand identity
17
+
18
+ Goal: update the starter to the real client name and support details.
19
+
20
+ - Edit `config/brand.ts`.
21
+ - Check `config/client.ts` or `config/bootstrap.ts` if starter copy still references old defaults.
22
+ - Validate the home page and `/preview/app-shell` after the change.
23
+
24
+ ## Replace starter routes with product routes
25
+
26
+ Goal: move from validation surfaces to product-owned pages.
27
+
28
+ - Build the real routes in `app/` first.
29
+ - Update `config/shell.ts` if navigation or toolbar behavior changes.
30
+ - Remove `/bootstrap`, `/preview/app-shell`, or `/playground/*` only after links and config references are cleaned up.
31
+
32
+ ## Make a module-aware change
33
+
34
+ Goal: add or modify functionality without assuming a module exists.
35
+
36
+ - Check `config/modules.ts` first.
37
+ - If the module is enabled, edit the app-owned route or API surface before considering package forks.
38
+ - If the module is not enabled, do not create links or routes that assume it exists.
@@ -0,0 +1,25 @@
1
+ # AGENTS.md
2
+
3
+ This generated project is a BrightWeb site 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 the starter surface list.
8
+ - `docs/ai/README.md`: app-specific routing guide for agents.
9
+ - `docs/ai/examples.md`: common setup and customization flows.
10
+ - `docs/ai/app-context.json`: machine-readable app summary for quick discovery.
11
+ - `config/site.ts`: site name, description, and starter CTAs.
12
+ - `app/page.tsx`: starter landing page composition.
13
+ - `app/globals.css`: global design language and theme tokens.
14
+
15
+ ## Working rules
16
+
17
+ - Treat this starter as app-owned. Prefer editing local routes, sections, and UI primitives instead of introducing shared BrightWeb runtime dependencies.
18
+ - Update `config/site.ts` before rewriting copy inline across multiple components.
19
+ - Keep reusable UI tweaks inside `components/ui/` when the change should affect multiple sections.
20
+
21
+ ## First validation pass
22
+
23
+ 1. Run the local dev server from this project or workspace.
24
+ 2. Open `/` and confirm the starter content renders correctly.
25
+ 3. Check `config/site.ts`, `app/page.tsx`, and `app/globals.css` together before making large copy or layout changes.
@@ -0,0 +1,37 @@
1
+ # Agent Guide
2
+
3
+ This file is the local routing guide for AI agents working inside a generated BrightWeb site 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 a local UI layer and light configuration.
10
+
11
+ - `app/`: route tree, layouts, and the starter landing page.
12
+ - `components/ui/`: local UI primitives used by the starter surface.
13
+ - `config/site.ts`: generated site identity, description, and CTA defaults.
14
+ - `lib/`: helper utilities such as class merging.
15
+
16
+ ## Fast routing map
17
+
18
+ - `docs/ai/app-context.json`: machine-readable summary of this app's template, starter routes, and first-read files.
19
+ - `docs/ai/examples.md`: common setup and customization workflows.
20
+ - `README.md`: first-run setup steps.
21
+ - `config/site.ts`: site name, description, eyebrow, and starter CTA links.
22
+ - `app/page.tsx`: starter composition for the home page.
23
+ - `app/globals.css`: theme tokens, layout defaults, and base typography.
24
+ - `components/ui/*`: local component primitives used by the starter.
25
+
26
+ ## Editing strategy
27
+
28
+ - Change `config/site.ts` first when the task is mostly copy or link updates.
29
+ - Change `app/page.tsx` first when the task is about page structure or section flow.
30
+ - Change `app/globals.css` and `components/ui/*` when the task affects the visual system.
31
+ - Keep changes local to this app unless the task explicitly requires a shared BrightWeb package.
32
+
33
+ ## Validation checklist
34
+
35
+ 1. Run the app locally.
36
+ 2. Validate `/` on desktop and mobile sizes.
37
+ 3. If you changed the visual system, check both `app/globals.css` and the affected `components/ui/*` files.
@@ -0,0 +1,35 @@
1
+ # Agent Examples
2
+
3
+ Use these workflows after reading `AGENTS.md`, `docs/ai/README.md`, and `docs/ai/app-context.json`.
4
+
5
+ ## First local setup
6
+
7
+ Goal: get the generated site starter running and ready for edits.
8
+
9
+ - Run the local dev server for this app or workspace.
10
+ - Open `/` and confirm the starter renders correctly.
11
+ - Review `config/site.ts`, `app/page.tsx`, and `app/globals.css` before making large changes.
12
+
13
+ ## Change site identity
14
+
15
+ Goal: update the starter name, description, and CTA links.
16
+
17
+ - Edit `config/site.ts`.
18
+ - Validate `/` after the change.
19
+ - If copy still exists outside the config file, update `app/page.tsx`.
20
+
21
+ ## Restyle the starter
22
+
23
+ Goal: change the visual language without rewriting everything at once.
24
+
25
+ - Start in `app/globals.css` for colors, spacing, and typography direction.
26
+ - Update `components/ui/*` if shared primitives need to change.
27
+ - Validate `/` on both desktop and mobile sizes.
28
+
29
+ ## Replace the starter sections
30
+
31
+ Goal: turn the scaffold into a real site.
32
+
33
+ - Edit `app/page.tsx` section by section.
34
+ - Keep shared button, badge, and card behavior in `components/ui/*`.
35
+ - Remove starter content only after the replacement section is in place.