create-bw-app 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/bin/bw.mjs +8 -0
- package/package.json +5 -2
- package/src/add.mjs +100 -0
- package/src/adopt.mjs +176 -0
- package/src/app-manifest.mjs +250 -0
- package/src/bw.mjs +53 -0
- package/src/constants.mjs +22 -11
- package/src/diff.mjs +85 -0
- package/src/doctor.mjs +99 -0
- package/src/generator.mjs +68 -12
- package/src/migrations.mjs +92 -0
- package/src/remove.mjs +100 -0
- package/src/scaffold.mjs +82 -0
- package/src/update.mjs +51 -3
- package/src/upgrade.mjs +73 -0
- package/template/base/AGENTS.md +2 -0
- package/template/base/app/globals.css +30 -14
- package/template/base/app/page.tsx +1 -0
- package/template/base/config/bootstrap.ts +1 -1
- package/template/base/config/brand.ts +0 -2
- package/template/base/config/modules.ts +2 -2
- package/template/base/config/shell.overrides.ts +16 -0
- package/template/base/docs/ai/README.md +6 -3
- package/template/base/docs/ai/examples.md +4 -2
- package/template/base/public/brand/logo-dark.svg +2 -2
- package/template/base/public/brand/logo-light.svg +2 -2
- package/template/base/public/brand/logo-mark.svg +2 -2
- package/template/module-manifests/admin/brightweb.module.json +6 -0
- package/template/module-manifests/crm/brightweb.module.json +7 -0
- package/template/module-manifests/orgs/brightweb.module.json +6 -0
- package/template/module-manifests/projects/brightweb.module.json +6 -0
- package/template/modules/crm/app/api/crm/contacts/route.ts +10 -0
- package/template/modules/crm/app/api/crm/timeline/route.ts +8 -0
- package/template/modules/crm/app/crm/layout.tsx +5 -0
- package/template/modules/crm/app/crm/page.tsx +5 -0
- package/template/supabase/module-registry.json +9 -3
- package/template/supabase/modules/crm/README.md +2 -0
- package/template/supabase/modules/crm/migrations/20260316092000_crm_v1.sql +3 -253
- package/template/supabase/modules/crm/migrations/20260316092010_crm_org_integration.sql +66 -0
- package/template/supabase/modules/crm/migrations/20260421201523_portal_read_indexes.sql +19 -0
- package/template/supabase/modules/orgs/README.md +4 -0
- package/template/supabase/modules/orgs/migrations/20260316091500_orgs_v1.sql +216 -0
- package/template/supabase/modules/projects/migrations/20260421201528_portal_read_indexes.sql +6 -0
- package/template/modules/crm/app/playground/crm/page.tsx +0 -103
package/src/constants.mjs
CHANGED
|
@@ -39,11 +39,15 @@ export const CORE_PACKAGES = [
|
|
|
39
39
|
"@brightweblabs/app-shell",
|
|
40
40
|
"@brightweblabs/core-auth",
|
|
41
41
|
"@brightweblabs/infra",
|
|
42
|
+
"@brightweblabs/theme",
|
|
42
43
|
"@brightweblabs/ui",
|
|
43
44
|
];
|
|
44
45
|
|
|
46
|
+
export const ORGS_PACKAGE_NAME = "@brightweblabs/module-orgs";
|
|
47
|
+
|
|
45
48
|
export const BRIGHTWEB_PACKAGE_NAMES = [
|
|
46
49
|
...CORE_PACKAGES,
|
|
50
|
+
ORGS_PACKAGE_NAME,
|
|
47
51
|
...SELECTABLE_MODULES.map((moduleDefinition) => moduleDefinition.packageName),
|
|
48
52
|
];
|
|
49
53
|
|
|
@@ -54,26 +58,34 @@ export const MODULE_STARTER_FILES = {
|
|
|
54
58
|
"app/playground/admin/page.tsx",
|
|
55
59
|
],
|
|
56
60
|
crm: [
|
|
61
|
+
"app/crm/layout.tsx",
|
|
62
|
+
"app/crm/page.tsx",
|
|
57
63
|
"app/api/crm/contacts/route.ts",
|
|
58
64
|
"app/api/crm/organizations/route.ts",
|
|
59
65
|
"app/api/crm/owners/route.ts",
|
|
60
66
|
"app/api/crm/stats/route.ts",
|
|
61
|
-
"app/
|
|
67
|
+
"app/api/crm/timeline/route.ts",
|
|
62
68
|
],
|
|
63
69
|
projects: [
|
|
64
70
|
"app/playground/projects/page.tsx",
|
|
65
71
|
],
|
|
66
72
|
};
|
|
67
73
|
|
|
74
|
+
export const PLATFORM_STARTER_FILES = [
|
|
75
|
+
"config/shell.overrides.ts",
|
|
76
|
+
];
|
|
77
|
+
|
|
68
78
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
69
|
-
"@brightweblabs/app-shell": "^0.
|
|
70
|
-
"@brightweblabs/core-auth": "^0.3.
|
|
71
|
-
"@brightweblabs/infra": "^0.
|
|
72
|
-
"@brightweblabs/module-admin": "^0.3.
|
|
73
|
-
"@brightweblabs/module-crm": "^0.
|
|
74
|
-
"@brightweblabs/module-
|
|
75
|
-
"@brightweblabs/
|
|
76
|
-
"
|
|
79
|
+
"@brightweblabs/app-shell": "^0.4.0",
|
|
80
|
+
"@brightweblabs/core-auth": "^0.3.4",
|
|
81
|
+
"@brightweblabs/infra": "^0.3.1",
|
|
82
|
+
"@brightweblabs/module-admin": "^0.3.4",
|
|
83
|
+
"@brightweblabs/module-crm": "^0.5.0",
|
|
84
|
+
"@brightweblabs/module-orgs": "^0.2.0",
|
|
85
|
+
"@brightweblabs/module-projects": "^0.4.2",
|
|
86
|
+
"@brightweblabs/theme": "^0.2.0",
|
|
87
|
+
"@brightweblabs/ui": "^1.0.0",
|
|
88
|
+
"lucide-react": "^1.8.0",
|
|
77
89
|
"next": "16.1.6",
|
|
78
90
|
"react": "19.2.3",
|
|
79
91
|
"react-dom": "19.2.3",
|
|
@@ -82,7 +94,7 @@ export const APP_DEPENDENCY_DEFAULTS = {
|
|
|
82
94
|
export const SITE_DEPENDENCY_DEFAULTS = {
|
|
83
95
|
"class-variance-authority": "^0.7.1",
|
|
84
96
|
"clsx": "^2.1.1",
|
|
85
|
-
"lucide-react": "^
|
|
97
|
+
"lucide-react": "^1.8.0",
|
|
86
98
|
"next": "16.1.6",
|
|
87
99
|
"react": "19.2.3",
|
|
88
100
|
"react-dom": "19.2.3",
|
|
@@ -110,7 +122,6 @@ export const DEFAULTS = {
|
|
|
110
122
|
tagline: "A configurable Brightweb starter app for new client instances.",
|
|
111
123
|
contactEmail: "hello@example.com",
|
|
112
124
|
supportEmail: "support@example.com",
|
|
113
|
-
primaryHex: "#1f7a45",
|
|
114
125
|
};
|
|
115
126
|
|
|
116
127
|
export const HELP_TEXT = `
|
package/src/diff.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stdout as output } from "node:process";
|
|
4
|
+
import { findWorkspaceRoot, readAppManifest } from "./app-manifest.mjs";
|
|
5
|
+
import { pathExists } from "./generator.mjs";
|
|
6
|
+
import { findTrackedTemplate, scaffoldDrift } from "./scaffold.mjs";
|
|
7
|
+
|
|
8
|
+
const HELP = `Usage: bw diff <relpath> [options]\n bw diff --list [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --list Print tracked scaffold drift status\n --help Show this help`;
|
|
9
|
+
|
|
10
|
+
function splitLines(content) {
|
|
11
|
+
const lines = content.split("\n");
|
|
12
|
+
if (lines.at(-1) === "") lines.pop();
|
|
13
|
+
return lines;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function unifiedLineDiff(beforeContent, afterContent, beforeName, afterName) {
|
|
17
|
+
const before = splitLines(beforeContent);
|
|
18
|
+
const after = splitLines(afterContent);
|
|
19
|
+
const table = Array.from({ length: before.length + 1 }, () => new Uint32Array(after.length + 1));
|
|
20
|
+
for (let left = before.length - 1; left >= 0; left -= 1) {
|
|
21
|
+
for (let right = after.length - 1; right >= 0; right -= 1) {
|
|
22
|
+
table[left][right] = before[left] === after[right]
|
|
23
|
+
? table[left + 1][right + 1] + 1
|
|
24
|
+
: Math.max(table[left + 1][right], table[left][right + 1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const body = [];
|
|
28
|
+
let left = 0;
|
|
29
|
+
let right = 0;
|
|
30
|
+
while (left < before.length || right < after.length) {
|
|
31
|
+
if (left < before.length && right < after.length && before[left] === after[right]) {
|
|
32
|
+
body.push(` ${before[left]}`); left += 1; right += 1;
|
|
33
|
+
} else if (right < after.length && (left === before.length || table[left][right + 1] >= table[left + 1][right])) {
|
|
34
|
+
body.push(`+${after[right]}`); right += 1;
|
|
35
|
+
} else {
|
|
36
|
+
body.push(`-${before[left]}`); left += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [
|
|
40
|
+
`--- a/${beforeName}`,
|
|
41
|
+
`+++ b/${afterName}`,
|
|
42
|
+
`@@ -1,${before.length} +1,${after.length} @@`,
|
|
43
|
+
...body,
|
|
44
|
+
"",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function diffBrightwebScaffold(relativePath, argvOptions = {}, runtimeOptions = {}) {
|
|
49
|
+
if (argvOptions.help) { output.write(`${HELP}\n`); return { help: true }; }
|
|
50
|
+
const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
|
|
51
|
+
const manifest = await readAppManifest(targetDir);
|
|
52
|
+
if (argvOptions.list) {
|
|
53
|
+
const drift = await scaffoldDrift(targetDir, manifest.scaffoldFiles);
|
|
54
|
+
output.write("STATUS\tSCAFFOLD FILE\n");
|
|
55
|
+
for (const status of ["current", "drifted", "missing"]) {
|
|
56
|
+
for (const file of drift[status]) output.write(`${status}\t${file}\n`);
|
|
57
|
+
}
|
|
58
|
+
return { list: true, drift };
|
|
59
|
+
}
|
|
60
|
+
if (!relativePath) throw new Error("bw diff requires a tracked scaffold <relpath>, or pass --list.");
|
|
61
|
+
const normalized = path.normalize(relativePath).replace(/^\.\//, "");
|
|
62
|
+
if (path.isAbsolute(relativePath) || normalized.startsWith(`..${path.sep}`)) throw new Error(`Scaffold path must be relative to the app: ${relativePath}`);
|
|
63
|
+
const workspaceRoot = runtimeOptions.workspaceRoot || argvOptions.workspaceRoot || await findWorkspaceRoot(targetDir);
|
|
64
|
+
const located = await findTrackedTemplate({ relativePath: normalized, manifest, targetDir, workspaceRoot });
|
|
65
|
+
if (!located.record) throw new Error(`${normalized} is not a tracked scaffold file.`);
|
|
66
|
+
if (!located.templatePath) {
|
|
67
|
+
const warning = `WARN Installed-package template unavailable for ${normalized}; diff is unsupported.`;
|
|
68
|
+
output.write(`${warning}\n`);
|
|
69
|
+
return { supported: false, warning };
|
|
70
|
+
}
|
|
71
|
+
const appPath = path.join(targetDir, normalized);
|
|
72
|
+
const [templateContent, appContent] = await Promise.all([
|
|
73
|
+
fs.readFile(located.templatePath, "utf8"),
|
|
74
|
+
pathExists(appPath) ? fs.readFile(appPath, "utf8") : Promise.resolve(""),
|
|
75
|
+
]);
|
|
76
|
+
if (templateContent === appContent) {
|
|
77
|
+
output.write(`${normalized}: identical\n`);
|
|
78
|
+
return { supported: true, identical: true, diff: "" };
|
|
79
|
+
}
|
|
80
|
+
const diff = unifiedLineDiff(templateContent, appContent, `template/${normalized}`, normalized);
|
|
81
|
+
output.write(diff);
|
|
82
|
+
return { supported: true, identical: false, diff };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { HELP as DIFF_HELP };
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stdout as output } from "node:process";
|
|
4
|
+
import { cursorMigrationStatus } from "./migrations.mjs";
|
|
5
|
+
import { findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, readConfiguredModuleFlags, satisfiesVersion, validateAppManifest, writeAppManifest } from "./app-manifest.mjs";
|
|
6
|
+
import { pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
7
|
+
import { scaffoldDrift } from "./scaffold.mjs";
|
|
8
|
+
|
|
9
|
+
const HELP = `Usage: bw doctor [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --strict Treat warnings as failures\n --report Stamp lastDoctor in the app manifest\n --help Show this help`;
|
|
10
|
+
|
|
11
|
+
export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
12
|
+
if (argvOptions.help) { output.write(`${HELP}\n`); return { help: true, ok: true }; }
|
|
13
|
+
const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
|
|
14
|
+
const checks = [];
|
|
15
|
+
const add = (status, id, message) => checks.push({ status, id, message });
|
|
16
|
+
let appManifest;
|
|
17
|
+
try { appManifest = await readAppManifest(targetDir); } catch (error) {
|
|
18
|
+
add("FAIL", "manifest", error instanceof Error ? error.message : String(error));
|
|
19
|
+
return finish(checks, argvOptions, null, targetDir);
|
|
20
|
+
}
|
|
21
|
+
const validationErrors = validateAppManifest(appManifest);
|
|
22
|
+
if (validationErrors.length > 0) add("FAIL", "manifest", validationErrors.join("; "));
|
|
23
|
+
else add("PASS", "manifest", "App manifest is valid.");
|
|
24
|
+
|
|
25
|
+
const packageJson = await readJsonIfPresent(path.join(targetDir, "package.json"));
|
|
26
|
+
const dependencyMap = { ...(packageJson?.dependencies || {}), ...(packageJson?.devDependencies || {}) };
|
|
27
|
+
const packageProblems = [];
|
|
28
|
+
for (const [key, entry] of Object.entries(appManifest.modules || {})) {
|
|
29
|
+
const packageName = MODULE_PACKAGES[key];
|
|
30
|
+
if (!packageName || !dependencyMap[packageName]) packageProblems.push(`${key}: ${packageName || "unknown package"} is missing`);
|
|
31
|
+
else if (!satisfiesVersion(entry.version, dependencyMap[packageName])) packageProblems.push(`${key}@${entry.version} does not satisfy package.json ${dependencyMap[packageName]}`);
|
|
32
|
+
}
|
|
33
|
+
for (const [key, packageName] of Object.entries(MODULE_PACKAGES)) {
|
|
34
|
+
if (dependencyMap[packageName] && !appManifest.modules[key]) packageProblems.push(`${packageName} is installed but absent from manifest.modules`);
|
|
35
|
+
}
|
|
36
|
+
add(packageProblems.length ? "FAIL" : "PASS", "packages", packageProblems.join("; ") || "Installed module packages agree with the manifest.");
|
|
37
|
+
|
|
38
|
+
const flags = await readConfiguredModuleFlags(targetDir);
|
|
39
|
+
const exposureProblems = Object.entries(appManifest.modules || {}).filter(([key, entry]) => typeof flags[key] === "boolean" && flags[key] !== entry.exposed).map(([key, entry]) => `${key}: manifest exposed=${entry.exposed}, config enabled=${String(flags[key])}`);
|
|
40
|
+
add(exposureProblems.length ? "FAIL" : "PASS", "exposure", exposureProblems.join("; ") || "Module exposure flags agree.");
|
|
41
|
+
|
|
42
|
+
const workspaceRoot = runtimeOptions.workspaceRoot || argvOptions.workspaceRoot || await findWorkspaceRoot(targetDir);
|
|
43
|
+
const catalog = await loadModuleCatalog({ targetDir, workspaceRoot });
|
|
44
|
+
const available = { core: catalog.core.version, admin: catalog.admin.version, ...Object.fromEntries(Object.entries(appManifest.modules || {}).map(([key, entry]) => [key, entry.version])) };
|
|
45
|
+
const topologyProblems = [];
|
|
46
|
+
for (const key of Object.keys(appManifest.modules || {})) {
|
|
47
|
+
for (const [requiredKey, range] of Object.entries(catalog[key]?.requires || {})) {
|
|
48
|
+
if (!available[requiredKey]) topologyProblems.push(`${key} requires missing ${requiredKey}@${range}`);
|
|
49
|
+
else if (!satisfiesVersion(available[requiredKey], range)) topologyProblems.push(`${key} requires ${requiredKey}@${range}, found ${available[requiredKey]}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
add(topologyProblems.length ? "FAIL" : "PASS", "topology", topologyProblems.join("; ") || "Module requirements are satisfied.");
|
|
53
|
+
|
|
54
|
+
const scaffold = await scaffoldDrift(targetDir, appManifest.scaffoldFiles);
|
|
55
|
+
add(scaffold.drifted.length || scaffold.missing.length ? "FAIL" : "PASS", "scaffold", `${scaffold.current.length} current, ${scaffold.drifted.length} drifted, ${scaffold.missing.length} missing; drifted scaffold files: ${scaffold.drifted.length} (see bw diff --list)${scaffold.missing.length ? `; missing: ${scaffold.missing.join(", ")}` : ""}`);
|
|
56
|
+
add("INFO", "owned-surfaces", `Owned surfaces: ${(appManifest.ownedSurfaces || []).join(", ") || "none"}.`);
|
|
57
|
+
|
|
58
|
+
const envNames = new Set(Object.keys(process.env));
|
|
59
|
+
const envPath = path.join(targetDir, ".env.local");
|
|
60
|
+
if (await pathExists(envPath)) {
|
|
61
|
+
for (const line of (await fs.readFile(envPath, "utf8")).split(/\r?\n/)) {
|
|
62
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
63
|
+
if (match) envNames.add(match[1]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const missingEnv = [];
|
|
67
|
+
for (const key of Object.keys(appManifest.modules || {})) for (const entry of catalog[key]?.manifest?.env || []) if (entry.required && !envNames.has(entry.name)) missingEnv.push(`${key}:${entry.name}`);
|
|
68
|
+
add(missingEnv.length ? "FAIL" : "PASS", "env", missingEnv.length ? `Missing required names: ${missingEnv.join(", ")}` : "Required environment variable names are present.");
|
|
69
|
+
|
|
70
|
+
const migrationProblems = [];
|
|
71
|
+
const migrationKeys = appManifest.app.template === "platform"
|
|
72
|
+
? Array.from(new Set(["core", "admin", ...Object.keys(appManifest.modules || {})]))
|
|
73
|
+
: [];
|
|
74
|
+
for (const key of migrationKeys) {
|
|
75
|
+
const cursor = appManifest.migrationCursor?.[key];
|
|
76
|
+
const status = await cursorMigrationStatus({ targetDir, moduleKey: key, cursor, catalogEntry: catalog[key] });
|
|
77
|
+
if (status.shipsMigrations && cursor == null) {
|
|
78
|
+
if (appManifest.adoptionNotes?.allowUncursored) add("WARN", `migration-cursor-${key}`, `${key}: migration cursor is null; adoption explicitly allowed uncursored operation.`);
|
|
79
|
+
else migrationProblems.push(`${key}: migration cursor is null (run bw adopt --force --cursor ${key}=<migrationFilename>, or explicitly adopt with --allow-uncursored)`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (status.shipsMigrations && status.missing.length > 0) migrationProblems.push(`${key}: ${status.missing.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
add(migrationProblems.length ? "FAIL" : "PASS", "migrations", migrationProblems.join("; ") || "Migration cursors and flattened files agree.");
|
|
85
|
+
add("WARN", "db-objects", "SKIP live database checks are not available yet.");
|
|
86
|
+
return finish(checks, argvOptions, appManifest, targetDir);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function finish(checks, options, appManifest, targetDir) {
|
|
90
|
+
for (const check of checks) output.write(`${check.status} ${check.id}: ${check.message}\n`);
|
|
91
|
+
const hasFailure = checks.some((check) => check.status === "FAIL") || (options.strict && checks.some((check) => check.status === "WARN"));
|
|
92
|
+
if (options.report && appManifest) {
|
|
93
|
+
appManifest.lastDoctor = { at: new Date().toISOString(), ok: !hasFailure };
|
|
94
|
+
await writeAppManifest(targetDir, appManifest);
|
|
95
|
+
}
|
|
96
|
+
return { ok: !hasFailure, checks };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { HELP as DOCTOR_HELP };
|
package/src/generator.mjs
CHANGED
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
CLI_DISPLAY_NAME,
|
|
11
11
|
CORE_PACKAGES,
|
|
12
12
|
DEFAULTS,
|
|
13
|
+
ORGS_PACKAGE_NAME,
|
|
13
14
|
SELECTABLE_MODULES,
|
|
14
15
|
SITE_DEPENDENCY_DEFAULTS,
|
|
15
16
|
SITE_DEV_DEPENDENCY_DEFAULTS,
|
|
16
17
|
TEMPLATE_OPTIONS,
|
|
17
18
|
} from "./constants.mjs";
|
|
19
|
+
import { createInitialAppManifest, writeAppManifest } from "./app-manifest.mjs";
|
|
18
20
|
|
|
19
21
|
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
22
|
export const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
@@ -24,8 +26,9 @@ const DEFAULT_DB_MODULE_REGISTRY = {
|
|
|
24
26
|
modules: {
|
|
25
27
|
core: { label: "Core", dependsOn: [] },
|
|
26
28
|
admin: { label: "Admin", dependsOn: ["core"] },
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
orgs: { label: "Organizations", dependsOn: ["core", "admin"] },
|
|
30
|
+
crm: { label: "CRM", dependsOn: ["core", "admin", "orgs"] },
|
|
31
|
+
projects: { label: "Projects", dependsOn: ["core", "admin", "orgs"] },
|
|
29
32
|
},
|
|
30
33
|
};
|
|
31
34
|
|
|
@@ -131,7 +134,8 @@ export async function getDbModuleRegistry(workspaceRoot) {
|
|
|
131
134
|
return DEFAULT_DB_MODULE_REGISTRY;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
// Duplicated by design: scripts/_db-modules.mjs — keep in sync.
|
|
138
|
+
export function resolveModuleOrder(registry, enabledModules) {
|
|
135
139
|
const resolved = [];
|
|
136
140
|
const visited = new Set();
|
|
137
141
|
const visiting = new Set();
|
|
@@ -247,7 +251,9 @@ export async function getVersionMap(workspaceRoot) {
|
|
|
247
251
|
"@brightweblabs/infra",
|
|
248
252
|
"@brightweblabs/module-admin",
|
|
249
253
|
"@brightweblabs/module-crm",
|
|
254
|
+
"@brightweblabs/module-orgs",
|
|
250
255
|
"@brightweblabs/module-projects",
|
|
256
|
+
"@brightweblabs/theme",
|
|
251
257
|
"@brightweblabs/ui",
|
|
252
258
|
]) {
|
|
253
259
|
const folderName = packageName.replace("@brightweblabs/", "");
|
|
@@ -270,7 +276,6 @@ function createDerivedBrandValues(slug) {
|
|
|
270
276
|
tagline: DEFAULTS.tagline,
|
|
271
277
|
contactEmail: DEFAULTS.contactEmail,
|
|
272
278
|
supportEmail: DEFAULTS.supportEmail,
|
|
273
|
-
primaryHex: DEFAULTS.primaryHex,
|
|
274
279
|
};
|
|
275
280
|
}
|
|
276
281
|
|
|
@@ -283,7 +288,6 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
|
|
|
283
288
|
" tagline: string;",
|
|
284
289
|
" contactEmail: string;",
|
|
285
290
|
" supportEmail: string;",
|
|
286
|
-
" primaryHex: string;",
|
|
287
291
|
"};",
|
|
288
292
|
"",
|
|
289
293
|
"export const starterBrandConfig: StarterBrandConfig = {",
|
|
@@ -293,7 +297,6 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
|
|
|
293
297
|
` tagline: ${JSON.stringify(brandValues.tagline)},`,
|
|
294
298
|
` contactEmail: ${JSON.stringify(brandValues.contactEmail)},`,
|
|
295
299
|
` supportEmail: ${JSON.stringify(brandValues.supportEmail)},`,
|
|
296
|
-
` primaryHex: ${JSON.stringify(brandValues.primaryHex)},`,
|
|
297
300
|
"};",
|
|
298
301
|
"",
|
|
299
302
|
].join("\n");
|
|
@@ -301,9 +304,10 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
|
|
|
301
304
|
|
|
302
305
|
export function createPlatformModulesConfigFile(selectedModules) {
|
|
303
306
|
const selected = new Set(selectedModules);
|
|
307
|
+
const orgsEnabled = selected.has("crm") || selected.has("projects");
|
|
304
308
|
|
|
305
309
|
return [
|
|
306
|
-
'export type StarterModuleKey = "core-auth" | "crm" | "projects" | "admin";',
|
|
310
|
+
'export type StarterModuleKey = "core-auth" | "orgs" | "crm" | "projects" | "admin";',
|
|
307
311
|
"",
|
|
308
312
|
"export type StarterModuleConfig = {",
|
|
309
313
|
" key: StarterModuleKey;",
|
|
@@ -312,7 +316,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
312
316
|
" enabled: boolean;",
|
|
313
317
|
" packageName: string;",
|
|
314
318
|
" playgroundHref?: string;",
|
|
315
|
-
' placement: "core" | "primary" | "admin";',
|
|
319
|
+
' placement: "core" | "primary" | "admin" | "hidden";',
|
|
316
320
|
"};",
|
|
317
321
|
"",
|
|
318
322
|
"export const starterModuleConfig: StarterModuleConfig[] = [",
|
|
@@ -326,12 +330,20 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
326
330
|
' placement: "core",',
|
|
327
331
|
" },",
|
|
328
332
|
" {",
|
|
333
|
+
' key: "orgs",',
|
|
334
|
+
' label: "Organizations",',
|
|
335
|
+
' description: "Shared organizations, membership, and invitation foundation for CRM and Projects.",',
|
|
336
|
+
` enabled: ${String(orgsEnabled)},`,
|
|
337
|
+
' packageName: "@brightweblabs/module-orgs",',
|
|
338
|
+
' placement: "hidden",',
|
|
339
|
+
" },",
|
|
340
|
+
" {",
|
|
329
341
|
' key: "crm",',
|
|
330
342
|
' label: "CRM",',
|
|
331
|
-
' description: "Contacts
|
|
343
|
+
' description: "Contacts and CRM server/data layer, with marketing-adjacent operational data stored in Supabase.",',
|
|
332
344
|
` enabled: ${String(selected.has("crm"))},`,
|
|
333
345
|
' packageName: "@brightweblabs/module-crm",',
|
|
334
|
-
' playgroundHref: "/
|
|
346
|
+
' playgroundHref: "/crm",',
|
|
335
347
|
' placement: "primary",',
|
|
336
348
|
" },",
|
|
337
349
|
" {",
|
|
@@ -431,7 +443,7 @@ function getPlatformStarterRoutes(selectedModules) {
|
|
|
431
443
|
"/bootstrap",
|
|
432
444
|
"/preview/app-shell",
|
|
433
445
|
"/playground/auth",
|
|
434
|
-
...selectedModules.map((moduleKey) => `/playground/${moduleKey}`),
|
|
446
|
+
...selectedModules.map((moduleKey) => moduleKey === "crm" ? "/crm" : `/playground/${moduleKey}`),
|
|
435
447
|
];
|
|
436
448
|
}
|
|
437
449
|
|
|
@@ -636,11 +648,13 @@ export function createAppContextFile({
|
|
|
636
648
|
"AGENTS.md",
|
|
637
649
|
"docs/ai/README.md",
|
|
638
650
|
"README.md",
|
|
651
|
+
"app/globals.css",
|
|
639
652
|
"config/brand.ts",
|
|
640
653
|
"config/modules.ts",
|
|
641
654
|
"config/client.ts",
|
|
642
655
|
"config/bootstrap.ts",
|
|
643
656
|
"config/shell.ts",
|
|
657
|
+
"config/shell.overrides.ts",
|
|
644
658
|
".env.local",
|
|
645
659
|
],
|
|
646
660
|
appRoutesRoot: "app",
|
|
@@ -661,6 +675,7 @@ export function createAppContextFile({
|
|
|
661
675
|
],
|
|
662
676
|
packageOwned: [
|
|
663
677
|
...CORE_PACKAGES,
|
|
678
|
+
...(selectedModules.includes("crm") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
|
|
664
679
|
...SELECTABLE_MODULES
|
|
665
680
|
.filter((moduleDefinition) => selectedModules.includes(moduleDefinition.key))
|
|
666
681
|
.map((moduleDefinition) => moduleDefinition.packageName),
|
|
@@ -722,6 +737,7 @@ export function createPackageJson({
|
|
|
722
737
|
"@brightweblabs/app-shell": internalDependencyVersion("@brightweblabs/app-shell"),
|
|
723
738
|
"@brightweblabs/core-auth": internalDependencyVersion("@brightweblabs/core-auth"),
|
|
724
739
|
"@brightweblabs/infra": internalDependencyVersion("@brightweblabs/infra"),
|
|
740
|
+
"@brightweblabs/theme": internalDependencyVersion("@brightweblabs/theme"),
|
|
725
741
|
"@brightweblabs/ui": internalDependencyVersion("@brightweblabs/ui"),
|
|
726
742
|
"lucide-react": versionMap["lucide-react"],
|
|
727
743
|
"next": versionMap.next,
|
|
@@ -734,6 +750,9 @@ export function createPackageJson({
|
|
|
734
750
|
dependencies[moduleDefinition.packageName] = internalDependencyVersion(moduleDefinition.packageName);
|
|
735
751
|
}
|
|
736
752
|
}
|
|
753
|
+
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
754
|
+
dependencies[ORGS_PACKAGE_NAME] = internalDependencyVersion(ORGS_PACKAGE_NAME);
|
|
755
|
+
}
|
|
737
756
|
|
|
738
757
|
return {
|
|
739
758
|
name: slug,
|
|
@@ -770,6 +789,9 @@ export function createNextConfig({ template, selectedModules }) {
|
|
|
770
789
|
}
|
|
771
790
|
|
|
772
791
|
const transpilePackages = [...CORE_PACKAGES];
|
|
792
|
+
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
793
|
+
transpilePackages.push(ORGS_PACKAGE_NAME);
|
|
794
|
+
}
|
|
773
795
|
|
|
774
796
|
for (const moduleDefinition of SELECTABLE_MODULES) {
|
|
775
797
|
if (selectedModules.includes(moduleDefinition.key)) {
|
|
@@ -795,6 +817,11 @@ export function createShellConfig(selectedModules) {
|
|
|
795
817
|
const importLines = [];
|
|
796
818
|
const registrationLines = [];
|
|
797
819
|
|
|
820
|
+
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
821
|
+
importLines.push('import { orgsModuleRegistration } from "@brightweblabs/module-orgs/registration";');
|
|
822
|
+
registrationLines.push(' if (enabled.has("orgs")) registrations.push(orgsModuleRegistration);');
|
|
823
|
+
}
|
|
824
|
+
|
|
798
825
|
if (selectedModules.includes("admin")) {
|
|
799
826
|
importLines.push('import { adminModuleRegistration } from "@brightweblabs/module-admin/registration";');
|
|
800
827
|
registrationLines.push(' if (enabled.has("admin")) registrations.push(adminModuleRegistration);');
|
|
@@ -811,8 +838,10 @@ export function createShellConfig(selectedModules) {
|
|
|
811
838
|
}
|
|
812
839
|
|
|
813
840
|
return [
|
|
841
|
+
"// MANAGED BY BRIGHTWEB — regenerated by create-bw-app update; put customizations in config/shell.overrides.ts",
|
|
814
842
|
'import { LayoutDashboard, Wrench } from "lucide-react";',
|
|
815
843
|
"import {",
|
|
844
|
+
" applyShellRegistrationOverrides,",
|
|
816
845
|
" buildClientAppShellRegistration,",
|
|
817
846
|
" resolveClientAppShellConfig,",
|
|
818
847
|
" type ClientAppShellRegistration,",
|
|
@@ -822,6 +851,7 @@ export function createShellConfig(selectedModules) {
|
|
|
822
851
|
...importLines,
|
|
823
852
|
'import { starterBrandConfig } from "./brand";',
|
|
824
853
|
'import { getEnabledStarterModules } from "./modules";',
|
|
854
|
+
'import { shellRegistrationOverrides } from "./shell.overrides";',
|
|
825
855
|
"",
|
|
826
856
|
"const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
|
|
827
857
|
' key: "dashboard",',
|
|
@@ -841,6 +871,10 @@ export function createShellConfig(selectedModules) {
|
|
|
841
871
|
"",
|
|
842
872
|
"export function getStarterShellConfig() {",
|
|
843
873
|
" const enabledModules = getEnabledStarterModules();",
|
|
874
|
+
" const modules = applyShellRegistrationOverrides(",
|
|
875
|
+
" getStarterModuleRegistrations(),",
|
|
876
|
+
" shellRegistrationOverrides,",
|
|
877
|
+
" );",
|
|
844
878
|
" const shellRegistration: ClientAppShellRegistration<ShellContextualAction> = {",
|
|
845
879
|
" brand: {",
|
|
846
880
|
' href: "/",',
|
|
@@ -868,7 +902,7 @@ export function createShellConfig(selectedModules) {
|
|
|
868
902
|
" icon: Wrench,",
|
|
869
903
|
' collapsedHref: enabledModules.find((moduleConfig) => moduleConfig.playgroundHref)?.playgroundHref || "/",',
|
|
870
904
|
" },",
|
|
871
|
-
" modules
|
|
905
|
+
" modules,",
|
|
872
906
|
" };",
|
|
873
907
|
"",
|
|
874
908
|
" const builtRegistration = buildClientAppShellRegistration(shellRegistration);",
|
|
@@ -1257,6 +1291,7 @@ async function scaffoldPlatformProject({
|
|
|
1257
1291
|
|
|
1258
1292
|
if (workspaceMode) {
|
|
1259
1293
|
await writeClientStack(workspaceRoot, answers.slug, dbInstallPlan, { workspaceMode: true });
|
|
1294
|
+
await writeSupabaseCliMigrations({ targetDir, dbInstallPlan });
|
|
1260
1295
|
} else {
|
|
1261
1296
|
await writeBundledSupabaseBaseline({
|
|
1262
1297
|
targetDir,
|
|
@@ -1265,6 +1300,17 @@ async function scaffoldPlatformProject({
|
|
|
1265
1300
|
registry: dbRegistry,
|
|
1266
1301
|
});
|
|
1267
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
const cliPackage = await readJsonIfPresent(path.join(PACKAGE_ROOT, "package.json"));
|
|
1305
|
+
await writeAppManifest(targetDir, await createInitialAppManifest({
|
|
1306
|
+
targetDir,
|
|
1307
|
+
slug: answers.slug,
|
|
1308
|
+
template: "platform",
|
|
1309
|
+
selectedModules,
|
|
1310
|
+
versionMap,
|
|
1311
|
+
dbInstallPlan,
|
|
1312
|
+
cliVersion: cliPackage?.version || "0.0.0",
|
|
1313
|
+
}));
|
|
1268
1314
|
}
|
|
1269
1315
|
|
|
1270
1316
|
async function scaffoldSiteProject({
|
|
@@ -1316,6 +1362,16 @@ async function scaffoldSiteProject({
|
|
|
1316
1362
|
packageManager,
|
|
1317
1363
|
}),
|
|
1318
1364
|
);
|
|
1365
|
+
const cliPackage = await readJsonIfPresent(path.join(PACKAGE_ROOT, "package.json"));
|
|
1366
|
+
await writeAppManifest(targetDir, await createInitialAppManifest({
|
|
1367
|
+
targetDir,
|
|
1368
|
+
slug: answers.slug,
|
|
1369
|
+
template: "site",
|
|
1370
|
+
selectedModules: [],
|
|
1371
|
+
versionMap,
|
|
1372
|
+
dbInstallPlan: { resolvedOrder: [] },
|
|
1373
|
+
cliVersion: cliPackage?.version || "0.0.0",
|
|
1374
|
+
}));
|
|
1319
1375
|
}
|
|
1320
1376
|
|
|
1321
1377
|
function printCompletionMessage({ targetDir, workspaceMode, slug, packageManager, install }) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TEMPLATE_ROOT, pathExists } from "./generator.mjs";
|
|
4
|
+
|
|
5
|
+
export async function findAppMigrationsDirectory(targetDir) {
|
|
6
|
+
let current = path.resolve(targetDir);
|
|
7
|
+
while (true) {
|
|
8
|
+
const candidate = path.join(current, "supabase", "migrations");
|
|
9
|
+
if (await pathExists(candidate)) return candidate;
|
|
10
|
+
const parent = path.dirname(current);
|
|
11
|
+
if (parent === current) return path.join(path.resolve(targetDir), "supabase", "migrations");
|
|
12
|
+
current = parent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getModuleMigrations(moduleKey, catalogEntry = {}) {
|
|
17
|
+
const candidates = [];
|
|
18
|
+
const configuredPath = catalogEntry.manifest?.database?.migrations;
|
|
19
|
+
if (catalogEntry.packageRoot && configuredPath) candidates.push(path.resolve(catalogEntry.packageRoot, configuredPath));
|
|
20
|
+
if (catalogEntry.packageRoot) candidates.push(path.join(catalogEntry.packageRoot, "migrations"));
|
|
21
|
+
candidates.push(path.join(TEMPLATE_ROOT, "supabase", "modules", moduleKey, "migrations"));
|
|
22
|
+
for (const directory of candidates) {
|
|
23
|
+
if (!(await pathExists(directory))) continue;
|
|
24
|
+
const fileNames = (await fs.readdir(directory)).filter((fileName) => fileName.endsWith(".sql")).sort();
|
|
25
|
+
if (fileNames.length > 0) return fileNames.map((fileName) => ({ fileName, sourcePath: path.join(directory, fileName) }));
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function planMigrationAppends({ targetDir, moduleKeys, catalog, migrationCursor = {} }) {
|
|
31
|
+
const migrationsDir = await findAppMigrationsDirectory(targetDir);
|
|
32
|
+
const existing = (await pathExists(migrationsDir))
|
|
33
|
+
? (await fs.readdir(migrationsDir)).filter((fileName) => fileName.endsWith(".sql")).sort()
|
|
34
|
+
: [];
|
|
35
|
+
let sequence = existing.reduce((maximum, fileName) => {
|
|
36
|
+
const match = fileName.match(/^(\d+)_/);
|
|
37
|
+
return Math.max(maximum, Number(match?.[1] || 0));
|
|
38
|
+
}, 0);
|
|
39
|
+
const writes = [];
|
|
40
|
+
const nextCursor = { ...migrationCursor };
|
|
41
|
+
for (const moduleKey of moduleKeys) {
|
|
42
|
+
const migrations = await getModuleMigrations(moduleKey, catalog[moduleKey]);
|
|
43
|
+
const cursor = migrationCursor[moduleKey];
|
|
44
|
+
const pending = cursor ? migrations.filter((entry) => entry.fileName > cursor) : migrations;
|
|
45
|
+
for (const entry of pending) {
|
|
46
|
+
sequence += 1;
|
|
47
|
+
const targetFileName = `${String(sequence).padStart(4, "0")}_${moduleKey}__${entry.fileName}`;
|
|
48
|
+
const source = await fs.readFile(entry.sourcePath, "utf8");
|
|
49
|
+
const version = catalog[moduleKey]?.version || "unknown";
|
|
50
|
+
writes.push({
|
|
51
|
+
moduleKey,
|
|
52
|
+
originalFileName: entry.fileName,
|
|
53
|
+
targetFileName,
|
|
54
|
+
targetPath: path.join(migrationsDir, targetFileName),
|
|
55
|
+
content: `-- bw-module: ${moduleKey}@${version} ${entry.fileName}\n${source}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (migrations.length > 0) nextCursor[moduleKey] = migrations.at(-1).fileName;
|
|
59
|
+
}
|
|
60
|
+
return { writes, nextCursor };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function applyMigrationWrites(writes) {
|
|
64
|
+
for (const write of writes) {
|
|
65
|
+
await fs.mkdir(path.dirname(write.targetPath), { recursive: true });
|
|
66
|
+
await fs.writeFile(write.targetPath, write.content, "utf8");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function cursorMigrationStatus({ targetDir, moduleKey, cursor, catalogEntry }) {
|
|
71
|
+
const migrations = await getModuleMigrations(moduleKey, catalogEntry);
|
|
72
|
+
if (migrations.length === 0) return { shipsMigrations: false, missing: [] };
|
|
73
|
+
if (!cursor) return { shipsMigrations: true, missing: ["migration cursor"] };
|
|
74
|
+
const expected = migrations.filter((entry) => entry.fileName <= cursor);
|
|
75
|
+
const migrationsDir = await findAppMigrationsDirectory(targetDir);
|
|
76
|
+
const installed = [];
|
|
77
|
+
if (await pathExists(migrationsDir)) {
|
|
78
|
+
for (const fileName of await fs.readdir(migrationsDir)) {
|
|
79
|
+
if (!fileName.endsWith(".sql")) continue;
|
|
80
|
+
const content = await fs.readFile(path.join(migrationsDir, fileName), "utf8");
|
|
81
|
+
installed.push({ fileName, content });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
shipsMigrations: true,
|
|
86
|
+
missing: expected.filter((entry) => !installed.some(({ fileName, content }) => {
|
|
87
|
+
if (fileName === entry.fileName || fileName.endsWith(`_${moduleKey}__${entry.fileName}`)) return true;
|
|
88
|
+
const header = content.match(/^\s*--\s*bw-module:\s*([^@\s]+)@[^\s]+\s+([^\s]+)/im);
|
|
89
|
+
return header?.[1] === moduleKey && header?.[2] === entry.fileName;
|
|
90
|
+
})).map((entry) => entry.fileName),
|
|
91
|
+
};
|
|
92
|
+
}
|