create-better-fullstack 2.1.2 → 2.1.4

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/index.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import "./errors-D9yiiGVq.mjs";
3
- import { a as docs, c as sponsors, i as createBtsCli, l as telemetry, n as builder, o as history, r as create, s as router, t as add } from "./run-DRzP53v3.mjs";
2
+ import "./errors-Cyol8zbN.mjs";
3
+ import { a as createBtsCli, c as history, d as telemetry, 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-BYse4yJy.mjs";
4
4
  import "./render-title-zvyKC1ej.mjs";
5
- import "./addons-setup-C_xrNtkL.mjs";
6
- import "./config-validation-C4glouQh.mjs";
7
- import "./compatibility-rules-D7zYNVjC.mjs";
8
- import "./install-dependencies-DDGF-zDG.mjs";
9
- import "./generated-checks-DUvVXWId.mjs";
5
+ import "./addons-setup-CyrP1IV-.mjs";
6
+ import "./file-formatter-B3dsev2l.mjs";
7
+ import "./install-dependencies-CgNh-aOy.mjs";
8
+ import "./generated-checks-C8hn9w2i.mjs";
10
9
  import { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, generateVirtualProject } from "@better-fullstack/template-generator";
11
10
 
12
11
  //#region src/index.ts
@@ -199,4 +198,4 @@ async function createVirtual(options) {
199
198
  }
200
199
 
201
200
  //#endregion
202
- export { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generateVirtualProject, history, router, sponsors, telemetry };
201
+ export { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, add, builder, check, create, createBtsCli, createVirtual, docs, doctor, generateVirtualProject, history, router, sponsors, telemetry };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, d as setLastPromptShownUI, r as exitCancelled, s as isFirstPrompt, u as setIsFirstPrompt$1, y as DEFAULT_CONFIG } from "./errors-D9yiiGVq.mjs";
3
- import { m as validateAddonCompatibility, r as getCompatibleAddons } from "./compatibility-rules-D7zYNVjC.mjs";
2
+ import { A as types_exports, d as setLastPromptShownUI, r as exitCancelled, s as isFirstPrompt, u as setIsFirstPrompt$1, y as DEFAULT_CONFIG } from "./errors-Cyol8zbN.mjs";
3
+ import { g as validateAddonCompatibility, o as getCompatibleAddons } from "./file-formatter-B3dsev2l.mjs";
4
4
  import { log, spinner } from "@clack/prompts";
5
5
  import pc from "picocolors";
6
6
  import fs from "fs-extra";
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./errors-D9yiiGVq.mjs";
3
- import "./config-validation-C4glouQh.mjs";
4
- import "./compatibility-rules-D7zYNVjC.mjs";
2
+ import "./errors-Cyol8zbN.mjs";
3
+ import "./file-formatter-B3dsev2l.mjs";
5
4
  import { i as startMcpServer, n as MCP_STACK_UPDATE_SCHEMA, r as getMcpGraphPreview, t as MCP_PLAN_CREATE_SCHEMA } from "./mcp-entry.mjs";
6
5
 
7
6
  export { startMcpServer };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, C as getEffectiveStack, D as getGraphSummary, _ as writeBtsConfig, f as buildBtsConfigForPersistence, m as readBtsConfig, p as previewBtsConfigUpdate, v as getLatestCLIVersion, x as getDefaultConfig } from "./errors-D9yiiGVq.mjs";
3
- import { a as generateReproducibleCommand, i as getTemplateDescription, r as getTemplateConfig, s as CreateCommandOptionsSchema, t as validateConfigForProgrammaticUse } from "./config-validation-C4glouQh.mjs";
4
- import "./compatibility-rules-D7zYNVjC.mjs";
2
+ import { A as types_exports, C as getEffectiveStack, D as getGraphSummary, _ as writeBtsConfig, f as buildBtsConfigForPersistence, m as readBtsConfig, p as previewBtsConfigUpdate, v as getLatestCLIVersion, x as getDefaultConfig } from "./errors-Cyol8zbN.mjs";
3
+ import { r as validateConfigForProgrammaticUse, t as formatCode, v as CreateCommandOptionsSchema } from "./file-formatter-B3dsev2l.mjs";
4
+ import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-CnTOtKjm.mjs";
5
5
  import z from "zod";
6
6
  import fs from "fs-extra";
7
7
  import path from "node:path";
@@ -153,6 +153,75 @@ function mergeStackPartSpecs(currentConfig, specs) {
153
153
  stackParts
154
154
  };
155
155
  }
