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