create-bw-app 0.27.1 → 0.27.3
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 +10 -2
- package/package.json +3 -2
- package/src/add.mjs +68 -19
- package/src/adopt.mjs +32 -10
- package/src/app-manifest.mjs +11 -35
- package/src/constants.mjs +9 -8
- package/src/doctor.mjs +16 -4
- package/src/file-policy.mjs +40 -0
- package/src/generator.mjs +22 -51
- package/src/migrations.mjs +8 -10
- package/src/mutation-paths.mjs +28 -0
- package/src/removal-dependents.mjs +32 -0
- package/src/remove.mjs +30 -29
- package/src/scaffold-cmd.mjs +7 -7
- package/src/scaffold.mjs +43 -2
- package/src/setup.mjs +6 -16
- package/src/update.mjs +42 -50
- package/src/upgrade.mjs +13 -17
- package/template/supabase/modules/admin/migrations/20260906101616_atomic_admin_invitation_acceptance.sql +50 -0
- package/template/supabase/modules/admin/migrations/20260908120000_admin_profile_directory_visibility.sql +9 -0
- package/template/supabase/modules/core/migrations/20260906111323_restrict_profile_identity_sync.sql +4 -0
- package/template/supabase/modules/crm/migrations/20260906101616_atomic_invitation_contact_link.sql +31 -0
- package/template/supabase/modules/marketing/migrations/20260906120900_atomic_marketing_workflow_nodes.sql +97 -0
- package/template/supabase/modules/orgs/migrations/20260906101616_atomic_organization_invitation_acceptance.sql +61 -0
- package/template/supabase/modules/orgs/migrations/20260906113855_atomic_organization_member_assignment.sql +76 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ bw admin create --email owner@example.com
|
|
|
40
40
|
- `bw add <moduleKey>` resolves requirements, installs thin package mounts and module wiring, and appends migrations.
|
|
41
41
|
- `bw upgrade [moduleKey]` includes the existing managed update flow plus forward-only module migrations. For a staged database rollout, `bw upgrade <moduleKey> --through-migration <filename>` still updates packages and managed files but appends and records that module only through the named migration.
|
|
42
42
|
- Destructive migrations are held at the preceding safe boundary. After compatible code is deployed and affected data is backed up, apply a held migration with an explicit module-scoped `bw upgrade <moduleKey> --include-destructive-migrations`. A destructive `--through-migration` target also requires that opt-in.
|
|
43
|
-
- `bw doctor` checks package, config, scaffold, environment-name, migration, configured function-region, and deployed function-region consistency. Pass `--deployment-url` to inspect the deployed `x-vercel-id`; add `--report` to stamp the result in the app manifest.
|
|
43
|
+
- `bw doctor` checks package, config, scaffold, environment-name, migration, configured function-region, and deployed function-region consistency. App-owned scaffold files are preserved on upgrade; doctor warns when their recorded template baseline is outdated and their contents differ from the current template. Use `bw diff`, reconcile relevant changes, and verify behavior before recording the reviewed baseline with `bw scaffold manage <path>` followed by `bw scaffold own <path>` (neither replaces file contents). Live database objects, applied migrations, and authenticated permissions are explicitly **NOT VERIFIED** by doctor; a successful local result does not certify deployed behavior. Pass `--deployment-url` to inspect the deployed `x-vercel-id`; add `--report` to stamp the result in the app manifest.
|
|
44
44
|
- `bw admin create --email <email>` creates a passwordless Supabase Auth user, transactionally ensures its profile and `admin` assignment, then sends the Core Auth `/reset-password` flow. It always refuses when the project already has an admin (use the in-app admin role controls to add administrators) and never promotes an existing Auth user.
|
|
45
45
|
- All mutating commands support `--dry-run`.
|
|
46
46
|
|
|
@@ -62,7 +62,7 @@ Current updater behavior:
|
|
|
62
62
|
- fails the update if npm resolution fails unless you pass `--allow-stale-fallback`
|
|
63
63
|
- re-syncs managed BrightWeb config files such as `next.config.ts`, `config/modules.ts`, and `config/shell.ts`
|
|
64
64
|
- preserves app-owned shell customizations in the scaffolded `config/shell.overrides.ts`
|
|
65
|
-
- reports missing or drifted scaffold mounts and only rewrites them with `--refresh-starters`
|
|
65
|
+
- reports missing or drifted scaffold mounts and only rewrites them with `--refresh-starters`; tracked `owned` and `skipped` files remain protected, including managed config routes
|
|
66
66
|
- prints the follow-up install command unless `--install` is passed
|
|
67
67
|
- preserves unrelated third-party dependencies and app-owned product pages
|
|
68
68
|
|
|
@@ -139,3 +139,11 @@ on generated paths. Existing JSON and page files are never replaced, even when
|
|
|
139
139
|
empty or customized; reruns preserve them. These files are not managed templates
|
|
140
140
|
or refreshable starters, and `bw update --refresh-starters` preserves them and
|
|
141
141
|
shell overrides. New marketing apps do not receive this feature until opted in.
|
|
142
|
+
|
|
143
|
+
## File and compatibility safety
|
|
144
|
+
|
|
145
|
+
Lifecycle commands resolve the selected app root and reject symlinks or invalid file types beneath it before writing planned outputs. Root aliases are supported; concurrent filesystem edits are outside this preflight guarantee. App-owned brand, theme, shell override and social content seeds are preserved. The generated app context lists exact generated/scaffold paths, defaults unknown paths to app ownership, and shares its generated inventory with the CLI.
|
|
146
|
+
|
|
147
|
+
Upgrade refuses unknown migration cursors and uses standard semantic-version ranges. Module removal refuses surviving literal imports of the removed package before changing files; reconcile the app-owned dependents explicitly. This conservative scan is not a complete bundler analysis.
|
|
148
|
+
|
|
149
|
+
Forced re-adoption and module removal preserve exact-path `owned` and `skipped` decisions. Re-adding a module respects retained decisions; use `bw scaffold manage` after re-adding to reset a path deliberately. Every pending destructive migration requires opt-in, even after a previous destructive step was approved. Managed platform outputs share one renderer across scaffolding, add, remove, and update, with enforced ownership-inventory parity.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bw-app",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.27.
|
|
4
|
+
"version": "0.27.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-bw-app": "bin/create-bw-app.mjs",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@inquirer/prompts": "^7.10.1",
|
|
29
|
-
"@supabase/supabase-js": "^2.110.8"
|
|
29
|
+
"@supabase/supabase-js": "^2.110.8",
|
|
30
|
+
"semver": "^7.8.5"
|
|
30
31
|
}
|
|
31
32
|
}
|
package/src/add.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { assertMutationTargets } from "./mutation-paths.mjs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { stdout as output } from "node:process";
|
|
4
5
|
import { SELECTABLE_MODULES } from "./constants.mjs";
|
|
5
|
-
import { TEMPLATE_ROOT,
|
|
6
|
+
import { TEMPLATE_ROOT, createDbInstallPlan, createManagedPlatformFiles, getDbModuleRegistry, getVersionMap, pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
6
7
|
import { collectScaffoldFiles, findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, resolveModuleClosure, satisfiesVersion, writeAppManifest } from "./app-manifest.mjs";
|
|
8
|
+
import { assertNoUntrackedScaffoldWrites, scaffoldDrift } from "./scaffold.mjs";
|
|
7
9
|
import { applyMigrationWrites, planMigrationAppends } from "./migrations.mjs";
|
|
8
10
|
|
|
9
11
|
const HELP = `Usage: bw add <moduleKey> [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --dry-run Print the install plan without writing\n --help Show this help`;
|
|
@@ -13,7 +15,7 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
|
|
|
13
15
|
output.write(`${HELP}\n`);
|
|
14
16
|
return { help: true };
|
|
15
17
|
}
|
|
16
|
-
const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
|
|
18
|
+
const targetDir = await fs.realpath(path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd()));
|
|
17
19
|
const appManifest = await readAppManifest(targetDir);
|
|
18
20
|
if (appManifest.app.template !== "platform") throw new Error("bw add is only available for platform apps.");
|
|
19
21
|
const packageJsonPath = path.join(targetDir, "package.json");
|
|
@@ -63,15 +65,51 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
|
|
|
63
65
|
const definition = SELECTABLE_MODULES.find((entry) => entry.key === key);
|
|
64
66
|
if (definition && await pathExists(path.join(TEMPLATE_ROOT, "modules", definition.templateFolder))) overlays.push({ key, source: path.join(TEMPLATE_ROOT, "modules", definition.templateFolder) });
|
|
65
67
|
}
|
|
66
|
-
const managedWrites = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
const managedWrites = await createManagedPlatformFiles({ slug: appManifest.app.slug, selectedModules: installedModuleKeys, dbInstallPlan });
|
|
69
|
+
|
|
70
|
+
const live = await scaffoldDrift(targetDir, appManifest.scaffoldFiles);
|
|
71
|
+
const { protectedPaths } = live;
|
|
72
|
+
for (const relativePath of protectedPaths) delete managedWrites[relativePath];
|
|
73
|
+
|
|
74
|
+
if (newModules.length === 0) for (const relativePath of Object.keys(managedWrites)) delete managedWrites[relativePath];
|
|
75
|
+
|
|
76
|
+
await assertNoUntrackedScaffoldWrites({
|
|
77
|
+
targetDir,
|
|
78
|
+
scaffoldFiles: appManifest.scaffoldFiles,
|
|
79
|
+
moduleKeys: installedModuleKeys,
|
|
80
|
+
relativePaths: Object.keys(managedWrites),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const overlayFiles = [];
|
|
84
|
+
// Refuse unknown overlay collisions before package, configuration or migration writes.
|
|
85
|
+
for (const overlay of overlays) {
|
|
86
|
+
const pending = [overlay.source];
|
|
87
|
+
while (pending.length) {
|
|
88
|
+
const directory = pending.pop();
|
|
89
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
90
|
+
const source = path.join(directory, entry.name);
|
|
91
|
+
const relativePath = path.relative(overlay.source, source);
|
|
92
|
+
const destination = path.join(targetDir, relativePath);
|
|
93
|
+
const existing = await fs.lstat(destination).catch((error) => {
|
|
94
|
+
if (error.code === "ENOENT") return null;
|
|
95
|
+
throw error;
|
|
96
|
+
});
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
if (existing && !existing.isDirectory()) {
|
|
99
|
+
throw new Error(`Cannot add ${moduleKey}: module scaffold requires a directory at ${relativePath}, but an app file or link exists. Reconcile it explicitly before adding the module.`);
|
|
100
|
+
}
|
|
101
|
+
pending.push(source);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
overlayFiles.push(relativePath);
|
|
105
|
+
if (!appManifest.scaffoldFiles[relativePath] && existing) {
|
|
106
|
+
throw new Error(`Cannot add ${moduleKey}: untracked app file conflicts with module scaffold: ${relativePath}. Move or reconcile it explicitly before adding the module.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await assertMutationTargets(targetDir, ["package.json", ".brightweb/app-manifest.json", ...Object.keys(managedWrites), ...overlayFiles, ...migrationPlan.writes.map((write) => path.relative(targetDir, write.targetPath))]);
|
|
75
113
|
|
|
76
114
|
const summary = [
|
|
77
115
|
"bw add",
|
|
@@ -83,8 +121,19 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
|
|
|
83
121
|
output.write(`${summary.join("\n")}\n`);
|
|
84
122
|
if (argvOptions.dryRun) return { dryRun: true, newModules, migrationPlan };
|
|
85
123
|
|
|
124
|
+
if (newModules.length === 0) return { dryRun: false, newModules, migrationPlan };
|
|
125
|
+
|
|
86
126
|
if (newModules.length > 0) await fs.writeFile(packageJsonPath, `${JSON.stringify(nextPackageJson, null, 2)}\n`, "utf8");
|
|
87
|
-
|
|
127
|
+
const copiedPaths = new Set();
|
|
128
|
+
for (const overlay of overlays) await fs.cp(overlay.source, targetDir, {
|
|
129
|
+
recursive: true,
|
|
130
|
+
filter: async (source, destination) => {
|
|
131
|
+
const relativePath = path.relative(targetDir, destination);
|
|
132
|
+
if (protectedPaths.has(relativePath)) return false;
|
|
133
|
+
if ((await fs.stat(source)).isFile()) copiedPaths.add(relativePath);
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
88
137
|
for (const [relativePath, content] of Object.entries(managedWrites)) {
|
|
89
138
|
const targetPath = path.join(targetDir, relativePath);
|
|
90
139
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
@@ -95,13 +144,13 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
|
|
|
95
144
|
for (const key of newModules) appManifest.modules[key] = { version: catalog[key].version, installedAt: now, exposed: true };
|
|
96
145
|
appManifest.migrationCursor = migrationPlan.nextCursor;
|
|
97
146
|
const collectedScaffoldFiles = await collectScaffoldFiles(targetDir, installedModuleKeys);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
147
|
+
for (const [relativePath, record] of Object.entries(collectedScaffoldFiles)) {
|
|
148
|
+
// Existing baselines describe generated bytes, never arbitrary app edits.
|
|
149
|
+
// Refresh only tracked outputs actually rendered by this command.
|
|
150
|
+
if (Object.hasOwn(managedWrites, relativePath) || copiedPaths.has(relativePath)) {
|
|
151
|
+
appManifest.scaffoldFiles[relativePath] = { ...appManifest.scaffoldFiles[relativePath], ...record };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
105
154
|
await writeAppManifest(targetDir, appManifest);
|
|
106
155
|
output.write(`Installed ${newModules.length} module${newModules.length === 1 ? "" : "s"}. ${migrationPlan.writes.length > 0 ? "Run your Supabase migration apply command. " : ""}Run your package manager install command next.\n`);
|
|
107
156
|
return { dryRun: false, newModules, migrationPlan };
|
package/src/adopt.mjs
CHANGED
|
@@ -10,15 +10,16 @@ import {
|
|
|
10
10
|
findWorkspaceRoot,
|
|
11
11
|
loadModuleCatalog,
|
|
12
12
|
readConfiguredModuleFlags,
|
|
13
|
+
readAppManifest,
|
|
13
14
|
writeAppManifest,
|
|
14
15
|
} from "./app-manifest.mjs";
|
|
15
16
|
import { pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
16
17
|
import { findAppMigrationsDirectory, getModuleMigrations } from "./migrations.mjs";
|
|
17
|
-
import { inventoryScaffoldFiles, resolveTemplateRoot } from "./scaffold.mjs";
|
|
18
|
+
import { inventoryScaffoldFiles, preserveScaffoldDecisions, resolveTemplateRoot, scaffoldDrift } from "./scaffold.mjs";
|
|
18
19
|
import { detectDependencyMode, detectTemplate } from "./update.mjs";
|
|
19
20
|
|
|
20
21
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
-
const HELP = `Usage: bw adopt [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --cursor <key>=<migrationFilename> Override a migration cursor (repeatable)\n --owned-surface <name> Record an app-owned surface (repeatable)\n --own <path> Mark an existing scaffold file app-owned (repeatable)\n --skip <path> Mark a missing scaffold file intentionally absent (repeatable)\n --allow-uncursored Allow doctor to warn instead of fail on null cursors\n --force
|
|
22
|
+
const HELP = `Usage: bw adopt [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --cursor <key>=<migrationFilename> Override a migration cursor (repeatable)\n --owned-surface <name> Record an app-owned surface (repeatable)\n --own <path> Mark an existing scaffold file app-owned (repeatable)\n --skip <path> Mark a missing scaffold file intentionally absent (repeatable)\n --allow-uncursored Allow doctor to warn instead of fail on null cursors\n --force Reconcile an existing manifest; preserve ownership decisions\n --dry-run Print the manifest and warnings without writing\n --help Show this help`;
|
|
22
23
|
|
|
23
24
|
function asList(value) {
|
|
24
25
|
if (value == null) return [];
|
|
@@ -54,13 +55,19 @@ function leadingBaselineDomain(content) {
|
|
|
54
55
|
return line?.match(/^\s*--\s*Brightweb\s+(.+?)\s+v1\s+baseline\.?\s*$/i)?.[1]?.trim().toLowerCase() || null;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
async function bootstrapCursor({ moduleKey, catalogEntry, appMigrations, override, warnings }) {
|
|
58
|
+
async function bootstrapCursor({ moduleKey, catalogEntry, appMigrations, override, retainedCursor, warnings }) {
|
|
58
59
|
const shipped = await getModuleMigrations(moduleKey, catalogEntry);
|
|
59
|
-
if (shipped.length === 0) return { shipsMigrations: false, cursor: undefined, strategy: "none" };
|
|
60
60
|
if (override) {
|
|
61
61
|
if (!shipped.some((entry) => entry.fileName === override)) throw new Error(`Cursor override for ${moduleKey} does not name a shipped migration: ${override}`);
|
|
62
62
|
return { shipsMigrations: true, cursor: override, strategy: "override" };
|
|
63
63
|
}
|
|
64
|
+
if (retainedCursor !== undefined) {
|
|
65
|
+
if (!shipped.some((entry) => entry.fileName === retainedCursor)) {
|
|
66
|
+
throw new Error(`Retained migration cursor for removed module ${moduleKey} does not name a shipped migration: ${retainedCursor}. Reconcile it with an explicit --cursor ${moduleKey}=<migrationFilename>.`);
|
|
67
|
+
}
|
|
68
|
+
return { shipsMigrations: true, cursor: retainedCursor, strategy: "retained" };
|
|
69
|
+
}
|
|
70
|
+
if (shipped.length === 0) return { shipsMigrations: false, cursor: undefined, strategy: "none" };
|
|
64
71
|
|
|
65
72
|
const escapedKey = moduleKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
73
|
const provenance = latestShippedMatch(shipped, (shippedName) => appMigrations.some(({ content }) => {
|
|
@@ -89,9 +96,10 @@ async function bootstrapCursor({ moduleKey, catalogEntry, appMigrations, overrid
|
|
|
89
96
|
|
|
90
97
|
export async function adoptBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
91
98
|
if (argvOptions.help) { output.write(`${HELP}\n`); return { help: true }; }
|
|
92
|
-
const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
|
|
99
|
+
const targetDir = await fs.realpath(path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd()));
|
|
93
100
|
const existingManifestPath = path.join(targetDir, APP_MANIFEST_PATH);
|
|
94
|
-
if (await pathExists(existingManifestPath) && !argvOptions.force) throw new Error(`Refusing to overwrite existing ${APP_MANIFEST_PATH}; pass --force to
|
|
101
|
+
if (await pathExists(existingManifestPath) && !argvOptions.force) throw new Error(`Refusing to overwrite existing ${APP_MANIFEST_PATH}; pass --force to reconcile it while preserving ownership decisions.`);
|
|
102
|
+
const previousManifest = await readAppManifest(targetDir, { required: false });
|
|
95
103
|
const packageJson = await readJsonIfPresent(path.join(targetDir, "package.json"));
|
|
96
104
|
if (!packageJson) throw new Error(`Target directory does not contain package.json: ${targetDir}`);
|
|
97
105
|
|
|
@@ -123,6 +131,11 @@ export async function adoptBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
|
123
131
|
const scaffold = template === "platform"
|
|
124
132
|
? await inventoryScaffoldFiles({ targetDir, moduleKeys: Object.keys(modules), templateRoot })
|
|
125
133
|
: { records: {}, unsupported: [] };
|
|
134
|
+
const previousScaffold = await scaffoldDrift(targetDir, previousManifest?.scaffoldFiles);
|
|
135
|
+
scaffold.records = preserveScaffoldDecisions(scaffold.records, previousManifest?.scaffoldFiles, previousScaffold.protectedPaths);
|
|
136
|
+
for (const entry of (await scaffoldDrift(targetDir, scaffold.records)).entries) {
|
|
137
|
+
scaffold.records[entry.relativePath].status = entry.status;
|
|
138
|
+
}
|
|
126
139
|
const ownedPaths = new Set(asList(argvOptions.own).map(String));
|
|
127
140
|
const skippedPaths = new Set(asList(argvOptions.skip).map(String));
|
|
128
141
|
for (const relativePath of [...ownedPaths, ...skippedPaths]) {
|
|
@@ -144,14 +157,23 @@ export async function adoptBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
const overrides = parseCursorOverrides(argvOptions.cursor);
|
|
147
|
-
const unknownOverride = Object.keys(overrides).find((key) => !catalog
|
|
160
|
+
const unknownOverride = Object.keys(overrides).find((key) => !Object.hasOwn(catalog, key));
|
|
148
161
|
if (unknownOverride) throw new Error(`Unknown module key in --cursor: ${unknownOverride}`);
|
|
149
162
|
const appMigrations = template === "platform" ? await migrationFiles(targetDir) : [];
|
|
150
163
|
const migrationCursor = {};
|
|
151
164
|
const cursorStrategies = {};
|
|
152
|
-
const
|
|
165
|
+
const installedMigrationKeys = new Set(template === "platform" ? ["core", "admin", ...Object.keys(modules)] : []);
|
|
166
|
+
const migrationKeys = template === "platform"
|
|
167
|
+
? Array.from(new Set([...installedMigrationKeys, ...Object.keys(previousManifest?.migrationCursor || {}), ...Object.keys(overrides)]))
|
|
168
|
+
: [];
|
|
153
169
|
for (const moduleKey of migrationKeys) {
|
|
154
|
-
|
|
170
|
+
if (!Object.hasOwn(catalog, moduleKey)) throw new Error(`Unknown retained migration module: ${moduleKey}. Reconcile its history before re-adopting.`);
|
|
171
|
+
// Removing package wiring never removes applied history. Preserve an exact
|
|
172
|
+
// retained cursor rather than advancing it from files copied by older tools.
|
|
173
|
+
const retainedCursor = !installedMigrationKeys.has(moduleKey)
|
|
174
|
+
? previousManifest?.migrationCursor?.[moduleKey]
|
|
175
|
+
: undefined;
|
|
176
|
+
const result = await bootstrapCursor({ moduleKey, catalogEntry: catalog[moduleKey], appMigrations, override: overrides[moduleKey], retainedCursor, warnings });
|
|
155
177
|
if (result.shipsMigrations) {
|
|
156
178
|
migrationCursor[moduleKey] = result.cursor;
|
|
157
179
|
cursorStrategies[moduleKey] = result.strategy;
|
|
@@ -172,7 +194,7 @@ export async function adoptBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
|
172
194
|
scaffoldFiles: scaffold.records,
|
|
173
195
|
managedFiles: template === "platform" ? MANAGED_APP_FILES : ["docs/ai/app-context.json"],
|
|
174
196
|
migrationCursor,
|
|
175
|
-
ownedSurfaces: Array.from(new Set(asList(argvOptions.ownedSurface).map(String))),
|
|
197
|
+
ownedSurfaces: Array.from(new Set([...(previousManifest?.ownedSurfaces || []), ...asList(argvOptions.ownedSurface).map(String)])),
|
|
176
198
|
adoptionNotes: {
|
|
177
199
|
allowUncursored: argvOptions.allowUncursored === true,
|
|
178
200
|
cursorStrategies,
|
package/src/app-manifest.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { MANAGED_PLATFORM_FILES, isAppOwnedSeed } from "./file-policy.mjs";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import { assertMutationTargets } from "./mutation-paths.mjs";
|
|
1
4
|
import crypto from "node:crypto";
|
|
2
5
|
import fs from "node:fs/promises";
|
|
3
6
|
import path from "node:path";
|
|
@@ -17,13 +20,7 @@ async function readJsonIfPresent(filePath) {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export const APP_MANIFEST_PATH = path.join(".brightweb", "app-manifest.json");
|
|
20
|
-
export const MANAGED_APP_FILES =
|
|
21
|
-
"next.config.ts",
|
|
22
|
-
"app/globals.css",
|
|
23
|
-
"config/modules.ts",
|
|
24
|
-
"config/shell.ts",
|
|
25
|
-
"docs/ai/app-context.json",
|
|
26
|
-
];
|
|
23
|
+
export const MANAGED_APP_FILES = MANAGED_PLATFORM_FILES;
|
|
27
24
|
|
|
28
25
|
export const MODULE_PACKAGES = {
|
|
29
26
|
admin: "@brightweblabs/module-admin",
|
|
@@ -44,36 +41,13 @@ const FALLBACK_REQUIRES = {
|
|
|
44
41
|
|
|
45
42
|
export function cleanVersion(version) {
|
|
46
43
|
if (typeof version !== "string") return null;
|
|
47
|
-
|
|
48
|
-
return match?.[1] || null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function compareVersions(left, right) {
|
|
52
|
-
const a = cleanVersion(left)?.split("-")[0].split(".").map(Number) || [];
|
|
53
|
-
const b = cleanVersion(right)?.split("-")[0].split(".").map(Number) || [];
|
|
54
|
-
for (let index = 0; index < 3; index += 1) {
|
|
55
|
-
if ((a[index] || 0) !== (b[index] || 0)) return (a[index] || 0) - (b[index] || 0);
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
44
|
+
return semver.valid(version.trim().replace(/^[~^]\s*/, ""));
|
|
58
45
|
}
|
|
59
46
|
|
|
60
47
|
export function satisfiesVersion(version, range) {
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
const expectedMatch = range.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
65
|
-
const expected = expectedMatch ? `${expectedMatch[1]}.${expectedMatch[2] || "0"}.${expectedMatch[3] || "0"}` : null;
|
|
66
|
-
if (!expected) return false;
|
|
67
|
-
if (range.trim().startsWith(">=")) return compareVersions(normalized, expected) >= 0;
|
|
68
|
-
if (range.trim().startsWith("^")) {
|
|
69
|
-
return normalized.split(".")[0] === expected.split(".")[0] && compareVersions(normalized, expected) >= 0;
|
|
70
|
-
}
|
|
71
|
-
if (range.trim().startsWith("~")) {
|
|
72
|
-
const actualParts = normalized.split(".");
|
|
73
|
-
const expectedParts = expected.split(".");
|
|
74
|
-
return actualParts[0] === expectedParts[0] && actualParts[1] === expectedParts[1] && compareVersions(normalized, expected) >= 0;
|
|
75
|
-
}
|
|
76
|
-
return normalized === expected;
|
|
48
|
+
if (typeof version !== "string" || typeof range !== "string" || !semver.valid(version)) return false;
|
|
49
|
+
if (range === "workspace:*") return true;
|
|
50
|
+
return semver.satisfies(version, range);
|
|
77
51
|
}
|
|
78
52
|
|
|
79
53
|
export async function hashFile(filePath) {
|
|
@@ -82,6 +56,7 @@ export async function hashFile(filePath) {
|
|
|
82
56
|
}
|
|
83
57
|
|
|
84
58
|
export async function readAppManifest(targetDir, { required = true } = {}) {
|
|
59
|
+
await assertMutationTargets(targetDir, [APP_MANIFEST_PATH]);
|
|
85
60
|
const manifestPath = path.join(targetDir, APP_MANIFEST_PATH);
|
|
86
61
|
const manifest = await readJsonIfPresent(manifestPath);
|
|
87
62
|
if (!manifest && required) {
|
|
@@ -95,6 +70,7 @@ export async function readAppManifest(targetDir, { required = true } = {}) {
|
|
|
95
70
|
}
|
|
96
71
|
|
|
97
72
|
export async function writeAppManifest(targetDir, manifest) {
|
|
73
|
+
await assertMutationTargets(targetDir, [APP_MANIFEST_PATH]);
|
|
98
74
|
const manifestPath = path.join(targetDir, APP_MANIFEST_PATH);
|
|
99
75
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
100
76
|
const temporaryPath = `${manifestPath}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
@@ -178,7 +154,7 @@ export async function collectScaffoldFiles(targetDir, selectedModules) {
|
|
|
178
154
|
const result = {};
|
|
179
155
|
for (const [relativePath, moduleKey] of Array.from(files.entries()).sort()) {
|
|
180
156
|
const targetPath = resolveSafeRelativePath(targetDir, relativePath, "Scaffold file path");
|
|
181
|
-
if (await pathExists(targetPath)) result[relativePath] = { module: moduleKey, hash: await hashFile(targetPath), status: "current" };
|
|
157
|
+
if (await pathExists(targetPath)) result[relativePath] = { module: moduleKey, hash: await hashFile(targetPath), status: "current", ...(isAppOwnedSeed(relativePath) ? { intent: "owned" } : {}) };
|
|
182
158
|
}
|
|
183
159
|
return result;
|
|
184
160
|
}
|
package/src/constants.mjs
CHANGED
|
@@ -116,6 +116,7 @@ export const MODULE_STARTER_FILES = {
|
|
|
116
116
|
"app/(shell)/projetos/[projectId]/page.tsx",
|
|
117
117
|
"app/(shell)/projetos/[projectId]/quadro/page.tsx",
|
|
118
118
|
"app/(shell)/projetos/[projectId]/tarefas/page.tsx",
|
|
119
|
+
"app/(shell)/account/perfil/page.tsx",
|
|
119
120
|
"app/(shell)/account/projetos/page.tsx",
|
|
120
121
|
"app/(shell)/account/projetos/loading.tsx",
|
|
121
122
|
"app/(shell)/account/projetos/[projectId]/page.tsx",
|
|
@@ -186,16 +187,16 @@ export const PLATFORM_STARTER_FILES = [
|
|
|
186
187
|
];
|
|
187
188
|
|
|
188
189
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
189
|
-
"@brightweblabs/app-shell": "^0.16.
|
|
190
|
-
"@brightweblabs/core-auth": "^0.12.
|
|
190
|
+
"@brightweblabs/app-shell": "^0.16.3",
|
|
191
|
+
"@brightweblabs/core-auth": "^0.12.2",
|
|
191
192
|
"@brightweblabs/infra": "^0.7.0",
|
|
192
|
-
"@brightweblabs/module-admin": "^0.9.
|
|
193
|
-
"@brightweblabs/module-crm": "^0.18.
|
|
194
|
-
"@brightweblabs/module-marketing": "^0.5.
|
|
195
|
-
"@brightweblabs/module-orgs": "^0.7.
|
|
196
|
-
"@brightweblabs/module-projects": "^0.19.
|
|
193
|
+
"@brightweblabs/module-admin": "^0.9.7",
|
|
194
|
+
"@brightweblabs/module-crm": "^0.18.4",
|
|
195
|
+
"@brightweblabs/module-marketing": "^0.5.2",
|
|
196
|
+
"@brightweblabs/module-orgs": "^0.7.3",
|
|
197
|
+
"@brightweblabs/module-projects": "^0.19.3",
|
|
197
198
|
"@brightweblabs/theme": "^0.8.4",
|
|
198
|
-
"@brightweblabs/ui": "^1.5.
|
|
199
|
+
"@brightweblabs/ui": "^1.5.6",
|
|
199
200
|
"geist": "1.7.2",
|
|
200
201
|
"lucide-react": "^1.8.0",
|
|
201
202
|
"next": "^16.0.0",
|
package/src/doctor.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
import { stdout as output } from "node:process";
|
|
4
4
|
import { APP_DEPENDENCY_DEFAULTS, BRIGHTWEB_PACKAGE_NAMES } from "./constants.mjs";
|
|
5
5
|
import { cursorMigrationStatus, exactMigrationCompatibilityStatus } from "./migrations.mjs";
|
|
6
|
-
import { findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, readConfiguredModuleFlags, satisfiesVersion, validateAppManifest, writeAppManifest } from "./app-manifest.mjs";
|
|
6
|
+
import { findWorkspaceRoot, hashFile, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, readConfiguredModuleFlags, satisfiesVersion, validateAppManifest, writeAppManifest } from "./app-manifest.mjs";
|
|
7
7
|
import { loadAppEnvironment, readFirstEnvironmentValue } from "./env.mjs";
|
|
8
8
|
import { pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
9
9
|
import { nearestVercelRegion, normalizeSupabaseRegion } from "./regions.mjs";
|
|
10
|
-
import { scaffoldDrift } from "./scaffold.mjs";
|
|
10
|
+
import { canonicalScaffoldHash, scaffoldDrift } from "./scaffold.mjs";
|
|
11
11
|
|
|
12
12
|
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 --deployment-url <url> Deployed app URL (defaults to PUBLIC_APP_URL/NEXT_PUBLIC_APP_URL)\n --supabase-region <id> Current Supabase project region override\n --strict Treat warnings as failures\n --report Stamp lastDoctor in the app manifest\n --help Show this help`;
|
|
13
13
|
const RUNTIME_PACKAGE_NAMES = ["react", "react-dom", "next"];
|
|
@@ -378,6 +378,18 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
378
378
|
add(topologyProblems.length ? "FAIL" : "PASS", "topology", topologyProblems.join("; ") || "Module requirements are satisfied.");
|
|
379
379
|
|
|
380
380
|
const scaffold = await scaffoldDrift(targetDir, appManifest.scaffoldFiles);
|
|
381
|
+
const ownedTemplateUpdates = [];
|
|
382
|
+
for (const entry of scaffold.entries) {
|
|
383
|
+
if (entry.intent !== "owned" || entry.status === "missing") continue;
|
|
384
|
+
const templateHash = await canonicalScaffoldHash({ relativePath: entry.relativePath, manifest: appManifest, targetDir, workspaceRoot });
|
|
385
|
+
// Ownership preserves the recorded baseline during upgrades; it does not prove
|
|
386
|
+
// that later starter fixes have been reconciled into the app's custom file.
|
|
387
|
+
if (templateHash && templateHash !== appManifest.scaffoldFiles[entry.relativePath].hash
|
|
388
|
+
&& templateHash !== await hashFile(path.join(targetDir, entry.relativePath))) {
|
|
389
|
+
ownedTemplateUpdates.push(entry.relativePath);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (ownedTemplateUpdates.length) add("WARN", "scaffold-owned-template-updates", `Current templates changed for preserved app-owned files: ${ownedTemplateUpdates.join(", ")}. Compatibility is NOT VERIFIED. Run bw diff, reconcile relevant fixes and verify behavior. After review, use bw scaffold manage followed by bw scaffold own for each custom file to record the current template baseline; these commands do not replace file contents.`);
|
|
381
393
|
const scaffoldGroups = {
|
|
382
394
|
current: scaffold.entries.filter((entry) => entry.status === "current" && entry.intent !== "skipped"),
|
|
383
395
|
owned: scaffold.entries.filter((entry) => entry.intent === "owned" && entry.status === "drifted"),
|
|
@@ -391,7 +403,7 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
391
403
|
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).`);
|
|
392
404
|
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).`);
|
|
393
405
|
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(", ")}.`);
|
|
394
|
-
const scaffoldStatus = scaffoldGroups.mismatched.length ? "FAIL" : scaffoldGroups.undecidedDrift.length || scaffoldGroups.undecidedMissing.length ? "WARN" : "PASS";
|
|
406
|
+
const scaffoldStatus = scaffoldGroups.mismatched.length ? "FAIL" : ownedTemplateUpdates.length || scaffoldGroups.undecidedDrift.length || scaffoldGroups.undecidedMissing.length ? "WARN" : "PASS";
|
|
395
407
|
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.`);
|
|
396
408
|
add("INFO", "owned-surfaces", `Owned surfaces: ${(appManifest.ownedSurfaces || []).join(", ") || "none"}.`);
|
|
397
409
|
|
|
@@ -456,7 +468,7 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
456
468
|
add("INFO", `migration-legacy-equivalent-${key}`, `${key}: ${status.legacyEquivalent.length} immutable historical migration file${status.legacyEquivalent.length === 1 ? " matches" : "s match"} a reviewed SQL-equivalent legacy hash.`);
|
|
457
469
|
}
|
|
458
470
|
}
|
|
459
|
-
add("WARN", "db-objects", "
|
|
471
|
+
add("WARN", "db-objects", "NOT VERIFIED: live database objects, applied migrations, and authenticated row-level permissions. Local migration provenance does not establish deployed behavior; apply pending migrations and run authenticated admin/client permission checks against the target database.");
|
|
460
472
|
return finish(checks, argvOptions, appManifest, targetDir);
|
|
461
473
|
}
|
|
462
474
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MODULE_STARTER_FILES, PLATFORM_STARTER_FILES } from "./constants.mjs";
|
|
2
|
+
|
|
3
|
+
export const MODULE_SELECTED_FILES = [
|
|
4
|
+
"app/api/invitations/_dependencies.ts",
|
|
5
|
+
"app/api/organizations/route.ts",
|
|
6
|
+
"app/api/organizations/[id]/route.ts",
|
|
7
|
+
"app/api/organizations/[id]/invitations/route.ts",
|
|
8
|
+
"app/api/organizations/[id]/invitations/[invitationId]/route.ts",
|
|
9
|
+
"app/api/organizations/[id]/members/[profileId]/route.ts",
|
|
10
|
+
];
|
|
11
|
+
export const MANAGED_PLATFORM_FILES = [
|
|
12
|
+
"next.config.ts", "app/globals.css", "config/module-toolbar-controls.tsx",
|
|
13
|
+
"config/modules.ts", "config/shell.ts", ...MODULE_SELECTED_FILES,
|
|
14
|
+
"docs/ai/app-context.json",
|
|
15
|
+
];
|
|
16
|
+
export const MANAGED_SITE_FILES = ["docs/ai/app-context.json"];
|
|
17
|
+
export const APP_OWNED_FILES = [
|
|
18
|
+
"config/brand.ts", "config/shell.overrides.ts", "app/theme.css", "app/fonts.ts",
|
|
19
|
+
"config/social-media-plan.json", "app/(shell)/marketing/social-media/page.tsx",
|
|
20
|
+
"public/brand/logo-mark.svg", "public/brand/logo-light.svg", "public/brand/logo-dark.svg",
|
|
21
|
+
"docs/ai/README.md", "docs/ai/examples.md", "AGENTS.md", "README.md",
|
|
22
|
+
];
|
|
23
|
+
export function fileOwnership({ template = "platform", modules = [] } = {}) {
|
|
24
|
+
const generated = template === "platform" ? MANAGED_PLATFORM_FILES : MANAGED_SITE_FILES;
|
|
25
|
+
const appOwned = template === "platform" ? APP_OWNED_FILES : ["config/site.ts", "app/fonts.ts", "app/globals.css", "app/layout.tsx", "next.config.ts", "postcss.config.mjs", "tsconfig.json", "docs/ai/README.md", "docs/ai/examples.md", "AGENTS.md", "README.md"];
|
|
26
|
+
const scaffold = template === "platform" ? [...new Set([...PLATFORM_STARTER_FILES, ...modules.flatMap((key) => MODULE_STARTER_FILES[key] || [])])].filter((p) => !generated.includes(p) && !appOwned.includes(p)) : [];
|
|
27
|
+
return { default: "app-owned", appOwned: [...appOwned], scaffoldManaged: scaffold, generated: [...generated], precedence: ["explicit-intent-or-drift", "exact-path-policy", "recorded-scaffold", "app-owned-default"] };
|
|
28
|
+
}
|
|
29
|
+
export function isAppOwnedSeed(relativePath) { return APP_OWNED_FILES.includes(relativePath); }
|
|
30
|
+
|
|
31
|
+
/** Fail before writing when a renderer and its ownership policy disagree. */
|
|
32
|
+
export function assertGeneratedFileInventory(files, expectedPaths, label) {
|
|
33
|
+
const actualPaths = Object.keys(files);
|
|
34
|
+
const missing = expectedPaths.filter((relativePath) => !Object.hasOwn(files, relativePath));
|
|
35
|
+
const unexpected = actualPaths.filter((relativePath) => !expectedPaths.includes(relativePath));
|
|
36
|
+
if (missing.length || unexpected.length) {
|
|
37
|
+
throw new Error(`${label} inventory mismatch: missing ${missing.join(", ") || "none"}; unexpected ${unexpected.join(", ") || "none"}.`);
|
|
38
|
+
}
|
|
39
|
+
return files;
|
|
40
|
+
}
|
package/src/generator.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fileOwnership, assertGeneratedFileInventory, MANAGED_PLATFORM_FILES, MODULE_SELECTED_FILES } from "./file-policy.mjs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -690,20 +691,7 @@ export function createAppContextFile({
|
|
|
690
691
|
},
|
|
691
692
|
starterRoutes: getSiteStarterRoutes(),
|
|
692
693
|
ownership: {
|
|
693
|
-
|
|
694
|
-
"config/**",
|
|
695
|
-
"docs/ai/**",
|
|
696
|
-
"public/**",
|
|
697
|
-
"AGENTS.md",
|
|
698
|
-
"README.md",
|
|
699
|
-
],
|
|
700
|
-
scaffoldManaged: [
|
|
701
|
-
"app/layout.tsx",
|
|
702
|
-
"app/globals.css",
|
|
703
|
-
"next.config.ts",
|
|
704
|
-
"postcss.config.mjs",
|
|
705
|
-
"tsconfig.json",
|
|
706
|
-
],
|
|
694
|
+
...fileOwnership({ template: "site" }),
|
|
707
695
|
packageOwned: [],
|
|
708
696
|
},
|
|
709
697
|
agentRules: {
|
|
@@ -753,22 +741,7 @@ export function createAppContextFile({
|
|
|
753
741
|
},
|
|
754
742
|
starterRoutes: getPlatformStarterRoutes(selectedModules),
|
|
755
743
|
ownership: {
|
|
756
|
-
|
|
757
|
-
"config/**",
|
|
758
|
-
"docs/ai/**",
|
|
759
|
-
"public/brand/**",
|
|
760
|
-
"AGENTS.md",
|
|
761
|
-
"README.md",
|
|
762
|
-
],
|
|
763
|
-
scaffoldManaged: [
|
|
764
|
-
"app/layout.tsx",
|
|
765
|
-
"app/globals.css",
|
|
766
|
-
"app/**/page.tsx",
|
|
767
|
-
"app/**/route.ts",
|
|
768
|
-
"next.config.ts",
|
|
769
|
-
"postcss.config.mjs",
|
|
770
|
-
"tsconfig.json",
|
|
771
|
-
],
|
|
744
|
+
...fileOwnership({ template: "platform", modules: selectedModules }),
|
|
772
745
|
packageOwned: [
|
|
773
746
|
...CORE_PACKAGES,
|
|
774
747
|
...(selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
|
|
@@ -1182,7 +1155,7 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1182
1155
|
'const acceptOrganizationInvitation = async (_client: never, _input: unknown): Promise<never> => { throw new Error("Convite não encontrado."); };',
|
|
1183
1156
|
] : []),
|
|
1184
1157
|
...(!crmEnabled ? [
|
|
1185
|
-
"const ensureCrmContactForProfile =
|
|
1158
|
+
"const ensureCrmContactForProfile = undefined;",
|
|
1186
1159
|
] : []),
|
|
1187
1160
|
(!adminEnabled || !orgsEnabled || !crmEnabled) ? "" : null,
|
|
1188
1161
|
"export const invitationHttpDependencies = {",
|
|
@@ -1213,7 +1186,7 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1213
1186
|
"",
|
|
1214
1187
|
].filter((line) => line !== null).join("\n");
|
|
1215
1188
|
|
|
1216
|
-
return {
|
|
1189
|
+
return assertGeneratedFileInventory({
|
|
1217
1190
|
"app/api/invitations/_dependencies.ts": invitationDependencies,
|
|
1218
1191
|
"app/api/organizations/route.ts": createOrganizationRoute([
|
|
1219
1192
|
crmEnabled
|
|
@@ -1287,7 +1260,20 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1287
1260
|
"}",
|
|
1288
1261
|
"",
|
|
1289
1262
|
].join("\n"),
|
|
1290
|
-
};
|
|
1263
|
+
}, MODULE_SELECTED_FILES, "Module-selected routes");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** The same module-aware outputs drive initial scaffolding and every topology refresh. */
|
|
1267
|
+
export async function createManagedPlatformFiles({ slug, selectedModules, dbInstallPlan }) {
|
|
1268
|
+
return assertGeneratedFileInventory({
|
|
1269
|
+
"next.config.ts": createNextConfig({ template: "platform", selectedModules }),
|
|
1270
|
+
"app/globals.css": await createPlatformGlobalsCss(selectedModules),
|
|
1271
|
+
"config/module-toolbar-controls.tsx": createModuleToolbarControlsConfig(selectedModules),
|
|
1272
|
+
"config/modules.ts": createPlatformModulesConfigFile(selectedModules),
|
|
1273
|
+
"config/shell.ts": createShellConfig(selectedModules),
|
|
1274
|
+
...createOptionalModuleRouteFiles(selectedModules),
|
|
1275
|
+
"docs/ai/app-context.json": createAppContextFile({ slug, template: "platform", selectedModules, dbInstallPlan }),
|
|
1276
|
+
}, MANAGED_PLATFORM_FILES, "Managed platform files");
|
|
1291
1277
|
}
|
|
1292
1278
|
|
|
1293
1279
|
function createSiteConfigFile(slug) {
|
|
@@ -1628,6 +1614,7 @@ async function scaffoldPlatformProject({
|
|
|
1628
1614
|
dbRegistry,
|
|
1629
1615
|
}) {
|
|
1630
1616
|
const brandValues = createDerivedBrandValues(answers.slug);
|
|
1617
|
+
const managedFiles = await createManagedPlatformFiles({ slug: answers.slug, selectedModules, dbInstallPlan });
|
|
1631
1618
|
const baseTemplateDir = path.join(TEMPLATE_ROOT, "base");
|
|
1632
1619
|
|
|
1633
1620
|
await ensureDirectory(path.dirname(targetDir));
|
|
@@ -1659,30 +1646,14 @@ async function scaffoldPlatformProject({
|
|
|
1659
1646
|
2,
|
|
1660
1647
|
)}\n`,
|
|
1661
1648
|
);
|
|
1662
|
-
await fs.writeFile(path.join(targetDir, "next.config.ts"), createNextConfig({ template: "platform", selectedModules }));
|
|
1663
|
-
await fs.writeFile(path.join(targetDir, "app", "globals.css"), await createPlatformGlobalsCss(selectedModules));
|
|
1664
1649
|
await fs.writeFile(
|
|
1665
1650
|
path.join(targetDir, "config", "brand.ts"),
|
|
1666
1651
|
createPlatformBrandConfigFile({ slug: answers.slug, brandValues }),
|
|
1667
1652
|
);
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
path.join(targetDir, "config", "module-toolbar-controls.tsx"),
|
|
1671
|
-
createModuleToolbarControlsConfig(selectedModules),
|
|
1672
|
-
);
|
|
1673
|
-
await fs.writeFile(path.join(targetDir, "config", "shell.ts"), createShellConfig(selectedModules));
|
|
1674
|
-
for (const [relativePath, content] of Object.entries(createOptionalModuleRouteFiles(selectedModules))) {
|
|
1653
|
+
for (const [relativePath, content] of Object.entries(managedFiles)) {
|
|
1654
|
+
await ensureDirectory(path.dirname(path.join(targetDir, relativePath)));
|
|
1675
1655
|
await fs.writeFile(path.join(targetDir, relativePath), content);
|
|
1676
1656
|
}
|
|
1677
|
-
await fs.writeFile(
|
|
1678
|
-
path.join(targetDir, "docs", "ai", "app-context.json"),
|
|
1679
|
-
createAppContextFile({
|
|
1680
|
-
slug: answers.slug,
|
|
1681
|
-
template: "platform",
|
|
1682
|
-
selectedModules,
|
|
1683
|
-
dbInstallPlan,
|
|
1684
|
-
}),
|
|
1685
|
-
);
|
|
1686
1657
|
|
|
1687
1658
|
const vercelConfig = createVercelConfig(answers.supabaseRegion);
|
|
1688
1659
|
|