156
+ const GRAPH_CACHE_CONFIG_KEYS = new Set([
157
+ "stackParts",
158
+ "graphSummary",
159
+ "effectiveStack"
160
+ ]);
161
+ function stableJson(value) {
162
+ return JSON.stringify(value);
163
+ }
164
+ function getChangedConfigKeys(currentConfig, proposedConfig) {
165
+ const keys = new Set([...Object.keys(currentConfig), ...Object.keys(proposedConfig)]);
166
+ const changed = /* @__PURE__ */ new Set();
167
+ for (const key of keys) {
168
+ if (GRAPH_CACHE_CONFIG_KEYS.has(key)) continue;
169
+ if (stableJson(currentConfig[key]) !== stableJson(proposedConfig[key])) changed.add(key);
170
+ }
171
+ return changed;
172
+ }
173
+ function getProjectedConfigKeys(part, parts) {
174
+ const owner = part.ownerPartId ? parts.find((candidate) => candidate.id === part.ownerPartId) : void 0;
175
+ const partProjection = (0, types_exports.stackPartsToLegacyProjectConfigPartial)(owner ? [owner, part] : [part]);
176
+ const keys = /* @__PURE__ */ new Set();
177
+ for (const key of Object.keys(partProjection)) {
178
+ if (GRAPH_CACHE_CONFIG_KEYS.has(key)) continue;
179
+ const value = partProjection[key];
180
+ if (value === part.toolId || Array.isArray(value) && value.includes(part.toolId)) keys.add(key);
181
+ }
182
+ return keys;
183
+ }
184
+ function getUpdatedSpecForChangedPart(part, parts, proposedConfig, changedKeys) {
185
+ const projectedKeys = getProjectedConfigKeys(part, parts);
186
+ for (const key of projectedKeys) {
187
+ if (!changedKeys.has(key)) continue;
188
+ const value = proposedConfig[key];
189
+ if (typeof value !== "string" || value === "none") continue;
190
+ return (0, types_exports.formatStackPartSpec)({
191
+ ...part,
192
+ toolId: value
193
+ }, parts);
194
+ }
195
+ }
196
+ function mergeDerivedStackPartsWithExistingGraph(currentConfig, proposedConfig) {
197
+ const currentStackParts = currentConfig.stackParts?.length ? currentConfig.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(currentConfig);
198
+ const derivedStackParts = (0, types_exports.legacyProjectConfigToStackParts)(proposedConfig);
199
+ if (currentStackParts.length === 0) return derivedStackParts;
200
+ const changedKeys = getChangedConfigKeys(currentConfig, proposedConfig);
201
+ const preservedParts = currentStackParts.filter((part) => {
202
+ if (part.source === "provided") return false;
203
+ if (part.toolId === "none") return false;
204
+ const projectedKeys = getProjectedConfigKeys(part, currentStackParts);
205
+ if (projectedKeys.size === 0) return true;
206
+ return [...projectedKeys].every((key) => !changedKeys.has(key));
207
+ });
208
+ const updatedSpecs = currentStackParts.filter((part) => part.source !== "provided" && part.toolId !== "none").flatMap((part) => {
209
+ const spec = getUpdatedSpecForChangedPart(part, currentStackParts, proposedConfig, changedKeys);
210
+ return spec ? [spec] : [];
211
+ });
212
+ const preservedSpecs = new Set(preservedParts.map((part) => (0, types_exports.formatStackPartSpec)(part, currentStackParts)));
213
+ const coveredParts = (0, types_exports.parseStackPartSpecs)([...new Set([...preservedSpecs, ...updatedSpecs])], "selected");
214
+ const preservedProjectedKeys = /* @__PURE__ */ new Set();
215
+ for (const part of coveredParts) for (const key of getProjectedConfigKeys(part, coveredParts)) preservedProjectedKeys.add(key);
216
+ const nextSpecs = [...new Set([...preservedSpecs, ...updatedSpecs])];
217
+ for (const part of derivedStackParts) {
218
+ if (part.source === "provided") continue;
219
+ const spec = (0, types_exports.formatStackPartSpec)(part, derivedStackParts);
220
+ if (nextSpecs.includes(spec)) continue;
221
+ if (![...getProjectedConfigKeys(part, derivedStackParts)].some((key) => preservedProjectedKeys.has(key))) nextSpecs.push(spec);
222
+ }
223
+ return (0, types_exports.parseStackPartSpecs)([...new Set(nextSpecs)], "selected");
224
+ }
156
225
  function asString(value, fallback = "none") {
157
226
  return typeof value === "string" ? value : fallback;
158
227
  }
@@ -543,6 +612,30 @@ async function generateTree(config) {
543
612
  if (!result.success || !result.tree) throw new Error(result.error ?? "Failed to generate virtual project");
544
613
  return result.tree;
545
614
  }
