create-bw-app 0.11.0 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.11.0",
4
+ "version": "0.12.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-bw-app": "bin/create-bw-app.mjs",
package/src/add.mjs CHANGED
@@ -91,7 +91,8 @@ export async function addBrightwebModule(moduleKey, argvOptions = {}, runtimeOpt
91
91
  const now = new Date().toISOString();
92
92
  for (const key of newModules) appManifest.modules[key] = { version: catalog[key].version, installedAt: now, exposed: true };
93
93
  appManifest.migrationCursor = migrationPlan.nextCursor;
94
- appManifest.scaffoldFiles = { ...appManifest.scaffoldFiles, ...await collectScaffoldFiles(targetDir, installedModuleKeys) };
94
+ const collectedScaffoldFiles = await collectScaffoldFiles(targetDir, installedModuleKeys);
95
+ appManifest.scaffoldFiles = { ...collectedScaffoldFiles, ...appManifest.scaffoldFiles };
95
96
  await writeAppManifest(targetDir, appManifest);
96
97
  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`);
97
98
  return { dryRun: false, newModules, migrationPlan };
package/src/adopt.mjs CHANGED
@@ -18,7 +18,7 @@ import { inventoryScaffoldFiles, resolveTemplateRoot } from "./scaffold.mjs";
18
18
  import { detectDependencyMode, detectTemplate } from "./update.mjs";
19
19
 
20
20
  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 --allow-uncursored Allow doctor to warn instead of fail on null cursors\n --force Replace an existing app manifest\n --dry-run Print the manifest and warnings without writing\n --help Show this help`;
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 Replace an existing app manifest\n --dry-run Print the manifest and warnings without writing\n --help Show this help`;
22
22
 
23
23
  function asList(value) {
24
24
  if (value == null) return [];
@@ -123,10 +123,24 @@ export async function adoptBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
123
123
  const scaffold = template === "platform"
124
124
  ? await inventoryScaffoldFiles({ targetDir, moduleKeys: Object.keys(modules), templateRoot })
125
125
  : { records: {}, unsupported: [] };
126
+ const ownedPaths = new Set(asList(argvOptions.own).map(String));
127
+ const skippedPaths = new Set(asList(argvOptions.skip).map(String));
128
+ for (const relativePath of [...ownedPaths, ...skippedPaths]) {
129
+ if (!scaffold.records[relativePath]) throw new Error(`${relativePath} is not a tracked scaffold file.`);
130
+ }
131
+ for (const relativePath of ownedPaths) {
132
+ if (skippedPaths.has(relativePath)) throw new Error(`${relativePath} cannot be both --own and --skip.`);
133
+ if (scaffold.records[relativePath].status === "missing") throw new Error(`Cannot own missing scaffold file: ${relativePath}`);
134
+ scaffold.records[relativePath].intent = "owned";
135
+ }
136
+ for (const relativePath of skippedPaths) {
137
+ if (scaffold.records[relativePath].status !== "missing") throw new Error(`Cannot skip existing scaffold file: ${relativePath}`);
138
+ scaffold.records[relativePath].intent = "skipped";
139
+ }
126
140
  for (const relativePath of scaffold.unsupported) warnings.push(`WARN ${relativePath}: installed-version template is unavailable; scaffold comparison is unsupported.`);
127
141
  for (const [relativePath, record] of Object.entries(scaffold.records)) {
128
- if (record.status === "drifted") warnings.push(`WARN drifted scaffold file: ${relativePath}`);
129
- if (record.status === "missing") warnings.push(`WARN missing scaffold file: ${relativePath} (not created; use bw diff and the current template as guidance).`);
142
+ if (record.status === "drifted" && record.intent !== "owned") warnings.push(`WARN drifted scaffold file: ${relativePath}`);
143
+ if (record.status === "missing" && record.intent !== "skipped") warnings.push(`WARN missing scaffold file: ${relativePath} (not created; use bw diff and the current template as guidance).`);
130
144
  }
131
145
 
132
146
  const overrides = parseCursorOverrides(argvOptions.cursor);
@@ -89,7 +89,14 @@ export async function readAppManifest(targetDir, { required = true } = {}) {
89
89
  export async function writeAppManifest(targetDir, manifest) {
90
90
  const manifestPath = path.join(targetDir, APP_MANIFEST_PATH);
91
91
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
92
- await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
92
+ const temporaryPath = `${manifestPath}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
93
+ try {
94
+ await fs.writeFile(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
95
+ await fs.rename(temporaryPath, manifestPath);
96
+ } catch (error) {
97
+ await fs.rm(temporaryPath, { force: true }).catch(() => {});
98
+ throw error;
99
+ }
93
100
  }
94
101
 
95
102
  export function validateAppManifest(manifest) {
@@ -107,7 +114,7 @@ export function validateAppManifest(manifest) {
107
114
  if (!entry || !cleanVersion(entry.version) || typeof entry.installedAt !== "string" || typeof entry.exposed !== "boolean") errors.push(`modules.${key} is invalid`);
108
115
  }
109
116
  for (const [relativePath, entry] of Object.entries(manifest.scaffoldFiles || {})) {
110
- if (!entry || typeof entry.module !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.hash || "") || !["current", "drifted", "missing"].includes(entry.status)) errors.push(`scaffoldFiles.${relativePath} is invalid`);
117
+ 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`);
111
118
  }
112
119
  if (manifest.lastDoctor != null && (typeof manifest.lastDoctor.at !== "string" || typeof manifest.lastDoctor.ok !== "boolean")) errors.push("lastDoctor is invalid");
113
120
  return errors;
package/src/bw.mjs CHANGED
@@ -3,16 +3,17 @@ import { adoptBrightwebApp } from "./adopt.mjs";
3
3
  import { diffBrightwebScaffold } from "./diff.mjs";
4
4
  import { doctorBrightwebApp } from "./doctor.mjs";
5
5
  import { removeBrightwebModule } from "./remove.mjs";
6
+ import { scaffoldBrightwebApp } from "./scaffold-cmd.mjs";
6
7
  import { updateBrightwebApp } from "./update.mjs";
7
8
  import { upgradeBrightwebApp } from "./upgrade.mjs";
8
9
 
9
- const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey> Install a module and its requirements\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
10
+ const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey> Install a module and its requirements\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n scaffold <action> List or record per-file scaffold intent\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
10
11
 
11
12
  function parseOptions(argv) {
12
13
  const options = {};
13
14
  const positionals = [];
14
15
  const booleanFlags = new Set(["help", "dry-run", "strict", "report", "install", "refresh-starters", "allow-stale-fallback", "allow-uncursored", "force", "list", "yes"]);
15
- const repeatableFlags = new Set(["cursor", "owned-surface"]);
16
+ const repeatableFlags = new Set(["cursor", "owned-surface", "own", "skip"]);
16
17
  for (let index = 0; index < argv.length; index += 1) {
17
18
  const token = argv[index];
18
19
  if (!token.startsWith("--")) { positionals.push(token); continue; }
@@ -37,6 +38,7 @@ export async function runBwCli(argv = process.argv.slice(2), runtimeOptions = {}
37
38
  if (command === "add") await addBrightwebModule(positionals[0], options, runtimeOptions);
38
39
  else if (command === "adopt") await adoptBrightwebApp(options, runtimeOptions);
39
40
  else if (command === "diff") await diffBrightwebScaffold(positionals[0], options, runtimeOptions);
41
+ else if (command === "scaffold") await scaffoldBrightwebApp(positionals[0], positionals.slice(1), options, runtimeOptions);
40
42
  else if (command === "remove") await removeBrightwebModule(positionals[0], options, runtimeOptions);
41
43
  else if (command === "upgrade") await upgradeBrightwebApp(positionals[0], options, runtimeOptions);
42
44
  else if (command === "update") await updateBrightwebApp(options, runtimeOptions);
package/src/constants.mjs CHANGED
@@ -76,15 +76,15 @@ export const PLATFORM_STARTER_FILES = [
76
76
  ];
77
77
 
78
78
  export const APP_DEPENDENCY_DEFAULTS = {
79
- "@brightweblabs/app-shell": "^0.4.0",
79
+ "@brightweblabs/app-shell": "^0.4.1",
80
80
  "@brightweblabs/core-auth": "^0.3.4",
81
81
  "@brightweblabs/infra": "^0.3.1",
82
- "@brightweblabs/module-admin": "^0.3.4",
83
- "@brightweblabs/module-crm": "^0.5.0",
84
- "@brightweblabs/module-orgs": "^0.2.0",
85
- "@brightweblabs/module-projects": "^0.4.2",
86
- "@brightweblabs/theme": "^0.2.0",
87
- "@brightweblabs/ui": "^1.0.0",
82
+ "@brightweblabs/module-admin": "^0.3.5",
83
+ "@brightweblabs/module-crm": "^0.5.2",
84
+ "@brightweblabs/module-orgs": "^0.2.2",
85
+ "@brightweblabs/module-projects": "^0.4.3",
86
+ "@brightweblabs/theme": "^0.2.1",
87
+ "@brightweblabs/ui": "^1.0.1",
88
88
  "lucide-react": "^1.8.0",
89
89
  "next": "16.1.6",
90
90
  "react": "19.2.3",
package/src/doctor.mjs CHANGED
@@ -52,7 +52,21 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
52
52
  add(topologyProblems.length ? "FAIL" : "PASS", "topology", topologyProblems.join("; ") || "Module requirements are satisfied.");
53
53
 
54
54
  const scaffold = await scaffoldDrift(targetDir, appManifest.scaffoldFiles);
55
- add(scaffold.drifted.length || scaffold.missing.length ? "FAIL" : "PASS", "scaffold", `${scaffold.current.length} current, ${scaffold.drifted.length} drifted, ${scaffold.missing.length} missing; drifted scaffold files: ${scaffold.drifted.length} (see bw diff --list)${scaffold.missing.length ? `; missing: ${scaffold.missing.join(", ")}` : ""}`);
55
+ const scaffoldGroups = {
56
+ current: scaffold.entries.filter((entry) => entry.status === "current" && entry.intent !== "skipped"),
57
+ owned: scaffold.entries.filter((entry) => entry.intent === "owned" && entry.status === "drifted"),
58
+ skipped: scaffold.entries.filter((entry) => entry.intent === "skipped" && entry.status === "missing"),
59
+ undecidedDrift: scaffold.entries.filter((entry) => entry.intent === "managed" && entry.status === "drifted"),
60
+ undecidedMissing: scaffold.entries.filter((entry) => entry.intent === "managed" && entry.status === "missing"),
61
+ mismatched: scaffold.entries.filter((entry) => (entry.intent === "owned" && entry.status === "missing") || (entry.intent === "skipped" && entry.status !== "missing")),
62
+ };
63
+ if (scaffoldGroups.owned.length) add("INFO", "scaffold-owned", `App-owned scaffold files: ${scaffoldGroups.owned.map((entry) => entry.relativePath).join(", ")}.`);
64
+ if (scaffoldGroups.skipped.length) add("INFO", "scaffold-skipped", `Intentionally skipped scaffold files: ${scaffoldGroups.skipped.map((entry) => entry.relativePath).join(", ")}.`);
65
+ 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).`);
66
+ 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).`);
67
+ 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(", ")}.`);
68
+ const scaffoldStatus = scaffoldGroups.mismatched.length ? "FAIL" : scaffoldGroups.undecidedDrift.length || scaffoldGroups.undecidedMissing.length ? "WARN" : "PASS";
69
+ 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.`);
56
70
  add("INFO", "owned-surfaces", `Owned surfaces: ${(appManifest.ownedSurfaces || []).join(", ") || "none"}.`);
57
71
 
58
72
  const envNames = new Set(Object.keys(process.env));
package/src/remove.mjs CHANGED
@@ -70,7 +70,7 @@ export async function removeBrightwebModule(moduleKey, argvOptions = {}, runtime
70
70
  if (record.module !== moduleKey) continue;
71
71
  const filePath = path.join(targetDir, relativePath);
72
72
  if (!(await pathExists(filePath))) continue;
73
- if (await hashFile(filePath) === record.hash) cleanFiles.push(relativePath);
73
+ if ((record.intent || "managed") === "managed" && await hashFile(filePath) === record.hash) cleanFiles.push(relativePath);
74
74
  else driftedFiles.push(relativePath);
75
75
  }
76
76
  const notice = databaseNotice(moduleKey, catalog[moduleKey]?.manifest?.database?.ownedObjects || []);
@@ -0,0 +1,74 @@
1
+ import path from "node:path";
2
+ import { stdout as output } from "node:process";
3
+ import { findWorkspaceRoot, hashFile, readAppManifest, writeAppManifest } from "./app-manifest.mjs";
4
+ import { pathExists } from "./generator.mjs";
5
+ import { findTrackedTemplate, scaffoldDrift } from "./scaffold.mjs";
6
+
7
+ const HELP = `Usage: bw scaffold <action> [paths...] [options]\n\nActions:\n list List tracked files, live status, and intent\n own <path>... Mark existing tracked files as app-owned\n skip <path>... Mark missing tracked files as intentionally absent\n manage <path>... Return tracked files to BrightWeb management\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --help Show this help`;
8
+
9
+ function normalizeTrackedPath(relativePath) {
10
+ const normalized = path.normalize(String(relativePath)).replace(/^\.\//, "");
11
+ if (path.isAbsolute(String(relativePath)) || normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
12
+ throw new Error(`Scaffold path must be relative to the app: ${relativePath}`);
13
+ }
14
+ return normalized;
15
+ }
16
+
17
+ export async function scaffoldBrightwebApp(action, paths = [], argvOptions = {}, runtimeOptions = {}) {
18
+ if (argvOptions.help || !action) { output.write(`${HELP}\n`); return { help: true }; }
19
+ if (!Array.isArray(paths)) paths = [paths];
20
+ if (!["list", "own", "skip", "manage"].includes(action)) throw new Error(`Unknown bw scaffold action: ${action}\n\n${HELP}`);
21
+ if (action !== "list" && paths.length === 0) throw new Error(`bw scaffold ${action} requires at least one tracked <path>.`);
22
+ if (action === "list" && paths.length > 0) throw new Error("bw scaffold list does not accept file paths.");
23
+
24
+ const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
25
+ const manifest = await readAppManifest(targetDir);
26
+ const live = await scaffoldDrift(targetDir, manifest.scaffoldFiles);
27
+ if (action === "list") {
28
+ output.write("PATH\tMODULE\tSTATUS\tINTENT\n");
29
+ for (const entry of live.entries) output.write(`${entry.relativePath}\t${entry.module}\t${entry.status}\t${entry.intent}\n`);
30
+ return { action, entries: live.entries };
31
+ }
32
+
33
+ const normalizedPaths = Array.from(new Set(paths.map(normalizeTrackedPath)));
34
+ for (const relativePath of normalizedPaths) {
35
+ if (!manifest.scaffoldFiles?.[relativePath]) throw new Error(`${relativePath} is not a tracked scaffold file.`);
36
+ }
37
+ const liveByPath = new Map(live.entries.map((entry) => [entry.relativePath, entry]));
38
+ for (const relativePath of normalizedPaths) {
39
+ const status = liveByPath.get(relativePath)?.status;
40
+ if (action === "own" && status === "missing") throw new Error(`Cannot own missing scaffold file: ${relativePath}`);
41
+ if (action === "skip" && status !== "missing") throw new Error(`Cannot skip existing scaffold file: ${relativePath}`);
42
+ }
43
+
44
+ const workspaceRoot = runtimeOptions.workspaceRoot || argvOptions.workspaceRoot || await findWorkspaceRoot(targetDir);
45
+ const changes = [];
46
+ for (const relativePath of normalizedPaths) {
47
+ const record = manifest.scaffoldFiles[relativePath];
48
+ const previousIntent = record.intent || "managed";
49
+ const nextIntent = action === "manage" ? "managed" : action === "own" ? "owned" : "skipped";
50
+ const appPath = path.join(targetDir, relativePath);
51
+ const exists = await pathExists(appPath);
52
+ if (action === "manage") {
53
+ const located = await findTrackedTemplate({ relativePath, manifest, targetDir, workspaceRoot });
54
+ if (!located.templatePath) throw new Error(`Installed-package template unavailable for ${relativePath}; cannot manage it safely.`);
55
+ const templateHash = await hashFile(located.templatePath);
56
+ if (exists) {
57
+ record.hash = await hashFile(appPath);
58
+ record.status = record.hash === templateHash ? "current" : "drifted";
59
+ } else {
60
+ record.status = "missing";
61
+ }
62
+ } else {
63
+ record.status = liveByPath.get(relativePath).status;
64
+ }
65
+ if (nextIntent === "managed") delete record.intent;
66
+ else record.intent = nextIntent;
67
+ changes.push({ relativePath, previousIntent, intent: nextIntent, status: record.status });
68
+ }
69
+ await writeAppManifest(targetDir, manifest);
70
+ for (const change of changes) output.write(`${change.relativePath}: intent ${change.previousIntent} -> ${change.intent} (${change.status})\n`);
71
+ return { action, changes, manifest };
72
+ }
73
+
74
+ export { HELP as SCAFFOLD_HELP };
package/src/scaffold.mjs CHANGED
@@ -58,13 +58,21 @@ export async function scaffoldDrift(targetDir, scaffoldFiles = {}) {
58
58
  const current = [];
59
59
  const drifted = [];
60
60
  const missing = [];
61
+ const entries = [];
61
62
  for (const [relativePath, record] of Object.entries(scaffoldFiles)) {
62
63
  const appPath = path.join(targetDir, relativePath);
63
- if (!(await pathExists(appPath))) missing.push(relativePath);
64
- else if (await hashFile(appPath) === record.hash) current.push(relativePath);
64
+ const intent = record.intent || "managed";
65
+ let status = "missing";
66
+ if (await pathExists(appPath)) {
67
+ const matchesRecordedHash = await hashFile(appPath) === record.hash;
68
+ status = matchesRecordedHash && record.status !== "drifted" ? "current" : "drifted";
69
+ }
70
+ entries.push({ relativePath, module: record.module, status, intent });
71
+ if (status === "missing") missing.push(relativePath);
72
+ else if (status === "current") current.push(relativePath);
65
73
  else drifted.push(relativePath);
66
74
  }
67
- return { current, drifted, missing };
75
+ return { current, drifted, missing, entries };
68
76
  }
69
77
 
70
78
  export async function findTrackedTemplate({ relativePath, manifest, targetDir, workspaceRoot }) {
package/src/upgrade.mjs CHANGED
@@ -18,13 +18,16 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
18
18
  const plan = await buildBrightwebAppUpdatePlan(updateOptions, runtimeOptions);
19
19
  const drifted = [];
20
20
  const missing = [];
21
+ const intentional = [];
21
22
  for (const [relativePath, record] of Object.entries(appManifest.scaffoldFiles)) {
23
+ if (["owned", "skipped"].includes(record.intent)) intentional.push(relativePath);
22
24
  const filePath = path.join(targetDir, relativePath);
23
25
  if (!(await pathExists(filePath))) { missing.push(relativePath); continue; }
24
26
  if (await hashFile(filePath) !== record.hash) drifted.push(relativePath);
25
27
  }
26
- const protectedPaths = new Set(drifted);
28
+ const protectedPaths = new Set([...drifted, ...intentional]);
27
29
  plan.fileWrites = plan.fileWrites.filter((entry) => entry.type !== "starter" || !protectedPaths.has(entry.relativePath));
30
+ plan.starterFilesToRefresh = plan.fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath);
28
31
  plan.starterFilesDrifted = Array.from(new Set([...plan.starterFilesDrifted, ...drifted]));
29
32
  plan.starterFilesMissing = Array.from(new Set([...plan.starterFilesMissing, ...missing]));
30
33
 
@@ -43,6 +46,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
43
46
  output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nMigrations to append: ${migrationPlan.writes.length}\n`);
44
47
  for (const relativePath of missing) output.write(`- missing: ${relativePath}\n`);
45
48
  for (const relativePath of drifted) output.write(`- drifted: ${relativePath}\n`);
49
+ for (const relativePath of intentional) output.write(`- intent-protected: ${relativePath}\n`);
46
50
  if (argvOptions.dryRun) return { dryRun: true, plan, migrationPlan, drifted, missing };
47
51
 
48
52
  for (const write of plan.fileWrites) {
@@ -58,6 +62,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
58
62
  for (const relativePath of plan.starterFilesToRefresh || []) {
59
63
  if (!protectedPaths.has(relativePath) && appManifest.scaffoldFiles[relativePath] && await pathExists(path.join(targetDir, relativePath))) {
60
64
  appManifest.scaffoldFiles[relativePath].hash = await hashFile(path.join(targetDir, relativePath));
65
+ appManifest.scaffoldFiles[relativePath].status = "current";
61
66
  }
62
67
  }
63
68
  await writeAppManifest(targetDir, appManifest);