create-bw-app 0.9.0 → 0.9.1

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Scaffold a new BrightWeb app from either the `platform` or `site` starter.
4
4
 
5
+ The CLI can also update an existing generated platform app in place.
6
+
5
7
  ## Workspace usage
6
8
 
7
9
  From the BrightWeb platform repo root:
@@ -21,9 +23,29 @@ Once this package is published to npm:
21
23
  ```bash
22
24
  pnpm dlx create-bw-app
23
25
  pnpm dlx create-bw-app --template site
26
+ pnpm dlx create-bw-app update
24
27
  npm create bw-app@latest
25
28
  ```
26
29
 
30
+ ## Update existing apps
31
+
32
+ Run the updater from an existing generated app directory, or point it at one with `--target-dir`:
33
+
34
+ ```bash
35
+ pnpm dlx create-bw-app update
36
+ pnpm dlx create-bw-app update --dry-run
37
+ pnpm dlx create-bw-app update --refresh-starters
38
+ pnpm dlx create-bw-app update --target-dir ./apps/client-portal
39
+ ```
40
+
41
+ Current updater behavior:
42
+
43
+ - updates installed `@brightweblabs/*` packages only
44
+ - re-syncs managed BrightWeb config files such as `next.config.ts`, `config/modules.ts`, and `config/shell.ts`
45
+ - reports missing or drifted starter files and only rewrites them with `--refresh-starters`
46
+ - prints the follow-up install command unless `--install` is passed
47
+ - preserves unrelated third-party dependencies and app-owned product pages
48
+
27
49
  ## Template behavior
28
50
 
29
51
  - prompts for app type: `platform` or `site`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.9.0",
4
+ "version": "0.9.1",
5
5
  "type": "module",
6
6
  "bin": "bin/create-bw-app.mjs",
