create-bw-app 0.25.1 → 0.26.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/package.json +1 -1
- package/src/app-manifest.mjs +8 -0
- package/src/constants.mjs +14 -9
- package/src/doctor.mjs +181 -1
- package/src/generator.mjs +29 -9
- package/src/migrations.mjs +116 -6
- package/src/upgrade.mjs +35 -2
- package/template/base/app/(shell)/shell-layout-client.tsx +1 -1
- package/template/base/app/api/organizations/[id]/invitations/[invitationId]/route.ts +8 -0
- package/template/base/app/api/organizations/[id]/invitations/route.ts +2 -2
- package/template/base/app/api/organizations/[id]/route.ts +2 -2
- package/template/base/app/api/organizations/route.ts +2 -2
- package/template/base/config/module-toolbar-controls.tsx +6 -4
- package/template/modules/projects/app/(shell)/projetos/[projectId]/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/[projectId]/quadro/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/[projectId]/tarefas/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/page.tsx +1 -1
- package/template/modules/projects/app/api/account/projects/[id]/route.ts +1 -1
- package/template/modules/projects/app/api/account/projects/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/activity/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/links/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/members/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +1 -1
- package/template/modules/projects/app/api/projects/organizations/route.ts +1 -1
- package/template/modules/projects/app/api/projects/route.ts +1 -1
- package/template/modules/projects/app/api/projects/setup-options/route.ts +1 -1
- package/template/modules/projects/app/api/projects/stats/route.ts +1 -1
- package/template/supabase/modules/crm/migrations/20260816120000_crm_contact_organizations.sql +102 -0
- package/template/supabase/modules/projects/migrations/20260815120000_project_client_access_member_roles.sql +77 -0
- package/template/supabase/modules/projects/migrations/20260815133000_project_admin_creation_and_task_permissions.sql +376 -0
- package/template/modules/projects/app/(shell)/projetos/projetos-live-mounts.tsx +0 -27
- package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +0 -63
- package/template/modules/projects/app/api/projects/_handlers.ts +0 -110
package/package.json
CHANGED
package/src/app-manifest.mjs
CHANGED
|
@@ -129,6 +129,14 @@ export function validateAppManifest(manifest) {
|
|
|
129
129
|
try { normalizeSafeRelativePath(relativePath, `scaffoldFiles path`); } catch (error) { errors.push(error.message); }
|
|
130
130
|
if (!entry || typeof entry.module !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.hash || "") || !["current", "drifted", "missing"].includes(entry.status) || (entry.intent != null && !["managed", "owned", "skipped"].includes(entry.intent))) errors.push(`scaffoldFiles.${relativePath} is invalid`);
|
|
131
131
|
}
|
|
132
|
+
if (manifest.migrationDeferrals != null) {
|
|
133
|
+
if (typeof manifest.migrationDeferrals !== "object" || Array.isArray(manifest.migrationDeferrals)) errors.push("migrationDeferrals must be an object");
|
|
134
|
+
else for (const [key, entry] of Object.entries(manifest.migrationDeferrals)) {
|
|
135
|
+
if (!entry || entry.reason !== "destructive" || typeof entry.cursor !== "string" || typeof entry.nextMigration !== "string") {
|
|
136
|
+
errors.push(`migrationDeferrals.${key} is invalid`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
132
140
|
if (manifest.lastDoctor != null && (typeof manifest.lastDoctor.at !== "string" || typeof manifest.lastDoctor.ok !== "boolean")) errors.push("lastDoctor is invalid");
|
|
133
141
|
if (
|
|
134
142
|
manifest.infrastructure != null
|
package/src/constants.mjs
CHANGED
|
@@ -113,8 +113,6 @@ export const MODULE_STARTER_FILES = {
|
|
|
113
113
|
projects: [
|
|
114
114
|
"app/(shell)/projetos/layout.tsx",
|
|
115
115
|
"app/(shell)/projetos/page.tsx",
|
|
116
|
-
"app/(shell)/projetos/projetos-live-mounts.tsx",
|
|
117
|
-
"app/(shell)/projetos/projetos-server-mounts.tsx",
|
|
118
116
|
"app/(shell)/projetos/[projectId]/page.tsx",
|
|
119
117
|
"app/(shell)/projetos/[projectId]/quadro/page.tsx",
|
|
120
118
|
"app/(shell)/projetos/[projectId]/tarefas/page.tsx",
|
|
@@ -122,7 +120,6 @@ export const MODULE_STARTER_FILES = {
|
|
|
122
120
|
"app/(shell)/account/projetos/loading.tsx",
|
|
123
121
|
"app/(shell)/account/projetos/[projectId]/page.tsx",
|
|
124
122
|
"app/(shell)/account/projetos/[projectId]/loading.tsx",
|
|
125
|
-
"app/api/projects/_handlers.ts",
|
|
126
123
|
"app/api/account/projects/route.ts",
|
|
127
124
|
"app/api/account/projects/[id]/route.ts",
|
|
128
125
|
"app/api/dashboard/projects/route.ts",
|
|
@@ -145,6 +142,14 @@ export const MODULE_STARTER_FILES = {
|
|
|
145
142
|
],
|
|
146
143
|
};
|
|
147
144
|
|
|
145
|
+
export const RETIRED_MODULE_STARTER_FILES = {
|
|
146
|
+
projects: [
|
|
147
|
+
"app/(shell)/projetos/projetos-live-mounts.tsx",
|
|
148
|
+
"app/(shell)/projetos/projetos-server-mounts.tsx",
|
|
149
|
+
"app/api/projects/_handlers.ts",
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
|
|
148
153
|
export const PLATFORM_STARTER_FILES = [
|
|
149
154
|
"app/(auth)/auth-provider.tsx",
|
|
150
155
|
"app/(auth)/layout.tsx",
|
|
@@ -182,13 +187,13 @@ export const PLATFORM_STARTER_FILES = [
|
|
|
182
187
|
|
|
183
188
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
184
189
|
"@brightweblabs/app-shell": "^0.16.0",
|
|
185
|
-
"@brightweblabs/core-auth": "^0.
|
|
190
|
+
"@brightweblabs/core-auth": "^0.12.0",
|
|
186
191
|
"@brightweblabs/infra": "^0.7.0",
|
|
187
|
-
"@brightweblabs/module-admin": "^0.9.
|
|
188
|
-
"@brightweblabs/module-crm": "^0.
|
|
189
|
-
"@brightweblabs/module-marketing": "^0.4.
|
|
190
|
-
"@brightweblabs/module-orgs": "^0.
|
|
191
|
-
"@brightweblabs/module-projects": "^0.
|
|
192
|
+
"@brightweblabs/module-admin": "^0.9.4",
|
|
193
|
+
"@brightweblabs/module-crm": "^0.18.0",
|
|
194
|
+
"@brightweblabs/module-marketing": "^0.4.24",
|
|
195
|
+
"@brightweblabs/module-orgs": "^0.7.0",
|
|
196
|
+
"@brightweblabs/module-projects": "^0.19.0",
|
|
192
197
|
"@brightweblabs/theme": "^0.8.3",
|
|
193
198
|
"@brightweblabs/ui": "^1.5.4",
|
|
194
199
|
"geist": "1.7.2",
|
package/src/doctor.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { stdout as output } from "node:process";
|
|
4
|
-
import {
|
|
4
|
+
import { APP_DEPENDENCY_DEFAULTS, BRIGHTWEB_PACKAGE_NAMES } from "./constants.mjs";
|
|
5
|
+
import { cursorMigrationStatus, exactMigrationCompatibilityStatus } from "./migrations.mjs";
|
|
5
6
|
import { findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, readConfiguredModuleFlags, satisfiesVersion, validateAppManifest, writeAppManifest } from "./app-manifest.mjs";
|
|
6
7
|
import { loadAppEnvironment, readFirstEnvironmentValue } from "./env.mjs";
|
|
7
8
|
import { pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
@@ -10,6 +11,143 @@ import { scaffoldDrift } from "./scaffold.mjs";
|
|
|
10
11
|
|
|
11
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`;
|
|
12
13
|
const RUNTIME_PACKAGE_NAMES = ["react", "react-dom", "next"];
|
|
14
|
+
const LOCAL_DEPENDENCY_PREFIXES = ["file:", "link:", "workspace:", "patch:"];
|
|
15
|
+
|
|
16
|
+
async function findUp(startDir, fileName) {
|
|
17
|
+
let current = path.resolve(startDir);
|
|
18
|
+
while (true) {
|
|
19
|
+
const candidate = path.join(current, fileName);
|
|
20
|
+
if (await pathExists(candidate)) return candidate;
|
|
21
|
+
const parent = path.dirname(current);
|
|
22
|
+
if (parent === current) return null;
|
|
23
|
+
current = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exactVersionFromDefault(packageName) {
|
|
28
|
+
const requested = APP_DEPENDENCY_DEFAULTS[packageName];
|
|
29
|
+
return typeof requested === "string" ? requested.match(/^(?:\^|~)?(.+)$/)?.[1] ?? null : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function escapeRegExp(value) {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function lockfileIntegrityForPackage(lockfile, packageName, version) {
|
|
37
|
+
const keyPattern = new RegExp(`^ ['\"]?${escapeRegExp(`${packageName}@${version}`)}['\"]?:$`);
|
|
38
|
+
const lines = lockfile.split("\n");
|
|
39
|
+
const start = lines.findIndex((line) => keyPattern.test(line));
|
|
40
|
+
if (start < 0) return null;
|
|
41
|
+
const block = [];
|
|
42
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
43
|
+
if (/^ \S/.test(lines[index])) break;
|
|
44
|
+
block.push(lines[index]);
|
|
45
|
+
}
|
|
46
|
+
return block.join("\n").match(/^ resolution:\s*\{[^}\n]*integrity:\s*([^,}\s]+)[^}\n]*\}/m)?.[1] ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function lockfileImporterResolution(lockfile, importerKey, packageName) {
|
|
50
|
+
const lines = lockfile.split("\n");
|
|
51
|
+
const importersIndex = lines.findIndex((line) => /^importers:\s*$/.test(line));
|
|
52
|
+
if (importersIndex < 0) return null;
|
|
53
|
+
const normalizedKey = importerKey || ".";
|
|
54
|
+
const importerPattern = new RegExp(`^ ['\"]?${escapeRegExp(normalizedKey)}['\"]?:\\s*$`);
|
|
55
|
+
const importerStart = lines.findIndex((line, index) => index > importersIndex && importerPattern.test(line));
|
|
56
|
+
if (importerStart < 0) return null;
|
|
57
|
+
let importerEnd = lines.length;
|
|
58
|
+
for (let index = importerStart + 1; index < lines.length; index += 1) {
|
|
59
|
+
if (/^ \S/.test(lines[index])) { importerEnd = index; break; }
|
|
60
|
+
}
|
|
61
|
+
const dependencyPattern = new RegExp(`^ ['\"]?${escapeRegExp(packageName)}['\"]?:\\s*$`);
|
|
62
|
+
const dependencyStart = lines.findIndex(
|
|
63
|
+
(line, index) => index > importerStart && index < importerEnd && dependencyPattern.test(line),
|
|
64
|
+
);
|
|
65
|
+
if (dependencyStart < 0) return null;
|
|
66
|
+
const block = [];
|
|
67
|
+
for (let index = dependencyStart + 1; index < importerEnd; index += 1) {
|
|
68
|
+
if (/^ \S/.test(lines[index]) || /^ \S/.test(lines[index])) break;
|
|
69
|
+
block.push(lines[index]);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
specifier: block.join("\n").match(/^ specifier:\s*['\"]?([^'\"\s]+)['\"]?\s*$/m)?.[1] ?? null,
|
|
73
|
+
version: block.join("\n").match(/^ version:\s*['\"]?([^'\"\s]+)['\"]?\s*$/m)?.[1] ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function inspectPackageProvenance(targetDir, dependencyMap) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
const verified = [];
|
|
80
|
+
const lockPath = await findUp(targetDir, "pnpm-lock.yaml");
|
|
81
|
+
const installRoot = path.join(targetDir, "node_modules");
|
|
82
|
+
if (!lockPath || !(await pathExists(installRoot))) {
|
|
83
|
+
return { issues, verified, lockPath, available: false };
|
|
84
|
+
}
|
|
85
|
+
const lockfile = lockPath ? await fs.readFile(lockPath, "utf8") : null;
|
|
86
|
+
const importerKey = lockPath
|
|
87
|
+
? path.relative(path.dirname(lockPath), targetDir).split(path.sep).join("/") || "."
|
|
88
|
+
: ".";
|
|
89
|
+
const workspaceManifest = lockPath ? await readJsonIfPresent(path.join(path.dirname(lockPath), "package.json")) : null;
|
|
90
|
+
const overrideMaps = [
|
|
91
|
+
workspaceManifest?.pnpm?.overrides,
|
|
92
|
+
workspaceManifest?.pnpm?.patchedDependencies,
|
|
93
|
+
workspaceManifest?.overrides,
|
|
94
|
+
workspaceManifest?.resolutions,
|
|
95
|
+
].filter((value) => value && typeof value === "object");
|
|
96
|
+
for (const packageName of BRIGHTWEB_PACKAGE_NAMES) {
|
|
97
|
+
const requested = dependencyMap[packageName];
|
|
98
|
+
if (!requested) continue;
|
|
99
|
+
if (LOCAL_DEPENDENCY_PREFIXES.some((prefix) => String(requested).startsWith(prefix))) {
|
|
100
|
+
issues.push(`${packageName}: local override ${JSON.stringify(requested)} is forbidden`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const overrideKey = overrideMaps.flatMap((overrides) => Object.keys(overrides)).find(
|
|
104
|
+
(key) => key === packageName || key.startsWith(`${packageName}@`),
|
|
105
|
+
);
|
|
106
|
+
if (overrideKey) {
|
|
107
|
+
issues.push(`${packageName}: workspace override or patch ${JSON.stringify(overrideKey)} is forbidden`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const expectedVersion = exactVersionFromDefault(packageName);
|
|
111
|
+
if (!expectedVersion) {
|
|
112
|
+
issues.push(`${packageName}: create-bw-app has no exact compatibility version`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const manifest = await readJsonIfPresent(path.join(targetDir, "node_modules", packageName, "package.json"));
|
|
116
|
+
if (!manifest) {
|
|
117
|
+
issues.push(`${packageName}: installed package manifest is missing`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (manifest.version !== expectedVersion) {
|
|
121
|
+
issues.push(`${packageName}: installed ${manifest.version ?? "unknown"}, expected exact ${expectedVersion}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!lockfile) {
|
|
125
|
+
issues.push(`${packageName}: pnpm-lock.yaml was not found`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const importerResolution = lockfileImporterResolution(lockfile, importerKey, packageName);
|
|
129
|
+
if (!importerResolution) {
|
|
130
|
+
issues.push(`${packageName}: target importer ${importerKey} has no lockfile resolution`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (LOCAL_DEPENDENCY_PREFIXES.some((prefix) => String(importerResolution.version).startsWith(prefix))) {
|
|
134
|
+
issues.push(`${packageName}: target importer resolves through forbidden ${JSON.stringify(importerResolution.version)}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const resolvedVersion = importerResolution.version?.split("(")[0] ?? null;
|
|
138
|
+
if (resolvedVersion !== expectedVersion) {
|
|
139
|
+
issues.push(`${packageName}: target importer resolves ${resolvedVersion ?? "unknown"}, expected exact ${expectedVersion}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const integrity = lockfileIntegrityForPackage(lockfile, packageName, expectedVersion);
|
|
143
|
+
if (!integrity?.startsWith("sha512-")) {
|
|
144
|
+
issues.push(`${packageName}@${expectedVersion}: registry integrity is missing from pnpm-lock.yaml`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
verified.push(`${packageName}@${expectedVersion}`);
|
|
148
|
+
}
|
|
149
|
+
return { issues, verified, lockPath, available: true };
|
|
150
|
+
}
|
|
13
151
|
|
|
14
152
|
function isLocalDeploymentUrl(value) {
|
|
15
153
|
try {
|
|
@@ -202,6 +340,17 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
202
340
|
}
|
|
203
341
|
add(packageProblems.length ? "FAIL" : "PASS", "packages", packageProblems.join("; ") || "Installed module packages agree with the manifest.");
|
|
204
342
|
|
|
343
|
+
const provenance = await inspectPackageProvenance(targetDir, dependencyMap);
|
|
344
|
+
if (!provenance.available) {
|
|
345
|
+
add("INFO", "package-provenance", "SKIP exact registry provenance check; install dependencies with a pnpm lockfile first.");
|
|
346
|
+
} else {
|
|
347
|
+
add(
|
|
348
|
+
provenance.issues.length ? "FAIL" : "PASS",
|
|
349
|
+
"package-provenance",
|
|
350
|
+
provenance.issues.join("; ") || `${provenance.verified.length} BrightWeb packages match the exact compatibility set and pnpm registry integrity records.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
205
354
|
const runtimeVersions = await findInstalledRuntimeVersions(targetDir);
|
|
206
355
|
const duplicateRuntimes = Object.entries(runtimeVersions)
|
|
207
356
|
.filter(([, versions]) => versions.length > 1)
|
|
@@ -276,6 +425,37 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
276
425
|
if (status.shipsMigrations && status.missing.length > 0) migrationProblems.push(`${key}: ${status.missing.join(", ")}`);
|
|
277
426
|
}
|
|
278
427
|
add(migrationProblems.length ? "FAIL" : "PASS", "migrations", migrationProblems.join("; ") || "Migration cursors and flattened files agree.");
|
|
428
|
+
for (const key of migrationKeys) {
|
|
429
|
+
const cursor = appManifest.migrationCursor?.[key];
|
|
430
|
+
if (cursor == null && appManifest.adoptionNotes?.allowUncursored) {
|
|
431
|
+
add("WARN", `migration-provenance-${key}`, `${key}: exact migration provenance is unavailable because uncursored adoption is enabled.`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const status = await exactMigrationCompatibilityStatus({
|
|
435
|
+
targetDir,
|
|
436
|
+
moduleKey: key,
|
|
437
|
+
cursor,
|
|
438
|
+
catalogEntry: catalog[key],
|
|
439
|
+
allowDeferred: appManifest.migrationDeferrals?.[key]?.reason === "destructive"
|
|
440
|
+
&& appManifest.migrationDeferrals[key].cursor === cursor,
|
|
441
|
+
});
|
|
442
|
+
if (!status.shipsMigrations) continue;
|
|
443
|
+
const deferral = appManifest.migrationDeferrals?.[key];
|
|
444
|
+
if (deferral && (!status.deferred?.length || deferral.nextMigration !== status.deferred[0] || !status.nextDeferredIsDestructive)) {
|
|
445
|
+
status.issues.push(`recorded destructive deferral ${deferral.nextMigration} does not match the next destructive package migration`);
|
|
446
|
+
}
|
|
447
|
+
add(
|
|
448
|
+
status.issues.length ? "FAIL" : "PASS",
|
|
449
|
+
`migration-provenance-${key}`,
|
|
450
|
+
status.issues.join("; ") || `${status.verified.length} ${key} migration files match compatible package provenance, source filename, current cursor, and sha256 content.`,
|
|
451
|
+
);
|
|
452
|
+
if (status.deferred?.length && status.issues.length === 0) {
|
|
453
|
+
add("INFO", `migration-deferred-${key}`, `${key}: ${status.deferred.length} later package migration${status.deferred.length === 1 ? " is" : "s are"} intentionally outside cursor ${cursor}: ${status.deferred.join(", ")}.`);
|
|
454
|
+
}
|
|
455
|
+
if (status.legacyEquivalent?.length) {
|
|
456
|
+
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
|
+
}
|
|
458
|
+
}
|
|
279
459
|
add("WARN", "db-objects", "SKIP live database checks are not available yet.");
|
|
280
460
|
return finish(checks, argvOptions, appManifest, targetDir);
|
|
281
461
|
}
|
package/src/generator.mjs
CHANGED
|
@@ -1044,7 +1044,7 @@ export function createModuleToolbarControlsConfig(selectedModules) {
|
|
|
1044
1044
|
}
|
|
1045
1045
|
if (selectedModules.includes("projects")) {
|
|
1046
1046
|
imports.push('import { ProjectBoardToolbarControls, ProjectsToolbarControls } from "@brightweblabs/module-projects/ui";');
|
|
1047
|
-
entries.push(' projects: () => <ProjectsToolbarControls />,');
|
|
1047
|
+
entries.push(' projects: (viewer) => <ProjectsToolbarControls viewer={viewer} />,');
|
|
1048
1048
|
entries.push(' "project-board": () => <ProjectBoardToolbarControls />,');
|
|
1049
1049
|
}
|
|
1050
1050
|
if (selectedModules.includes("marketing")) {
|
|
@@ -1060,13 +1060,15 @@ export function createModuleToolbarControlsConfig(selectedModules) {
|
|
|
1060
1060
|
"// MANAGED BY BRIGHTWEB — regenerated when modules are added, removed, or updated.",
|
|
1061
1061
|
...imports,
|
|
1062
1062
|
"",
|
|
1063
|
-
"
|
|
1063
|
+
"type ModuleToolbarViewer = { isAdmin: boolean };",
|
|
1064
|
+
"",
|
|
1065
|
+
"const toolbarControlBySurface: Partial<Record<ShellToolbarSurface, (viewer: ModuleToolbarViewer) => ReactNode>> = {",
|
|
1064
1066
|
...entries,
|
|
1065
1067
|
"};",
|
|
1066
1068
|
"",
|
|
1067
|
-
"export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[]) {",
|
|
1069
|
+
"export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[], viewer: ModuleToolbarViewer) {",
|
|
1068
1070
|
" const surface = resolveShellToolbarSurface(pathname, toolbarRoutes);",
|
|
1069
|
-
" return surface ? toolbarControlBySurface[surface]?.() ?? null : null;",
|
|
1071
|
+
" return surface ? toolbarControlBySurface[surface]?.(viewer) ?? null : null;",
|
|
1070
1072
|
"}",
|
|
1071
1073
|
"",
|
|
1072
1074
|
].join("\n");
|
|
@@ -1074,11 +1076,11 @@ export function createModuleToolbarControlsConfig(selectedModules) {
|
|
|
1074
1076
|
|
|
1075
1077
|
function createOrganizationRoute(methods, enabled) {
|
|
1076
1078
|
const lines = ['export const dynamic = "force-dynamic";', ""];
|
|
1077
|
-
for (const { method, handler, context = false } of methods) {
|
|
1079
|
+
for (const { method, handler, context = false, packageName = "@brightweblabs/module-orgs" } of methods) {
|
|
1078
1080
|
if (enabled) {
|
|
1079
1081
|
lines.push(
|
|
1080
1082
|
`export async function ${method}(request: Request${context ? ', context: { params: Promise<{ id: string }> }' : ""}) {`,
|
|
1081
|
-
` const { ${handler} } = await import("
|
|
1083
|
+
` const { ${handler} } = await import("${packageName}");`,
|
|
1082
1084
|
` return ${handler}(request${context ? ", context" : ""});`,
|
|
1083
1085
|
"}",
|
|
1084
1086
|
"",
|
|
@@ -1214,20 +1216,34 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1214
1216
|
return {
|
|
1215
1217
|
"app/api/invitations/_dependencies.ts": invitationDependencies,
|
|
1216
1218
|
"app/api/organizations/route.ts": createOrganizationRoute([
|
|
1217
|
-
|
|
1219
|
+
crmEnabled
|
|
1220
|
+
? { method: "POST", handler: "handleCrmOrganizationsPostRequest", packageName: "@brightweblabs/module-crm" }
|
|
1221
|
+
: { method: "POST", handler: "handleOrganizationsPostRequest" },
|
|
1218
1222
|
], orgsEnabled),
|
|
1219
1223
|
"app/api/organizations/[id]/route.ts": createOrganizationRoute([
|
|
1220
|
-
|
|
1224
|
+
crmEnabled
|
|
1225
|
+
? { method: "PATCH", handler: "handleCrmOrganizationPatchRequest", packageName: "@brightweblabs/module-crm", context: true }
|
|
1226
|
+
: { method: "PATCH", handler: "handleOrganizationPatchRequest", context: true },
|
|
1221
1227
|
{ method: "DELETE", handler: "handleOrganizationDeleteRequest", context: true },
|
|
1222
1228
|
], orgsEnabled),
|
|
1223
1229
|
"app/api/organizations/[id]/invitations/route.ts": createOrganizationRoute([
|
|
1224
1230
|
{ method: "GET", handler: "handleOrganizationInvitationsGetRequest", context: true },
|
|
1225
|
-
|
|
1231
|
+
crmEnabled
|
|
1232
|
+
? { method: "POST", handler: "handleCrmOrganizationInvitationsPostRequest", packageName: "@brightweblabs/module-crm", context: true }
|
|
1233
|
+
: { method: "POST", handler: "handleOrganizationInvitationsPostRequest", context: true },
|
|
1226
1234
|
], orgsEnabled),
|
|
1227
1235
|
"app/api/organizations/[id]/invitations/[invitationId]/route.ts": orgsEnabled
|
|
1228
1236
|
? [
|
|
1229
1237
|
'export const dynamic = "force-dynamic";',
|
|
1230
1238
|
"",
|
|
1239
|
+
"export async function POST(",
|
|
1240
|
+
" request: Request,",
|
|
1241
|
+
" context: { params: Promise<{ id: string; invitationId: string }> },",
|
|
1242
|
+
") {",
|
|
1243
|
+
' const { handleOrganizationInvitationResendRequest } = await import("@brightweblabs/module-orgs");',
|
|
1244
|
+
" return handleOrganizationInvitationResendRequest(request, context);",
|
|
1245
|
+
"}",
|
|
1246
|
+
"",
|
|
1231
1247
|
"export async function DELETE(",
|
|
1232
1248
|
" request: Request,",
|
|
1233
1249
|
" context: { params: Promise<{ id: string; invitationId: string }> },",
|
|
@@ -1242,6 +1258,10 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1242
1258
|
"",
|
|
1243
1259
|
'type RouteContext = { params: Promise<{ id: string; invitationId: string }> };',
|
|
1244
1260
|
"",
|
|
1261
|
+
"export async function POST(_request: Request, _context: RouteContext) {",
|
|
1262
|
+
" return new Response(null, { status: 404 });",
|
|
1263
|
+
"}",
|
|
1264
|
+
"",
|
|
1245
1265
|
"export async function DELETE(_request: Request, _context: RouteContext) {",
|
|
1246
1266
|
" return new Response(null, { status: 404 });",
|
|
1247
1267
|
"}",
|
package/src/migrations.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { resolveSafeRelativePath } from "./safe-path.mjs";
|
|
4
5
|
import { TEMPLATE_ROOT, pathExists } from "./generator.mjs";
|
|
5
6
|
|
|
@@ -49,13 +50,17 @@ export async function planMigrationAppends({
|
|
|
49
50
|
}) {
|
|
50
51
|
const migrationsDir = await findAppMigrationsDirectory(targetDir);
|
|
51
52
|
const existing = (await pathExists(migrationsDir))
|
|
52
|
-
? (await fs.readdir(migrationsDir)).filter((fileName) => fileName.endsWith(".sql")).sort()
|
|
53
|
+
? await Promise.all((await fs.readdir(migrationsDir)).filter((fileName) => fileName.endsWith(".sql")).sort().map(async (fileName) => ({
|
|
54
|
+
fileName,
|
|
55
|
+
targetPath: path.join(migrationsDir, fileName),
|
|
56
|
+
content: await fs.readFile(path.join(migrationsDir, fileName), "utf8"),
|
|
57
|
+
})))
|
|
53
58
|
: [];
|
|
54
|
-
let sequence = existing.reduce((maximum,
|
|
55
|
-
const match = fileName.match(/^(\d+)_/);
|
|
59
|
+
let sequence = existing.reduce((maximum, entry) => {
|
|
60
|
+
const match = entry.fileName.match(/^(\d+)_/);
|
|
56
61
|
return Math.max(maximum, Number(match?.[1] || 0));
|
|
57
62
|
}, 0);
|
|
58
|
-
const
|
|
63
|
+
const appends = [];
|
|
59
64
|
const deferred = [];
|
|
60
65
|
const nextCursor = { ...migrationCursor };
|
|
61
66
|
for (const moduleKey of moduleKeys) {
|
|
@@ -88,7 +93,7 @@ export async function planMigrationAppends({
|
|
|
88
93
|
const targetFileName = `${String(sequence).padStart(4, "0")}_${moduleKey}__${entry.fileName}`;
|
|
89
94
|
const source = await fs.readFile(entry.sourcePath, "utf8");
|
|
90
95
|
const version = catalog[moduleKey]?.version || "unknown";
|
|
91
|
-
|
|
96
|
+
appends.push({
|
|
92
97
|
moduleKey,
|
|
93
98
|
originalFileName: entry.fileName,
|
|
94
99
|
targetFileName,
|
|
@@ -98,7 +103,7 @@ export async function planMigrationAppends({
|
|
|
98
103
|
}
|
|
99
104
|
if (migrationsInScope.length > 0) nextCursor[moduleKey] = migrationsInScope.at(-1).fileName;
|
|
100
105
|
}
|
|
101
|
-
return { writes, deferred, nextCursor };
|
|
106
|
+
return { writes: appends, repairs: [], appends, deferred, nextCursor };
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export async function applyMigrationWrites(writes) {
|
|
@@ -131,3 +136,108 @@ export async function cursorMigrationStatus({ targetDir, moduleKey, cursor, cata
|
|
|
131
136
|
})).map((entry) => entry.fileName),
|
|
132
137
|
};
|
|
133
138
|
}
|
|
139
|
+
|
|
140
|
+
function sha256(source) {
|
|
141
|
+
return createHash("sha256").update(source).digest("hex");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Historical generated files are immutable. These hashes are reviewed SQL-equivalent
|
|
145
|
+
// renderings shipped by early consumers before canonical provenance was enforced.
|
|
146
|
+
const REVIEWED_LEGACY_MIGRATION_HASHES = new Map([
|
|
147
|
+
["core/20260731120000_core_notifications.sql", new Set(["20fbd2158741871ac6b65c4033db5d7c9e889b03a5876d1f32f2b368b35cf24a"])],
|
|
148
|
+
["projects/20260731121000_project_notification_audiences.sql", new Set(["1726f2f2403401cda677419e26b49ffa521e49ef0bfa72b56e42428ded7c6312"])],
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
function compareSemver(left, right) {
|
|
152
|
+
const leftParts = String(left).split(".").map((value) => Number.parseInt(value, 10));
|
|
153
|
+
const rightParts = String(right).split(".").map((value) => Number.parseInt(value, 10));
|
|
154
|
+
if (leftParts.length !== 3 || rightParts.length !== 3 || [...leftParts, ...rightParts].some(Number.isNaN)) return null;
|
|
155
|
+
for (let index = 0; index < 3; index += 1) {
|
|
156
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] < rightParts[index] ? -1 : 1;
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function exactMigrationCompatibilityStatus({ targetDir, moduleKey, cursor, catalogEntry, allowDeferred = false }) {
|
|
162
|
+
const migrations = await getModuleMigrations(moduleKey, catalogEntry);
|
|
163
|
+
if (migrations.length === 0) return { shipsMigrations: false, issues: [], verified: [] };
|
|
164
|
+
const issues = [];
|
|
165
|
+
const latest = migrations.at(-1)?.fileName ?? null;
|
|
166
|
+
let migrationsInScope = [];
|
|
167
|
+
if (!cursor) {
|
|
168
|
+
issues.push("migration cursor is missing");
|
|
169
|
+
} else if (!migrations.some((entry) => entry.fileName === cursor)) {
|
|
170
|
+
issues.push(`cursor ${cursor} does not exist in the shipped migration history`);
|
|
171
|
+
} else {
|
|
172
|
+
migrationsInScope = migrations.filter((entry) => entry.fileName <= cursor);
|
|
173
|
+
if (cursor !== latest && !allowDeferred) {
|
|
174
|
+
issues.push(`cursor ${cursor} is stale; exact package compatibility requires ${latest}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const migrationsDir = await findAppMigrationsDirectory(targetDir);
|
|
179
|
+
const installed = [];
|
|
180
|
+
if (await pathExists(migrationsDir)) {
|
|
181
|
+
for (const fileName of await fs.readdir(migrationsDir)) {
|
|
182
|
+
if (!fileName.endsWith(".sql")) continue;
|
|
183
|
+
installed.push({ fileName, content: await fs.readFile(path.join(migrationsDir, fileName), "utf8") });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const verified = [];
|
|
188
|
+
const legacyEquivalent = [];
|
|
189
|
+
for (const entry of migrationsInScope) {
|
|
190
|
+
const matches = installed.filter(({ fileName, content }) => {
|
|
191
|
+
const header = content.match(/^\s*--\s*bw-module:\s*([^@\s]+)@([^\s]+)\s+([^\s]+)\s*\n/im);
|
|
192
|
+
return (header?.[1] === moduleKey && header?.[3] === entry.fileName)
|
|
193
|
+
|| fileName === entry.fileName
|
|
194
|
+
|| fileName.endsWith(`_${moduleKey}__${entry.fileName}`);
|
|
195
|
+
});
|
|
196
|
+
if (matches.length === 0) {
|
|
197
|
+
issues.push(`${entry.fileName}: generated migration file is missing`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (matches.length > 1) {
|
|
201
|
+
issues.push(`${entry.fileName}: appears in multiple generated migration files (${matches.map(({ fileName }) => fileName).join(", ")})`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const [{ fileName, content }] = matches;
|
|
205
|
+
const header = content.match(/^\s*--\s*bw-module:\s*([^@\s]+)@([^\s]+)\s+([^\s]+)\s*\n/im);
|
|
206
|
+
const expectedVersion = catalogEntry?.version;
|
|
207
|
+
if (expectedVersion && header) {
|
|
208
|
+
const comparison = compareSemver(header[2], expectedVersion);
|
|
209
|
+
if (comparison == null || comparison > 0) {
|
|
210
|
+
issues.push(`${fileName}: provenance version ${header[2]} is incompatible with installed ${moduleKey}@${expectedVersion}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const source = await fs.readFile(entry.sourcePath, "utf8");
|
|
215
|
+
const generatedSource = header ? content.slice(header[0].length) : content;
|
|
216
|
+
const expectedHash = sha256(source);
|
|
217
|
+
const actualHash = sha256(generatedSource);
|
|
218
|
+
if (actualHash !== expectedHash) {
|
|
219
|
+
if (REVIEWED_LEGACY_MIGRATION_HASHES.get(`${moduleKey}/${entry.fileName}`)?.has(actualHash)) {
|
|
220
|
+
verified.push({ fileName, originalFileName: entry.fileName, sha256: actualHash });
|
|
221
|
+
legacyEquivalent.push({ fileName, originalFileName: entry.fileName, sha256: actualHash, canonicalSha256: expectedHash });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
issues.push(`${fileName}: sha256 ${actualHash} does not match ${entry.fileName} sha256 ${expectedHash}`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
verified.push({ fileName, originalFileName: entry.fileName, sha256: expectedHash });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const deferredEntries = cursor && migrations.some((entry) => entry.fileName === cursor)
|
|
231
|
+
? migrations.filter((entry) => entry.fileName > cursor)
|
|
232
|
+
: [];
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
shipsMigrations: true,
|
|
236
|
+
latest,
|
|
237
|
+
issues,
|
|
238
|
+
verified,
|
|
239
|
+
legacyEquivalent,
|
|
240
|
+
deferred: deferredEntries.map((entry) => entry.fileName),
|
|
241
|
+
nextDeferredIsDestructive: deferredEntries[0]?.destructive === true,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/src/upgrade.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { pathExists, runInstall } from "./generator.mjs";
|
|
|
6
6
|
import { applyMigrationWrites, getModuleMigrations, planMigrationAppends } from "./migrations.mjs";
|
|
7
7
|
import { buildBrightwebAppUpdatePlan } from "./update.mjs";
|
|
8
8
|
import { resolveSafeRelativePath } from "./safe-path.mjs";
|
|
9
|
+
import { RETIRED_MODULE_STARTER_FILES } from "./constants.mjs";
|
|
9
10
|
|
|
10
11
|
const HELP = `Usage: bw upgrade [moduleKey] [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --through-migration <file> Advance the named module only through this migration\n --include-destructive-migrations Explicitly include held destructive migrations\n --allow-stale-fallback Use baked-in versions if npm lookup fails\n --install Install changed dependencies\n --refresh-starters Refresh unchanged starter files\n --dry-run Print the upgrade plan without writing\n --help Show this help`;
|
|
11
12
|
|
|
@@ -40,6 +41,19 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
40
41
|
if (await hashFile(filePath) !== record.hash) drifted.push(relativePath);
|
|
41
42
|
}
|
|
42
43
|
const protectedPaths = new Set([...drifted, ...intentional]);
|
|
44
|
+
const obsoleteScaffoldFiles = [];
|
|
45
|
+
if (argvOptions.refreshStarters) {
|
|
46
|
+
for (const moduleKey of Object.keys(appManifest.modules)) {
|
|
47
|
+
for (const relativePath of RETIRED_MODULE_STARTER_FILES[moduleKey] ?? []) {
|
|
48
|
+
const record = appManifest.scaffoldFiles[relativePath];
|
|
49
|
+
if (!record || protectedPaths.has(relativePath)) continue;
|
|
50
|
+
const targetPath = resolveSafeRelativePath(targetDir, relativePath, "Obsolete scaffold file path");
|
|
51
|
+
if (!(await pathExists(targetPath)) || await hashFile(targetPath) !== record.hash) continue;
|
|
52
|
+
obsoleteScaffoldFiles.push({ relativePath, targetPath });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
plan.fileDeletes = obsoleteScaffoldFiles;
|
|
43
57
|
plan.fileWrites = plan.fileWrites.filter((entry) => entry.type !== "starter" || !protectedPaths.has(entry.relativePath));
|
|
44
58
|
plan.starterFilesToRefresh = plan.fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath);
|
|
45
59
|
plan.starterFilesDrifted = Array.from(new Set([...plan.starterFilesDrifted, ...drifted]));
|
|
@@ -93,7 +107,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
93
107
|
migrationCursor: appManifest.migrationCursor,
|
|
94
108
|
migrationUpperBounds,
|
|
95
109
|
});
|
|
96
|
-
output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nMigrations to append: ${migrationPlan.
|
|
110
|
+
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`);
|
|
97
111
|
if (throughMigration) output.write(`Migration cutoff: ${moduleKey} through ${throughMigration}\n`);
|
|
98
112
|
for (const boundary of safetyBoundaries) {
|
|
99
113
|
output.write(`Migration safety boundary: ${boundary.moduleKey} through ${boundary.boundary}; held ${boundary.destructiveMigration}\n`);
|
|
@@ -107,14 +121,33 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
107
121
|
for (const relativePath of missing) output.write(`- missing: ${relativePath}\n`);
|
|
108
122
|
for (const relativePath of drifted) output.write(`- drifted: ${relativePath}\n`);
|
|
109
123
|
for (const relativePath of intentional) output.write(`- intent-protected: ${relativePath}\n`);
|
|
124
|
+
for (const entry of plan.fileDeletes) output.write(`- obsolete managed file: ${entry.relativePath}\n`);
|
|
110
125
|
if (argvOptions.dryRun) return { dryRun: true, plan, migrationPlan, drifted, missing };
|
|
111
126
|
|
|
112
127
|
for (const write of plan.fileWrites) {
|
|
113
128
|
await fs.mkdir(path.dirname(write.targetPath), { recursive: true });
|
|
114
129
|
await fs.writeFile(write.targetPath, write.content, "utf8");
|
|
115
130
|
}
|
|
131
|
+
for (const deletion of plan.fileDeletes) {
|
|
132
|
+
await fs.unlink(deletion.targetPath);
|
|
133
|
+
delete appManifest.scaffoldFiles[deletion.relativePath];
|
|
134
|
+
}
|
|
116
135
|
await applyMigrationWrites(migrationPlan.writes);
|
|
117
136
|
appManifest.migrationCursor = migrationPlan.nextCursor;
|
|
137
|
+
appManifest.migrationDeferrals ??= {};
|
|
138
|
+
for (const key of moduleKeys) {
|
|
139
|
+
const firstDeferred = migrationPlan.deferred.find((entry) => entry.moduleKey === key);
|
|
140
|
+
if (firstDeferred?.destructive) {
|
|
141
|
+
appManifest.migrationDeferrals[key] = {
|
|
142
|
+
reason: "destructive",
|
|
143
|
+
cursor: migrationPlan.nextCursor[key],
|
|
144
|
+
nextMigration: firstDeferred.fileName,
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
delete appManifest.migrationDeferrals[key];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (Object.keys(appManifest.migrationDeferrals).length === 0) delete appManifest.migrationDeferrals;
|
|
118
151
|
for (const [key, entry] of Object.entries(appManifest.modules)) {
|
|
119
152
|
const packageName = catalog[key]?.packageName;
|
|
120
153
|
if (catalog[key]?.packageRoot || plan.targetVersions?.[packageName]) entry.version = catalog[key].version;
|
|
@@ -144,7 +177,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
144
177
|
const runner = runtimeOptions.installRunner || runInstall;
|
|
145
178
|
await runner(plan.packageManager, plan.dependencyMode === "workspace" && plan.workspaceRoot ? plan.workspaceRoot : targetDir);
|
|
146
179
|
}
|
|
147
|
-
output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"} and ${migrationPlan.
|
|
180
|
+
output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"}, removed ${plan.fileDeletes.length} obsolete managed file${plan.fileDeletes.length === 1 ? "" : "s"}, and added ${migrationPlan.appends.length} new migration${migrationPlan.appends.length === 1 ? "" : "s"}.\n`);
|
|
148
181
|
return { dryRun: false, plan, migrationPlan, drifted, missing };
|
|
149
182
|
}
|
|
150
183
|
|
|
@@ -84,7 +84,7 @@ function ShellLayoutInner({
|
|
|
84
84
|
viewer.email ||
|
|
85
85
|
"Conta";
|
|
86
86
|
const projectsBaseHref = pathname.startsWith("/projects") ? "/projects" : "/projetos";
|
|
87
|
-
const toolbarControls = getModuleToolbarControls(pathname, toolbarRoutes);
|
|
87
|
+
const toolbarControls = getModuleToolbarControls(pathname, toolbarRoutes, viewer);
|
|
88
88
|
|
|
89
89
|
const dispatchShellAction = useShellActionDispatch();
|
|
90
90
|
useShellAction("projects-back-to-portfolio", () => {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
2
|
|
|
3
|
+
export async function POST(
|
|
4
|
+
request: Request,
|
|
5
|
+
context: { params: Promise<{ id: string; invitationId: string }> },
|
|
6
|
+
) {
|
|
7
|
+
const { handleOrganizationInvitationResendRequest } = await import("@brightweblabs/module-orgs");
|
|
8
|
+
return handleOrganizationInvitationResendRequest(request, context);
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export async function DELETE(
|
|
4
12
|
request: Request,
|
|
5
13
|
context: { params: Promise<{ id: string; invitationId: string }> },
|
|
@@ -6,6 +6,6 @@ export async function GET(request: Request, context: { params: Promise<{ id: str
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export async function POST(request: Request, context: { params: Promise<{ id: string }> }) {
|
|
9
|
-
const {
|
|
10
|
-
return
|
|
9
|
+
const { handleCrmOrganizationInvitationsPostRequest } = await import("@brightweblabs/module-crm");
|
|
10
|
+
return handleCrmOrganizationInvitationsPostRequest(request, context);
|
|
11
11
|
}
|