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
@@ -0,0 +1,74 @@
1
+ import path from "node:path";
2
+ import { stdout as output } from "node:process";
3
+ import { findWorkspaceRoot, hashFile, readAppManifest, writeAppManifest } from "./app-manifest.mjs";
4
+ import { pathExists } from "./generator.mjs";
5
+ import { findTrackedTemplate, scaffoldDrift } from "./scaffold.mjs";
6
+
7
+ const HELP = `Usage: bw scaffold <action> [paths...] [options]\n\nActions:\n list List tracked files, live status, and intent\n own <path>... Mark existing tracked files as app-owned\n skip <path>... Mark missing tracked files as intentionally absent\n manage <path>... Return tracked files to BrightWeb management\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --help Show this help`;
8
+
9
+ function normalizeTrackedPath(relativePath) {
10
+ const normalized = path.normalize(String(relativePath)).replace(/^\.\//, "");
11
+ if (path.isAbsolute(String(relativePath)) || normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
12
+ throw new Error(`Scaffold path must be relative to the app: ${relativePath}`);
13
+ }
14
+ return normalized;
15
+ }
16
+
17
+ export async function scaffoldBrightwebApp(action, paths = [], argvOptions = {}, runtimeOptions = {}) {
18
+ if (argvOptions.help || !action) { output.write(`${HELP}\n`); return { help: true }; }
19
+ if (!Array.isArray(paths)) paths = [paths];
20
+ if (!["list", "own", "skip", "manage"].includes(action)) throw new Error(`Unknown bw scaffold action: ${action}\n\n${HELP}`);
21
+ if (action !== "list" && paths.length === 0) throw new Error(`bw scaffold ${action} requires at least one tracked <path>.`);
22
+ if (action === "list" && paths.length > 0) throw new Error("bw scaffold list does not accept file paths.");
23
+
24
+ const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
25
+ const manifest = await readAppManifest(targetDir);
26
+ const live = await scaffoldDrift(targetDir, manifest.scaffoldFiles);
27
+ if (action === "list") {
28
+ output.write("PATH\tMODULE\tSTATUS\tINTENT\n");
29
+ for (const entry of live.entries) output.write(`${entry.relativePath}\t${entry.module}\t${entry.status}\t${entry.intent}\n`);
30
+ return { action, entries: live.entries };
31
+ }
32
+
33
+ const normalizedPaths = Array.from(new Set(paths.map(normalizeTrackedPath)));
34
+ for (const relativePath of normalizedPaths) {
35
+ if (!manifest.scaffoldFiles?.[relativePath]) throw new Error(`${relativePath} is not a tracked scaffold file.`);
36
+ }
37
+ const liveByPath = new Map(live.entries.map((entry) => [entry.relativePath, entry]));
38
+ for (const relativePath of normalizedPaths) {
39
+ const status = liveByPath.get(relativePath)?.status;
40
+ if (action === "own" && status === "missing") throw new Error(`Cannot own missing scaffold file: ${relativePath}`);
41
+ if (action === "skip" && status !== "missing") throw new Error(`Cannot skip existing scaffold file: ${relativePath}`);
42
+ }
43
+
44
+ const workspaceRoot = runtimeOptions.workspaceRoot || argvOptions.workspaceRoot || await findWorkspaceRoot(targetDir);
45
+ const changes = [];
46
+ for (const relativePath of normalizedPaths) {
47
+ const record = manifest.scaffoldFiles[relativePath];
48
+ const previousIntent = record.intent || "managed";
49
+ const nextIntent = action === "manage" ? "managed" : action === "own" ? "owned" : "skipped";
50
+ const appPath = path.join(targetDir, relativePath);
51
+ const exists = await pathExists(appPath);
52
+ if (action === "manage") {
53
+ const located = await findTrackedTemplate({ relativePath, manifest, targetDir, workspaceRoot });
54
+ if (!located.templatePath) throw new Error(`Installed-package template unavailable for ${relativePath}; cannot manage it safely.`);
55
+ const templateHash = await hashFile(located.templatePath);
56
+ if (exists) {
57
+ record.hash = await hashFile(appPath);
58
+ record.status = record.hash === templateHash ? "current" : "drifted";
59
+ } else {
60
+ record.status = "missing";
61
+ }
62
+ } else {
63
+ record.status = liveByPath.get(relativePath).status;
64
+ }
65
+ if (nextIntent === "managed") delete record.intent;
66
+ else record.intent = nextIntent;
67
+ changes.push({ relativePath, previousIntent, intent: nextIntent, status: record.status });
68
+ }
69
+ await writeAppManifest(targetDir, manifest);
70
+ for (const change of changes) output.write(`${change.relativePath}: intent ${change.previousIntent} -> ${change.intent} (${change.status})\n`);
71
+ return { action, changes, manifest };
72
+ }
73
+
74
+ export { HELP as SCAFFOLD_HELP };
package/src/scaffold.mjs CHANGED
@@ -58,13 +58,21 @@ export async function scaffoldDrift(targetDir, scaffoldFiles = {}) {
58
58
  const current = [];
59
59
  const drifted = [];
60
60
  const missing = [];
61
+ const entries = [];
61
62
  for (const [relativePath, record] of Object.entries(scaffoldFiles)) {
62
63
  const appPath = path.join(targetDir, relativePath);
63
- if (!(await pathExists(appPath))) missing.push(relativePath);
64
- else if (await hashFile(appPath) === record.hash) current.push(relativePath);
64
+ const intent = record.intent || "managed";
65
+ let status = "missing";
66
+ if (await pathExists(appPath)) {
67
+ const matchesRecordedHash = await hashFile(appPath) === record.hash;
68
+ status = matchesRecordedHash && record.status !== "drifted" ? "current" : "drifted";
69
+ }
70
+ entries.push({ relativePath, module: record.module, status, intent });
71
+ if (status === "missing") missing.push(relativePath);
72
+ else if (status === "current") current.push(relativePath);
65
73
  else drifted.push(relativePath);
66
74
  }
67
- return { current, drifted, missing };
75
+ return { current, drifted, missing, entries };
68
76
  }
69
77
 
70
78
  export async function findTrackedTemplate({ relativePath, manifest, targetDir, workspaceRoot }) {
package/src/update.mjs CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  createDbInstallPlan,
16
16
  createNextConfig,
17
17
  createPackageJson,
18
+ createPlatformGlobalsCss,
18
19
  createPlatformModulesConfigFile,
19
20
  createShellConfig,
20
21
  detectPackageManager,
@@ -27,6 +28,7 @@ import {
27
28
 
28
29
  const MANAGED_PLATFORM_FILES = [
29
30
  "next.config.ts",
31
+ path.join("app", "globals.css"),
30
32
  path.join("config", "modules.ts"),
31
33
  path.join("config", "shell.ts"),
32
34
  path.join("docs", "ai", "app-context.json"),
@@ -490,6 +492,7 @@ export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptio
490
492
  if (template === "platform") {
491
493
  const canonicalConfigFiles = {
492
494
  "next.config.ts": createNextConfig({ template: "platform", selectedModules: installedModules }),
495
+ [path.join("app", "globals.css")]: await createPlatformGlobalsCss(installedModules),
493
496
  [path.join("config", "modules.ts")]: createPlatformModulesConfigFile(installedModules),
494
497
  [path.join("config", "shell.ts")]: createShellConfig(installedModules),
495
498
  [path.join("docs", "ai", "app-context.json")]: createAppContextFile({
package/src/upgrade.mjs CHANGED
@@ -18,13 +18,16 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
18
18
  const plan = await buildBrightwebAppUpdatePlan(updateOptions, runtimeOptions);
19
19
  const drifted = [];
20
20
  const missing = [];
21
+ const intentional = [];
21
22
  for (const [relativePath, record] of Object.entries(appManifest.scaffoldFiles)) {
23
+ if (["owned", "skipped"].includes(record.intent)) intentional.push(relativePath);
22
24
  const filePath = path.join(targetDir, relativePath);
23
25
  if (!(await pathExists(filePath))) { missing.push(relativePath); continue; }
24
26
  if (await hashFile(filePath) !== record.hash) drifted.push(relativePath);
25
27
  }
26
- const protectedPaths = new Set(drifted);
28
+ const protectedPaths = new Set([...drifted, ...intentional]);
27
29
  plan.fileWrites = plan.fileWrites.filter((entry) => entry.type !== "starter" || !protectedPaths.has(entry.relativePath));
30
+ plan.starterFilesToRefresh = plan.fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath);
28
31
  plan.starterFilesDrifted = Array.from(new Set([...plan.starterFilesDrifted, ...drifted]));
29
32
  plan.starterFilesMissing = Array.from(new Set([...plan.starterFilesMissing, ...missing]));
30
33
 
@@ -43,6 +46,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
43
46
  output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nMigrations to append: ${migrationPlan.writes.length}\n`);
44
47
  for (const relativePath of missing) output.write(`- missing: ${relativePath}\n`);
45
48
  for (const relativePath of drifted) output.write(`- drifted: ${relativePath}\n`);
49
+ for (const relativePath of intentional) output.write(`- intent-protected: ${relativePath}\n`);
46
50
  if (argvOptions.dryRun) return { dryRun: true, plan, migrationPlan, drifted, missing };
47
51
 
48
52
  for (const write of plan.fileWrites) {
@@ -58,6 +62,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
58
62
  for (const relativePath of plan.starterFilesToRefresh || []) {
59
63
  if (!protectedPaths.has(relativePath) && appManifest.scaffoldFiles[relativePath] && await pathExists(path.join(targetDir, relativePath))) {
60
64
  appManifest.scaffoldFiles[relativePath].hash = await hashFile(path.join(targetDir, relativePath));
65
+ appManifest.scaffoldFiles[relativePath].status = "current";
61
66
  }
62
67
  }
63
68
  await writeAppManifest(targetDir, appManifest);
@@ -1,31 +1,27 @@
1
1
  # AGENTS.md
2
2
 
3
- This generated project is a BrightWeb platform starter. Use this file as the local entrypoint for AI agents working inside the app.
3
+ This is a thin BrightWeb platform app. The app owns environment values and settings; reusable UI, domain behavior, and helpers belong in `@brightweblabs/*` packages.
4
4
 
5
5
  ## Start here
6
6
 
7
- - `README.md`: local setup commands and starter routes.
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
- - `components/`: local app components used by starter routes and future product surfaces.
12
- - `config/brand.ts`: client identity, naming, and contact defaults.
13
- - `app/globals.css`: global design tokens, theme mapping, and shared visual styling.
14
- - `config/modules.ts`: selected module set and runtime enablement.
15
- - `config/client.ts`: starter-facing derived state used by the home page and setup surfaces.
16
- - `.env.local`: runtime service values for local development.
7
+ - `README.md`: setup commands and mounted package routes.
8
+ - `docs/ai/app-context.json`: machine-readable module, path, and ownership summary.
9
+ - `config/brand.ts`: client identity and contact defaults.
10
+ - `config/modules.ts`: selected module set and route metadata.
11
+ - `config/shell.overrides.ts`: app-owned shell customizations.
12
+ - `app/theme.css`: app-owned theme token overrides.
13
+ - `.env.local`: runtime service values.
17
14
 
18
15
  ## Working rules
19
16
 
20
- - 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.
21
- - Keep identity/contact in `config/brand.ts`, and keep all color/theme tokens in `app/globals.css`.
22
- - Check `config/modules.ts` before assuming CRM, Projects, or Admin routes exist.
23
- - Prefer composing app-level routes and config before forking logic from `@brightweblabs/*` packages.
24
- - Keep edits local to this app unless the change is intentionally shared across multiple BrightWeb projects.
17
+ - Keep every `route.ts` as a direct package re-export; a `page.tsx` may instead import one package component and return only that mount.
18
+ - Do not add feature components, demos, hooks, data access, or helper libraries to this app.
19
+ - Put reusable behavior in the owning package, then mount its export here.
20
+ - Keep identity and theme changes in config and theme files.
21
+ - Check `config/modules.ts` before assuming an optional route exists. Projects currently has no default UI route.
25
22
 
26
23
  ## First validation pass
27
24
 
28
- 1. Run the local dev server from this project or workspace.
29
- 2. Open `/`, `/bootstrap`, `/preview/app-shell`, and `/playground/auth`.
30
- 3. If optional modules are enabled, open the matching `/playground/*` route for each one.
31
- 4. Confirm `.env.local` contains real service values before debugging runtime behavior.
25
+ 1. Fill `.env.local` with real service values.
26
+ 2. Run the local type check or build.
27
+ 3. Validate `/crm` and `/admin/users` only when their modules are enabled.