create-better-fullstack 2.1.8 → 2.3.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.
Files changed (29) hide show
  1. package/README.md +4 -4
  2. package/dist/{add-handler-ztNjzoMN.mjs → add-handler-z4NCxoq6.mjs} +9 -8
  3. package/dist/addons-setup-COE1e1db.mjs +7 -0
  4. package/dist/{addons-setup-LaAj43NP.mjs → addons-setup-DKtiNexf.mjs} +1 -1
  5. package/dist/{analytics-DVltG11u.mjs → analytics-BD8hZvIj.mjs} +1 -1
  6. package/dist/{bts-config-DQVWvPDs.mjs → bts-config-Bwd6GB7B.mjs} +59 -0
  7. package/dist/cli.mjs +2 -2
  8. package/dist/command-exists-DtX9ldL8.mjs +15 -0
  9. package/dist/{config-processing-D9-F2Us9.mjs → config-processing-B-RT_mVE.mjs} +32 -3
  10. package/dist/{doctor-a4ca3SMd.mjs → doctor-LmNs-hDr.mjs} +4 -3
  11. package/dist/{file-formatter-gvmrpd-g.mjs → file-formatter-DxF56-mO.mjs} +550 -24
  12. package/dist/{gen-CCClL7Ve.mjs → gen-CQfQU0uZ.mjs} +1 -1
  13. package/dist/{generated-checks-BV9jol5h.mjs → generated-checks-BUcI5VM2.mjs} +55 -22
  14. package/dist/index.d.mts +741 -211
  15. package/dist/index.mjs +30 -9
  16. package/dist/{install-dependencies-RoUyaE8o.mjs → install-dependencies-CJiUklm3.mjs} +132 -23
  17. package/dist/{mcp-492OkjcS.mjs → mcp-BBammItk.mjs} +5 -5
  18. package/dist/mcp-entry.mjs +76 -236
  19. package/dist/{registry-DSf2CEaU.mjs → registry-D0JfLCoH.mjs} +1 -1
  20. package/dist/{run-T6g1jkfy.mjs → run-BAIeghcU.mjs} +1948 -318
  21. package/dist/run-CAl4L4uU.mjs +16 -0
  22. package/dist/scaffold-manifest-CQwvyttq.mjs +4 -0
  23. package/dist/{scaffold-manifest-Dyi0voqE.mjs → scaffold-manifest-Dr3Anmlf.mjs} +34 -5
  24. package/dist/{update-DJ8CI5KW.mjs → update-CnrBdWrh.mjs} +182 -25
  25. package/package.json +24 -7
  26. package/dist/addons-setup-DnLjAzTw.mjs +0 -7
  27. package/dist/run-CD8f7PHh.mjs +0 -15
  28. package/dist/scaffold-manifest-DGRyepdb.mjs +0 -4
  29. /package/dist/{update-deps-aD-iQw4U.mjs → update-deps-G3FgqBlW.mjs} +0 -0
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import "./bts-config-Bwd6GB7B.mjs";
3
+ import { a as createBtsCli, c as history, d as telemetry, f as update, i as create, l as router, n as builder, o as docs, r as check, s as doctor, t as add, u as sponsors } from "./run-BAIeghcU.mjs";
4
+ import "./render-title-zvyKC1ej.mjs";
5
+ import "./errors-ns_o2OKg.mjs";
6
+ import "./prompt-environment-BR0Kkw2W.mjs";
7
+ import "./analytics-BD8hZvIj.mjs";
8
+ import "./file-formatter-DxF56-mO.mjs";
9
+ import "./install-dependencies-CJiUklm3.mjs";
10
+ import "./command-exists-DtX9ldL8.mjs";
11
+ import "./config-processing-B-RT_mVE.mjs";
12
+ import "./generated-checks-BUcI5VM2.mjs";
13
+ import "./scaffold-manifest-Dr3Anmlf.mjs";
14
+ import "./addons-setup-DKtiNexf.mjs";
15
+
16
+ export { createBtsCli };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { a as isStructuredBaselinePath, c as refreshScaffoldManifestFiles, i as hashContent, l as writeScaffoldManifest, n as collectStructuredBaselines, o as readScaffoldManifest, r as computeScaffoldHashes, s as recordScaffoldManifest, t as SCAFFOLD_MANIFEST_FILE } from "./scaffold-manifest-Dr3Anmlf.mjs";
3
+
4
+ export { collectStructuredBaselines, recordScaffoldManifest };
@@ -39,6 +39,23 @@ const EXCLUDED_FILE_NAMES = new Set([
39
39
  "go.sum",
40
40
  "mix.lock"
41
41
  ]);
