create-better-fullstack 2.1.8 → 2.2.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/dist/{add-handler-ztNjzoMN.mjs → add-handler-CE3XIs3y.mjs} +8 -8
- package/dist/{addons-setup-LaAj43NP.mjs → addons-setup-DR924M7b.mjs} +1 -1
- package/dist/addons-setup-Du2VSv8V.mjs +7 -0
- package/dist/{analytics-DVltG11u.mjs → analytics-BXlOG6u6.mjs} +1 -1
- package/dist/{bts-config-DQVWvPDs.mjs → bts-config-B_HhcMXL.mjs} +30 -0
- package/dist/cli.mjs +2 -2
- package/dist/{config-processing-D9-F2Us9.mjs → config-processing-DJisMeW-.mjs} +17 -1
- package/dist/{doctor-a4ca3SMd.mjs → doctor-Cc7sLuow.mjs} +2 -2
- package/dist/{file-formatter-gvmrpd-g.mjs → file-formatter-BgbfpYf4.mjs} +442 -22
- package/dist/{gen-CCClL7Ve.mjs → gen-BZ0QCxCC.mjs} +1 -1
- package/dist/{generated-checks-BV9jol5h.mjs → generated-checks-CbioGjCh.mjs} +1 -1
- package/dist/index.d.mts +461 -183
- package/dist/index.mjs +20 -9
- package/dist/{install-dependencies-RoUyaE8o.mjs → install-dependencies-D__bmW4k.mjs} +86 -18
- package/dist/{mcp-492OkjcS.mjs → mcp-DNKAo8nO.mjs} +5 -5
- package/dist/mcp-entry.mjs +37 -225
- package/dist/{registry-DSf2CEaU.mjs → registry-Cm3D45oT.mjs} +1 -1
- package/dist/run-C_S7ujfY.mjs +15 -0
- package/dist/{run-T6g1jkfy.mjs → run-DmN7QRt5.mjs} +1155 -292
- package/dist/scaffold-manifest-ByKtMhTd.mjs +4 -0
- package/dist/{scaffold-manifest-Dyi0voqE.mjs → scaffold-manifest-CNFz-lmH.mjs} +34 -5
- package/dist/{update-DJ8CI5KW.mjs → update-B270pWHc.mjs} +182 -25
- package/package.json +6 -6
- package/dist/addons-setup-DnLjAzTw.mjs +0 -7
- package/dist/run-CD8f7PHh.mjs +0 -15
- package/dist/scaffold-manifest-DGRyepdb.mjs +0 -4
|
@@ -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-CNFz-lmH.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:
|
|
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
|
-
/**
|
|
122
|
-
|
|
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 {
|
|
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-
|
|
2
|
+
import { r as readBtsConfig } from "./bts-config-B_HhcMXL.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-
|
|
6
|
-
import { a as
|
|
7
|
-
import { c as
|
|
5
|
+
import "./file-formatter-BgbfpYf4.mjs";
|
|
6
|
+
import { a as isStructuredBaselinePath, i as hashContent, l as writeScaffoldManifest, n as collectStructuredBaselines, o as readScaffoldManifest, s as recordScaffoldManifest } from "./scaffold-manifest-CNFz-lmH.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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
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: [
|
|
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 (
|
|
274
|
+
if (isConservativeFile(filePath)) {
|
|
163
275
|
files.push({
|
|
164
276
|
path: filePath,
|
|
165
277
|
category: "manual",
|
|
166
|
-
reason: "
|
|
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
|
|
214
|
-
* brand-new template files,
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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
|
|
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 +
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-fullstack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Scaffold production-ready fullstack apps in seconds. Pick your stack from 425 options — the CLI wires everything together.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"algolia",
|
|
@@ -128,15 +128,15 @@
|
|
|
128
128
|
"prepublishOnly": "npm run build"
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@better-fullstack/template-generator": "^2.
|
|
132
|
-
"@better-fullstack/types": "^2.
|
|
131
|
+
"@better-fullstack/template-generator": "^2.2.0",
|
|
132
|
+
"@better-fullstack/types": "^2.2.0",
|
|
133
133
|
"@clack/core": "^0.5.0",
|
|
134
|
-
"@clack/prompts": "^1.
|
|
135
|
-
"@orpc/server": "^1.14.
|
|
134
|
+
"@clack/prompts": "^1.7.0",
|
|
135
|
+
"@orpc/server": "^1.14.8",
|
|
136
136
|
"consola": "^3.4.2",
|
|
137
137
|
"env-paths": "^4.0.0",
|
|
138
138
|
"execa": "^9.6.1",
|
|
139
|
-
"fs-extra": "^11.3.
|
|
139
|
+
"fs-extra": "^11.3.6",
|
|
140
140
|
"gradient-string": "^3.0.0",
|
|
141
141
|
"jsonc-parser": "^3.3.1",
|
|
142
142
|
"oxfmt": "^0.19.0",
|
package/dist/run-CD8f7PHh.mjs
DELETED
|
@@ -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 };
|