create-bw-app 0.11.0 → 0.13.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 (53) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/src/add.mjs +4 -2
  4. package/src/adopt.mjs +17 -3
  5. package/src/app-manifest.mjs +10 -2
  6. package/src/bw.mjs +4 -2
  7. package/src/constants.mjs +10 -15
  8. package/src/doctor.mjs +15 -1
  9. package/src/generator.mjs +64 -56
  10. package/src/remove.mjs +3 -1
  11. package/src/scaffold-cmd.mjs +74 -0
  12. package/src/scaffold.mjs +11 -3
  13. package/src/update.mjs +3 -0
  14. package/src/upgrade.mjs +6 -1
  15. package/template/base/AGENTS.md +16 -20
  16. package/template/base/app/globals.css +5 -569
  17. package/template/base/app/theme.css +5 -0
  18. package/template/base/config/modules.ts +3 -5
  19. package/template/base/docs/ai/README.md +15 -41
  20. package/template/base/docs/ai/examples.md +13 -34
  21. package/template/base/public/brand/logo-dark.svg +2 -2
  22. package/template/base/public/brand/logo-light.svg +2 -2
  23. package/template/modules/admin/app/admin/users/page.tsx +2 -0
  24. package/template/modules/admin/app/api/admin/users/roles/route.ts +1 -6
  25. package/template/modules/admin/app/api/admin/users/route.ts +1 -6
  26. package/template/modules/crm/app/api/crm/contacts/route.ts +3 -18
  27. package/template/modules/crm/app/api/crm/organizations/route.ts +1 -8
  28. package/template/modules/crm/app/api/crm/owners/route.ts +1 -8
  29. package/template/modules/crm/app/api/crm/stats/route.ts +1 -8
  30. package/template/modules/crm/app/api/crm/timeline/route.ts +1 -8
  31. package/template/site/base/AGENTS.md +6 -19
  32. package/template/site/base/docs/ai/README.md +6 -33
  33. package/template/site/base/docs/ai/examples.md +6 -28
  34. package/template/base/app/bootstrap/page.tsx +0 -75
  35. package/template/base/app/page.tsx +0 -145
  36. package/template/base/app/playground/auth/page.tsx +0 -5
  37. package/template/base/app/playground/layout.tsx +0 -41
  38. package/template/base/app/preview/app-shell/page.tsx +0 -11
  39. package/template/base/components/app-shell-preview.tsx +0 -185
  40. package/template/base/components/auth-playground.tsx +0 -112
  41. package/template/base/config/bootstrap.ts +0 -131
  42. package/template/base/config/client.ts +0 -18
  43. package/template/base/config/env.ts +0 -100
  44. package/template/base/lib/email/resend-base.ts +0 -13
  45. package/template/modules/admin/app/playground/admin/page.tsx +0 -102
  46. package/template/modules/crm/app/api/crm/_shared/create-module-route-handler.ts +0 -13
  47. package/template/modules/projects/app/playground/projects/page.tsx +0 -93
  48. package/template/site/base/app/page.tsx +0 -165
  49. package/template/site/base/components/ui/badge.tsx +0 -28
  50. package/template/site/base/components/ui/button.tsx +0 -52
  51. package/template/site/base/components/ui/card.tsx +0 -28
  52. package/template/site/base/components.json +0 -17
  53. package/template/site/base/lib/utils.ts +0 -6
package/README.md CHANGED
@@ -35,7 +35,7 @@ bw upgrade
35
35
  bw doctor
