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