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