7
7
  "files": [
package/src/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { HELP_TEXT } from "./constants.mjs";
2
2
  import { createBrightwebClientApp } from "./generator.mjs";
3
+ import { updateBrightwebApp } from "./update.mjs";
3
4
 
4
5
  function toCamelCase(flagName) {
5
6
  return flagName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
@@ -56,7 +57,8 @@ function parseArgv(argv) {
56
57
  }
57
58
 
58
59
  export async function runCreateBwAppCli(argv = process.argv.slice(2), runtimeOptions = {}) {
59
- const argvOptions = parseArgv(argv);
60
+ const isUpdateCommand = argv[0] === "update";
61
+ const argvOptions = parseArgv(isUpdateCommand ? argv.slice(1) : argv);
60
62
 
61
63
  if (argvOptions.help) {
62
64
  process.stdout.write(`${HELP_TEXT}\n`);
@@ -64,6 +66,11 @@ export async function runCreateBwAppCli(argv = process.argv.slice(2), runtimeOpt
64
66
  }
65
67
 
66
68
  try {
69
+ if (isUpdateCommand) {
70
+ await updateBrightwebApp(argvOptions, runtimeOptions);
71
+ return;
72
+ }
73
+
67
74
  await createBrightwebClientApp(argvOptions, runtimeOptions);
68
75
  } catch (error) {
69
76
  const message = error instanceof Error ? error.message : "Unknown error";
package/src/constants.mjs CHANGED
@@ -42,6 +42,29 @@ export const CORE_PACKAGES = [
42
42
  "@brightweblabs/ui",
43
43
  ];
44
44
 
45
+ export const BRIGHTWEB_PACKAGE_NAMES = [
46
+ ...CORE_PACKAGES,
47
+ ...SELECTABLE_MODULES.map((moduleDefinition) => moduleDefinition.packageName),
48
+ ];
49
+
50
+ export const MODULE_STARTER_FILES = {
51
+ admin: [
52
+ "app/api/admin/users/route.ts",
53
+ "app/api/admin/users/roles/route.ts",
54
+ "app/playground/admin/page.tsx",
55
+ ],
56
+ crm: [
57
+ "app/api/crm/contacts/route.ts",
58
+ "app/api/crm/organizations/route.ts",
59
+ "app/api/crm/owners/route.ts",
60
+ "app/api/crm/stats/route.ts",
61
+ "app/playground/crm/page.tsx",
62
+ ],
63
+ projects: [
64
+ "app/playground/projects/page.tsx",
65
+ ],
66
+ };
67
+
45
68
  export const APP_DEPENDENCY_DEFAULTS = {
46
69
  "@brightweblabs/app-shell": "^0.1.1",
47
70
  "@brightweblabs/core-auth": "^0.1.1",
@@ -93,8 +116,9 @@ export const DEFAULTS = {
93
116
  export const HELP_TEXT = `
94
117
  Usage:
95
118
  create-bw-app [options]
119
+ create-bw-app update [options]
96
120
 
97
- Options:
121
+ Scaffold options:
98
122
  --template <platform|site> Scaffold a platform app or a standalone site
99
123
  --name, --slug <name> Project name and default directory name
100
124
  --modules <list> Comma-separated modules: crm,projects,admin
@@ -107,4 +131,12 @@ Options:
107
131
  --yes Accept defaults for any missing optional prompt
108
132
  --dry-run Print planned actions without writing files
109
133
  --help Show this help message
134
+
135
+ Update options:
136
+ --target-dir <path> Existing app directory to update (defaults to cwd)
137
+ --workspace-root <path> BrightWeb workspace root for workspace:* apps
138
+ --package-manager <name> Override package manager: pnpm, npm, yarn, or bun
139
+ --install Run install after writing package changes
140
+ --refresh-starters Rewrite starter route files from the latest template
141
+ --dry-run Print the update plan without writing files
110
142
  `.trim();
package/src/generator.mjs CHANGED
@@ -16,8 +16,8 @@ import {
16
16
  TEMPLATE_OPTIONS,
17
17
  } from "./constants.mjs";
18
18
 
19
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
- const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
19
+ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ export const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
21
21
  const TEMPLATE_KEY_SET = new Set(TEMPLATE_OPTIONS.map((templateOption) => templateOption.key));
22
22
  const DEFAULT_DB_MODULE_REGISTRY = {
23
23
  modules: {
@@ -77,7 +77,7 @@ function parseTemplateInput(rawValue) {
77
77
  throw new Error(`Unknown template: ${rawValue}`);
78
78
  }
79
79
 
80
- function detectPackageManager(explicitManager) {
80
+ export function detectPackageManager(explicitManager) {
81
81
  if (explicitManager) return explicitManager;
82
82
 
83
83
  const userAgent = process.env.npm_config_user_agent || "";
@@ -88,13 +88,13 @@ function detectPackageManager(explicitManager) {
88
88
  return "pnpm";
89
89
  }
90
90
 
91
- function sortObjectKeys(inputObject) {
91
+ export function sortObjectKeys(inputObject) {
92
92
  return Object.fromEntries(
93
93
  Object.entries(inputObject).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)),
94
94
  );
95
95
  }
96
96
 
97
- async function pathExists(targetPath) {
97
+ export async function pathExists(targetPath) {
98
98
  try {
99
99
  await fs.access(targetPath);
100
100
  return true;
@@ -103,7 +103,7 @@ async function pathExists(targetPath) {
103
103
  }
104
104
  }
105
105
 
106
- async function readJsonIfPresent(filePath) {
106
+ export async function readJsonIfPresent(filePath) {
107
107
  if (!(await pathExists(filePath))) {
108
108
  return null;
109
109
  }
@@ -111,7 +111,7 @@ async function readJsonIfPresent(filePath) {
111
111
  return JSON.parse(await fs.readFile(filePath, "utf8"));
112
112
  }
113
113
 
114
- async function getDbModuleRegistry(workspaceRoot) {
114
+ export async function getDbModuleRegistry(workspaceRoot) {
115
115
  if (!workspaceRoot) {
116
116
  return DEFAULT_DB_MODULE_REGISTRY;
117
117
  }
@@ -171,7 +171,7 @@ function getModuleLabel(moduleKey) {
171
171
  return titleizeSlug(moduleKey);
172
172
  }
173
173
 
174
- function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
174
+ export function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
175
175
  if (!workspaceMode) {
176
176
  return {
177
177
  selectedLabels: getSelectedModuleLabels(selectedModules),
@@ -218,7 +218,7 @@ function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
218
218
  };
219
219
  }
220
220
 
221
- async function getVersionMap(workspaceRoot) {
221
+ export async function getVersionMap(workspaceRoot) {
222
222
  const versionMap = {
223
223
  ...APP_DEPENDENCY_DEFAULTS,
224
224
  ...APP_DEV_DEPENDENCY_DEFAULTS,
@@ -294,7 +294,7 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
294
294
  ].join("\n");
295
295
  }
296
296
 
297
- function createPlatformModulesConfigFile(selectedModules) {
297
+ export function createPlatformModulesConfigFile(selectedModules) {
298
298
  const selected = new Set(selectedModules);
299
299
 
300
300
  return [
@@ -515,7 +515,7 @@ function createSiteReadme({ slug, workspaceMode, packageManager }) {
515
515
  ].join("\n");
516
516
  }
517
517
 
518
- function createPackageJson({
518
+ export function createPackageJson({
519
519
  slug,
520
520
  dependencyMode,
521
521
  selectedModules,
@@ -595,7 +595,7 @@ function createPackageJson({
595
595
  };
596
596
  }
597
597
 
598
- function createNextConfig({ template, selectedModules }) {
598
+ export function createNextConfig({ template, selectedModules }) {
599
599
  if (template === "site") {
600
600
  return [
601
601
  'import type { NextConfig } from "next";',
@@ -629,7 +629,7 @@ function createNextConfig({ template, selectedModules }) {
629
629
  ].join("\n");
630
630
  }
631
631
 
632
- function createShellConfig(selectedModules) {
632
+ export function createShellConfig(selectedModules) {
633
633
  const importLines = [];
634
634
  const registrationLines = [];
635
635
 
@@ -745,7 +745,7 @@ async function copyDirectory(sourceDir, targetDir) {
745
745
  await fs.cp(sourceDir, targetDir, { recursive: true });
746
746
  }
747
747
 
748
- async function ensureDirectory(targetDir) {
748
+ export async function ensureDirectory(targetDir) {
749
749
  await fs.mkdir(targetDir, { recursive: true });
750
750
  }
751
751
 
@@ -793,7 +793,7 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
793
793
  );
794
794
  }
795
795
 
796
- async function runInstall(command, cwd) {
796
+ export async function runInstall(command, cwd) {
797
797
  return new Promise((resolve, reject) => {
798
798
  const child = spawn(command, ["install"], {
799
799
  cwd,
@@ -1140,7 +1140,8 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1140
1140
 
1141
1141
  if (install) {
1142
1142
  const installCwd = workspaceMode ? workspaceRoot : targetDir;
1143
- await runInstall(packageManager, installCwd);
1143
+ const installRunner = runtimeOptions.installRunner || runInstall;
1144
+ await installRunner(packageManager, installCwd);
1144
1145
  }
1145
1146
 
1146
1147
  printCompletionMessage({
package/src/update.mjs ADDED
@@ -0,0 +1,481 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stdout as output } from "node:process";
4
+ import {
5
+ BRIGHTWEB_PACKAGE_NAMES,
6
+ CLI_DISPLAY_NAME,
7
+ MODULE_STARTER_FILES,
8
+ SELECTABLE_MODULES,
9
+ } from "./constants.mjs";
10
+ import {
11
+ TEMPLATE_ROOT,
12
+ createDbInstallPlan,
13
+ createNextConfig,
14
+ createPackageJson,
15
+ createPlatformModulesConfigFile,
16
+ createShellConfig,
17
+ detectPackageManager,
18
+ getDbModuleRegistry,
19
+ getVersionMap,
20
+ pathExists,
21
+ readJsonIfPresent,
22
+ runInstall,
23
+ } from "./generator.mjs";
24
+
25
+ const MANAGED_PLATFORM_FILES = [
26
+ "next.config.ts",
27
+ path.join("config", "modules.ts"),
28
+ path.join("config", "shell.ts"),
29
+ ];
30
+
31
+ function resolveUpdateTargetDirectory(runtimeOptions, argvOptions) {
32
+ if (runtimeOptions.targetDir || argvOptions.targetDir) {
33
+ return path.resolve(runtimeOptions.targetDir || argvOptions.targetDir);
34
+ }
35
+
36
+ return process.cwd();
37
+ }
38
+
39
+ async function findWorkspaceRoot(startDir) {
40
+ let currentDir = path.resolve(startDir);
41
+
42
+ while (true) {
43
+ const registryPath = path.join(currentDir, "supabase", "module-registry.json");
44
+ const cliPackagePath = path.join(currentDir, "packages", "create-bw-app", "package.json");
45
+
46
+ if ((await pathExists(registryPath)) && (await pathExists(cliPackagePath))) {
47
+ return currentDir;
48
+ }
49
+
50
+ const parentDir = path.dirname(currentDir);
51
+ if (parentDir === currentDir) {
52
+ return null;
53
+ }
54
+
55
+ currentDir = parentDir;
56
+ }
57
+ }
58
+
59
+ function collectInstalledBrightwebPackages(manifest) {
60
+ const installed = new Map();
61
+
62
+ for (const section of ["dependencies", "devDependencies"]) {
63
+ const sectionManifest = manifest[section] || {};
64
+ for (const [packageName, version] of Object.entries(sectionManifest)) {
65
+ if (!packageName.startsWith("@brightweblabs/")) continue;
66
+ installed.set(packageName, { section, version });
67
+ }
68
+ }
69
+
70
+ return installed;
71
+ }
72
+
73
+ function parseConfiguredModules(content) {
74
+ const enabledModules = [];
75
+
76
+ for (const moduleDefinition of SELECTABLE_MODULES) {
77
+ const matcher = new RegExp(`key:\\s*"${moduleDefinition.key}"[\\s\\S]*?enabled:\\s*(true|false)`);
78
+ const match = content.match(matcher);
79
+ if (match?.[1] === "true") {
80
+ enabledModules.push(moduleDefinition.key);
81
+ }
82
+ }
83
+
84
+ return enabledModules;
85
+ }
86
+
87
+ async function detectTemplate(targetDir, installedBrightwebPackages) {
88
+ if (await pathExists(path.join(targetDir, "config", "modules.ts"))) {
89
+ return "platform";
90
+ }
91
+
92
+ return installedBrightwebPackages.size > 0 ? "platform" : "site";
93
+ }
94
+
95
+ function detectDependencyMode(installedBrightwebPackages) {
96
+ for (const { version } of installedBrightwebPackages.values()) {
97
+ if (typeof version === "string" && version.startsWith("workspace:")) {
98
+ return "workspace";
99
+ }
100
+ }
101
+
102
+ return "published";
103
+ }
104
+
105
+ function detectInstalledModules(installedBrightwebPackages) {
106
+ return SELECTABLE_MODULES
107
+ .filter((moduleDefinition) => installedBrightwebPackages.has(moduleDefinition.packageName))
108
+ .map((moduleDefinition) => moduleDefinition.key);
109
+ }
110
+
111
+ function getCanonicalBrightwebVersions({ manifest, template, dependencyMode, installedModules, versionMap }) {
112
+ const canonicalManifest = createPackageJson({
113
+ slug: manifest.name || path.basename(process.cwd()),
114
+ dependencyMode,
115
+ selectedModules: installedModules,
116
+ versionMap,
117
+ template,
118
+ });
119
+
120
+ const versions = {};
121
+
122
+ for (const section of ["dependencies", "devDependencies"]) {
123
+ for (const [packageName, version] of Object.entries(canonicalManifest[section] || {})) {
124
+ if (BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) {
125
+ versions[packageName] = version;
126
+ }
127
+ }
128
+ }
129
+
130
+ return versions;
131
+ }
132
+
133
+ function mergeManagedPackageUpdates({ manifest, targetVersions, installedBrightwebPackages }) {
134
+ let changed = false;
135
+ const nextManifest = {
136
+ ...manifest,
137
+ dependencies: manifest.dependencies ? { ...manifest.dependencies } : undefined,
138
+ devDependencies: manifest.devDependencies ? { ...manifest.devDependencies } : undefined,
139
+ };
140
+ const packageUpdates = [];
141
+
142
+ for (const [packageName, details] of installedBrightwebPackages.entries()) {
143
+ if (!BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) continue;
144
+ const targetVersion = targetVersions[packageName];
145
+ if (!targetVersion) continue;
146
+
147
+ const currentSection = nextManifest[details.section] || {};
148
+ if (currentSection[packageName] === targetVersion) continue;
149
+
150
+ currentSection[packageName] = targetVersion;
151
+ nextManifest[details.section] = currentSection;
152
+ packageUpdates.push({
153
+ packageName,
154
+ from: details.version,
155
+ to: targetVersion,
156
+ section: details.section,
157
+ });
158
+ changed = true;
159
+ }
160
+
161
+ return {
162
+ changed,
163
+ packageUpdates,
164
+ content: `${JSON.stringify(nextManifest, null, 2)}\n`,
165
+ };
166
+ }
167
+
168
+ async function getStarterFileStatus(targetDir, installedModules) {
169
+ const starterFiles = [];
170
+
171
+ for (const moduleKey of installedModules) {
172
+ const templateFolder = SELECTABLE_MODULES.find((moduleDefinition) => moduleDefinition.key === moduleKey)?.templateFolder;
173
+ if (!templateFolder) continue;
174
+
175
+ for (const relativePath of MODULE_STARTER_FILES[moduleKey] || []) {
176
+ const sourcePath = path.join(TEMPLATE_ROOT, "modules", templateFolder, relativePath);
177
+ const targetPath = path.join(targetDir, relativePath);
178
+ const exists = await pathExists(targetPath);
179
+
180
+ if (!exists) {
181
+ starterFiles.push({
182
+ moduleKey,
183
+ relativePath,
184
+ sourcePath,
185
+ targetPath,
186
+ status: "missing",
187
+ });
188
+ continue;
189
+ }
190
+
191
+ const [sourceContent, targetContent] = await Promise.all([
192
+ fs.readFile(sourcePath, "utf8"),
193
+ fs.readFile(targetPath, "utf8"),
194
+ ]);
195
+
196
+ starterFiles.push({
197
+ moduleKey,
198
+ relativePath,
199
+ sourcePath,
200
+ targetPath,
201
+ status: sourceContent === targetContent ? "current" : "drifted",
202
+ });
203
+ }
204
+ }
205
+
206
+ return starterFiles;
207
+ }
208
+
209
+ async function detectModulesConfigMismatch(targetDir, installedModules) {
210
+ const modulesConfigPath = path.join(targetDir, "config", "modules.ts");
211
+ if (!(await pathExists(modulesConfigPath))) {
212
+ return null;
213
+ }
214
+
215
+ const content = await fs.readFile(modulesConfigPath, "utf8");
216
+ const configuredModules = parseConfiguredModules(content);
217
+ const installed = new Set(installedModules);
218
+ const configured = new Set(configuredModules);
219
+
220
+ const mismatch = installedModules.length !== configuredModules.length
221
+ || installedModules.some((moduleKey) => !configured.has(moduleKey))
222
+ || configuredModules.some((moduleKey) => !installed.has(moduleKey));
223
+
224
+ if (!mismatch) {
225
+ return null;
226
+ }
227
+
228
+ return {
229
+ installedModules,
230
+ configuredModules,
231
+ };
232
+ }
233
+
234
+ function renderInstallCommand({ packageManager, dependencyMode, targetDir, workspaceRoot }) {
235
+ if (dependencyMode === "workspace" && workspaceRoot) {
236
+ return `cd ${workspaceRoot} && ${packageManager} install`;
237
+ }
238
+
239
+ return `cd ${targetDir} && ${packageManager} install`;
240
+ }
241
+
242
+ function renderPlanSummary(plan, options = {}) {
243
+ const lines = [
244
+ `${CLI_DISPLAY_NAME} update`,
245
+ "",
246
+ `Detected app type: ${plan.template}`,
247
+ `Dependency mode: ${plan.dependencyMode}`,
248
+ `Installed BrightWeb packages: ${plan.installedBrightwebPackages.length > 0 ? plan.installedBrightwebPackages.join(", ") : "none"}`,
249
+ `Installed modules: ${plan.installedModules.length > 0 ? plan.installedModules.join(", ") : "none"}`,
250
+ ];
251
+
252
+ if (plan.modulesConfigMismatch) {
253
+ lines.push(
254
+ `Config mismatch: installed modules (${plan.modulesConfigMismatch.installedModules.join(", ") || "none"}) differ from config/modules.ts (${plan.modulesConfigMismatch.configuredModules.join(", ") || "none"}). Using installed packages as source of truth.`,
255
+ );
256
+ }
257
+
258
+ lines.push("");
259
+
260
+ if (plan.packageUpdates.length > 0) {
261
+ lines.push("Packages to update:");
262
+ for (const update of plan.packageUpdates) {
263
+ lines.push(`- ${update.packageName}: ${update.from} -> ${update.to}`);
264
+ }
265
+ } else {
266
+ lines.push("Packages to update: none");
267
+ }
268
+
269
+ lines.push("");
270
+
271
+ if (plan.configFilesToWrite.length > 0) {
272
+ lines.push("Config files to rewrite:");
273
+ for (const relativePath of plan.configFilesToWrite) {
274
+ lines.push(`- ${relativePath}`);
275
+ }
276
+ } else {
277
+ lines.push("Config files to rewrite: none");
278
+ }
279
+
280
+ lines.push("");
281
+
282
+ if (plan.starterFilesMissing.length > 0 || plan.starterFilesDrifted.length > 0) {
283
+ lines.push("Starter file status:");
284
+ for (const relativePath of plan.starterFilesMissing) {
285
+ lines.push(`- missing: ${relativePath}`);
286
+ }
287
+ for (const relativePath of plan.starterFilesDrifted) {
288
+ lines.push(`- drifted: ${relativePath}`);
289
+ }
290
+ } else {
291
+ lines.push("Starter file status: all current");
292
+ }
293
+
294
+ lines.push("");
295
+
296
+ if (plan.dbInstallPlan.resolvedOrder.length > 0) {
297
+ lines.push(`Resolved database stack: ${plan.dbInstallPlan.resolvedOrder.join(" -> ")}`);
298
+ }
299
+
300
+ if (plan.dbInstallPlan.notes.length > 0) {
301
+ lines.push("Database notes:");
302
+ for (const note of plan.dbInstallPlan.notes) {
303
+ lines.push(`- ${note}`);
304
+ }
305
+ }
306
+
307
+ if (options.installCommand) {
308
+ lines.push("", `Next install command: ${options.installCommand}`);
309
+ }
310
+
311
+ return `${lines.join("\n")}\n`;
312
+ }
313
+
314
+ export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptions = {}) {
315
+ const targetDir = resolveUpdateTargetDirectory(runtimeOptions, argvOptions);
316
+ const packageJsonPath = path.join(targetDir, "package.json");
317
+ const manifest = await readJsonIfPresent(packageJsonPath);
318
+
319
+ if (!manifest) {
320
+ throw new Error(`Target directory does not contain package.json: ${targetDir}`);
321
+ }
322
+
323
+ const installedBrightwebPackagesMap = collectInstalledBrightwebPackages(manifest);
324
+ const template = await detectTemplate(targetDir, installedBrightwebPackagesMap);
325
+ const dependencyMode = detectDependencyMode(installedBrightwebPackagesMap);
326
+ const workspaceRoot = runtimeOptions.workspaceRoot
327
+ ? path.resolve(runtimeOptions.workspaceRoot)
328
+ : argvOptions.workspaceRoot
329
+ ? path.resolve(argvOptions.workspaceRoot)
330
+ : dependencyMode === "workspace"
331
+ ? await findWorkspaceRoot(targetDir)
332
+ : null;
333
+ const packageManager = detectPackageManager(argvOptions.packageManager || runtimeOptions.packageManager);
334
+ const installedModules = detectInstalledModules(installedBrightwebPackagesMap);
335
+ const versionMap = await getVersionMap(workspaceRoot);
336
+ const dbRegistry = await getDbModuleRegistry(workspaceRoot);
337
+ const dbInstallPlan = template === "platform"
338
+ ? createDbInstallPlan({
339
+ selectedModules: installedModules,
340
+ workspaceMode: true,
341
+ registry: dbRegistry,
342
+ })
343
+ : {
344
+ selectedLabels: [],
345
+ resolvedOrder: [],
346
+ notes: [],
347
+ };
348
+ const canonicalVersions = getCanonicalBrightwebVersions({
349
+ manifest,
350
+ template,
351
+ dependencyMode,
352
+ installedModules,
353
+ versionMap,
354
+ });
355
+ const packageJsonUpdate = mergeManagedPackageUpdates({
356
+ manifest,
357
+ targetVersions: canonicalVersions,
358
+ installedBrightwebPackages: installedBrightwebPackagesMap,
359
+ });
360
+
361
+ const fileWrites = [];
362
+ if (packageJsonUpdate.changed) {
363
+ fileWrites.push({
364
+ relativePath: "package.json",
365
+ targetPath: packageJsonPath,
366
+ content: packageJsonUpdate.content,
367
+ type: "config",
368
+ });
369
+ }
370
+
371
+ if (template === "platform") {
372
+ const canonicalConfigFiles = {
373
+ "next.config.ts": createNextConfig({ template: "platform", selectedModules: installedModules }),
374
+ [path.join("config", "modules.ts")]: createPlatformModulesConfigFile(installedModules),
375
+ [path.join("config", "shell.ts")]: createShellConfig(installedModules),
376
+ };
377
+
378
+ for (const relativePath of MANAGED_PLATFORM_FILES) {
379
+ const targetPath = path.join(targetDir, relativePath);
380
+ const currentContent = (await pathExists(targetPath)) ? await fs.readFile(targetPath, "utf8") : null;
381
+ const nextContent = canonicalConfigFiles[relativePath];
382
+
383
+ if (currentContent !== nextContent) {
384
+ fileWrites.push({
385
+ relativePath,
386
+ targetPath,
387
+ content: nextContent,
388
+ type: "config",
389
+ });
390
+ }
391
+ }
392
+ }
393
+
394
+ const starterFiles = template === "platform"
395
+ ? await getStarterFileStatus(targetDir, installedModules)
396
+ : [];
397
+ const starterFilesMissing = starterFiles.filter((entry) => entry.status === "missing");
398
+ const starterFilesDrifted = starterFiles.filter((entry) => entry.status === "drifted");
399
+
400
+ if (argvOptions.refreshStarters) {
401
+ for (const entry of starterFiles.filter((candidate) => candidate.status !== "current")) {
402
+ fileWrites.push({
403
+ relativePath: entry.relativePath,
404
+ targetPath: entry.targetPath,
405
+ content: await fs.readFile(entry.sourcePath, "utf8"),
406
+ type: "starter",
407
+ });
408
+ }
409
+ }
410
+
411
+ const modulesConfigMismatch = template === "platform"
412
+ ? await detectModulesConfigMismatch(targetDir, installedModules)
413
+ : null;
414
+
415
+ return {
416
+ targetDir,
417
+ workspaceRoot,
418
+ template,
419
+ dependencyMode,
420
+ packageManager,
421
+ manifest,
422
+ installedModules,
423
+ installedBrightwebPackages: Array.from(installedBrightwebPackagesMap.keys()).sort(),
424
+ packageUpdates: packageJsonUpdate.packageUpdates,
425
+ configFilesToWrite: fileWrites.filter((entry) => entry.type === "config").map((entry) => entry.relativePath),
426
+ starterFilesMissing: starterFilesMissing.map((entry) => entry.relativePath),
427
+ starterFilesDrifted: starterFilesDrifted.map((entry) => entry.relativePath),
428
+ starterFilesToRefresh: fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath),
429
+ dbInstallPlan,
430
+ modulesConfigMismatch,
431
+ fileWrites,
432
+ };
433
+ }
434
+
435
+ export async function updateBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
436
+ const plan = await buildBrightwebAppUpdatePlan(argvOptions, runtimeOptions);
437
+ const installCommand = renderInstallCommand({
438
+ packageManager: plan.packageManager,
439
+ dependencyMode: plan.dependencyMode,
440
+ targetDir: plan.targetDir,
441
+ workspaceRoot: plan.workspaceRoot,
442
+ });
443
+
444
+ output.write(renderPlanSummary(plan, { installCommand }));
445
+
446
+ if (argvOptions.dryRun) {
447
+ return {
448
+ dryRun: true,
449
+ plan,
450
+ installCommand,
451
+ };
452
+ }
453
+
454
+ for (const fileWrite of plan.fileWrites) {
455
+ await fs.mkdir(path.dirname(fileWrite.targetPath), { recursive: true });
456
+ await fs.writeFile(fileWrite.targetPath, fileWrite.content, "utf8");
457
+ }
458
+
459
+ const packageJsonChanged = plan.fileWrites.some((entry) => entry.relativePath === "package.json");
460
+ if (argvOptions.install && packageJsonChanged) {
461
+ const installRunner = runtimeOptions.installRunner || runInstall;
462
+ const installCwd = plan.dependencyMode === "workspace" && plan.workspaceRoot ? plan.workspaceRoot : plan.targetDir;
463
+ await installRunner(plan.packageManager, installCwd);
464
+ }
465
+
466
+ if (!argvOptions.install && packageJsonChanged) {
467
+ output.write(`Run \`${installCommand}\` to install updated package versions.\n`);
468
+ }
469
+
470
+ if (plan.fileWrites.length === 0) {
471
+ output.write("No managed changes were required.\n");
472
+ } else {
473
+ output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"}.\n`);
474
+ }
475
+
476
+ return {
477
+ dryRun: false,
478
+ plan,
479
+ installCommand,
480
+ };
481
+ }