create-bw-app 0.10.1 → 0.12.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 +101 -0
- package/src/adopt.mjs +190 -0
- package/src/app-manifest.mjs +257 -0
- package/src/bw.mjs +55 -0
- package/src/constants.mjs +22 -10
- package/src/diff.mjs +85 -0
- package/src/doctor.mjs +113 -0
- package/src/generator.mjs +66 -8
- package/src/migrations.mjs +92 -0
- package/src/remove.mjs +100 -0
- package/src/scaffold-cmd.mjs +74 -0
- package/src/scaffold.mjs +90 -0
- package/src/update.mjs +51 -3
- package/src/upgrade.mjs +78 -0
- package/template/base/app/globals.css +1 -0
- package/template/base/config/bootstrap.ts +1 -1
- package/template/base/config/modules.ts +1 -1
- package/template/base/config/shell.overrides.ts +16 -0
- package/template/base/docs/ai/README.md +3 -2
- package/template/base/docs/ai/examples.md +2 -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/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 +0 -3
- package/template/supabase/modules/orgs/README.md +4 -0
- package/template/supabase/modules/orgs/migrations/20260316091500_orgs_v1.sql +216 -0
- package/template/modules/crm/app/playground/crm/page.tsx +0 -103
package/src/bw.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { addBrightwebModule } from "./add.mjs";
|
|
2
|
+
import { adoptBrightwebApp } from "./adopt.mjs";
|
|
3
|
+
import { diffBrightwebScaffold } from "./diff.mjs";
|
|
4
|
+
import { doctorBrightwebApp } from "./doctor.mjs";
|
|
5
|
+
import { removeBrightwebModule } from "./remove.mjs";
|
|
6
|
+
import { scaffoldBrightwebApp } from "./scaffold-cmd.mjs";
|
|
7
|
+
import { updateBrightwebApp } from "./update.mjs";
|
|
8
|
+
import { upgradeBrightwebApp } from "./upgrade.mjs";
|
|
9
|
+
|
|
10
|
+
const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey> Install a module and its requirements\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n scaffold <action> List or record per-file scaffold intent\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
|
|
11
|
+
|
|
12
|
+
function parseOptions(argv) {
|
|
13
|
+
const options = {};
|
|
14
|
+
const positionals = [];
|
|
15
|
+
const booleanFlags = new Set(["help", "dry-run", "strict", "report", "install", "refresh-starters", "allow-stale-fallback", "allow-uncursored", "force", "list", "yes"]);
|
|
16
|
+
const repeatableFlags = new Set(["cursor", "owned-surface", "own", "skip"]);
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const token = argv[index];
|
|
19
|
+
if (!token.startsWith("--")) { positionals.push(token); continue; }
|
|
20
|
+
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
21
|
+
const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
22
|
+
if (booleanFlags.has(rawKey)) options[key] = true;
|
|
23
|
+
else {
|
|
24
|
+
const value = inlineValue ?? argv[index + 1];
|
|
25
|
+
if (inlineValue == null) index += 1;
|
|
26
|
+
if (repeatableFlags.has(rawKey)) options[key] = [...(options[key] || []), value];
|
|
27
|
+
else options[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { options, positionals };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runBwCli(argv = process.argv.slice(2), runtimeOptions = {}) {
|
|
34
|
+
const command = argv[0];
|
|
35
|
+
if (!command || command === "--help" || command === "help") { process.stdout.write(`${HELP}\n`); return; }
|
|
36
|
+
const { options, positionals } = parseOptions(argv.slice(1));
|
|
37
|
+
try {
|
|
38
|
+
if (command === "add") await addBrightwebModule(positionals[0], options, runtimeOptions);
|
|
39
|
+
else if (command === "adopt") await adoptBrightwebApp(options, runtimeOptions);
|
|
40
|
+
else if (command === "diff") await diffBrightwebScaffold(positionals[0], options, runtimeOptions);
|
|
41
|
+
else if (command === "scaffold") await scaffoldBrightwebApp(positionals[0], positionals.slice(1), options, runtimeOptions);
|
|
42
|
+
else if (command === "remove") await removeBrightwebModule(positionals[0], options, runtimeOptions);
|
|
43
|
+
else if (command === "upgrade") await upgradeBrightwebApp(positionals[0], options, runtimeOptions);
|
|
44
|
+
else if (command === "update") await updateBrightwebApp(options, runtimeOptions);
|
|
45
|
+
else if (command === "doctor") {
|
|
46
|
+
const result = await doctorBrightwebApp(options, runtimeOptions);
|
|
47
|
+
if (!result.ok) process.exitCode = 1;
|
|
48
|
+
} else throw new Error(`Unknown command: ${command}\n\n${HELP}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`\nbw failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { HELP as BW_HELP };
|
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.3.
|
|
72
|
-
"@brightweblabs/module-admin": "^0.3.
|
|
73
|
-
"@brightweblabs/module-crm": "^0.
|
|
74
|
-
"@brightweblabs/module-
|
|
75
|
-
"@brightweblabs/
|
|
76
|
-
"
|
|
79
|
+
"@brightweblabs/app-shell": "^0.4.1",
|
|
80
|
+
"@brightweblabs/core-auth": "^0.3.4",
|
|
81
|
+
"@brightweblabs/infra": "^0.3.1",
|
|
82
|
+
"@brightweblabs/module-admin": "^0.3.5",
|
|
83
|
+
"@brightweblabs/module-crm": "^0.5.2",
|
|
84
|
+
"@brightweblabs/module-orgs": "^0.2.2",
|
|
85
|
+
"@brightweblabs/module-projects": "^0.4.3",
|
|
86
|
+
"@brightweblabs/theme": "^0.2.1",
|
|
87
|
+
"@brightweblabs/ui": "^1.0.1",
|
|
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",
|
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,113 @@
|
|
|
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
|
+
const scaffoldGroups = {
|
|
56
|
+
current: scaffold.entries.filter((entry) => entry.status === "current" && entry.intent !== "skipped"),
|
|
57
|
+
owned: scaffold.entries.filter((entry) => entry.intent === "owned" && entry.status === "drifted"),
|
|
58
|
+
skipped: scaffold.entries.filter((entry) => entry.intent === "skipped" && entry.status === "missing"),
|
|
59
|
+
undecidedDrift: scaffold.entries.filter((entry) => entry.intent === "managed" && entry.status === "drifted"),
|
|
60
|
+
undecidedMissing: scaffold.entries.filter((entry) => entry.intent === "managed" && entry.status === "missing"),
|
|
61
|
+
mismatched: scaffold.entries.filter((entry) => (entry.intent === "owned" && entry.status === "missing") || (entry.intent === "skipped" && entry.status !== "missing")),
|
|
62
|
+
};
|
|
63
|
+
if (scaffoldGroups.owned.length) add("INFO", "scaffold-owned", `App-owned scaffold files: ${scaffoldGroups.owned.map((entry) => entry.relativePath).join(", ")}.`);
|
|
64
|
+
if (scaffoldGroups.skipped.length) add("INFO", "scaffold-skipped", `Intentionally skipped scaffold files: ${scaffoldGroups.skipped.map((entry) => entry.relativePath).join(", ")}.`);
|
|
65
|
+
if (scaffoldGroups.undecidedDrift.length) add("WARN", "scaffold-undecided-drift", `Unacknowledged drift: ${scaffoldGroups.undecidedDrift.map((entry) => entry.relativePath).join(", ")} (use bw scaffold own or bw diff).`);
|
|
66
|
+
if (scaffoldGroups.undecidedMissing.length) add("WARN", "scaffold-undecided-missing", `Unacknowledged missing files: ${scaffoldGroups.undecidedMissing.map((entry) => entry.relativePath).join(", ")} (use bw scaffold skip after review).`);
|
|
67
|
+
if (scaffoldGroups.mismatched.length) add("FAIL", "scaffold-intent-mismatch", `Recorded scaffold intent no longer matches reality: ${scaffoldGroups.mismatched.map((entry) => `${entry.relativePath} (${entry.intent}, ${entry.status})`).join(", ")}.`);
|
|
68
|
+
const scaffoldStatus = scaffoldGroups.mismatched.length ? "FAIL" : scaffoldGroups.undecidedDrift.length || scaffoldGroups.undecidedMissing.length ? "WARN" : "PASS";
|
|
69
|
+
add(scaffoldStatus, "scaffold", `${scaffoldGroups.current.length} current, ${scaffoldGroups.owned.length} owned, ${scaffoldGroups.skipped.length} skipped, ${scaffoldGroups.undecidedDrift.length} undecided-drift, ${scaffoldGroups.undecidedMissing.length} undecided-missing, ${scaffoldGroups.mismatched.length} intent-mismatch.`);
|
|
70
|
+
add("INFO", "owned-surfaces", `Owned surfaces: ${(appManifest.ownedSurfaces || []).join(", ") || "none"}.`);
|
|
71
|
+
|
|
72
|
+
const envNames = new Set(Object.keys(process.env));
|
|
73
|
+
const envPath = path.join(targetDir, ".env.local");
|
|
74
|
+
if (await pathExists(envPath)) {
|
|
75
|
+
for (const line of (await fs.readFile(envPath, "utf8")).split(/\r?\n/)) {
|
|
76
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
77
|
+
if (match) envNames.add(match[1]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const missingEnv = [];
|
|
81
|
+
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}`);
|
|
82
|
+
add(missingEnv.length ? "FAIL" : "PASS", "env", missingEnv.length ? `Missing required names: ${missingEnv.join(", ")}` : "Required environment variable names are present.");
|
|
83
|
+
|
|
84
|
+
const migrationProblems = [];
|
|
85
|
+
const migrationKeys = appManifest.app.template === "platform"
|
|
86
|
+
? Array.from(new Set(["core", "admin", ...Object.keys(appManifest.modules || {})]))
|
|
87
|
+
: [];
|
|
88
|
+
for (const key of migrationKeys) {
|
|
89
|
+
const cursor = appManifest.migrationCursor?.[key];
|
|
90
|
+
const status = await cursorMigrationStatus({ targetDir, moduleKey: key, cursor, catalogEntry: catalog[key] });
|
|
91
|
+
if (status.shipsMigrations && cursor == null) {
|
|
92
|
+
if (appManifest.adoptionNotes?.allowUncursored) add("WARN", `migration-cursor-${key}`, `${key}: migration cursor is null; adoption explicitly allowed uncursored operation.`);
|
|
93
|
+
else migrationProblems.push(`${key}: migration cursor is null (run bw adopt --force --cursor ${key}=<migrationFilename>, or explicitly adopt with --allow-uncursored)`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (status.shipsMigrations && status.missing.length > 0) migrationProblems.push(`${key}: ${status.missing.join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
add(migrationProblems.length ? "FAIL" : "PASS", "migrations", migrationProblems.join("; ") || "Migration cursors and flattened files agree.");
|
|
99
|
+
add("WARN", "db-objects", "SKIP live database checks are not available yet.");
|
|
100
|
+
return finish(checks, argvOptions, appManifest, targetDir);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function finish(checks, options, appManifest, targetDir) {
|
|
104
|
+
for (const check of checks) output.write(`${check.status} ${check.id}: ${check.message}\n`);
|
|
105
|
+
const hasFailure = checks.some((check) => check.status === "FAIL") || (options.strict && checks.some((check) => check.status === "WARN"));
|
|
106
|
+
if (options.report && appManifest) {
|
|
107
|
+
appManifest.lastDoctor = { at: new Date().toISOString(), ok: !hasFailure };
|
|
108
|
+
await writeAppManifest(targetDir, appManifest);
|
|
109
|
+
}
|
|
110
|
+
return { ok: !hasFailure, checks };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
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/", "");
|
|
@@ -298,9 +304,10 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
|
|
|
298
304
|
|
|
299
305
|
export function createPlatformModulesConfigFile(selectedModules) {
|
|
300
306
|
const selected = new Set(selectedModules);
|
|
307
|
+
const orgsEnabled = selected.has("crm") || selected.has("projects");
|
|
301
308
|
|
|
302
309
|
return [
|
|
303
|
-
'export type StarterModuleKey = "core-auth" | "crm" | "projects" | "admin";',
|
|
310
|
+
'export type StarterModuleKey = "core-auth" | "orgs" | "crm" | "projects" | "admin";',
|
|
304
311
|
"",
|
|
305
312
|
"export type StarterModuleConfig = {",
|
|
306
313
|
" key: StarterModuleKey;",
|
|
@@ -309,7 +316,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
309
316
|
" enabled: boolean;",
|
|
310
317
|
" packageName: string;",
|
|
311
318
|
" playgroundHref?: string;",
|
|
312
|
-
' placement: "core" | "primary" | "admin";',
|
|
319
|
+
' placement: "core" | "primary" | "admin" | "hidden";',
|
|
313
320
|
"};",
|
|
314
321
|
"",
|
|
315
322
|
"export const starterModuleConfig: StarterModuleConfig[] = [",
|
|
@@ -323,12 +330,20 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
323
330
|
' placement: "core",',
|
|
324
331
|
" },",
|
|
325
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
|
+
" {",
|
|
326
341
|
' key: "crm",',
|
|
327
342
|
' label: "CRM",',
|
|
328
343
|
' description: "Contacts and CRM server/data layer, with marketing-adjacent operational data stored in Supabase.",',
|
|
329
344
|
` enabled: ${String(selected.has("crm"))},`,
|
|
330
345
|
' packageName: "@brightweblabs/module-crm",',
|
|
331
|
-
' playgroundHref: "/
|
|
346
|
+
' playgroundHref: "/crm",',
|
|
332
347
|
' placement: "primary",',
|
|
333
348
|
" },",
|
|
334
349
|
" {",
|
|
@@ -428,7 +443,7 @@ function getPlatformStarterRoutes(selectedModules) {
|
|
|
428
443
|
"/bootstrap",
|
|
429
444
|
"/preview/app-shell",
|
|
430
445
|
"/playground/auth",
|
|
431
|
-
...selectedModules.map((moduleKey) => `/playground/${moduleKey}`),
|
|
446
|
+
...selectedModules.map((moduleKey) => moduleKey === "crm" ? "/crm" : `/playground/${moduleKey}`),
|
|
432
447
|
];
|
|
433
448
|
}
|
|
434
449
|
|
|
@@ -639,6 +654,7 @@ export function createAppContextFile({
|
|
|
639
654
|
"config/client.ts",
|
|
640
655
|
"config/bootstrap.ts",
|
|
641
656
|
"config/shell.ts",
|
|
657
|
+
"config/shell.overrides.ts",
|
|
642
658
|
".env.local",
|
|
643
659
|
],
|
|
644
660
|
appRoutesRoot: "app",
|
|
@@ -659,6 +675,7 @@ export function createAppContextFile({
|
|
|
659
675
|
],
|
|
660
676
|
packageOwned: [
|
|
661
677
|
...CORE_PACKAGES,
|
|
678
|
+
...(selectedModules.includes("crm") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
|
|
662
679
|
...SELECTABLE_MODULES
|
|
663
680
|
.filter((moduleDefinition) => selectedModules.includes(moduleDefinition.key))
|
|
664
681
|
.map((moduleDefinition) => moduleDefinition.packageName),
|
|
@@ -720,6 +737,7 @@ export function createPackageJson({
|
|
|
720
737
|
"@brightweblabs/app-shell": internalDependencyVersion("@brightweblabs/app-shell"),
|
|
721
738
|
"@brightweblabs/core-auth": internalDependencyVersion("@brightweblabs/core-auth"),
|
|
722
739
|
"@brightweblabs/infra": internalDependencyVersion("@brightweblabs/infra"),
|
|
740
|
+
"@brightweblabs/theme": internalDependencyVersion("@brightweblabs/theme"),
|
|
723
741
|
"@brightweblabs/ui": internalDependencyVersion("@brightweblabs/ui"),
|
|
724
742
|
"lucide-react": versionMap["lucide-react"],
|
|
725
743
|
"next": versionMap.next,
|
|
@@ -732,6 +750,9 @@ export function createPackageJson({
|
|
|
732
750
|
dependencies[moduleDefinition.packageName] = internalDependencyVersion(moduleDefinition.packageName);
|
|
733
751
|
}
|
|
734
752
|
}
|
|
753
|
+
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
754
|
+
dependencies[ORGS_PACKAGE_NAME] = internalDependencyVersion(ORGS_PACKAGE_NAME);
|
|
755
|
+
}
|
|
735
756
|
|
|
736
757
|
return {
|
|
737
758
|
name: slug,
|
|
@@ -768,6 +789,9 @@ export function createNextConfig({ template, selectedModules }) {
|
|
|
768
789
|
}
|
|
769
790
|
|
|
770
791
|
const transpilePackages = [...CORE_PACKAGES];
|
|
792
|
+
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
793
|
+
transpilePackages.push(ORGS_PACKAGE_NAME);
|
|
794
|
+
}
|
|
771
795
|
|
|
772
796
|
for (const moduleDefinition of SELECTABLE_MODULES) {
|
|
773
797
|
if (selectedModules.includes(moduleDefinition.key)) {
|
|
@@ -793,6 +817,11 @@ export function createShellConfig(selectedModules) {
|
|
|
793
817
|
const importLines = [];
|
|
794
818
|
const registrationLines = [];
|
|
795
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
|
+
|
|
796
825
|
if (selectedModules.includes("admin")) {
|
|
797
826
|
importLines.push('import { adminModuleRegistration } from "@brightweblabs/module-admin/registration";');
|
|
798
827
|
registrationLines.push(' if (enabled.has("admin")) registrations.push(adminModuleRegistration);');
|
|
@@ -809,8 +838,10 @@ export function createShellConfig(selectedModules) {
|
|
|
809
838
|
}
|
|
810
839
|
|
|
811
840
|
return [
|
|
841
|
+
"// MANAGED BY BRIGHTWEB — regenerated by create-bw-app update; put customizations in config/shell.overrides.ts",
|
|
812
842
|
'import { LayoutDashboard, Wrench } from "lucide-react";',
|
|
813
843
|
"import {",
|
|
844
|
+
" applyShellRegistrationOverrides,",
|
|
814
845
|
" buildClientAppShellRegistration,",
|
|
815
846
|
" resolveClientAppShellConfig,",
|
|
816
847
|
" type ClientAppShellRegistration,",
|
|
@@ -820,6 +851,7 @@ export function createShellConfig(selectedModules) {
|
|
|
820
851
|
...importLines,
|
|
821
852
|
'import { starterBrandConfig } from "./brand";',
|
|
822
853
|
'import { getEnabledStarterModules } from "./modules";',
|
|
854
|
+
'import { shellRegistrationOverrides } from "./shell.overrides";',
|
|
823
855
|
"",
|
|
824
856
|
"const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
|
|
825
857
|
' key: "dashboard",',
|
|
@@ -839,6 +871,10 @@ export function createShellConfig(selectedModules) {
|
|
|
839
871
|
"",
|
|
840
872
|
"export function getStarterShellConfig() {",
|
|
841
873
|
" const enabledModules = getEnabledStarterModules();",
|
|
874
|
+
" const modules = applyShellRegistrationOverrides(",
|
|
875
|
+
" getStarterModuleRegistrations(),",
|
|
876
|
+
" shellRegistrationOverrides,",
|
|
877
|
+
" );",
|
|
842
878
|
" const shellRegistration: ClientAppShellRegistration<ShellContextualAction> = {",
|
|
843
879
|
" brand: {",
|
|
844
880
|
' href: "/",',
|
|
@@ -866,7 +902,7 @@ export function createShellConfig(selectedModules) {
|
|
|
866
902
|
" icon: Wrench,",
|
|
867
903
|
' collapsedHref: enabledModules.find((moduleConfig) => moduleConfig.playgroundHref)?.playgroundHref || "/",',
|
|
868
904
|
" },",
|
|
869
|
-
" modules
|
|
905
|
+
" modules,",
|
|
870
906
|
" };",
|
|
871
907
|
"",
|
|
872
908
|
" const builtRegistration = buildClientAppShellRegistration(shellRegistration);",
|
|
@@ -1255,6 +1291,7 @@ async function scaffoldPlatformProject({
|
|
|
1255
1291
|
|
|
1256
1292
|
if (workspaceMode) {
|
|
1257
1293
|
await writeClientStack(workspaceRoot, answers.slug, dbInstallPlan, { workspaceMode: true });
|
|
1294
|
+
await writeSupabaseCliMigrations({ targetDir, dbInstallPlan });
|
|
1258
1295
|
} else {
|
|
1259
1296
|
await writeBundledSupabaseBaseline({
|
|
1260
1297
|
targetDir,
|
|
@@ -1263,6 +1300,17 @@ async function scaffoldPlatformProject({
|
|
|
1263
1300
|
registry: dbRegistry,
|
|
1264
1301
|
});
|
|
1265
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
|
+
}));
|
|
1266
1314
|
}
|
|
1267
1315
|
|
|
1268
1316
|
async function scaffoldSiteProject({
|
|
@@ -1314,6 +1362,16 @@ async function scaffoldSiteProject({
|
|
|
1314
1362
|
packageManager,
|
|
1315
1363
|
}),
|
|
1316
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
|
+
}));
|
|
1317
1375
|
}
|
|
1318
1376
|
|
|
1319
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
|
+
}
|