create-better-fullstack 2.1.3 → 2.1.5

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.
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+ import { c as isSilent, g as updateBtsConfig, l as runWithContextAsync, m as readBtsConfig, n as UserCancelledError, t as CLIError, x as getDefaultConfig } from "./errors-Cyol8zbN.mjs";
3
+ import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
+ import { t as setupAddons } from "./addons-setup-CyrP1IV-.mjs";
5
+ import "./file-formatter-B3dsev2l.mjs";
6
+ import { c as applyDependencyVersionChannel, t as installDependencies, u as getAddonsToAdd } from "./install-dependencies-CgNh-aOy.mjs";
7
+ import { a as applyStackUpdate, o as planStackUpdate } from "./mcp-entry.mjs";
8
+ import { intro, log, outro } from "@clack/prompts";
9
+ import pc from "picocolors";
10
+ import fs from "fs-extra";
11
+ import path from "node:path";
12
+ import { EMBEDDED_TEMPLATES, VirtualFileSystem, processAddonTemplates, processAddonsDeps } from "@better-fullstack/template-generator";
13
+ import { writeTreeToFilesystem } from "@better-fullstack/template-generator/fs-writer";
14
+
15
+ //#region src/helpers/core/add-handler.ts
16
+ const ADD_CONTROL_KEYS = new Set([
17
+ "projectDir",
18
+ "install",
19
+ "dryRun"
20
+ ]);
21
+ function buildStackUpdateRequest(input) {
22
+ const request = {};
23
+ for (const [key, value] of Object.entries(input)) {
24
+ if (ADD_CONTROL_KEYS.has(key)) continue;
25
+ if (value === void 0 || value === false) continue;
26
+ if (Array.isArray(value) && value.length === 0) continue;
27
+ request[key] = value;
28
+ }
29
+ return request;
30
+ }
31
+ function getNewAddons(input, currentConfig) {
32
+ const requestedAddons = (input.addons ?? []).filter((addon) => addon !== "none");
33
+ const existingAddons = new Set(currentConfig.addons ?? []);
34
+ return requestedAddons.filter((addon) => !existingAddons.has(addon));
35
+ }
36
+ function formatCount(count, noun) {
37
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
38
+ }
39
+ function countDependencyChanges(plan) {
40
+ return Object.values(plan.dependencyChanges).reduce((count, deps) => count + Object.keys(deps).length, 0);
41
+ }
42
+ function countEnvChanges(plan) {
43
+ return Object.values(plan.envChanges).reduce((count, keys) => count + keys.length, 0);
44
+ }
45
+ function logStackUpdateSummary(plan, dryRun) {
46
+ if (isSilent()) return;
47
+ log.info(pc.cyan(dryRun ? "Stack update plan:" : "Stack update result:"));
48
+ const requestedFields = Object.keys(plan.requestedChanges);
49
+ if (requestedFields.length > 0) log.info(pc.dim(`Requested: ${requestedFields.sort().join(", ")}`));
50
+ if (plan.graphSummary) log.info(pc.dim(`Stack: ${plan.graphSummary}`));
51
+ if (plan.filesToAdd.length + plan.filesToPatch.length === 0 && plan.manualReviewBlockers.length === 0) log.info(pc.dim("No stack changes to apply."));
52
+ else log.info(pc.dim(`Files: ${formatCount(plan.filesToAdd.length, "file add")}, ${formatCount(plan.filesToPatch.length, "file update")}, ${formatCount(plan.filesUnchanged.length, "unchanged file")}`));
53
+ const dependencyCount = countDependencyChanges(plan);
54
+ if (dependencyCount > 0) log.info(pc.dim(`Dependencies: ${formatCount(dependencyCount, "change")}`));
55
+ const envCount = countEnvChanges(plan);
56
+ if (envCount > 0) log.info(pc.dim(`Env vars: ${formatCount(envCount, "addition")}`));
57
+ for (const adjustment of plan.compatibilityAdjustments) log.info(pc.dim(`Adjusted: ${adjustment}`));
58
+ for (const blocker of plan.manualReviewBlockers) log.warn(pc.yellow(`Manual review: ${blocker}`));
59
+ }
60
+ function buildAddonSetupConfig(projectDir, projectName, currentConfig, plan, addonsToSetup) {
61
+ const baseConfig = getDefaultConfig();
62
+ return {
63
+ ...baseConfig,
64
+ ...currentConfig,
65
+ ...plan.proposedConfig,
66
+ projectName,
67
+ projectDir,
68
+ relativePath: ".",
69
+ packageManager: plan.proposedConfig.packageManager || currentConfig.packageManager || baseConfig.packageManager,
70
+ addons: addonsToSetup,
71
+ frontend: plan.proposedConfig.frontend || currentConfig.frontend || baseConfig.frontend,
72
+ examples: plan.proposedConfig.examples || currentConfig.examples || [],
73
+ rustLibraries: plan.proposedConfig.rustLibraries || currentConfig.rustLibraries || [],
74
+ pythonAi: plan.proposedConfig.pythonAi || currentConfig.pythonAi || [],
75
+ aiDocs: plan.proposedConfig.aiDocs || currentConfig.aiDocs || []
76
+ };
77
+ }
78
+ async function runStackUpdateAdd(input, projectDir, projectName, currentConfig, request) {
79
+ const dryRun = input.dryRun ?? false;
80
+ const result = dryRun ? await planStackUpdate(projectDir, request) : await applyStackUpdate(projectDir, request);
81
+ if (!result.success) throw new CLIError(result.error);
82
+ logStackUpdateSummary(result, dryRun);
83
+ if (dryRun) {
84
+ if (!isSilent()) outro(pc.magenta("Dry run complete. No files were written."));
85
+ return {
86
+ success: true,
87
+ addedAddons: [],
88
+ projectDir
89
+ };
90
+ }
91
+ const addonsToSetup = getNewAddons(input, currentConfig);
92
+ const setupConfig = buildAddonSetupConfig(projectDir, projectName, currentConfig, result, addonsToSetup);
93
+ const setupWarnings = addonsToSetup.length > 0 ? await setupAddons(setupConfig) : [];
94
+ await applyDependencyVersionChannel(projectDir, result.proposedConfig.versionChannel);
95
+ let installFailed = false;
96
+ if (input.install) {
97
+ if (result.proposedConfig.ecosystem === "typescript" || result.proposedConfig.ecosystem === "react-native") installFailed = !(await installDependencies({
98
+ projectDir,
99
+ packageManager: setupConfig.packageManager
100
+ })).success;
101
+ else if (!isSilent()) log.warn(pc.yellow(`Automatic --install is only supported for JavaScript package-manager installs. Run '${result.installCommand}' instead.`));
102
+ }
103
+ if (!isSilent()) {
104
+ if (addonsToSetup.length > 0) log.success(pc.green(`Successfully added: ${addonsToSetup.join(", ")}`));
105
+ else if ((input.addons ?? []).some((addon) => addon !== "none")) log.info(pc.dim("No new addons selected."));
106
+ log.success(pc.green("Stack update applied."));
107
+ for (const warning of setupWarnings) log.warn(pc.yellow(warning));
108
+ if (!input.install) log.info(pc.yellow(`Run '${result.installCommand}' to install new dependencies.`));
109
+ else if (installFailed) log.warn(pc.yellow(`Dependency installation failed. Run '${result.installCommand}' after resolving the error above.`));
110
+ outro(pc.magenta("Project updated successfully!"));
111
+ }
112
+ return {
113
+ success: true,
114
+ addedAddons: addonsToSetup,
115
+ projectDir,
116
+ setupWarnings: setupWarnings.length > 0 ? setupWarnings : void 0
117
+ };
118
+ }
119
+ async function addHandler(input, options = {}) {
120
+ const { silent = false } = options;
121
+ return runWithContextAsync({ silent }, async () => {
122
+ try {
123
+ return await addHandlerInternal(input);
124
+ } catch (error) {
125
+ if (error instanceof UserCancelledError) {
126
+ if (isSilent()) return {
127
+ success: false,
128
+ addedAddons: [],
129
+ projectDir: "",
130
+ error: error.message
131
+ };
132
+ return;
133
+ }
134
+ if (error instanceof CLIError) {
135
+ if (isSilent()) return {
136
+ success: false,
137
+ addedAddons: [],
138
+ projectDir: "",
139
+ error: error.message
140
+ };
141
+ throw error;
142
+ }
143
+ if (isSilent()) return {
144
+ success: false,
145
+ addedAddons: [],
146
+ projectDir: "",
147
+ error: error instanceof Error ? error.message : String(error)
148
+ };
149
+ throw error;
150
+ }
151
+ });
152
+ }
153
+ async function addHandlerInternal(input) {
154
+ const projectDir = path.resolve(input.projectDir || process.cwd());
155
+ if (!isSilent()) {
156
+ renderTitle();
157
+ intro(pc.magenta("Update your Better Fullstack project"));
158
+ }
159
+ const btsConfig = await readBtsConfig(projectDir);
160
+ if (!btsConfig) throw new CLIError(`No Better Fullstack project found in ${projectDir}. Make sure bts.jsonc exists.`);
161
+ const projectName = path.basename(projectDir);
162
+ if (!isSilent()) log.info(pc.dim(`Detected project: ${projectName}`));
163
+ const stackUpdateRequest = buildStackUpdateRequest(input);
164
+ if (Object.keys(stackUpdateRequest).length > 0 || input.dryRun) return runStackUpdateAdd(input, projectDir, projectName, btsConfig, stackUpdateRequest);
165
+ const existingAddons = btsConfig.addons || [];
166
+ let addonsToAdd = [];
167
+ if (input.addons && input.addons.length > 0) addonsToAdd = input.addons.filter((addon) => addon !== "none" && !existingAddons.includes(addon));
168
+ else addonsToAdd = (await getAddonsToAdd(btsConfig.frontend || [], existingAddons, btsConfig.auth)).filter((addon) => addon !== "none");
169
+ if (addonsToAdd.length === 0) {
170
+ if (!isSilent()) {
171
+ log.info(pc.dim("No new addons selected."));
172
+ outro(pc.magenta("Nothing to add."));
173
+ }
174
+ return {
175
+ success: true,
176
+ addedAddons: [],
177
+ projectDir
178
+ };
179
+ }
180
+ if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
181
+ const baseConfig = getDefaultConfig();
182
+ const config = {
183
+ ...baseConfig,
184
+ ...btsConfig,
185
+ projectName,
186
+ projectDir,
187
+ relativePath: ".",
188
+ packageManager: input.packageManager || btsConfig.packageManager || baseConfig.packageManager,
189
+ addons: addonsToAdd,
190
+ frontend: btsConfig.frontend || baseConfig.frontend,
191
+ examples: btsConfig.examples || [],
192
+ rustLibraries: btsConfig.rustLibraries || [],
193
+ pythonAi: btsConfig.pythonAi || [],
194
+ aiDocs: btsConfig.aiDocs || []
195
+ };
196
+ const vfs = new VirtualFileSystem();
197
+ const packageJsonPaths = await collectPackageJsonPaths(projectDir);
198
+ for (const pkgPath of packageJsonPaths) {
199
+ const fullPath = path.join(projectDir, pkgPath);
200
+ const content = await fs.readFile(fullPath, "utf-8");
201
+ vfs.writeFile(pkgPath, content);
202
+ }
203
+ await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
204
+ processAddonsDeps(vfs, config);
205
+ await writeTreeToFilesystem({
206
+ root: vfs.toTree(projectName),
207
+ fileCount: vfs.getFileCount(),
208
+ directoryCount: vfs.getDirectoryCount(),
209
+ config
210
+ }, projectDir);
211
+ const setupWarnings = await setupAddons(config);
212
+ await applyDependencyVersionChannel(projectDir, config.versionChannel);
213
+ const configUpdates = { addons: [...new Set([...existingAddons, ...addonsToAdd])] };
214
+ if (input.webDeploy !== void 0) configUpdates.webDeploy = input.webDeploy;
215
+ if (input.serverDeploy !== void 0) configUpdates.serverDeploy = input.serverDeploy;
216
+ await updateBtsConfig(projectDir, configUpdates);
217
+ let addonInstallFailed = false;
218
+ if (input.install) addonInstallFailed = !(await installDependencies({
219
+ projectDir,
220
+ packageManager: config.packageManager
221
+ })).success;
222
+ if (!isSilent()) {
223
+ log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
224
+ for (const warning of setupWarnings) log.warn(pc.yellow(warning));
225
+ const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
226
+ if (!input.install) log.info(pc.yellow(`Run '${installCmd}' to install new dependencies.`));
227
+ else if (addonInstallFailed) log.warn(pc.yellow(`Dependency installation failed. Run '${installCmd}' after resolving the error above.`));
228
+ outro(pc.magenta("Addons added successfully!"));
229
+ }
230
+ return {
231
+ success: true,
232
+ addedAddons: addonsToAdd,
233
+ projectDir,
234
+ setupWarnings: setupWarnings.length > 0 ? setupWarnings : void 0
235
+ };
236
+ }
237
+ async function collectPackageJsonPaths(projectDir) {
238
+ const results = [];
239
+ async function walk(currentDir) {
240
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
241
+ for (const entry of entries) {
242
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".turbo") continue;
243
+ const fullPath = path.join(currentDir, entry.name);
244
+ if (entry.isDirectory()) {
245
+ await walk(fullPath);
246
+ continue;
247
+ }
248
+ if (entry.isFile() && entry.name === "package.json") results.push(path.relative(projectDir, fullPath).replaceAll(path.sep, "/"));
249
+ }
250
+ }
251
+ await walk(projectDir);
252
+ if (!results.includes("package.json") && await fs.pathExists(path.join(projectDir, "package.json"))) results.push("package.json");
253
+ return results;
254
+ }
255
+
256
+ //#endregion
257
+ export { addHandler };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as isSilent, m as readBtsConfig, r as exitCancelled } from "./errors-D9yiiGVq.mjs";
2
+ import { c as isSilent, m as readBtsConfig, r as exitCancelled } from "./errors-Cyol8zbN.mjs";
3
3
  import { autocompleteMultiselect, group, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
4
4
  import pc from "picocolors";
5
5
  import fs from "fs-extra";
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./errors-Cyol8zbN.mjs";
3
+ import { t as setupAddons } from "./addons-setup-CyrP1IV-.mjs";
4
+
5
+ export { setupAddons };
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  //#region src/cli.ts
3
- if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-BXLhb7wW.mjs").then((m) => m.startMcpServer());
4
- else import("./run-BmRKR2wG.mjs").then((m) => m.createBtsCli().run());
3
+ if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-CsW3i66c.mjs").then((m) => m.startMcpServer());
4
+ else import("./run-_cf_sFwM.mjs").then((m) => m.createBtsCli().run());
5
5
 
6
6
  //#endregion
7
7
  export { };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { a as handleError, m as readBtsConfig } from "./errors-D9yiiGVq.mjs";
2
+ import { a as handleError, m as readBtsConfig } from "./errors-Cyol8zbN.mjs";
3
3
  import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
- import { t as runGeneratedChecks } from "./generated-checks-DUvVXWId.mjs";
4
+ import { t as runGeneratedChecks } from "./generated-checks-C8hn9w2i.mjs";
5
5
  import { intro, log, spinner } from "@clack/prompts";
6
6
  import pc from "picocolors";
7
7
  import fs from "fs-extra";
@@ -73,7 +73,7 @@ const TOOL_LABELS = {
73
73
  "spring-data-jpa": "Spring Data JPA"
74
74
  };
75
75
  function getSelectedGraphParts(config) {
76
- return (config.stackParts ?? []).filter((part) => part.source !== "provided");
76
+ return (config.stackParts ?? []).filter((part) => part.source !== "provided" && part.toolId !== "none");
77
77
  }
78
78
  function getGraphPart(config, role, ecosystem) {
79
79
  return getSelectedGraphParts(config).find((part) => part.role === role && (!ecosystem || part.ecosystem === ecosystem));
@@ -244,6 +244,22 @@ const MOBILE_CONFIG_FIELDS = [
244
244
  "mobileOTA",
245
245
  "mobileDeepLinking"
246
246
  ];
247
+ const MOBILE_CONFIG_FIELD_ROLES = {
248
+ mobileNavigation: "navigation",
249
+ mobileUI: "ui",
250
+ mobileStorage: "storage",
251
+ mobileTesting: "testing",
252
+ mobilePush: "push",
253
+ mobileOTA: "ota",
254
+ mobileDeepLinking: "deepLinking"
255
+ };
256
+ function hasExplicitMobileGraphField(stackParts, field) {
257
+ const role = MOBILE_CONFIG_FIELD_ROLES[field];
258
+ return stackParts.some((part) => {
259
+ if (part.source === "provided" || part.role !== role || part.ecosystem !== "react-native") return false;
260
+ return (part.ownerPartId ? stackParts.find((candidate) => candidate.id === part.ownerPartId) : void 0)?.role === "mobile";
261
+ });
262
+ }
247
263
  function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
248
264
  if (!stackParts) return projectConfig;
249
265
  const legacyConfig = (0, types_exports.stackPartsToLegacyProjectConfigPartial)(stackParts);
@@ -349,7 +365,7 @@ function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
349
365
  if (hasNativeFrontend) for (const field of MOBILE_CONFIG_FIELDS) {
350
366
  const projectValue = projectConfig[field];
351
367
  const legacyValue = legacyConfig[field];
352
- if (projectValue !== void 0 && projectValue !== "none" && legacyValue === "none") normalized[field] = projectValue;
368
+ if (projectValue !== void 0 && projectValue !== "none" && legacyValue === "none" && !hasExplicitMobileGraphField(stackParts, field)) normalized[field] = projectValue;
353
369
  }
354
370
  if (!selectedEcosystems.has("react-native") && !hasNativeFrontend) {
355
371
  normalized.mobileNavigation = "none";