42
+ /** Files whose render content is stored in the manifest for structured merges. */
43
+ function isStructuredBaselinePath(relPath) {
44
+ const name = path.basename(relPath);
45
+ return name === "package.json" || name.endsWith(".env.example");
46
+ }
47
+ const BINARY_FILE_MARKER = "[Binary file]";
48
+ /** Extract structured-merge baseline contents from a generated virtual tree. */
49
+ function collectStructuredBaselines(tree) {
50
+ const baselines = {};
51
+ function walk(nodes) {
52
+ for (const node of nodes) if (node.type === "file") {
53
+ if (isStructuredBaselinePath(node.path) && node.content !== BINARY_FILE_MARKER) baselines[node.path] = node.content;
54
+ } else walk(node.children);
55
+ }
56
+ walk(tree.root.children);
57
+ return baselines;
58
+ }
42
59
  function hashContent(content) {
43
60
  return createHash("sha256").update(content).digest("hex");
44
61
  }
@@ -78,10 +95,12 @@ async function computeScaffoldHashes(projectDir) {
78
95
  return Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)));
79
96
  }
80
97
  async function writeScaffoldManifest(projectDir, manifest) {
98
+ const sortEntries = (record) => Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
81
99
  const sorted = {
82
100
  version: manifest.version,
83
101
  createdAt: manifest.createdAt,
84
- hashes: Object.fromEntries(Object.entries(manifest.hashes).sort(([a], [b]) => a.localeCompare(b)))
102
+ hashes: sortEntries(manifest.hashes),
103
+ ...manifest.baselines && Object.keys(manifest.baselines).length > 0 ? { baselines: sortEntries(manifest.baselines) } : {}
85
104
  };
86
105
  const manifestPath = path.join(projectDir, SCAFFOLD_MANIFEST_FILE);
87
106
  await fs.writeFile(manifestPath, `${JSON.stringify(sorted, null, 2)}\n`, "utf-8");
@@ -98,7 +117,8 @@ async function recordScaffoldManifest(projectDir, metadata = {}) {
98
117
  const manifest = {
99
118
  version: MANIFEST_VERSION,
100
119
  createdAt: metadata.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
101
- hashes: await computeScaffoldHashes(projectDir)
120
+ hashes: await computeScaffoldHashes(projectDir),
121
+ baselines: metadata.baselines
102
122
  };
103
123
  await writeScaffoldManifest(projectDir, manifest);
104
124
  return manifest;
@@ -113,13 +133,18 @@ async function readScaffoldManifest(projectDir) {
113
133
  const raw = await fs.readFile(manifestPath, "utf-8");
114
134
  const parsed = JSON.parse(raw);
115
135
  if (!parsed || typeof parsed !== "object" || typeof parsed.hashes !== "object") return null;
136
+ if (parsed.baselines !== void 0 && typeof parsed.baselines !== "object") delete parsed.baselines;
116
137
  return parsed;
117
138
  } catch {
118
139
  return null;
119
140
  }
120
141
  }
121
- /** Refresh only files deliberately written by an in-place stack update. */
122
- async function refreshScaffoldManifestFiles(projectDir, relativePaths) {
142
+ /**
143
+ * Refresh only files deliberately written by an in-place stack update.
144
+ * `baselines` (path -> render content) advances the structured-merge baselines
145
+ * to the render the project was just reconciled against.
146
+ */
147
+ async function refreshScaffoldManifestFiles(projectDir, relativePaths, baselines) {
123
148
  const manifest = await readScaffoldManifest(projectDir);
124
149
  if (!manifest) return;
125
150
  for (const relativePath of new Set(relativePaths)) {
@@ -128,8 +153,12 @@ async function refreshScaffoldManifestFiles(projectDir, relativePaths) {
128
153
  if (!(await fs.stat(fullPath).catch(() => null))?.isFile()) continue;
129
154
  manifest.hashes[relativePath.split(path.sep).join("/")] = hashContent(await fs.readFile(fullPath));
130
155
  }
156
+ if (baselines && Object.keys(baselines).length > 0) manifest.baselines = {
157
+ ...manifest.baselines,
158
+ ...baselines
159
+ };
131
160
  await writeScaffoldManifest(projectDir, manifest);
132
161
  }
133
162
 
134
163
  //#endregion
135
- export { recordScaffoldManifest as a, readScaffoldManifest as i, computeScaffoldHashes as n, refreshScaffoldManifestFiles as o, hashContent as r, writeScaffoldManifest as s, SCAFFOLD_MANIFEST_FILE as t };
164
+ export { isStructuredBaselinePath as a, refreshScaffoldManifestFiles as c, hashContent as i, writeScaffoldManifest as l, collectStructuredBaselines as n, readScaffoldManifest as o, computeScaffoldHashes as r, recordScaffoldManifest as s, SCAFFOLD_MANIFEST_FILE as t };
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { r as readBtsConfig } from "./bts-config-DQVWvPDs.mjs";
2
+ import { r as readBtsConfig } from "./bts-config-Bwd6GB7B.mjs";
3
3
  import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
4
  import { a as handleError } from "./errors-ns_o2OKg.mjs";
5
- import "./file-formatter-gvmrpd-g.mjs";
6
- import { a as recordScaffoldManifest, i as readScaffoldManifest, r as hashContent, s as writeScaffoldManifest } from "./scaffold-manifest-Dyi0voqE.mjs";
7
- import { c as formatGeneratedTree, d as treeToFileMap, l as generateTree, s as configFromBtsConfig } from "./mcp-entry.mjs";
5
+ import "./file-formatter-DxF56-mO.mjs";
6
+ import { a as isStructuredBaselinePath, i as hashContent, l as writeScaffoldManifest, n as collectStructuredBaselines, o as readScaffoldManifest, s as recordScaffoldManifest } from "./scaffold-manifest-Dr3Anmlf.mjs";
7
+ import { c as configFromBtsConfig, d as mergeEnvExample, f as mergePackageJson, l as formatGeneratedTree, m as treeToFileMap, o as PACKAGE_JSON_SECTIONS, u as generateTree } from "./mcp-entry.mjs";
8
8
  import { intro, log, outro } from "@clack/prompts";
9
9
  import pc from "picocolors";
10
10
  import fs from "fs-extra";
@@ -15,16 +15,23 @@ import { writeSelectedFiles } from "@better-fullstack/template-generator/fs-writ
15
15
  //#region src/helpers/core/scaffold-upgrade.ts
16
16
  const BINARY_FILE_MARKER = "[Binary file]";
17
17
  /**
18
+ * Files that are never auto-patched: lockfiles are install artifacts and `.env`
19
+ * holds user secrets — both always go to manual review. package.json and
20
+ * *.env.example (see isStructuredBaselinePath) get a structured merge instead.
21
+ */
22
+ function isConservativeFile(relPath) {
23
+ const name = path.basename(relPath);
24
+ return name === ".env" || name === "bun.lock" || name === "bun.lockb" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
25
+ }
26
+ /**
18
27
  * Files whose on-disk bytes are mutated by create-time post-processing
19
28
  * (package-manager version, dependency version channel, db-setup, addons) or by
20
29
  * dependency install, so their scaffold baseline is not a pure-template render.
21
- * Never auto-patched always routed to manual review. A structured merge
22
- * (reusing stack-update's mergePackageJson / mergeEnvExample) is a deferred
23
- * follow-up; the MVP is conservative to avoid clobbering post-processed deps.
30
+ * They never take the plain hash-comparison path: they are either merged
31
+ * structurally or routed to manual review.
24
32
  */
25
33
  function isStructuredMergeFile(relPath) {
26
- const name = path.basename(relPath);
27
- return name === "package.json" || name === ".env" || name.endsWith(".env.example") || name === "bun.lock" || name === "bun.lockb" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
34
+ return isConservativeFile(relPath) || isStructuredBaselinePath(relPath);
28
35
  }
29
36
  /**
30
37
  * Generated docs (README) are re-derived from project mode / stack summary at
@@ -85,9 +92,108 @@ async function renderCurrentProject(projectDir) {
85
92
  return { error: `Failed to render current templates: ${error instanceof Error ? error.message : String(error)}` };
86
93
  }
87
94
  }
95
+ /** Deep equality ignoring object key order (renders may reorder catalog maps etc.). */
96
+ function deepEqualUnordered(a, b) {
97
+ if (a === b) return true;
98
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => deepEqualUnordered(item, b[index]));
99
+ if (a && b && typeof a === "object" && typeof b === "object") {
100
+ const aRecord = a;
101
+ const bRecord = b;
102
+ const aKeys = Object.keys(aRecord);
103
+ return aKeys.length === Object.keys(bRecord).length && aKeys.every((key) => key in bRecord && deepEqualUnordered(aRecord[key], bRecord[key]));
104
+ }
105
+ return false;
106
+ }
107
+ /**
108
+ * Template-side package.json changes mergePackageJson cannot express: key
109
+ * removals inside the merged sections and any change to other top-level fields
110
+ * (exports, workspaces, type, ...). Files with such changes go to manual review
111
+ * instead of being silently labeled user-edited or partially merged.
112
+ */
113
+ function findUnmergeableTemplateChanges(previousContent, proposedContent) {
114
+ let previous;
115
+ let proposed;
116
+ try {
117
+ previous = JSON.parse(previousContent);
118
+ proposed = JSON.parse(proposedContent);
119
+ } catch {
120
+ return [];
121
+ }
122
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
123
+ if (!isRecord(previous) || !isRecord(proposed)) return [];
124
+ const changes = [];
125
+ const mergedSections = new Set(PACKAGE_JSON_SECTIONS);
126
+ for (const section of PACKAGE_JSON_SECTIONS) {
127
+ const previousSection = isRecord(previous[section]) ? previous[section] : {};
128
+ const proposedSection = isRecord(proposed[section]) ? proposed[section] : {};
129
+ for (const key of Object.keys(previousSection)) if (!(key in proposedSection)) changes.push(`${section}.${key} removed`);
130
+ }
131
+ for (const key of new Set([...Object.keys(previous), ...Object.keys(proposed)])) {
132
+ if (mergedSections.has(key)) continue;
133
+ if (!deepEqualUnordered(previous[key], proposed[key])) changes.push(key);
134
+ }
135
+ return changes;
136
+ }
137
+ /**
138
+ * Structured 3-way merge for package.json / *.env.example, reusing stack-update's
139
+ * merge semantics: template-side changes (proposed vs the recorded render
140
+ * baseline) are folded into the user's file; keys the user (or create-time
141
+ * post-processing) changed are never overwritten — if the template also changed
142
+ * such a key, the whole file becomes a conflict naming the blocked keys.
143
+ */
144
+ function classifyStructuredMerge(filePath, existingContent, proposedContent, baselineContent) {
145
+ if (proposedContent === void 0 || proposedContent === BINARY_FILE_MARKER) return {
146
+ path: filePath,
147
+ category: "manual",
148
+ reason: "no comparable template render"
149
+ };
150
+ if (baselineContent === void 0) return {
151
+ path: filePath,
152
+ category: "manual",
153
+ reason: "no structured-merge baseline recorded — merge by hand or re-run `update --record-baseline`"
154
+ };
155
+ if (path.basename(filePath) === "package.json") {
156
+ const merged$1 = mergePackageJson(existingContent, baselineContent, proposedContent);
157
+ if (merged$1.blockers.length > 0) return {
158
+ path: filePath,
159
+ category: "conflict",
160
+ reason: `template and local copy both changed: ${merged$1.blockers.join(", ")}`
161
+ };
162
+ const uncovered = findUnmergeableTemplateChanges(baselineContent, proposedContent);
163
+ if (uncovered.length > 0) return {
164
+ path: filePath,
165
+ category: "manual",
166
+ reason: `template changes the merge cannot apply (${uncovered.join(", ")}) — update by hand`
167
+ };
168
+ if (merged$1.content) return {
169
+ path: filePath,
170
+ category: "merged",
171
+ reason: merged$1.summary.join("; "),
172
+ mergedContent: merged$1.content
173
+ };
174
+ return {
175
+ path: filePath,
176
+ category: "user-edited",
177
+ reason: "template dependencies/scripts unchanged — local changes kept"
178
+ };
179
+ }
180
+ const merged = mergeEnvExample(existingContent, baselineContent, proposedContent);
181
+ if (merged.content) return {
182
+ path: filePath,
183
+ category: "merged",
184
+ reason: `adds ${merged.keys.join(", ")}`,
185
+ mergedContent: merged.content
186
+ };
187
+ return {
188
+ path: filePath,
189
+ category: "user-edited",
190
+ reason: "no new template env keys — local changes kept"
191
+ };
192
+ }
88
193
  function summarize(projectDir, files, manifest) {
89
194
  const byCategory = (category) => files.filter((file) => file.category === category).map((file) => file.path);
90
195
  const drift = byCategory("drift");
196
+ const merged = byCategory("merged");
91
197
  const newFiles = byCategory("new-file");
92
198
  return {
93
199
  success: true,
@@ -100,9 +206,14 @@ function summarize(projectDir, files, manifest) {
100
206
  userEdited: byCategory("user-edited"),
101
207
  conflicts: byCategory("conflict"),
102
208
  manual: files.filter((file) => file.category === "manual"),
209
+ merged,
103
210
  newFiles,
104
211
  removed: byCategory("removed"),
105
- actionable: [...drift, ...newFiles].sort()
212
+ actionable: [
213
+ ...drift,
214
+ ...merged,
215
+ ...newFiles
216
+ ].sort()
106
217
  };
107
218
  }
108
219
  /**
@@ -117,7 +228,8 @@ async function planScaffoldUpgrade(projectDirInput) {
117
228
  projectDir,
118
229
  error: rendered.error
119
230
  };
120
- const { renderHashes } = rendered;
231
+ const { tree, renderHashes } = rendered;
232
+ const renderFiles = treeToFileMap(tree);
121
233
  const manifest = await readScaffoldManifest(projectDir);
122
234
  const baseline = manifest?.hashes ?? {};
123
235
  const hasBaseline = manifest !== null;
@@ -159,14 +271,18 @@ async function planScaffoldUpgrade(projectDirInput) {
159
271
  continue;
160
272
  }
161
273
  if (isSkippableDoc(filePath)) continue;
162
- if (isStructuredMergeFile(filePath)) {
274
+ if (isConservativeFile(filePath)) {
163
275
  files.push({
164
276
  path: filePath,
165
277
  category: "manual",
166
- reason: "post-processed file — merge dependencies/env by hand"
278
+ reason: "lockfile / secrets file — never auto-patched"
167
279
  });
168
280
  continue;
169
281
  }
282
+ if (isStructuredBaselinePath(filePath)) {
283
+ files.push(classifyStructuredMerge(filePath, diskBytes.toString("utf-8"), renderFiles.get(filePath)?.content, manifest?.baselines?.[filePath]));
284
+ continue;
285
+ }
170
286
  const baselineHash = baseline[filePath];
171
287
  if (baselineHash === void 0) {
172
288
  files.push({
@@ -210,10 +326,11 @@ async function planScaffoldUpgrade(projectDirInput) {
210
326
  return summarize(projectDir, files, manifest);
211
327
  }
212
328
  /**
213
- * Apply the safe part of the plan: overwrite template-drift files and write
214
- * brand-new template files, then refresh the baseline for every file that now
215
- * matches the current render. Conflicts, local edits, and post-processed files
216
- * are left untouched (and reported by the caller for manual review).
329
+ * Apply the safe part of the plan: overwrite template-drift files, write
330
+ * brand-new template files, write structured merges (package.json /
331
+ * *.env.example), then refresh the baseline for every file that was reconciled
332
+ * with the current render. Conflicts, local edits, and lockfiles/secrets are
333
+ * left untouched (and reported by the caller for manual review).
217
334
  */
218
335
  async function applyScaffoldUpgrade(projectDirInput) {
219
336
  const plan = await planScaffoldUpgrade(projectDirInput);
@@ -228,22 +345,47 @@ async function applyScaffoldUpgrade(projectDirInput) {
228
345
  const { tree, renderHashes } = rendered;
229
346
  const toWrite = new Set([...plan.drift, ...plan.newFiles]);
230
347
  if (toWrite.size > 0) await writeSelectedFiles(tree, projectDir, (candidate) => toWrite.has(candidate));
348
+ const mergedEntries = plan.files.filter((file) => file.category === "merged" && file.mergedContent !== void 0);
349
+ for (const entry of mergedEntries) await fs.writeFile(path.join(projectDir, entry.path), entry.mergedContent, "utf-8");
231
350
  const manifest = await readScaffoldManifest(projectDir);
232
351
  if (manifest) {
233
352
  for (const filePath of new Set([...plan.unchanged, ...toWrite])) {
234
353
  const renderHash = renderHashes.get(filePath);
235
354
  if (renderHash) manifest.hashes[filePath] = renderHash;
236
355
  }
356
+ for (const entry of mergedEntries) manifest.hashes[entry.path] = hashContent(Buffer.from(entry.mergedContent, "utf-8"));
357
+ const renderFiles = treeToFileMap(tree);
358
+ const reconciled = [
359
+ ...plan.unchanged,
360
+ ...toWrite,
361
+ ...mergedEntries.map((entry) => entry.path)
362
+ ];
363
+ for (const filePath of reconciled) {
364
+ if (!isStructuredBaselinePath(filePath)) continue;
365
+ const content = renderFiles.get(filePath)?.content;
366
+ if (content !== void 0 && content !== BINARY_FILE_MARKER) (manifest.baselines ??= {})[filePath] = content;
367
+ }
237
368
  await writeScaffoldManifest(projectDir, manifest);
238
369
  }
239
370
  return {
240
371
  ...plan,
241
372
  applied: {
242
373
  patched: [...plan.drift],
243
- added: [...plan.newFiles]
374
+ added: [...plan.newFiles],
375
+ merged: mergedEntries.map((entry) => entry.path)
244
376
  }
245
377
  };
246
378
  }
379
+ /**
380
+ * Record the scaffold baseline for an existing project (`update
381
+ * --record-baseline`): disk hashes plus, when the project still renders, the
382
+ * structured-merge content baselines for package.json / *.env.example.
383
+ */
384
+ async function recordUpgradeBaseline(projectDirInput) {
385
+ const projectDir = path.resolve(projectDirInput);
386
+ const rendered = await renderCurrentProject(projectDir);
387
+ return recordScaffoldManifest(projectDir, { baselines: "error" in rendered ? void 0 : collectStructuredBaselines(rendered.tree) });
388
+ }
247
389
 
248
390
  //#endregion
249
391
  //#region src/commands/update.ts
@@ -271,6 +413,12 @@ function reportManual(entries) {
271
413
  log.message(`Needs manual review (${entries.length}):`);
272
414
  for (const entry of entries) log.message(pc.dim(` ! ${entry.path}${entry.reason ? ` — ${entry.reason}` : ""}`));
273
415
  }
416
+ function reportMerged(plan) {
417
+ const merged = plan.files.filter((file) => file.category === "merged");
418
+ if (merged.length === 0) return;
419
+ log.message(`Structured merges (template changes folded into your file) (${merged.length}):`);
420
+ for (const entry of merged) log.message(pc.dim(` ± ${entry.path}${entry.reason ? ` — ${entry.reason}` : ""}`));
421
+ }
274
422
  function reportRemoved(plan) {
275
423
  const removed = plan.files.filter((file) => file.category === "removed");
276
424
  if (removed.length === 0) return;
@@ -282,13 +430,14 @@ function renderPlan(plan) {
282
430
  log.info(pc.dim(plan.hasBaseline ? `Baseline: bts.lock.json${plan.baselineCreatedAt ? ` (recorded ${plan.baselineCreatedAt})` : ""}` : "Baseline: none — run `update --record-baseline` to enable safe auto-patching"));
283
431
  log.message("");
284
432
  reportGroup("Template drift (safe to patch)", "~", plan.drift);
433
+ reportMerged(plan);
285
434
  reportGroup("New files from templates", "+", plan.newFiles);
286
435
  reportGroup("Locally edited (kept as-is)", "*", plan.userEdited);
287
436
  reportGroup("Conflicts (template + local both changed)", "!", plan.conflicts);
288
437
  reportManual(plan.manual);
289
438
  reportRemoved(plan);
290
439
  log.message("");
291
- log.message(pc.dim(`${plan.unchanged.length} up to date · ${plan.drift.length} drift · ${plan.newFiles.length} new · ${plan.userEdited.length} local · ${plan.conflicts.length} conflict · ${plan.manual.length} manual`));
440
+ log.message(pc.dim(`${plan.unchanged.length} up to date · ${plan.drift.length} drift · ${plan.merged.length} merge · ${plan.newFiles.length} new · ${plan.userEdited.length} local · ${plan.conflicts.length} conflict · ${plan.manual.length} manual`));
292
441
  }
293
442
  function toJsonPlan(plan) {
294
443
  return {
@@ -298,6 +447,7 @@ function toJsonPlan(plan) {
298
447
  summary: {
299
448
  unchanged: plan.unchanged.length,
300
449
  drift: plan.drift.length,
450
+ merged: plan.merged.length,
301
451
  newFiles: plan.newFiles.length,
302
452
  userEdited: plan.userEdited.length,
303
453
  conflicts: plan.conflicts.length,
@@ -305,10 +455,17 @@ function toJsonPlan(plan) {
305
455
  removed: plan.removed.length
306
456
  },
307
457
  drift: plan.drift,
458
+ merged: plan.files.filter((file) => file.category === "merged").map(({ path: filePath, reason }) => ({
459
+ path: filePath,
460
+ reason
461
+ })),
308
462
  newFiles: plan.newFiles,
309
463
  userEdited: plan.userEdited,
310
464
  conflicts: plan.conflicts,
311
- manual: plan.manual,
465
+ manual: plan.manual.map(({ path: filePath, reason }) => ({
466
+ path: filePath,
467
+ reason
468
+ })),
312
469
  removed: plan.removed,
313
470
  actionable: plan.actionable
314
471
  };
@@ -338,7 +495,7 @@ async function updateCommand(input) {
338
495
  handleError(message);
339
496
  }
340
497
  if (recordBaseline) {
341
- const manifest = await recordScaffoldManifest(projectDir);
498
+ const manifest = await recordUpgradeBaseline(projectDir);
342
499
  if (json) {
343
500
  console.log(JSON.stringify({
344
501
  projectDir,
@@ -384,15 +541,15 @@ async function updateCommand(input) {
384
541
  renderPlan(plan);
385
542
  log.message("");
386
543
  if (applied) {
387
- if (applied.patched.length + applied.added.length === 0) log.success(pc.green("Already up to date. No template-drift patches to apply."));
388
- else log.success(pc.green(`Applied ${formatCount(applied.patched.length, "patch")} and added ${formatCount(applied.added.length, "file")}.`));
544
+ if (applied.patched.length + applied.added.length + applied.merged.length === 0) log.success(pc.green("Already up to date. No template-drift patches to apply."));
545
+ else log.success(pc.green(`Applied ${formatCount(applied.patched.length, "patch")}, ${formatCount(applied.merged.length, "structured merge")}, and added ${formatCount(applied.added.length, "file")}.`));
389
546
  const leftover = plan.conflicts.length + plan.manual.length;
390
- if (leftover > 0) log.warn(pc.yellow(`${formatCount(leftover, "file")} still need manual review (conflicts + post-processed files).`));
547
+ if (leftover > 0) log.warn(pc.yellow(`${formatCount(leftover, "file")} still need manual review (conflicts + lockfiles/manual files).`));
391
548
  outro(pc.magenta("Update complete."));
392
549
  return;
393
550
  }
394
551
  if (plan.actionable.length === 0) log.success(pc.green("Up to date with the current templates."));
395
- else log.info(pc.cyan(`Run \`bfs update --apply\` to patch ${formatCount(plan.drift.length, "drift file")} and add ${formatCount(plan.newFiles.length, "new file")}.`));
552
+ else log.info(pc.cyan(`Run \`bfs update --apply\` to patch ${formatCount(plan.drift.length, "drift file")}, apply ${formatCount(plan.merged.length, "structured merge")}, and add ${formatCount(plan.newFiles.length, "new file")}.`));
396
553
  outro(pc.magenta(apply ? "Update complete." : "Dry run — no files were written."));
397
554
  if (check && plan.actionable.length > 0) process.exit(1);
398
555
  }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "create-better-fullstack",
3
- "version": "2.1.8",
4
- "description": "Scaffold production-ready fullstack apps in seconds. Pick your stack from 425 optionsthe CLI wires everything together.",
3
+ "version": "2.3.0",
4
+ "description": "Scaffold production-ready fullstack apps in TypeScript, React Native, Rust, Go, Python, Java, .NET, and Elixir visual builder, CLI, and MCP server, with every integration wired together.",
5
5
  "keywords": [
6
+ "ai-agent",
6
7
  "algolia",
7
8
  "angular",
9
+ "aspnet",
8
10
  "astro",
9
11
  "authentication",
10
12
  "better-auth",
@@ -12,14 +14,19 @@
12
14
  "biome",
13
15
  "boilerplate",
14
16
  "bun",
17
+ "claude-code",
18
+ "clerk",
15
19
  "cli",
16
20
  "create",
17
21
  "create-app",
22
+ "csharp",
18
23
  "desktop",
19
24
  "developer-tools",
20
25
  "diesel",
21
26
  "docker",
27
+ "dotnet",
22
28
  "drizzle",
29
+ "elixir",
23
30
  "elysia",
24
31
  "ent",
25
32
  "expo",
@@ -32,8 +39,12 @@
32
39
  "graphql-yoga",
33
40
  "hono",
34
41
  "i18next",
42
+ "java",
43
+ "kotlin",
35
44
  "litestar",
45
+ "mcp",
36
46
  "mobile",
47
+ "model-context-protocol",
37
48
  "moka",
38
49
  "monorepo",
39
50
  "next-intl",
@@ -41,7 +52,9 @@
41
52
  "nuxt",
42
53
  "paraglide",
43
54
  "payments",
55
+ "phoenix",
44
56
  "pnpm",
57
+ "postgresql",
45
58
  "pothos",
46
59
  "prisma",
47
60
  "project-generator",
@@ -55,11 +68,15 @@
55
68
  "scaffolding",
56
69
  "shadcn",
57
70
  "solid",
71
+ "spring-boot",
72
+ "sqlite",
58
73
  "starter",
59
74
  "starter-kit",
60
75
  "strawberry",
76
+ "stripe",
61
77
  "svelte",
62
78
  "sveltekit",
79
+ "t3-stack",
63
80
  "tailwind",
64
81
  "tanstack",
65
82
  "tauri",
@@ -128,15 +145,15 @@
128
145
  "prepublishOnly": "npm run build"
129
146
  },
130
147
  "dependencies": {
131
- "@better-fullstack/template-generator": "^2.1.8",
132
- "@better-fullstack/types": "^2.1.8",
148
+ "@better-fullstack/template-generator": "^2.3.0",
149
+ "@better-fullstack/types": "^2.3.0",
133
150
  "@clack/core": "^0.5.0",
134
- "@clack/prompts": "^1.6.0",
135
- "@orpc/server": "^1.14.6",
151
+ "@clack/prompts": "^1.7.0",
152
+ "@orpc/server": "^1.14.8",
136
153
  "consola": "^3.4.2",
137
154
  "env-paths": "^4.0.0",
138
155
  "execa": "^9.6.1",
139
- "fs-extra": "^11.3.5",
156
+ "fs-extra": "^11.3.6",
140
157
  "gradient-string": "^3.0.0",
141
158
  "jsonc-parser": "^3.3.1",
142
159
  "oxfmt": "^0.19.0",
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./bts-config-DQVWvPDs.mjs";
3
- import "./errors-ns_o2OKg.mjs";
4
- import "./prompt-environment-BR0Kkw2W.mjs";
5
- import { t as setupAddons } from "./addons-setup-LaAj43NP.mjs";
6
-
7
- export { setupAddons };
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./bts-config-DQVWvPDs.mjs";
3
- import { a as createBtsCli, c as history, d as telemetry, f as update, i as create, l as router, n as builder, o as docs, r as check, s as doctor, t as add, u as sponsors } from "./run-T6g1jkfy.mjs";
4
- import "./render-title-zvyKC1ej.mjs";
5
- import "./errors-ns_o2OKg.mjs";
6
- import "./prompt-environment-BR0Kkw2W.mjs";
7
- import "./analytics-DVltG11u.mjs";
8
- import "./file-formatter-gvmrpd-g.mjs";
9
- import "./install-dependencies-RoUyaE8o.mjs";
10
- import "./generated-checks-BV9jol5h.mjs";
11
- import "./config-processing-D9-F2Us9.mjs";
12
- import "./scaffold-manifest-Dyi0voqE.mjs";
13
- import "./addons-setup-LaAj43NP.mjs";
14
-
15
- export { createBtsCli };
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as recordScaffoldManifest, i as readScaffoldManifest, n as computeScaffoldHashes, o as refreshScaffoldManifestFiles, r as hashContent, s as writeScaffoldManifest, t as SCAFFOLD_MANIFEST_FILE } from "./scaffold-manifest-Dyi0voqE.mjs";
3
-
4
- export { recordScaffoldManifest };