615
+ async function formatGeneratedTree(tree) {
616
+ const denoConfigDirs = /* @__PURE__ */ new Set();
617
+ function collectDenoConfigDirs(nodes) {
618
+ for (const node of nodes) if (node.type === "file") {
619
+ if (node.name === "deno.json") denoConfigDirs.add(path.posix.dirname(node.path));
620
+ } else collectDenoConfigDirs(node.children);
621
+ }
622
+ function isUnderDenoConfig(filePath) {
623
+ return [...denoConfigDirs].some((dir) => dir === "." || filePath === dir || filePath.startsWith(`${dir}/`));
624
+ }
625
+ async function formatNodes(nodes) {
626
+ await Promise.all(nodes.map(async (node) => {
627
+ if (node.type === "file") {
628
+ if (node.content === BINARY_FILE_MARKER || isUnderDenoConfig(node.path)) return;
629
+ const formatted = await formatCode(node.path, node.content);
630
+ if (formatted) node.content = formatted;
631
+ return;
632
+ }
633
+ await formatNodes(node.children);
634
+ }));
635
+ }
636
+ collectDenoConfigDirs(tree.root.children);
637
+ await formatNodes(tree.root.children);
638
+ }
546
639
  function treeToFileMap(tree) {
547
640
  const files = /* @__PURE__ */ new Map();
548
641
  function walk(nodes) {
@@ -552,6 +645,27 @@ function treeToFileMap(tree) {
552
645
  walk(tree.root.children);
553
646
  return files;
554
647
  }
648
+ function treeToFileSnapshotMap(tree) {
649
+ const files = /* @__PURE__ */ new Map();
650
+ function walk(nodes) {
651
+ for (const node of nodes) if (node.type === "file") files.set(node.path, {
652
+ content: node.content,
653
+ sourcePath: node.sourcePath
654
+ });
655
+ else walk(node.children);
656
+ }
657
+ walk(tree.root.children);
658
+ return files;
659
+ }
660
+ function uniqueContents(contents) {
661
+ return [...new Set(contents.filter((content) => content !== void 0))];
662
+ }
663
+ function contentMatchesAny(content, candidates) {
664
+ return candidates.some((candidate) => candidate === content);
665
+ }
666
+ function contentSetsIntersect(left, right) {
667
+ return left.some((candidate) => right.includes(candidate));
668
+ }
555
669
  function parseJson(content) {
556
670
  if (!content) return null;
557
671
  try {
@@ -668,11 +782,61 @@ function collectEnvReferences(content) {
668
782
  ]) for (const match of content.matchAll(pattern)) if (match[1]) keys.add(match[1]);
669
783
  return keys;
670
784
  }
785
+ function collectImplicitEnvKeys(config) {
786
+ const keys = /* @__PURE__ */ new Set();
787
+ if (config.email === "resend") {
788
+ keys.add("RESEND_API_KEY");
789
+ keys.add("RESEND_FROM_EMAIL");
790
+ }
791
+ if (config.observability === "sentry") keys.add("SENTRY_DSN");
792
+ return [...keys].sort();
793
+ }
671
794
  function recordEnvReferenceChanges(envChanges, filePath, previousContent, proposedContent) {
672
795
  const previous = collectEnvReferences(previousContent);
673
796
  const added = [...collectEnvReferences(proposedContent)].filter((key) => !previous.has(key)).sort();
674
797
  if (added.length > 0) envChanges[filePath] = [...new Set([...envChanges[filePath] ?? [], ...added])].sort();
675
798
  }
799
+ function appendMissingEnvKeys(content, keys) {
800
+ const existingKeys = parseEnvKeys(content);
801
+ const missing = keys.filter((key) => !existingKeys.has(key));
802
+ if (missing.length === 0) return { keys: [] };
803
+ return {
804
+ content: `${content}${content.trim().length === 0 ? "" : content.endsWith("\n") ? "\n" : "\n\n"}${missing.map((key) => `${key}=`).join("\n")}\n`,
805
+ keys: missing
806
+ };
807
+ }
808
+ async function addMissingEnvExampleOperation(options) {
809
+ const requiredKeys = [...new Set([...Object.entries(options.envChanges).filter(([filePath]) => !isEnvFilePath(filePath)).flatMap(([, keys]) => keys), ...collectImplicitEnvKeys(options.proposedConfig)])].sort();
810
+ if (requiredKeys.length === 0) return;
811
+ const candidatePaths = new Set([
812
+ "apps/server/.env.example",
813
+ ".env.example",
814
+ ...[...options.proposedGeneratedFiles.keys()].filter(isEnvFilePath)
815
+ ]);
816
+ let targetPath;
817
+ for (const candidate of candidatePaths) if (options.proposedGeneratedFiles.has(candidate) || await fs.pathExists(path.join(options.projectDir, candidate))) {
818
+ targetPath = candidate;
819
+ break;
820
+ }
821
+ if (!targetPath) return;
822
+ const existingOperation = options.operations.find((operation) => operation.path === targetPath && operation.writeMode === "content");
823
+ const targetFilePath = path.join(options.projectDir, targetPath);
824
+ const merged = appendMissingEnvKeys(existingOperation?.content ?? (await fs.pathExists(targetFilePath) ? await fs.readFile(targetFilePath, "utf-8") : ""), requiredKeys);
825
+ if (!merged.content) return;
826
+ if (existingOperation) {
827
+ existingOperation.content = merged.content;
828
+ existingOperation.summary = [...existingOperation.summary, `env refs: ${merged.keys.join(", ")}`];
829
+ } else options.operations.push({
830
+ kind: "merge",
831
+ path: targetPath,
832
+ writeMode: "content",
833
+ content: merged.content,
834
+ summary: [`env refs: ${merged.keys.join(", ")}`]
835
+ });
836
+ if (await fs.pathExists(targetFilePath)) options.filesToPatch.push(targetPath);
837
+ else options.filesToAdd.push(targetPath);
838
+ options.envChanges[targetPath] = [...new Set([...options.envChanges[targetPath] ?? [], ...merged.keys])].sort();
839
+ }
676
840
  function getInstallCommand$1(config) {
677
841
  switch (config.ecosystem) {
678
842
  case "rust": return "cargo build";
@@ -687,7 +851,7 @@ function getGraphPreview(config) {
687
851
  const stackParts = config.stackParts?.length ? config.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(config);
688
852
  const graphSummary = stackParts.length > 0 ? getGraphSummary({ stackParts }) : void 0;
689
853
  const effectiveStack = stackParts.length > 0 ? getEffectiveStack({ stackParts }) : void 0;
690
- const stackPartSpecs = stackParts.filter((part) => part.source !== "provided").map((part) => (0, types_exports.formatStackPartSpec)(part, stackParts));
854
+ const stackPartSpecs = stackParts.filter((part) => part.source !== "provided" && part.toolId !== "none").map((part) => (0, types_exports.formatStackPartSpec)(part, stackParts));
691
855
  return {
692
856
  ...graphSummary ? {
693
857
  graphSummary,
@@ -721,7 +885,7 @@ async function planStackUpdate(projectDirInput, input) {
721
885
  };
722
886
  const compatibilityAdjustments = [...dependencyExpansion.adjustments, ...compatibilityResult.changes.map((change) => `${change.category}: ${change.message}`)];
723
887
  if (compatibilityResult.adjustedStack) proposedConfig = mergeProjectConfig(proposedConfig, compatibilityChangesToProjectConfig(compatibilityResult.adjustedStack, proposedConfig));
724
- proposedConfig.stackParts = (0, types_exports.legacyProjectConfigToStackParts)(proposedConfig);
888
+ proposedConfig.stackParts = mergeDerivedStackPartsWithExistingGraph(currentConfig, proposedConfig);
725
889
  Object.assign(proposedConfig, mergeStackPartSpecs(proposedConfig, stackPartSpecs));
726
890
  try {
727
891
  validateConfigForProgrammaticUse(proposedConfig);
@@ -748,6 +912,9 @@ async function planStackUpdate(projectDirInput, input) {
748
912
  error: `Failed to generate stack update plan: ${error instanceof Error ? error.message : String(error)}`
749
913
  };
750
914
  }
915
+ const currentRawGeneratedFiles = treeToFileSnapshotMap(currentTree);
916
+ const proposedRawGeneratedFiles = treeToFileSnapshotMap(proposedTree);
917
+ await Promise.all([formatGeneratedTree(currentTree), formatGeneratedTree(proposedTree)]);
751
918
  const currentGeneratedFiles = treeToFileMap(currentTree);
752
919
  const proposedGeneratedFiles = treeToFileMap(proposedTree);
753
920
  const operations = [];
@@ -770,8 +937,12 @@ async function planStackUpdate(projectDirInput, input) {
770
937
  }));
771
938
  for (const { filePath, proposedFile, exists, existingBuffer } of proposedFileEntries) {
772
939
  const previousFile = currentGeneratedFiles.get(filePath);
940
+ const previousRawFile = currentRawGeneratedFiles.get(filePath);
941
+ const proposedRawFile = proposedRawGeneratedFiles.get(filePath);
773
942
  const proposedContent = proposedFile.content;
774
943
  const previousContent = previousFile?.content;
944
+ const currentBaselineContents = uniqueContents([previousRawFile?.content, previousContent]);
945
+ const proposedBaselineContents = uniqueContents([proposedRawFile?.content, proposedContent]);
775
946
  const isBinaryFile = isGeneratedBinaryFile(proposedFile) || isGeneratedBinaryFile(previousFile);
776
947
  const existingContent = exists && existingBuffer && !isBinaryFile ? existingBuffer.toString("utf-8") : void 0;
777
948
  if (!exists) {
@@ -803,7 +974,7 @@ async function planStackUpdate(projectDirInput, input) {
803
974
  manualReviewBlockers.push(`${filePath}: existing binary file differs from the generated baseline`);
804
975
  continue;
805
976
  }
806
- if (existingContent === void 0 || existingContent === proposedContent) {
977
+ if (existingContent === void 0 || contentMatchesAny(existingContent, proposedBaselineContents)) {
807
978
  filesUnchanged.push(filePath);
808
979
  continue;
809
980
  }
@@ -839,7 +1010,11 @@ async function planStackUpdate(projectDirInput, input) {
839
1010
  }
840
1011
  continue;
841
1012
  }
842
- if (previousContent !== void 0 && existingContent === previousContent) {
1013
+ if (existingContent !== void 0 && contentMatchesAny(existingContent, currentBaselineContents)) {
1014
+ if (contentSetsIntersect(currentBaselineContents, proposedBaselineContents)) {
1015
+ filesUnchanged.push(filePath);
1016
+ continue;
1017
+ }
843
1018
  filesToPatch.push(filePath);
844
1019
  operations.push({
845
1020
  kind: "replace",
@@ -852,6 +1027,15 @@ async function planStackUpdate(projectDirInput, input) {
852
1027
  if (isSkippableGeneratedDoc(filePath)) continue;
853
1028
  manualReviewBlockers.push(`${filePath}: existing file differs from the generated baseline`);
854
1029
  }
1030
+ await addMissingEnvExampleOperation({
1031
+ projectDir,
1032
+ proposedGeneratedFiles,
1033
+ operations,
1034
+ filesToAdd,
1035
+ filesToPatch,
1036
+ envChanges,
1037
+ proposedConfig: normalizedProposedConfig
1038
+ });
855
1039
  const graphPreview = getGraphPreview(persistedProposedConfig);
856
1040
  return {
857
1041
  success: true,
@@ -882,6 +1066,7 @@ async function applyStackUpdate(projectDirInput, input) {
882
1066
  const projectName = await inferProjectName(plan.projectDir);
883
1067
  const proposedConfig = configFromBtsConfig(plan.proposedConfig, plan.projectDir, projectName);
884
1068
  const proposedTree = await generateTree(proposedConfig);
1069
+ await formatGeneratedTree(proposedTree);
885
1070
  const generatedPaths = new Set(plan.operations.filter((operation) => operation.writeMode === "generated").map((operation) => operation.path));
886
1071
  if (generatedPaths.size > 0) await writeSelectedFiles(proposedTree, plan.projectDir, (filePath) => generatedPaths.has(filePath));
887
1072
  await Promise.all(plan.operations.filter((operation) => operation.writeMode === "content").map(async (operation) => {
@@ -1336,7 +1521,7 @@ function getMcpGraphPreview(config) {
1336
1521
  const stackParts = config.stackParts?.length ? config.stackParts : legacyProjectConfigToStackParts(config);
1337
1522
  const graphSummary = stackParts.length > 0 ? getGraphSummary({ stackParts }) : void 0;
1338
1523
  const effectiveStack = stackParts.length > 0 ? getEffectiveStack({ stackParts }) : void 0;
1339
- const stackPartSpecs = stackParts.filter((part) => part.source !== "provided").map((part) => formatStackPartSpec(part, stackParts));
1524
+ const stackPartSpecs = stackParts.filter((part) => part.source !== "provided" && part.toolId !== "none").map((part) => formatStackPartSpec(part, stackParts));
1340
1525
  return {
1341
1526
  ...graphSummary ? {
1342
1527
  graphSummary,
@@ -2208,7 +2393,7 @@ async function startMcpServer() {
2208
2393
  const graphPreview = getMcpGraphPreview(config);
2209
2394
  let addonWarnings = [];
2210
2395
  if (config.addons.length > 0 && config.addons[0] !== "none") {
2211
- const { setupAddons } = await import("./addons-setup-C8eaCaH5.mjs");
2396
+ const { setupAddons } = await import("./addons-setup-DyMm42a5.mjs");
2212
2397
  addonWarnings = await setupAddons(config);
2213
2398
  }
2214
2399
  const installCmd = getInstallCommand(input.ecosystem ?? "typescript", projectName, input.packageManager, input.javaBuildTool, input.javaWebFramework);
@@ -2533,4 +2718,4 @@ async function startMcpServer() {
2533
2718
  startMcpServer();
2534
2719
 
2535
2720
  //#endregion
2536
- export { startMcpServer as i, MCP_STACK_UPDATE_SCHEMA as n, getMcpGraphPreview as r, MCP_PLAN_CREATE_SCHEMA as t };
2721
+ export { applyStackUpdate as a, startMcpServer as i, MCP_STACK_UPDATE_SCHEMA as n, planStackUpdate as o, getMcpGraphPreview as r, MCP_PLAN_CREATE_SCHEMA as t };
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, D as getGraphSummary, E as getGraphPart, S as getUserPkgManager, T as getGraphBackendUrl, _ as writeBtsConfig, a as handleError, b as DEFAULT_UI_LIBRARY_BY_FRONTEND, c as isSilent, d as setLastPromptShownUI, f as buildBtsConfigForPersistence, h as readBtsConfigFromFile, i as exitWithError, k as hasGraphPart, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt, v as getLatestCLIVersion, w as getGraphBackendDeployInstructions, x as getDefaultConfig, y as DEFAULT_CONFIG } from "./errors-D9yiiGVq.mjs";
2
+ import { A as types_exports, D as getGraphSummary, E as getGraphPart, S as getUserPkgManager, T as getGraphBackendUrl, _ as writeBtsConfig, a as handleError, b as DEFAULT_UI_LIBRARY_BY_FRONTEND, c as isSilent, d as setLastPromptShownUI, f as buildBtsConfigForPersistence, h as readBtsConfigFromFile, i as exitWithError, k as hasGraphPart, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt, v as getLatestCLIVersion, w as getGraphBackendDeployInstructions, x as getDefaultConfig, y as DEFAULT_CONFIG } from "./errors-Cyol8zbN.mjs";
3
3
  import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
- import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-C_xrNtkL.mjs";
5
- import { a as generateReproducibleCommand, i as getTemplateDescription, n as validateFullConfig, o as CreateCommandInputSchema, r as getTemplateConfig, t as validateConfigForProgrammaticUse } from "./config-validation-C4glouQh.mjs";
6
- import { a as getCompatibleUILibraries$1, c as isExampleChatSdkAllowed$1, d as requiresChatSdkVercelAI, f as splitFrontends$1, i as getCompatibleCSSFrameworks$1, l as isFrontendAllowedWithBackend$1, o as hasWebStyling$1, s as isExampleAIAllowed$1, t as allowedApisForFrontends$1, u as isWebFrontend$1 } from "./compatibility-rules-D7zYNVjC.mjs";
7
- import { _ as isGoBack, a as runMavenTests, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt$1, i as runGradleTests, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies } from "./install-dependencies-DDGF-zDG.mjs";
8
- import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-DUvVXWId.mjs";
4
+ import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-CyrP1IV-.mjs";
5
+ import { _ as CreateCommandInputSchema, a as allowedApisForFrontends$1, c as getCompatibleUILibraries$1, d as isExampleChatSdkAllowed$1, f as isFrontendAllowedWithBackend$1, h as splitFrontends$1, i as validateFullConfig, l as hasWebStyling$1, m as requiresChatSdkVercelAI, n as formatProject, p as isWebFrontend$1, r as validateConfigForProgrammaticUse, s as getCompatibleCSSFrameworks$1, u as isExampleAIAllowed$1, v as CreateCommandOptionsSchema } from "./file-formatter-B3dsev2l.mjs";
6
+ import { _ as isGoBack, a as runMavenTests, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt$1, i as runGradleTests, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies } from "./install-dependencies-CgNh-aOy.mjs";
7
+ import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-C8hn9w2i.mjs";
8
+ import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-CnTOtKjm.mjs";
9
9
  import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
10
10
  import { createRouterClient, os } from "@orpc/server";
11
11
  import pc from "picocolors";
@@ -19,7 +19,6 @@ import consola, { consola as consola$1 } from "consola";
19
19
  import { $, execa } from "execa";
20
20
  import { cliInputToProjectConfigPartial } from "@better-fullstack/types/stack-translation";
21
21
  import os$1 from "node:os";
22
- import { format } from "oxfmt";
23
22
 
24
23
  //#region src/utils/project-history.ts
25
24
  const paths$1 = envPaths("better-fullstack", { suffix: "" });
@@ -5500,7 +5499,7 @@ async function gatherMultiEcosystemConfig(flags, projectName, projectDir, relati
5500
5499
  if (pythonOrm !== "none") stackPartSpecs.push(`backend.orm:python:${pythonOrm}`);
5501
5500
  if (pythonAuth !== "none") stackPartSpecs.push(`backend.auth:python:${pythonAuth}`);
5502
5501
  if (pythonTaskQueue !== "none") stackPartSpecs.push(`backend.jobQueue:python:${pythonTaskQueue}`);
5503
- if (pythonGraphql !== "none") stackPartSpecs.push(`backend.api:python:${pythonGraphql}`);
5502
+ if (pythonGraphql !== "none") stackPartSpecs.push(`backend.graphql:python:${pythonGraphql}`);
5504
5503
  for (const testing of pythonTesting) if (testing !== "none") stackPartSpecs.push(`backend.testing:python:${testing}`);
5505
5504
  if (pythonCaching !== "none") stackPartSpecs.push(`backend.caching:python:${pythonCaching}`);
5506
5505
  if (pythonRealtime !== "none") stackPartSpecs.push(`backend.realtime:python:${pythonRealtime}`);
@@ -7278,7 +7277,10 @@ const COPYABLE_CREATE_INPUT_KEYS = Object.keys(types_exports.CreateInputSchema.s
7278
7277
  * result can be safely overlaid by explicitly-passed CLI flags.
7279
7278
  */
7280
7279
  function betterTStackConfigToCreateInput(config) {
7281
- const source = config;
7280
+ const source = buildBtsConfigForPersistence(config, {
7281
+ version: config.version,
7282
+ createdAt: config.createdAt
7283
+ });
7282
7284
  const result = {};
7283
7285
  for (const key of COPYABLE_CREATE_INPUT_KEYS) {
7284
7286
  const value = source[key];
@@ -7341,7 +7343,7 @@ async function resolveCreateConfigBase(input) {
7341
7343
  //#endregion
7342
7344
  //#region src/utils/display-config.ts
7343
7345
  function getSelectedGraphPart(config, role, ownerPartId) {
7344
- return config.stackParts?.find((part) => part.role === role && part.ownerPartId === ownerPartId && part.source !== "provided");
7346
+ return config.stackParts?.find((part) => part.role === role && part.ownerPartId === ownerPartId && part.source !== "provided" && part.toolId !== "none");
7345
7347
  }
7346
7348
  function getGraphDisplayValue(config, role) {
7347
7349
  const primaryBackend = getSelectedGraphPart(config, "backend");
@@ -7473,7 +7475,7 @@ function displayConfig(config) {
7473
7475
  if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
7474
7476
  if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
7475
7477
  if (config.stackParts?.length) {
7476
- const stackParts = config.stackParts.filter((part) => part.source !== "provided").map((part) => formatStackPartSpec(part, config.stackParts ?? []));
7478
+ const stackParts = config.stackParts.filter((part) => part.source !== "provided" && part.toolId !== "none").map((part) => formatStackPartSpec(part, config.stackParts ?? []));
7477
7479
  if (stackParts.length > 0) configDisplay.push(`${pc.blue("Stack Parts:")} ${stackParts.join(", ")}`);
7478
7480
  }
7479
7481
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
@@ -7692,39 +7694,6 @@ function validateConfigCompatibility(config, providedFlags, options) {
7692
7694
  else validateConfigForProgrammaticUse(config);
7693
7695
  }
7694
7696
 
7695
- //#endregion
7696
- //#region src/utils/file-formatter.ts
7697
- const formatOptions = {
7698
- experimentalSortPackageJson: true,
7699
- experimentalSortImports: { order: "asc" }
7700
- };
7701
- async function formatCode(filePath, content) {
7702
- try {
7703
- const result = await format(path.basename(filePath), content, formatOptions);
7704
- if (result.errors && result.errors.length > 0) return null;
7705
- return result.code;
7706
- } catch {
7707
- return null;
7708
- }
7709
- }
7710
- async function formatProject(projectDir) {
7711
- async function formatDirectory(dir) {
7712
- const denoConfigPath = path.join(dir, "deno.json");
7713
- if (await fs.pathExists(denoConfigPath)) return;
7714
- const entries = await fs.readdir(dir, { withFileTypes: true });
7715
- await Promise.all(entries.map(async (entry) => {
7716
- const fullPath = path.join(dir, entry.name);
7717
- if (entry.isDirectory()) await formatDirectory(fullPath);
7718
- else if (entry.isFile()) try {
7719
- const content = await fs.readFile(fullPath, "utf-8");
7720
- const formatted = await formatCode(fullPath, content);
7721
- if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
7722
- } catch {}
7723
- }));
7724
- }
7725
- await formatDirectory(projectDir);
7726
- }
7727
-
7728
7697
  //#endregion
7729
7698
  //#region src/utils/env-utils.ts
7730
7699
  async function addEnvVariablesToFile(envPath, variables) {
@@ -9169,7 +9138,10 @@ function getAuthSetupInstructions(auth, backend, frontend) {
9169
9138
  const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
9170
9139
  if (auth === "nextauth") return `${pc.bold("NextAuth.js Setup:")}\n${pc.cyan("•")} Generate a secret: ${pc.white("npx auth secret")}\n${pc.cyan("•")} Set ${pc.white("AUTH_SECRET")} in ${pc.white(envPath)}\n${pc.cyan("•")} Configure your OAuth providers (e.g. ${pc.white("AUTH_GITHUB_ID")}, ${pc.white("AUTH_GITHUB_SECRET")})\n${pc.cyan("•")} Docs: ${pc.underline("https://authjs.dev/getting-started")}`;
9171
9140
  if (auth === "stack-auth") return `${pc.bold("Stack Auth Setup:")}\n${pc.cyan("•")} Create a project at ${pc.underline("https://app.stack-auth.com")}\n${pc.cyan("•")} Set ${pc.white("NEXT_PUBLIC_STACK_PROJECT_ID")}, ${pc.white("NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY")},\n and ${pc.white("STACK_SECRET_SERVER_KEY")} in ${pc.white(envPath)}`;
9172
- if (auth === "supabase-auth") return `${pc.bold("Supabase Auth Setup:")}\n${pc.cyan("•")} Create a project at ${pc.underline("https://supabase.com/dashboard")}\n${pc.cyan("•")} Set ${pc.white("NEXT_PUBLIC_SUPABASE_URL")}, ${pc.white("NEXT_PUBLIC_SUPABASE_ANON_KEY")},\n and ${pc.white("SUPABASE_SERVICE_ROLE_KEY")} in ${pc.white(envPath)}`;
9141
+ if (auth === "supabase-auth") {
9142
+ const supabasePublicPrefix = frontend.includes("tanstack-start") ? "VITE_" : "NEXT_PUBLIC_";
9143
+ return `${pc.bold("Supabase Auth Setup:")}\n${pc.cyan("•")} Create a project at ${pc.underline("https://supabase.com/dashboard")}\n${pc.cyan("•")} Set ${pc.white(`${supabasePublicPrefix}SUPABASE_URL`)}, ${pc.white(`${supabasePublicPrefix}SUPABASE_ANON_KEY`)},\n and ${pc.white("SUPABASE_SERVICE_ROLE_KEY")} in ${pc.white(envPath)}`;
9144
+ }
9173
9145
  if (auth === "auth0") return `${pc.bold("Auth0 Setup:")}\n${pc.cyan("•")} Create an application at ${pc.underline("https://manage.auth0.com")}\n${pc.cyan("•")} Set ${pc.white("AUTH0_SECRET")}, ${pc.white("AUTH0_CLIENT_ID")}, ${pc.white("AUTH0_CLIENT_SECRET")},\n and ${pc.white("AUTH0_ISSUER_BASE_URL")} in ${pc.white(envPath)}\n${pc.cyan("•")} Docs: ${pc.underline("https://auth0.com/docs/quickstart")}`;
9174
9146
  return "";
9175
9147
  }
@@ -10220,6 +10192,24 @@ function displaySponsorsBox(sponsors$1) {
10220
10192
  //#endregion
10221
10193
  //#region src/run.ts
10222
10194
  const OPTION_ENTRY_COUNT = Object.values(types_exports.OPTION_CATEGORY_METADATA).reduce((sum, metadata) => sum + metadata.options.length, 0);
10195
+ const AddCommandInputSchema = CreateCommandOptionsSchema.omit({
10196
+ template: true,
10197
+ fromHistory: true,
10198
+ config: true,
10199
+ yes: true,
10200
+ yolo: true,
10201
+ verbose: true,
10202
+ verify: true,
10203
+ git: true,
10204
+ directoryConflict: true,
10205
+ renderTitle: true,
10206
+ disableAnalytics: true,
10207
+ manualDb: true
10208
+ }).extend({ projectDir: z.string().optional().describe("Project directory (defaults to current)") });
10209
+ const ProjectCheckInputSchema = z.tuple([z.string().optional().describe("Project directory to diagnose (defaults to current directory)"), z.object({
10210
+ skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
10211
+ json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
10212
+ })]);
10223
10213
  const router = os.router({
10224
10214
  create: os.meta({
10225
10215
  description: `Scaffold a new Better Fullstack project from ${OPTION_ENTRY_COUNT} compatible stack options`,
@@ -10260,15 +10250,8 @@ const router = os.router({
10260
10250
  log.message(`Please visit ${BUILDER_URL}`);
10261
10251
  }
10262
10252
  }),
10263
- add: os.meta({ description: "Add addons or deployment targets to an existing Better Fullstack project using its bts.jsonc config" }).input(z.object({
10264
- addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
10265
- webDeploy: types_exports.WebDeploySchema.optional().describe("Web deployment option to set"),
10266
- serverDeploy: types_exports.ServerDeploySchema.optional().describe("Server deployment option to set"),
10267
- install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
10268
- packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
10269
- projectDir: z.string().optional().describe("Project directory (defaults to current)")
10270
- })).handler(async ({ input }) => {
10271
- const { addHandler } = await import("./add-handler-9F-AsGM-.mjs");
10253
+ add: os.meta({ description: "Add addons, deploy targets, or stack capabilities to an existing Better Fullstack project using its bts.jsonc config" }).input(AddCommandInputSchema).handler(async ({ input }) => {
10254
+ const { addHandler } = await import("./add-handler-BNSL6HdM.mjs");
10272
10255
  await addHandler(input);
10273
10256
  }),
10274
10257
  history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
@@ -10312,12 +10295,17 @@ const router = os.router({
10312
10295
  log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
10313
10296
  log.message("Run: create-better-fullstack mcp");
10314
10297
  }),
10315
- doctor: os.meta({ description: "Diagnose a scaffolded Better Fullstack project: verify its bts.jsonc, installed dependencies, required env vars, and run ecosystem build/type checks" }).input(z.tuple([z.string().optional().describe("Project directory to diagnose (defaults to current directory)"), z.object({
10316
- skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
10317
- json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
10318
- })])).handler(async ({ input }) => {
10298
+ doctor: os.meta({ description: "Diagnose a scaffolded Better Fullstack project: verify its bts.jsonc, installed dependencies, required env vars, and run ecosystem build/type checks" }).input(ProjectCheckInputSchema).handler(async ({ input }) => {
10319
10299
  const [projectDir, options] = input;
10320
- const { doctorCommand } = await import("./doctor-BFSSbS-U.mjs");
10300
+ const { doctorCommand } = await import("./doctor-DBoq7bZ9.mjs");
10301
+ await doctorCommand({
10302
+ projectDir,
10303
+ ...options
10304
+ });
10305
+ }),
10306
+ check: os.meta({ description: "Check a scaffolded Better Fullstack project for config, dependency, env, and generated build/type drift" }).input(ProjectCheckInputSchema).handler(async ({ input }) => {
10307
+ const [projectDir, options] = input;
10308
+ const { doctorCommand } = await import("./doctor-DBoq7bZ9.mjs");
10321
10309
  await doctorCommand({
10322
10310
  projectDir,
10323
10311
  ...options
@@ -10387,7 +10375,7 @@ async function builder() {
10387
10375
  return caller.builder();
10388
10376
  }
10389
10377
  async function add(input) {
10390
- const { addHandler } = await import("./add-handler-9F-AsGM-.mjs");
10378
+ const { addHandler } = await import("./add-handler-BNSL6HdM.mjs");
10391
10379
  return addHandler(input, { silent: true });
10392
10380
  }
10393
10381
  async function history(options) {
@@ -10400,6 +10388,18 @@ async function history(options) {
10400
10388
  async function telemetry(action = "status", options) {
10401
10389
  return caller.telemetry([action, { json: options?.json ?? false }]);
10402
10390
  }
10391
+ async function doctor(projectDir, options) {
10392
+ return caller.doctor([projectDir, {
10393
+ skipChecks: options?.skipChecks ?? false,
10394
+ json: options?.json ?? false
10395
+ }]);
10396
+ }
10397
+ async function check(projectDir, options) {
10398
+ return caller.check([projectDir, {
10399
+ skipChecks: options?.skipChecks ?? false,
10400
+ json: options?.json ?? false
10401
+ }]);
10402
+ }
10403
10403
 
10404
10404
  //#endregion
10405
- export { docs as a, sponsors as c, createBtsCli as i, telemetry as l, builder as n, history as o, create as r, router as s, add as t };
10405
+ export { createBtsCli as a, history as c, telemetry as d, create as i, router as l, builder as n, docs as o, check as r, doctor as s, add as t, sponsors as u };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import "./errors-Cyol8zbN.mjs";
3
+ import { a as createBtsCli, c as history, d as telemetry, 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-BYse4yJy.mjs";
4
+ import "./render-title-zvyKC1ej.mjs";
5
+ import "./addons-setup-CyrP1IV-.mjs";
6
+ import "./file-formatter-B3dsev2l.mjs";
7
+ import "./install-dependencies-CgNh-aOy.mjs";
8
+ import "./generated-checks-C8hn9w2i.mjs";
9
+
10
+ export { createBtsCli };