36
36
  ```
37
37
 
38
- - `bw add <moduleKey>` resolves requirements, installs module wiring and starter overlays, and appends migrations.
38
+ - `bw add <moduleKey>` resolves requirements, installs thin package mounts and module wiring, and appends migrations.
39
39
  - `bw upgrade [moduleKey]` includes the existing managed update flow plus forward-only module migrations.
40
40
  - `bw doctor` checks package, config, scaffold, environment-name, and migration consistency. Add `--report` to stamp the result in the app manifest.
41
41
  - All mutating commands support `--dry-run`.
@@ -58,7 +58,7 @@ Current updater behavior:
58
58
  - fails the update if npm resolution fails unless you pass `--allow-stale-fallback`
59
59
  - re-syncs managed BrightWeb config files such as `next.config.ts`, `config/modules.ts`, and `config/shell.ts`
60
60
  - preserves app-owned shell customizations in the scaffolded `config/shell.overrides.ts`
61
- - reports missing or drifted starter files and only rewrites them with `--refresh-starters`
61
+ - reports missing or drifted scaffold mounts and only rewrites them with `--refresh-starters`
62
62
  - prints the follow-up install command unless `--install` is passed
63
63
  - preserves unrelated third-party dependencies and app-owned product pages
64
64
 
@@ -69,10 +69,10 @@ Current updater behavior:
69
69
  - prompts for optional platform modules: `admin`, `crm`, and `projects`
70
70
  - prompts to install dependencies immediately
71
71
  - copies a clean Next.js App Router starter template
72
- - platform apps include BrightWeb auth, shell wiring, and optional module starter surfaces
73
- - platform apps include a local `components/` folder for app-owned UI alongside the shared BrightWeb packages
72
+ - platform apps include BrightWeb auth, shell wiring, and optional direct package mounts
73
+ - generated route files contain direct `@brightweblabs/*` re-exports only
74
74
  - platform apps in published mode also write `supabase/config.toml`, a CLI-ready flat `supabase/migrations/` folder, `supabase/module-registry.json`, `supabase/clients/<slug>/stack.json`, and the resolved shared SQL migrations under `supabase/modules/<module>/migrations`
75
- - site apps include Next.js, Tailwind CSS v4, and local component primitives
75
+ - site apps include a thin Next.js and Tailwind CSS shell with no default feature page or local component library
76
76
  - writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
77
77
  - 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
78
78
  - 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
@@ -88,7 +88,7 @@ When this package runs in BrightWeb workspace mode, it can:
88
88
  - create `supabase/clients/<slug>/stack.json`
89
89
  - create a client-only migrations folder so database planning stays aligned with scaffolded modules
90
90
 
91
- Platform mode always resolves to the `Core + Admin` database baseline. Selecting `admin` affects the Admin starter UI and package wiring, not whether the Admin database layer exists.
91
+ Platform mode always resolves to the `Core + Admin` database baseline. Selecting `admin` affects the Admin package mount and wiring, not whether the Admin database layer exists.
92
92
 
93
93
  ## Related references
94
94
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.11.0",
4
+ "version": "0.13.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-bw-app": "bin/create-bw-app.mjs",
package/src/add.mjs CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { stdout as output } from "node:process";
4
4
  import { SELECTABLE_MODULES } from "./constants.mjs";
5
- import { TEMPLATE_ROOT, createAppContextFile, createDbInstallPlan, createNextConfig, createPlatformModulesConfigFile, createShellConfig, getDbModuleRegistry, getVersionMap, pathExists, readJsonIfPresent } from "./generator.mjs";
5
+ import { TEMPLATE_ROOT, createAppContextFile, createDbInstallPlan, createNextConfig, createPlatformGlobalsCss, createPlatformModulesConfigFile, createShellConfig, getDbModuleRegistry, getVersionMap, pathExists, readJsonIfPresent } from "./generator.mjs";
6
6
  import { collectScaffoldFiles, findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, resolveModuleClosure, satisfiesVersion, writeAppManifest } from "./app-manifest.mjs";
7
7
  import { applyMigrationWrites, planMigrationAppends } from "./migrations.mjs";
8
8
 
@@ -65,6 +65,7 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
65
65
  }
66
66
  const managedWrites = {
67
67
  "next.config.ts": createNextConfig({ template: "platform", selectedModules: installedModuleKeys }),
68
+ "app/globals.css": await createPlatformGlobalsCss(installedModuleKeys),
68
69
  "config/modules.ts": createPlatformModulesConfigFile(installedModuleKeys),
69
70
  "config/shell.ts": createShellConfig(installedModuleKeys),
70
71
  "docs/ai/app-context.json": createAppContextFile({ slug: appManifest.app.slug, template: "platform", selectedModules: installedModuleKeys.filter((key) => key !== "orgs"), dbInstallPlan }),
@@ -91,7 +92,8 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
91
92
  const now = new Date().toISOString();
92
93
  for (const key of newModules) appManifest.modules[key] = { version: catalog[key].version, installedAt: now, exposed: true };
93
94
  appManifest.migrationCursor = migrationPlan.nextCursor;
94
- appManifest.scaffoldFiles = { ...appManifest.scaffoldFiles, ...await collectScaffoldFiles(targetDir, installedModuleKeys) };
95
+ const collectedScaffoldFiles = await collectScaffoldFiles(targetDir, installedModuleKeys);
96
+ appManifest.scaffoldFiles = { ...collectedScaffoldFiles, ...appManifest.scaffoldFiles };
95
97
  await writeAppManifest(targetDir, appManifest);
96
98
  output.write(`Installed ${newModules.length} module${newModules.length === 1 ? "" : "s"}. ${migrationPlan.writes.length > 0 ? "Run your Supabase migration apply command. " : ""}Run your package manager install command next.\n`);
97
99
  return { dryRun: false, newModules, migrationPlan };
package/src/adopt.mjs CHANGED
@@ -18,7 +18,7 @@ import { inventoryScaffoldFiles, resolveTemplateRoot } from "./scaffold.mjs";
18
18
  import { detectDependencyMode, detectTemplate } from "./update.mjs";
19
19
 
20
20
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21
- const HELP = `Usage: bw adopt [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --cursor <key>=<migrationFilename> Override a migration cursor (repeatable)\n --owned-surface <name> Record an app-owned surface (repeatable)\n --allow-uncursored Allow doctor to warn instead of fail on null cursors\n --force Replace an existing app manifest\n --dry-run Print the manifest and warnings without writing\n --help Show this help`;
21
+ const HELP = `Usage: bw adopt [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --cursor <key>=<migrationFilename> Override a migration cursor (repeatable)\n --owned-surface <name> Record an app-owned surface (repeatable)\n --own <path> Mark an existing scaffold file app-owned (repeatable)\n --skip <path> Mark a missing scaffold file intentionally absent (repeatable)\n --allow-uncursored Allow doctor to warn instead of fail on null cursors\n --force Replace an existing app manifest\n --dry-run Print the manifest and warnings without writing\n --help Show this help`;
22
22
 
23
23
  function asList(value) {
24
24
  if (value == null) return [];
@@ -123,10 +123,24 @@ export async function adoptBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
123
123
  const scaffold = template === "platform"
124
124
  ? await inventoryScaffoldFiles({ targetDir, moduleKeys: Object.keys(modules), templateRoot })
125
125
  : { records: {}, unsupported: [] };
126
+ const ownedPaths = new Set(asList(argvOptions.own).map(String));
127
+ const skippedPaths = new Set(asList(argvOptions.skip).map(String));
128
+ for (const relativePath of [...ownedPaths, ...skippedPaths]) {
129
+ if (!scaffold.records[relativePath]) throw new Error(`${relativePath} is not a tracked scaffold file.`);
130
+ }
131
+ for (const relativePath of ownedPaths) {
132
+ if (skippedPaths.has(relativePath)) throw new Error(`${relativePath} cannot be both --own and --skip.`);
133
+ if (scaffold.records[relativePath].status === "missing") throw new Error(`Cannot own missing scaffold file: ${relativePath}`);
134
+ scaffold.records[relativePath].intent = "owned";
135
+ }
136
+ for (const relativePath of skippedPaths) {
137
+ if (scaffold.records[relativePath].status !== "missing") throw new Error(`Cannot skip existing scaffold file: ${relativePath}`);
138
+ scaffold.records[relativePath].intent = "skipped";
139
+ }
126
140
  for (const relativePath of scaffold.unsupported) warnings.push(`WARN ${relativePath}: installed-version template is unavailable; scaffold comparison is unsupported.`);
127
141
  for (const [relativePath, record] of Object.entries(scaffold.records)) {
128
- if (record.status === "drifted") warnings.push(`WARN drifted scaffold file: ${relativePath}`);
129
- if (record.status === "missing") warnings.push(`WARN missing scaffold file: ${relativePath} (not created; use bw diff and the current template as guidance).`);
142
+ if (record.status === "drifted" && record.intent !== "owned") warnings.push(`WARN drifted scaffold file: ${relativePath}`);
143
+ if (record.status === "missing" && record.intent !== "skipped") warnings.push(`WARN missing scaffold file: ${relativePath} (not created; use bw diff and the current template as guidance).`);
130
144
  }
131
145
 
132
146
  const overrides = parseCursorOverrides(argvOptions.cursor);
@@ -18,6 +18,7 @@ async function readJsonIfPresent(filePath) {
18
18
  export const APP_MANIFEST_PATH = path.join(".brightweb", "app-manifest.json");
19
19
  export const MANAGED_APP_FILES = [
20
20
  "next.config.ts",
21
+ "app/globals.css",
21
22
  "config/modules.ts",
22
23
  "config/shell.ts",
23
24
  "docs/ai/app-context.json",
@@ -89,7 +90,14 @@ export async function readAppManifest(targetDir, { required = true } = {}) {
89
90
  export async function writeAppManifest(targetDir, manifest) {
90
91
  const manifestPath = path.join(targetDir, APP_MANIFEST_PATH);
91
92
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
92
- await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
93
+ const temporaryPath = `${manifestPath}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
94
+ try {
95
+ await fs.writeFile(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
96
+ await fs.rename(temporaryPath, manifestPath);
97
+ } catch (error) {
98
+ await fs.rm(temporaryPath, { force: true }).catch(() => {});
99
+ throw error;
100
+ }
93
101
  }
94
102
 
95
103
  export function validateAppManifest(manifest) {
@@ -107,7 +115,7 @@ export function validateAppManifest(manifest) {
107
115
  if (!entry || !cleanVersion(entry.version) || typeof entry.installedAt !== "string" || typeof entry.exposed !== "boolean") errors.push(`modules.${key} is invalid`);
108
116
  }
109
117
  for (const [relativePath, entry] of Object.entries(manifest.scaffoldFiles || {})) {
110
- if (!entry || typeof entry.module !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.hash || "") || !["current", "drifted", "missing"].includes(entry.status)) errors.push(`scaffoldFiles.${relativePath} is invalid`);
118
+ if (!entry || typeof entry.module !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.hash || "") || !["current", "drifted", "missing"].includes(entry.status) || (entry.intent != null && !["managed", "owned", "skipped"].includes(entry.intent))) errors.push(`scaffoldFiles.${relativePath} is invalid`);
111
119
  }
112
120
  if (manifest.lastDoctor != null && (typeof manifest.lastDoctor.at !== "string" || typeof manifest.lastDoctor.ok !== "boolean")) errors.push("lastDoctor is invalid");
113
121
  return errors;
package/src/bw.mjs CHANGED
@@ -3,16 +3,17 @@ import { adoptBrightwebApp } from "./adopt.mjs";
3
3
  import { diffBrightwebScaffold } from "./diff.mjs";
4
4
  import { doctorBrightwebApp } from "./doctor.mjs";
5
5
  import { removeBrightwebModule } from "./remove.mjs";
6
+ import { scaffoldBrightwebApp } from "./scaffold-cmd.mjs";
6
7
  import { updateBrightwebApp } from "./update.mjs";
7
8
  import { upgradeBrightwebApp } from "./upgrade.mjs";
8
9
 
9
- const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey> Install a module and its requirements\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
10
+ const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey> Install a module and its requirements\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n scaffold <action> List or record per-file scaffold intent\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
10
11
 
11
12
  function parseOptions(argv) {
12
13
  const options = {};
13
14
  const positionals = [];
14
15
  const booleanFlags = new Set(["help", "dry-run", "strict", "report", "install", "refresh-starters", "allow-stale-fallback", "allow-uncursored", "force", "list", "yes"]);
15
- const repeatableFlags = new Set(["cursor", "owned-surface"]);
16
+ const repeatableFlags = new Set(["cursor", "owned-surface", "own", "skip"]);
16
17
  for (let index = 0; index < argv.length; index += 1) {
17
18
  const token = argv[index];
18
19
  if (!token.startsWith("--")) { positionals.push(token); continue; }
@@ -37,6 +38,7 @@ export async function runBwCli(argv = process.argv.slice(2), runtimeOptions = {}
37
38
  if (command === "add") await addBrightwebModule(positionals[0], options, runtimeOptions);
38
39
  else if (command === "adopt") await adoptBrightwebApp(options, runtimeOptions);
39
40
  else if (command === "diff") await diffBrightwebScaffold(positionals[0], options, runtimeOptions);
41
+ else if (command === "scaffold") await scaffoldBrightwebApp(positionals[0], positionals.slice(1), options, runtimeOptions);
40
42
  else if (command === "remove") await removeBrightwebModule(positionals[0], options, runtimeOptions);
41
43
  else if (command === "upgrade") await upgradeBrightwebApp(positionals[0], options, runtimeOptions);
42
44
  else if (command === "update") await updateBrightwebApp(options, runtimeOptions);
package/src/constants.mjs CHANGED
@@ -10,7 +10,7 @@ export const TEMPLATE_OPTIONS = [
10
10
  {
11
11
  key: "site",
12
12
  label: "Site",
13
- description: "Standalone Next.js + Tailwind site starter with a local UI layer.",
13
+ description: "Thin Next.js + Tailwind site shell for package-owned surfaces.",
14
14
  },
15
15
  ];
16
16
 
@@ -55,7 +55,7 @@ export const MODULE_STARTER_FILES = {
55
55
  admin: [
56
56
  "app/api/admin/users/route.ts",
57
57
  "app/api/admin/users/roles/route.ts",
58
- "app/playground/admin/page.tsx",
58
+ "app/admin/users/page.tsx",
59
59
  ],
60
60
  crm: [
61
61
  "app/crm/layout.tsx",
@@ -67,7 +67,6 @@ export const MODULE_STARTER_FILES = {
67
67
  "app/api/crm/timeline/route.ts",
68
68
  ],
69
69
  projects: [
70
- "app/playground/projects/page.tsx",
71
70
  ],
72
71
  };
73
72
 
@@ -76,15 +75,15 @@ export const PLATFORM_STARTER_FILES = [
76
75
  ];
77
76
 
78
77
  export const APP_DEPENDENCY_DEFAULTS = {
79
- "@brightweblabs/app-shell": "^0.4.0",
78
+ "@brightweblabs/app-shell": "^0.4.1",
80
79
  "@brightweblabs/core-auth": "^0.3.4",
81
80
  "@brightweblabs/infra": "^0.3.1",
82
- "@brightweblabs/module-admin": "^0.3.4",
83
- "@brightweblabs/module-crm": "^0.5.0",
84
- "@brightweblabs/module-orgs": "^0.2.0",
85
- "@brightweblabs/module-projects": "^0.4.2",
86
- "@brightweblabs/theme": "^0.2.0",
87
- "@brightweblabs/ui": "^1.0.0",
81
+ "@brightweblabs/module-admin": "^0.4.0",
82
+ "@brightweblabs/module-crm": "^0.5.2",
83
+ "@brightweblabs/module-orgs": "^0.2.2",
84
+ "@brightweblabs/module-projects": "^0.4.3",
85
+ "@brightweblabs/theme": "^0.2.1",
86
+ "@brightweblabs/ui": "^1.0.1",
88
87
  "lucide-react": "^1.8.0",
89
88
  "next": "16.1.6",
90
89
  "react": "19.2.3",
@@ -92,13 +91,9 @@ export const APP_DEPENDENCY_DEFAULTS = {
92
91
  };
93
92
 
94
93
  export const SITE_DEPENDENCY_DEFAULTS = {
95
- "class-variance-authority": "^0.7.1",
96
- "clsx": "^2.1.1",
97
- "lucide-react": "^1.8.0",
98
94
  "next": "16.1.6",
99
95
  "react": "19.2.3",
100
96
  "react-dom": "19.2.3",
101
- "tailwind-merge": "^3.4.0",
102
97
  };
103
98
 
104
99
  export const APP_DEV_DEPENDENCY_DEFAULTS = {
@@ -149,6 +144,6 @@ Update options:
149
144
  --package-manager <name> Override package manager: pnpm, npm, yarn, or bun
150
145
  --allow-stale-fallback Use baked-in BrightWeb package versions if npm lookup fails
151
146
  --install Run install after writing package changes
152
- --refresh-starters Rewrite starter route files from the latest template
147
+ --refresh-starters Rewrite tracked package mount files from the latest template
153
148
  --dry-run Print the update plan without writing files
154
149
  `.trim();
package/src/doctor.mjs CHANGED
@@ -52,7 +52,21 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
52
52
  add(topologyProblems.length ? "FAIL" : "PASS", "topology", topologyProblems.join("; ") || "Module requirements are satisfied.");
53
53
 
54
54
  const scaffold = await scaffoldDrift(targetDir, appManifest.scaffoldFiles);
55
- add(scaffold.drifted.length || scaffold.missing.length ? "FAIL" : "PASS", "scaffold", `${scaffold.current.length} current, ${scaffold.drifted.length} drifted, ${scaffold.missing.length} missing; drifted scaffold files: ${scaffold.drifted.length} (see bw diff --list)${scaffold.missing.length ? `; missing: ${scaffold.missing.join(", ")}` : ""}`);
55
+ const scaffoldGroups = {
56
+ current: scaffold.entries.filter((entry) => entry.status === "current" && entry.intent !== "skipped"),
57
+ owned: scaffold.entries.filter((entry) => entry.intent === "owned" && entry.status === "drifted"),
58
+ skipped: scaffold.entries.filter((entry) => entry.intent === "skipped" && entry.status === "missing"),
59
+ undecidedDrift: scaffold.entries.filter((entry) => entry.intent === "managed" && entry.status === "drifted"),
60
+ undecidedMissing: scaffold.entries.filter((entry) => entry.intent === "managed" && entry.status === "missing"),
61
+ mismatched: scaffold.entries.filter((entry) => (entry.intent === "owned" && entry.status === "missing") || (entry.intent === "skipped" && entry.status !== "missing")),
62
+ };
63
+ if (scaffoldGroups.owned.length) add("INFO", "scaffold-owned", `App-owned scaffold files: ${scaffoldGroups.owned.map((entry) => entry.relativePath).join(", ")}.`);
64
+ if (scaffoldGroups.skipped.length) add("INFO", "scaffold-skipped", `Intentionally skipped scaffold files: ${scaffoldGroups.skipped.map((entry) => entry.relativePath).join(", ")}.`);
65
+ if (scaffoldGroups.undecidedDrift.length) add("WARN", "scaffold-undecided-drift", `Unacknowledged drift: ${scaffoldGroups.undecidedDrift.map((entry) => entry.relativePath).join(", ")} (use bw scaffold own or bw diff).`);
66
+ if (scaffoldGroups.undecidedMissing.length) add("WARN", "scaffold-undecided-missing", `Unacknowledged missing files: ${scaffoldGroups.undecidedMissing.map((entry) => entry.relativePath).join(", ")} (use bw scaffold skip after review).`);
67
+ if (scaffoldGroups.mismatched.length) add("FAIL", "scaffold-intent-mismatch", `Recorded scaffold intent no longer matches reality: ${scaffoldGroups.mismatched.map((entry) => `${entry.relativePath} (${entry.intent}, ${entry.status})`).join(", ")}.`);
68
+ const scaffoldStatus = scaffoldGroups.mismatched.length ? "FAIL" : scaffoldGroups.undecidedDrift.length || scaffoldGroups.undecidedMissing.length ? "WARN" : "PASS";
69
+ add(scaffoldStatus, "scaffold", `${scaffoldGroups.current.length} current, ${scaffoldGroups.owned.length} owned, ${scaffoldGroups.skipped.length} skipped, ${scaffoldGroups.undecidedDrift.length} undecided-drift, ${scaffoldGroups.undecidedMissing.length} undecided-missing, ${scaffoldGroups.mismatched.length} intent-mismatch.`);
56
70
  add("INFO", "owned-surfaces", `Owned surfaces: ${(appManifest.ownedSurfaces || []).join(", ") || "none"}.`);
57
71
 
58
72
  const envNames = new Set(Object.keys(process.env));
package/src/generator.mjs CHANGED
@@ -198,7 +198,7 @@ export function createDbInstallPlan({ selectedModules, workspaceMode, registry }
198
198
  const notes = [];
199
199
 
200
200
  if (!selectedModules.includes("admin") && resolvedOrder.includes("admin")) {
201
- notes.push("Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin starter UI and package wiring are scaffolded.");
201
+ notes.push("Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin package mount and wiring are scaffolded.");
202
202
  }
203
203
 
204
204
  for (const moduleKey of resolvedOrder) {
@@ -315,7 +315,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
315
315
  " description: string;",
316
316
  " enabled: boolean;",
317
317
  " packageName: string;",
318
- " playgroundHref?: string;",
318
+ " routeHref?: string;",
319
319
  ' placement: "core" | "primary" | "admin" | "hidden";',
320
320
  "};",
321
321
  "",
@@ -326,7 +326,6 @@ export function createPlatformModulesConfigFile(selectedModules) {
326
326
  ' description: "Login, reset-password, callback URLs, and shared auth validation utilities.",',
327
327
  " enabled: true,",
328
328
  ' packageName: "@brightweblabs/core-auth",',
329
- ' playgroundHref: "/playground/auth",',
330
329
  ' placement: "core",',
331
330
  " },",
332
331
  " {",
@@ -343,7 +342,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
343
342
  ' description: "Contacts and CRM server/data layer, with marketing-adjacent operational data stored in Supabase.",',
344
343
  ` enabled: ${String(selected.has("crm"))},`,
345
344
  ' packageName: "@brightweblabs/module-crm",',
346
- ' playgroundHref: "/crm",',
345
+ ' routeHref: "/crm",',
347
346
  ' placement: "primary",',
348
347
  " },",
349
348
  " {",
@@ -352,7 +351,6 @@ export function createPlatformModulesConfigFile(selectedModules) {
352
351
  ' description: "Project portfolio, detail routes, and work-management server logic.",',
353
352
  ` enabled: ${String(selected.has("projects"))},`,
354
353
  ' packageName: "@brightweblabs/module-projects",',
355
- ' playgroundHref: "/playground/projects",',
356
354
  ' placement: "primary",',
357
355
  " },",
358
356
  " {",
@@ -361,7 +359,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
361
359
  ' description: "User role governance, admin tools, and access-control surfaces.",',
362
360
  ` enabled: ${String(selected.has("admin"))},`,
363
361
  ' packageName: "@brightweblabs/module-admin",',
364
- ' playgroundHref: "/playground/admin",',
362
+ ' routeHref: "/admin/users",',
365
363
  ' placement: "admin",',
366
364
  " },",
367
365
  "];",
@@ -438,17 +436,15 @@ function createGitignore() {
438
436
  }
439
437
 
440
438
  function getPlatformStarterRoutes(selectedModules) {
441
- return [
442
- "/",
443
- "/bootstrap",
444
- "/preview/app-shell",
445
- "/playground/auth",
446
- ...selectedModules.map((moduleKey) => moduleKey === "crm" ? "/crm" : `/playground/${moduleKey}`),
447
- ];
439
+ return selectedModules.flatMap((moduleKey) => {
440
+ if (moduleKey === "crm") return ["/crm"];
441
+ if (moduleKey === "admin") return ["/admin/users"];
442
+ return [];
443
+ });
448
444
  }
449
445
 
450
446
  function getSiteStarterRoutes() {
451
- return ["/"];
447
+ return [];
452
448
  }
453
449
 
454
450
  function createPlatformReadme({
@@ -508,7 +504,7 @@ function createPlatformReadme({
508
504
  "",
509
505
  ]
510
506
  : []),
511
- "## Starter routes",
507
+ "## Package mounts",
512
508
  "",
513
509
  ...getPlatformStarterRoutes(selectedModules).map((route) => `- \`${route}\``),
514
510
  "",
@@ -542,17 +538,17 @@ function createSiteReadme({ slug, workspaceMode, packageManager }) {
542
538
  "",
543
539
  "- Next.js App Router",
544
540
  "- Tailwind CSS v4",
545
- "- Local shadcn-style component primitives",
541
+ "- App-owned settings and theme tokens",
546
542
  "",
547
543
  "## Local setup",
548
544
  "",
549
545
  ...localSteps,
550
546
  "",
551
- "## Starter surfaces",
547
+ "## Package mounts",
552
548
  "",
553
549
  ...getSiteStarterRoutes().map((route) => `- \`${route}\``),
554
550
  "",
555
- "Edit `config/site.ts` to change the site name, copy, and public links.",
551
+ "This thin scaffold intentionally ships no feature page. Add routes only as thin mounts of package-owned surfaces.",
556
552
  "",
557
553
  "## AI handoff",
558
554
  "",
@@ -591,33 +587,32 @@ export function createAppContextFile({
591
587
  "docs/ai/README.md",
592
588
  "README.md",
593
589
  "config/site.ts",
594
- "app/page.tsx",
595
590
  "app/globals.css",
596
591
  ],
597
592
  appRoutesRoot: "app",
598
593
  configRoot: "config",
599
- componentsRoot: "components",
600
- uiComponentsRoot: "components/ui",
601
- libRoot: "lib",
602
594
  },
603
595
  starterRoutes: getSiteStarterRoutes(),
604
596
  ownership: {
605
597
  appOwned: [
606
- "app/**",
607
- "components/**",
608
598
  "config/**",
609
599
  "docs/ai/**",
610
- "lib/**",
611
600
  "public/**",
612
601
  "AGENTS.md",
613
602
  "README.md",
614
603
  ],
604
+ scaffoldManaged: [
605
+ "app/layout.tsx",
606
+ "app/globals.css",
607
+ "next.config.ts",
608
+ "postcss.config.mjs",
609
+ "tsconfig.json",
610
+ ],
615
611
  packageOwned: [],
616
612
  },
617
613
  agentRules: {
618
- editConfigBeforeCopyTweaks: true,
619
- keepUiPrimitiveChangesLocal: true,
620
- treatStarterHomeAsAppOwned: true,
614
+ keepRoutesAsThinPackageMounts: true,
615
+ doNotAddFeatureOrLibraryCodeToTheApp: true,
621
616
  },
622
617
  },
623
618
  null,
@@ -649,30 +644,35 @@ export function createAppContextFile({
649
644
  "docs/ai/README.md",
650
645
  "README.md",
651
646
  "app/globals.css",
647
+ "app/theme.css",
652
648
  "config/brand.ts",
653
649
  "config/modules.ts",
654
- "config/client.ts",
655
- "config/bootstrap.ts",
656
650
  "config/shell.ts",
657
651
  "config/shell.overrides.ts",
658
652
  ".env.local",
659
653
  ],
660
654
  appRoutesRoot: "app",
661
- componentsRoot: "components",
662
655
  configRoot: "config",
663
656
  brandAssetsRoot: "public/brand",
664
657
  },
665
658
  starterRoutes: getPlatformStarterRoutes(selectedModules),
666
659
  ownership: {
667
660
  appOwned: [
668
- "app/**",
669
- "components/**",
670
661
  "config/**",
671
662
  "docs/ai/**",
672
663
  "public/brand/**",
673
664
  "AGENTS.md",
674
665
  "README.md",
675
666
  ],
667
+ scaffoldManaged: [
668
+ "app/layout.tsx",
669
+ "app/globals.css",
670
+ "app/**/page.tsx",
671
+ "app/**/route.ts",
672
+ "next.config.ts",
673
+ "postcss.config.mjs",
674
+ "tsconfig.json",
675
+ ],
676
676
  packageOwned: [
677
677
  ...CORE_PACKAGES,
678
678
  ...(selectedModules.includes("crm") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
@@ -683,8 +683,8 @@ export function createAppContextFile({
683
683
  },
684
684
  agentRules: {
685
685
  checkModulesBeforeEditing: true,
686
- preferAppLevelCompositionOverPackageForks: true,
687
- treatStarterRoutesAsRemovable: true,
686
+ keepRoutesAsThinPackageMounts: true,
687
+ moveFeatureAndLibraryCodeToPackages: true,
688
688
  },
689
689
  },
690
690
  null,
@@ -711,13 +711,9 @@ export function createPackageJson({
711
711
  lint: "tsc --noEmit",
712
712
  },
713
713
  dependencies: sortObjectKeys({
714
- "class-variance-authority": versionMap["class-variance-authority"],
715
- "clsx": versionMap.clsx,
716
- "lucide-react": versionMap["lucide-react"],
717
714
  "next": versionMap.next,
718
715
  "react": versionMap.react,
719
716
  "react-dom": versionMap["react-dom"],
720
- "tailwind-merge": versionMap["tailwind-merge"],
721
717
  }),
722
718
  devDependencies: sortObjectKeys({
723
719
  "@tailwindcss/postcss": versionMap["@tailwindcss/postcss"],
@@ -813,6 +809,27 @@ export function createNextConfig({ template, selectedModules }) {
813
809
  ].join("\n");
814
810
  }
815
811
 
812
+ export async function createPlatformGlobalsCss(selectedModules) {
813
+ const template = await fs.readFile(path.join(TEMPLATE_ROOT, "base", "app", "globals.css"), "utf8");
814
+ const sourcePackages = [
815
+ "@brightweblabs/ui",
816
+ "@brightweblabs/app-shell",
817
+ ...SELECTABLE_MODULES
818
+ .filter((moduleDefinition) => selectedModules.includes(moduleDefinition.key))
819
+ .map((moduleDefinition) => moduleDefinition.packageName),
820
+ ];
821
+ const sourceBlock = [
822
+ "/* BRIGHTWEB_SOURCES_START */",
823
+ ...sourcePackages.map((packageName) => `@source "../node_modules/${packageName}/src";`),
824
+ "/* BRIGHTWEB_SOURCES_END */",
825
+ ].join("\n");
826
+
827
+ return template.replace(
828
+ /\/\* BRIGHTWEB_SOURCES_START \*\/[\s\S]*?\/\* BRIGHTWEB_SOURCES_END \*\//,
829
+ sourceBlock,
830
+ );
831
+ }
832
+
816
833
  export function createShellConfig(selectedModules) {
817
834
  const importLines = [];
818
835
  const registrationLines = [];
@@ -832,14 +849,9 @@ export function createShellConfig(selectedModules) {
832
849
  registrationLines.push(' if (enabled.has("crm")) registrations.push(crmModuleRegistration);');
833
850
  }
834
851
 
835
- if (selectedModules.includes("projects")) {
836
- importLines.push('import { projectsModuleRegistration } from "@brightweblabs/module-projects/registration";');
837
- registrationLines.push(' if (enabled.has("projects")) registrations.push(projectsModuleRegistration);');
838
- }
839
-
840
852
  return [
841
853
  "// MANAGED BY BRIGHTWEB — regenerated by create-bw-app update; put customizations in config/shell.overrides.ts",
842
- 'import { LayoutDashboard, Wrench } from "lucide-react";',
854
+ 'import { Wrench } from "lucide-react";',
843
855
  "import {",
844
856
  " applyShellRegistrationOverrides,",
845
857
  " buildClientAppShellRegistration,",
@@ -853,16 +865,9 @@ export function createShellConfig(selectedModules) {
853
865
  'import { getEnabledStarterModules } from "./modules";',
854
866
  'import { shellRegistrationOverrides } from "./shell.overrides";',
855
867
  "",
856
- "const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
857
- ' key: "dashboard",',
858
- ' placement: "primary",',
859
- ' navItems: [{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }],',
860
- ' toolbarRoutes: [{ surface: "dashboard", match: { exact: ["/dashboard"] } }],',
861
- "};",
862
- "",
863
868
  "function getStarterModuleRegistrations() {",
864
869
  ' const enabled = new Set(getEnabledStarterModules().map((moduleConfig) => moduleConfig.key));',
865
- " const registrations: ShellModuleRegistration<ShellContextualAction>[] = [dashboardModuleRegistration];",
870
+ " const registrations: ShellModuleRegistration<ShellContextualAction>[] = [];",
866
871
  "",
867
872
  ...registrationLines,
868
873
  "",
@@ -900,7 +905,7 @@ export function createShellConfig(selectedModules) {
900
905
  ' key: "tools",',
901
906
  ' label: "Ferramentas",',
902
907
  " icon: Wrench,",
903
- ' collapsedHref: enabledModules.find((moduleConfig) => moduleConfig.playgroundHref)?.playgroundHref || "/",',
908
+ ' collapsedHref: enabledModules.find((moduleConfig) => moduleConfig.routeHref)?.routeHref || "/",',
904
909
  " },",
905
910
  " modules,",
906
911
  " };",
@@ -1013,7 +1018,7 @@ async function writeClientStack(baseRoot, slug, dbInstallPlan, options = {}) {
1013
1018
  : "Generated by create-bw-app in published mode.",
1014
1019
  `Selected app modules: ${dbInstallPlan.selectedLabels.length > 0 ? dbInstallPlan.selectedLabels.join(", ") : "none"}.`,
1015
1020
  `Resolved database stack: ${enabledModules.map((moduleKey) => getModuleLabel(moduleKey)).join(" -> ")}.`,
1016
- "Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin starter UI and package wiring are scaffolded.",
1021
+ "Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin package mount and wiring are scaffolded.",
1017
1022
  "The database install order is resolved from supabase/module-registry.json.",
1018
1023
  ],
1019
1024
  },
@@ -1240,7 +1245,9 @@ async function scaffoldPlatformProject({
1240
1245
  for (const moduleDefinition of SELECTABLE_MODULES) {
1241
1246
  if (!selectedModules.includes(moduleDefinition.key)) continue;
1242
1247
  const moduleTemplateDir = path.join(TEMPLATE_ROOT, "modules", moduleDefinition.templateFolder);
1243
- await copyDirectory(moduleTemplateDir, targetDir);
1248
+ if (await pathExists(moduleTemplateDir)) {
1249
+ await copyDirectory(moduleTemplateDir, targetDir);
1250
+ }
1244
1251
  }
1245
1252
 
1246
1253
  await fs.writeFile(
@@ -1258,6 +1265,7 @@ async function scaffoldPlatformProject({
1258
1265
  )}\n`,
1259
1266
  );
1260
1267
  await fs.writeFile(path.join(targetDir, "next.config.ts"), createNextConfig({ template: "platform", selectedModules }));
1268
+ await fs.writeFile(path.join(targetDir, "app", "globals.css"), await createPlatformGlobalsCss(selectedModules));
1261
1269
  await fs.writeFile(
1262
1270
  path.join(targetDir, "config", "brand.ts"),
1263
1271
  createPlatformBrandConfigFile({ slug: answers.slug, brandValues }),
package/src/remove.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  createAppContextFile,
14
14
  createDbInstallPlan,
15
15
  createNextConfig,
16
+ createPlatformGlobalsCss,
16
17
  createPlatformModulesConfigFile,
17
18
  createShellConfig,
18
19
  getDbModuleRegistry,
@@ -59,6 +60,7 @@ export async function removeBrightwebModule(moduleKey, argvOptions = {}, runtime
59
60
  });
60
61
  const managedWrites = {
61
62
  "next.config.ts": createNextConfig({ template: "platform", selectedModules: remainingModules }),
63
+ "app/globals.css": await createPlatformGlobalsCss(remainingModules),
62
64
  "config/modules.ts": createPlatformModulesConfigFile(remainingModules),
63
65
  "config/shell.ts": createShellConfig(remainingModules),
64
66
  "docs/ai/app-context.json": createAppContextFile({ slug: appManifest.app.slug, template: "platform", selectedModules: remainingModules.filter((key) => key !== "orgs"), dbInstallPlan }),
@@ -70,7 +72,7 @@ export async function removeBrightwebModule(moduleKey, argvOptions = {}, runtime
70
72
  if (record.module !== moduleKey) continue;
71
73
  const filePath = path.join(targetDir, relativePath);
72
74
  if (!(await pathExists(filePath))) continue;
73
- if (await hashFile(filePath) === record.hash) cleanFiles.push(relativePath);
75
+ if ((record.intent || "managed") === "managed" && await hashFile(filePath) === record.hash) cleanFiles.push(relativePath);
74
76
  else driftedFiles.push(relativePath);
75
77
  }
76
78
  const notice = databaseNotice(moduleKey, catalog[moduleKey]?.manifest?.database?.ownedObjects || []);