create-bw-app 0.27.2 → 0.27.4
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 +1 -1
- package/package.json +1 -1
- package/src/add.mjs +3 -2
- package/src/constants.mjs +7 -6
- package/src/doctor.mjs +16 -4
- package/src/migrations.mjs +28 -0
- package/src/upgrade.mjs +3 -2
- package/template/supabase/modules/admin/migrations/20260908120000_admin_profile_directory_visibility.sql +9 -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
|
|
package/package.json
CHANGED
package/src/add.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { SELECTABLE_MODULES } from "./constants.mjs";
|
|
|
6
6
|
import { TEMPLATE_ROOT, createDbInstallPlan, createManagedPlatformFiles, getDbModuleRegistry, getVersionMap, pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
7
7
|
import { collectScaffoldFiles, findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, resolveModuleClosure, satisfiesVersion, writeAppManifest } from "./app-manifest.mjs";
|
|
8
8
|
import { assertNoUntrackedScaffoldWrites, scaffoldDrift } from "./scaffold.mjs";
|
|
9
|
-
import { applyMigrationWrites, planMigrationAppends } from "./migrations.mjs";
|
|
9
|
+
import { assertMigrationMutationTargets, applyMigrationWrites, planMigrationAppends } from "./migrations.mjs";
|
|
10
10
|
|
|
11
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`;
|
|
12
12
|
|
|
@@ -109,7 +109,8 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
await assertMutationTargets(targetDir, ["package.json", ".brightweb/app-manifest.json", ...Object.keys(managedWrites), ...overlayFiles
|
|
112
|
+
await assertMutationTargets(targetDir, ["package.json", ".brightweb/app-manifest.json", ...Object.keys(managedWrites), ...overlayFiles]);
|
|
113
|
+
await assertMigrationMutationTargets(targetDir, migrationPlan.writes);
|
|
113
114
|
|
|
114
115
|
const summary = [
|
|
115
116
|
"bw add",
|
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,14 +187,14 @@ export const PLATFORM_STARTER_FILES = [
|
|
|
186
187
|
];
|
|
187
188
|
|
|
188
189
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
189
|
-
"@brightweblabs/app-shell": "^0.16.
|
|
190
|
+
"@brightweblabs/app-shell": "^0.16.4",
|
|
190
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.8",
|
|
194
|
+
"@brightweblabs/module-crm": "^0.18.5",
|
|
195
|
+
"@brightweblabs/module-marketing": "^0.5.3",
|
|
196
|
+
"@brightweblabs/module-orgs": "^0.7.4",
|
|
197
|
+
"@brightweblabs/module-projects": "^0.19.4",
|
|
197
198
|
"@brightweblabs/theme": "^0.8.4",
|
|
198
199
|
"@brightweblabs/ui": "^1.5.6",
|
|
199
200
|
"geist": "1.7.2",
|
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
|
|
package/src/migrations.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import semver from "semver";
|
|
2
|
+
import { assertMutationTargets } from "./mutation-paths.mjs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
@@ -16,6 +17,33 @@ export async function findAppMigrationsDirectory(targetDir) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// Database migrations may belong to the containing consumer workspace, while
|
|
21
|
+
// app files remain bounded to the app itself. Never realpath the migration
|
|
22
|
+
// directory: that would hide links below the legitimate workspace boundary.
|
|
23
|
+
export async function assertMigrationMutationTargets(targetDir, writes) {
|
|
24
|
+
if (writes.length === 0) return;
|
|
25
|
+
const appRoot = await fs.realpath(path.resolve(targetDir));
|
|
26
|
+
let boundary = appRoot;
|
|
27
|
+
let current = appRoot;
|
|
28
|
+
while (true) {
|
|
29
|
+
if (await pathExists(path.join(current, "pnpm-workspace.yaml"))
|
|
30
|
+
|| await pathExists(path.join(current, ".git"))) {
|
|
31
|
+
boundary = current;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
const parent = path.dirname(current);
|
|
35
|
+
if (parent === current) break;
|
|
36
|
+
current = parent;
|
|
37
|
+
}
|
|
38
|
+
const migrationsDir = await findAppMigrationsDirectory(appRoot);
|
|
39
|
+
for (const write of writes) {
|
|
40
|
+
if (path.dirname(write.targetPath) !== migrationsDir) {
|
|
41
|
+
throw new Error("Migration write must target the app's discovered migrations directory.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await assertMutationTargets(boundary, writes.map((write) => path.relative(boundary, write.targetPath)));
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
export async function getModuleMigrations(moduleKey, catalogEntry = {}) {
|
|
20
48
|
const candidates = [];
|
|
21
49
|
const configuredPath = catalogEntry.manifest?.database?.migrations;
|
package/src/upgrade.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { stdout as output } from "node:process";
|
|
5
5
|
import { hashFile, findWorkspaceRoot, loadModuleCatalog, readAppManifest, writeAppManifest, cleanVersion } from "./app-manifest.mjs";
|
|
6
6
|
import { pathExists, runInstall } from "./generator.mjs";
|
|
7
|
-
import { applyMigrationWrites, getModuleMigrations, planMigrationAppends } from "./migrations.mjs";
|
|
7
|
+
import { assertMigrationMutationTargets, applyMigrationWrites, getModuleMigrations, planMigrationAppends } from "./migrations.mjs";
|
|
8
8
|
import { buildBrightwebAppUpdatePlan } from "./update.mjs";
|
|
9
9
|
import { resolveSafeRelativePath } from "./safe-path.mjs";
|
|
10
10
|
import { scaffoldDrift } from "./scaffold.mjs";
|
|
@@ -102,7 +102,8 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
102
102
|
migrationCursor: appManifest.migrationCursor,
|
|
103
103
|
migrationUpperBounds,
|
|
104
104
|
});
|
|
105
|
-
await assertMutationTargets(targetDir, [".brightweb/app-manifest.json", ...plan.fileWrites.map((write) => write.relativePath), ...plan.fileDeletes.map((entry) => entry.relativePath)
|
|
105
|
+
await assertMutationTargets(targetDir, [".brightweb/app-manifest.json", ...plan.fileWrites.map((write) => write.relativePath), ...plan.fileDeletes.map((entry) => entry.relativePath)]);
|
|
106
|
+
await assertMigrationMutationTargets(targetDir, migrationPlan.writes);
|
|
106
107
|
output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nObsolete managed files to remove: ${plan.fileDeletes.length}\nMigrations to append: ${migrationPlan.appends.length}\n`);
|
|
107
108
|
if (throughMigration) output.write(`Migration cutoff: ${moduleKey} through ${throughMigration}\n`);
|
|
108
109
|
for (const boundary of safetyBoundaries) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- The admin directory joins role assignments to profiles using the caller's
|
|
2
|
+
-- authenticated session. Grant the corresponding profile reads through RLS.
|
|
3
|
+
-- Staff and clients retain their existing profile visibility and write rules.
|
|
4
|
+
DROP POLICY IF EXISTS "Admins can view profiles" ON public.profiles;
|
|
5
|
+
CREATE POLICY "Admins can view profiles"
|
|
6
|
+
ON public.profiles
|
|
7
|
+
FOR SELECT
|
|
8
|
+
TO authenticated
|
|
9
|
+
USING (public.is_admin());
|