create-better-fullstack 2.0.3 → 2.1.1
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-BQN6pwGJ.mjs → add-handler-9F-AsGM-.mjs} +11 -9
- package/dist/addons-setup-C8eaCaH5.mjs +5 -0
- package/dist/{addons-setup-HSghQS7c.mjs → addons-setup-C_xrNtkL.mjs} +3 -83
- package/dist/cli.mjs +2 -2
- package/dist/compatibility-rules-D7zYNVjC.mjs +372 -0
- package/dist/config-validation-C4glouQh.mjs +1512 -0
- package/dist/doctor-BFSSbS-U.mjs +280 -0
- package/dist/{bts-config-Bg1Qea9Y.mjs → errors-D9yiiGVq.mjs} +215 -72
- package/dist/generated-checks-DUvVXWId.mjs +91 -0
- package/dist/index.d.mts +85 -29
- package/dist/index.mjs +11 -6
- package/dist/{install-dependencies-D0Z1dZEx.mjs → install-dependencies-DDGF-zDG.mjs} +102 -397
- package/dist/mcp-BXLhb7wW.mjs +7 -0
- package/dist/mcp-entry.mjs +1802 -296
- package/dist/render-title-zvyKC1ej.mjs +34 -0
- package/dist/run-BmRKR2wG.mjs +11 -0
- package/dist/{run-BhcXBjxh.mjs → run-DRzP53v3.mjs} +534 -1717
- package/package.json +5 -4
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/mcp-58r70ZcL.mjs +0 -5
- package/dist/run-DCxVLUMS.mjs +0 -7
- /package/dist/{update-deps-CLebIM70.mjs → update-deps-D5OG0KmJ.mjs} +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as handleError, m as readBtsConfig } from "./errors-D9yiiGVq.mjs";
|
|
3
|
+
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
+
import { t as runGeneratedChecks } from "./generated-checks-DUvVXWId.mjs";
|
|
5
|
+
import { intro, log, spinner } from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { $ } from "execa";
|
|
10
|
+
|
|
11
|
+
//#region src/commands/doctor.ts
|
|
12
|
+
const NON_TS_BACKEND_ECOSYSTEMS = new Set([
|
|
13
|
+
"go",
|
|
14
|
+
"rust",
|
|
15
|
+
"python",
|
|
16
|
+
"elixir",
|
|
17
|
+
"java",
|
|
18
|
+
"dotnet"
|
|
19
|
+
]);
|
|
20
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
21
|
+
"node_modules",
|
|
22
|
+
".git",
|
|
23
|
+
"dist",
|
|
24
|
+
"build",
|
|
25
|
+
".next",
|
|
26
|
+
".expo",
|
|
27
|
+
".turbo",
|
|
28
|
+
"target",
|
|
29
|
+
".venv",
|
|
30
|
+
"deps",
|
|
31
|
+
"_build"
|
|
32
|
+
]);
|
|
33
|
+
const JS_LOCKFILES = [
|
|
34
|
+
"bun.lock",
|
|
35
|
+
"bun.lockb",
|
|
36
|
+
"pnpm-lock.yaml",
|
|
37
|
+
"package-lock.json",
|
|
38
|
+
"yarn.lock"
|
|
39
|
+
];
|
|
40
|
+
const NATIVE_LOCKFILES = {
|
|
41
|
+
rust: {
|
|
42
|
+
file: "Cargo.lock",
|
|
43
|
+
hint: "cargo build"
|
|
44
|
+
},
|
|
45
|
+
go: {
|
|
46
|
+
file: "go.sum",
|
|
47
|
+
hint: "go mod tidy"
|
|
48
|
+
},
|
|
49
|
+
python: {
|
|
50
|
+
file: "uv.lock",
|
|
51
|
+
hint: "uv sync"
|
|
52
|
+
},
|
|
53
|
+
elixir: {
|
|
54
|
+
file: "mix.lock",
|
|
55
|
+
hint: "mix deps.get"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function statusIcon(status) {
|
|
59
|
+
switch (status) {
|
|
60
|
+
case "pass": return pc.green("✓");
|
|
61
|
+
case "warn": return pc.yellow("!");
|
|
62
|
+
case "fail": return pc.red("✗");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function hasNativeChecks(config) {
|
|
66
|
+
if (NON_TS_BACKEND_ECOSYSTEMS.has(config.ecosystem)) return true;
|
|
67
|
+
return (config.stackParts ?? []).some((part) => part.source !== "provided" && part.role === "backend" && NON_TS_BACKEND_ECOSYSTEMS.has(part.ecosystem));
|
|
68
|
+
}
|
|
69
|
+
async function checkInstalledDependencies(projectDir, config) {
|
|
70
|
+
const checks = [];
|
|
71
|
+
if (await fs.pathExists(path.join(projectDir, "package.json"))) {
|
|
72
|
+
const lockfile = JS_LOCKFILES.find((name) => fs.existsSync(path.join(projectDir, name)));
|
|
73
|
+
checks.push(lockfile ? {
|
|
74
|
+
label: "Lockfile",
|
|
75
|
+
status: "pass",
|
|
76
|
+
detail: lockfile
|
|
77
|
+
} : {
|
|
78
|
+
label: "Lockfile",
|
|
79
|
+
status: "warn",
|
|
80
|
+
detail: "No JavaScript lockfile found at the project root"
|
|
81
|
+
});
|
|
82
|
+
const nodeModulesExists = await fs.pathExists(path.join(projectDir, "node_modules"));
|
|
83
|
+
checks.push(nodeModulesExists ? {
|
|
84
|
+
label: "node_modules",
|
|
85
|
+
status: "pass"
|
|
86
|
+
} : {
|
|
87
|
+
label: "node_modules",
|
|
88
|
+
status: "fail",
|
|
89
|
+
detail: `Dependencies are not installed. Run \`${config.packageManager ?? "npm"} install\`.`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const native = NATIVE_LOCKFILES[config.ecosystem];
|
|
93
|
+
if (native) {
|
|
94
|
+
const exists = await fs.pathExists(path.join(projectDir, native.file)) || await fs.pathExists(path.join(projectDir, "apps/server", native.file));
|
|
95
|
+
checks.push(exists ? {
|
|
96
|
+
label: native.file,
|
|
97
|
+
status: "pass"
|
|
98
|
+
} : {
|
|
99
|
+
label: native.file,
|
|
100
|
+
status: "warn",
|
|
101
|
+
detail: `Not found. Run \`${native.hint}\` to fetch dependencies.`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return checks;
|
|
105
|
+
}
|
|
106
|
+
async function findEnvExampleFiles(rootDir) {
|
|
107
|
+
const results = [];
|
|
108
|
+
async function walk(dir, depth) {
|
|
109
|
+
if (depth > 5) return;
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
117
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
118
|
+
await walk(path.join(dir, entry.name), depth + 1);
|
|
119
|
+
} else if (entry.name === ".env.example") results.push(path.join(dir, entry.name));
|
|
120
|
+
}
|
|
121
|
+
await walk(rootDir, 0);
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
function parseEnvKeys(content) {
|
|
125
|
+
const map = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const rawLine of content.split("\n")) {
|
|
127
|
+
const line = rawLine.trim();
|
|
128
|
+
if (!line || line.startsWith("#")) continue;
|
|
129
|
+
const eq = line.indexOf("=");
|
|
130
|
+
if (eq === -1) continue;
|
|
131
|
+
const key = line.slice(0, eq).trim();
|
|
132
|
+
if (key) map.set(key, line.slice(eq + 1).trim());
|
|
133
|
+
}
|
|
134
|
+
return map;
|
|
135
|
+
}
|
|
136
|
+
async function checkEnvFiles(projectDir) {
|
|
137
|
+
const checks = [];
|
|
138
|
+
const exampleFiles = await findEnvExampleFiles(projectDir);
|
|
139
|
+
for (const examplePath of exampleFiles) {
|
|
140
|
+
const envPath = examplePath.replace(/\.example$/, "");
|
|
141
|
+
const relExample = path.relative(projectDir, examplePath) || ".env.example";
|
|
142
|
+
const exampleKeys = parseEnvKeys(await fs.readFile(examplePath, "utf-8"));
|
|
143
|
+
if (exampleKeys.size === 0) continue;
|
|
144
|
+
if (!await fs.pathExists(envPath)) {
|
|
145
|
+
checks.push({
|
|
146
|
+
label: relExample,
|
|
147
|
+
status: "warn",
|
|
148
|
+
detail: `Missing ${path.relative(projectDir, envPath)} (copy from .env.example and fill in values)`
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const envKeys = parseEnvKeys(await fs.readFile(envPath, "utf-8"));
|
|
153
|
+
const missing = [];
|
|
154
|
+
for (const key of exampleKeys.keys()) {
|
|
155
|
+
const value = envKeys.get(key);
|
|
156
|
+
if (value === void 0 || value === "") missing.push(key);
|
|
157
|
+
}
|
|
158
|
+
checks.push(missing.length > 0 ? {
|
|
159
|
+
label: path.relative(projectDir, envPath),
|
|
160
|
+
status: "warn",
|
|
161
|
+
detail: `Missing or empty: ${missing.join(", ")}`
|
|
162
|
+
} : {
|
|
163
|
+
label: path.relative(projectDir, envPath),
|
|
164
|
+
status: "pass"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return checks;
|
|
168
|
+
}
|
|
169
|
+
async function runBuildChecks(config, json) {
|
|
170
|
+
const checks = [];
|
|
171
|
+
const projectDir = config.projectDir;
|
|
172
|
+
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
|
173
|
+
if (await fs.pathExists(rootPackageJsonPath)) {
|
|
174
|
+
if ((await fs.readJson(rootPackageJsonPath).catch(() => ({}))).scripts?.["check-types"]) {
|
|
175
|
+
const pm = config.packageManager ?? "npm";
|
|
176
|
+
const s = json ? null : spinner();
|
|
177
|
+
s?.start("Running type checks (check-types)...");
|
|
178
|
+
const result = await $({
|
|
179
|
+
cwd: projectDir,
|
|
180
|
+
reject: false,
|
|
181
|
+
stdout: json ? "ignore" : "inherit",
|
|
182
|
+
stderr: json ? "ignore" : "inherit"
|
|
183
|
+
})`${pm} run check-types`;
|
|
184
|
+
if (result.exitCode === 0) {
|
|
185
|
+
s?.stop("Type checks passed");
|
|
186
|
+
checks.push({
|
|
187
|
+
label: "check-types",
|
|
188
|
+
status: "pass"
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
s?.stop(pc.red("Type checks failed"));
|
|
192
|
+
checks.push({
|
|
193
|
+
label: "check-types",
|
|
194
|
+
status: "fail",
|
|
195
|
+
detail: `\`${pm} run check-types\` exited with code ${result.exitCode ?? `signal ${result.signal}`}`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (hasNativeChecks(config)) if (json) checks.push({
|
|
201
|
+
label: "ecosystem build checks",
|
|
202
|
+
status: "warn",
|
|
203
|
+
detail: "Skipped in --json mode. Re-run without --json to execute native build checks."
|
|
204
|
+
});
|
|
205
|
+
else try {
|
|
206
|
+
await runGeneratedChecks(config);
|
|
207
|
+
checks.push({
|
|
208
|
+
label: "ecosystem build checks",
|
|
209
|
+
status: "pass"
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
checks.push({
|
|
213
|
+
label: "ecosystem build checks",
|
|
214
|
+
status: "fail",
|
|
215
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return checks;
|
|
219
|
+
}
|
|
220
|
+
async function doctorCommand(input) {
|
|
221
|
+
const projectDir = path.resolve(input.projectDir || process.cwd());
|
|
222
|
+
const json = input.json ?? false;
|
|
223
|
+
const btsConfig = await readBtsConfig(projectDir);
|
|
224
|
+
if (!btsConfig) {
|
|
225
|
+
if (json) {
|
|
226
|
+
console.log(JSON.stringify({
|
|
227
|
+
projectDir,
|
|
228
|
+
ok: false,
|
|
229
|
+
error: "No Better Fullstack project found (bts.jsonc missing or invalid)."
|
|
230
|
+
}, null, 2));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
handleError(`No Better Fullstack project found in ${projectDir}. Make sure bts.jsonc exists.`);
|
|
234
|
+
}
|
|
235
|
+
const config = {
|
|
236
|
+
...btsConfig,
|
|
237
|
+
projectDir
|
|
238
|
+
};
|
|
239
|
+
if (!json) {
|
|
240
|
+
renderTitle();
|
|
241
|
+
intro(pc.magenta(`Diagnosing ${pc.cyan(path.basename(projectDir))}`));
|
|
242
|
+
log.info(pc.dim(`Path: ${projectDir}`));
|
|
243
|
+
log.info(pc.dim(`Ecosystem: ${btsConfig.ecosystem}`));
|
|
244
|
+
if (btsConfig.graphSummary) log.info(pc.dim(`Stack: ${btsConfig.graphSummary}`));
|
|
245
|
+
}
|
|
246
|
+
const checks = [{
|
|
247
|
+
label: "bts.jsonc",
|
|
248
|
+
status: "pass",
|
|
249
|
+
detail: `version ${btsConfig.version}`
|
|
250
|
+
}];
|
|
251
|
+
checks.push(...await checkInstalledDependencies(projectDir, btsConfig));
|
|
252
|
+
checks.push(...await checkEnvFiles(projectDir));
|
|
253
|
+
if (!input.skipChecks) checks.push(...await runBuildChecks(config, json));
|
|
254
|
+
const counts = {
|
|
255
|
+
pass: 0,
|
|
256
|
+
warn: 0,
|
|
257
|
+
fail: 0
|
|
258
|
+
};
|
|
259
|
+
for (const check of checks) counts[check.status] += 1;
|
|
260
|
+
if (json) console.log(JSON.stringify({
|
|
261
|
+
projectDir,
|
|
262
|
+
ecosystem: btsConfig.ecosystem,
|
|
263
|
+
ok: counts.fail === 0,
|
|
264
|
+
summary: counts,
|
|
265
|
+
checks
|
|
266
|
+
}, null, 2));
|
|
267
|
+
else {
|
|
268
|
+
log.message("");
|
|
269
|
+
for (const check of checks) log.message(`${statusIcon(check.status)} ${check.label}${check.detail ? pc.dim(` — ${check.detail}`) : ""}`);
|
|
270
|
+
log.message("");
|
|
271
|
+
const summaryLine = `${pc.green(`${counts.pass} passed`)}, ${pc.yellow(`${counts.warn} warnings`)}, ${pc.red(`${counts.fail} failed`)}`;
|
|
272
|
+
if (counts.fail > 0) log.error(`Diagnosis complete: ${summaryLine}`);
|
|
273
|
+
else if (counts.warn > 0) log.warn(`Diagnosis complete: ${summaryLine}`);
|
|
274
|
+
else log.success(`Diagnosis complete: ${summaryLine}`);
|
|
275
|
+
}
|
|
276
|
+
if (counts.fail > 0) process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
export { doctorCommand };
|
|
@@ -1,76 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __reExport } from "./chunk-CCII7kTE.mjs";
|
|
3
|
+
import { cancel } from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
3
5
|
import fs from "fs-extra";
|
|
4
6
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createCliDefaultProjectConfigBase } from "@better-fullstack/types";
|
|
7
7
|
import * as JSONC from "jsonc-parser";
|
|
8
|
+
import { createCliDefaultProjectConfigBase } from "@better-fullstack/types";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
|
+
import consola from "consola";
|
|
8
12
|
|
|
9
|
-
//#region src/utils/get-package-manager.ts
|
|
10
|
-
const getUserPkgManager = () => {
|
|
11
|
-
const userAgent = process.env.npm_config_user_agent;
|
|
12
|
-
if (userAgent?.startsWith("pnpm")) return "pnpm";
|
|
13
|
-
if (userAgent?.startsWith("bun")) return "bun";
|
|
14
|
-
if (userAgent?.startsWith("yarn")) return "yarn";
|
|
15
|
-
return "npm";
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/constants.ts
|
|
20
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
-
const distPath = path.dirname(__filename);
|
|
22
|
-
const PKG_ROOT = path.join(distPath, "../");
|
|
23
|
-
const DEFAULT_CONFIG_BASE = createCliDefaultProjectConfigBase(getUserPkgManager());
|
|
24
|
-
function getDefaultConfig() {
|
|
25
|
-
return {
|
|
26
|
-
...DEFAULT_CONFIG_BASE,
|
|
27
|
-
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
|
|
28
|
-
packageManager: getUserPkgManager(),
|
|
29
|
-
frontend: [...DEFAULT_CONFIG_BASE.frontend],
|
|
30
|
-
addons: [...DEFAULT_CONFIG_BASE.addons],
|
|
31
|
-
examples: [...DEFAULT_CONFIG_BASE.examples],
|
|
32
|
-
rustLibraries: [...DEFAULT_CONFIG_BASE.rustLibraries],
|
|
33
|
-
pythonAi: [...DEFAULT_CONFIG_BASE.pythonAi],
|
|
34
|
-
javaLibraries: [...DEFAULT_CONFIG_BASE.javaLibraries],
|
|
35
|
-
javaTestingLibraries: [...DEFAULT_CONFIG_BASE.javaTestingLibraries],
|
|
36
|
-
aiDocs: [...DEFAULT_CONFIG_BASE.aiDocs]
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const DEFAULT_CONFIG = getDefaultConfig();
|
|
40
|
-
/**
|
|
41
|
-
* Default UI library for each frontend framework
|
|
42
|
-
* Falls back based on what's compatible
|
|
43
|
-
*/
|
|
44
|
-
const DEFAULT_UI_LIBRARY_BY_FRONTEND = {
|
|
45
|
-
"tanstack-router": "shadcn-ui",
|
|
46
|
-
"react-router": "shadcn-ui",
|
|
47
|
-
"react-vite": "shadcn-ui",
|
|
48
|
-
"tanstack-start": "shadcn-ui",
|
|
49
|
-
next: "shadcn-ui",
|
|
50
|
-
vinext: "shadcn-ui",
|
|
51
|
-
nuxt: "daisyui",
|
|
52
|
-
svelte: "daisyui",
|
|
53
|
-
solid: "daisyui",
|
|
54
|
-
"solid-start": "daisyui",
|
|
55
|
-
astro: "daisyui",
|
|
56
|
-
qwik: "daisyui",
|
|
57
|
-
angular: "daisyui",
|
|
58
|
-
redwood: "daisyui",
|
|
59
|
-
fresh: "daisyui",
|
|
60
|
-
"native-bare": "none",
|
|
61
|
-
"native-uniwind": "none",
|
|
62
|
-
"native-unistyles": "none",
|
|
63
|
-
none: "none"
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
//#region src/utils/get-latest-cli-version.ts
|
|
68
|
-
const getLatestCLIVersion = () => {
|
|
69
|
-
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
70
|
-
return fs.readJSONSync(packageJsonPath).version ?? "1.0.0";
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
//#endregion
|
|
74
13
|
//#region src/types.ts
|
|
75
14
|
var types_exports = {};
|
|
76
15
|
import * as import__better_fullstack_types from "@better-fullstack/types";
|
|
@@ -228,16 +167,93 @@ function getGraphBackendDeployInstructions(config) {
|
|
|
228
167
|
}
|
|
229
168
|
}
|
|
230
169
|
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/utils/get-package-manager.ts
|
|
172
|
+
const getUserPkgManager = () => {
|
|
173
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
174
|
+
if (userAgent?.startsWith("pnpm")) return "pnpm";
|
|
175
|
+
if (userAgent?.startsWith("bun")) return "bun";
|
|
176
|
+
if (userAgent?.startsWith("yarn")) return "yarn";
|
|
177
|
+
return "npm";
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/constants.ts
|
|
182
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
183
|
+
const distPath = path.dirname(__filename);
|
|
184
|
+
const PKG_ROOT = path.join(distPath, "../");
|
|
185
|
+
const DEFAULT_CONFIG_BASE = createCliDefaultProjectConfigBase(getUserPkgManager());
|
|
186
|
+
function getDefaultConfig() {
|
|
187
|
+
return {
|
|
188
|
+
...DEFAULT_CONFIG_BASE,
|
|
189
|
+
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
|
|
190
|
+
packageManager: getUserPkgManager(),
|
|
191
|
+
frontend: [...DEFAULT_CONFIG_BASE.frontend],
|
|
192
|
+
addons: [...DEFAULT_CONFIG_BASE.addons],
|
|
193
|
+
examples: [...DEFAULT_CONFIG_BASE.examples],
|
|
194
|
+
rustLibraries: [...DEFAULT_CONFIG_BASE.rustLibraries],
|
|
195
|
+
pythonAi: [...DEFAULT_CONFIG_BASE.pythonAi],
|
|
196
|
+
javaLibraries: [...DEFAULT_CONFIG_BASE.javaLibraries],
|
|
197
|
+
javaTestingLibraries: [...DEFAULT_CONFIG_BASE.javaTestingLibraries],
|
|
198
|
+
aiDocs: [...DEFAULT_CONFIG_BASE.aiDocs]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const DEFAULT_CONFIG = getDefaultConfig();
|
|
202
|
+
/**
|
|
203
|
+
* Default UI library for each frontend framework
|
|
204
|
+
* Falls back based on what's compatible
|
|
205
|
+
*/
|
|
206
|
+
const DEFAULT_UI_LIBRARY_BY_FRONTEND = {
|
|
207
|
+
"tanstack-router": "shadcn-ui",
|
|
208
|
+
"react-router": "shadcn-ui",
|
|
209
|
+
"react-vite": "shadcn-ui",
|
|
210
|
+
"tanstack-start": "shadcn-ui",
|
|
211
|
+
next: "shadcn-ui",
|
|
212
|
+
vinext: "shadcn-ui",
|
|
213
|
+
nuxt: "daisyui",
|
|
214
|
+
svelte: "daisyui",
|
|
215
|
+
solid: "daisyui",
|
|
216
|
+
"solid-start": "daisyui",
|
|
217
|
+
astro: "daisyui",
|
|
218
|
+
qwik: "daisyui",
|
|
219
|
+
angular: "daisyui",
|
|
220
|
+
redwood: "daisyui",
|
|
221
|
+
fresh: "daisyui",
|
|
222
|
+
"native-bare": "none",
|
|
223
|
+
"native-uniwind": "none",
|
|
224
|
+
"native-unistyles": "none",
|
|
225
|
+
none: "none"
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/utils/get-latest-cli-version.ts
|
|
230
|
+
const getLatestCLIVersion = () => {
|
|
231
|
+
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
232
|
+
return fs.readJSONSync(packageJsonPath).version ?? "1.0.0";
|
|
233
|
+
};
|
|
234
|
+
|
|
231
235
|
//#endregion
|
|
232
236
|
//#region src/utils/bts-config.ts
|
|
233
237
|
const BTS_CONFIG_FILE = "bts.jsonc";
|
|
238
|
+
const MOBILE_CONFIG_FIELDS = [
|
|
239
|
+
"mobileNavigation",
|
|
240
|
+
"mobileUI",
|
|
241
|
+
"mobileStorage",
|
|
242
|
+
"mobileTesting",
|
|
243
|
+
"mobilePush",
|
|
244
|
+
"mobileOTA",
|
|
245
|
+
"mobileDeepLinking"
|
|
246
|
+
];
|
|
234
247
|
function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
|
|
235
248
|
if (!stackParts) return projectConfig;
|
|
236
249
|
const legacyConfig = (0, types_exports.stackPartsToLegacyProjectConfigPartial)(stackParts);
|
|
237
250
|
const selectedEcosystems = new Set(stackParts.filter((part) => part.source !== "provided").map((part) => part.ecosystem));
|
|
251
|
+
selectedEcosystems.add(projectConfig.ecosystem);
|
|
252
|
+
const hasSelectedEcosystemStackParts = stackParts.some((part) => part.source !== "provided" && part.ecosystem !== "universal");
|
|
238
253
|
const normalized = {
|
|
239
254
|
...projectConfig,
|
|
240
|
-
...legacyConfig
|
|
255
|
+
...legacyConfig,
|
|
256
|
+
ecosystem: hasSelectedEcosystemStackParts ? legacyConfig.ecosystem ?? projectConfig.ecosystem : projectConfig.ecosystem
|
|
241
257
|
};
|
|
242
258
|
if (!selectedEcosystems.has("rust")) {
|
|
243
259
|
normalized.rustWebFramework = "none";
|
|
@@ -285,6 +301,7 @@ function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
|
|
|
285
301
|
normalized.goConfig = "none";
|
|
286
302
|
normalized.goObservability = "none";
|
|
287
303
|
}
|
|
304
|
+
if (selectedEcosystems.has("go") && projectConfig.auth === "go-better-auth" && legacyConfig.auth === "none") normalized.auth = projectConfig.auth;
|
|
288
305
|
if (!selectedEcosystems.has("java")) {
|
|
289
306
|
normalized.javaWebFramework = "none";
|
|
290
307
|
normalized.javaBuildTool = "none";
|
|
@@ -326,7 +343,15 @@ function normalizeGraphConfigForPersistence(projectConfig, stackParts) {
|
|
|
326
343
|
normalized.elixirDeploy = "none";
|
|
327
344
|
normalized.elixirLibraries = [];
|
|
328
345
|
}
|
|
329
|
-
|
|
346
|
+
const projectHasNativeFrontend = projectConfig.frontend.some((frontend) => frontend.startsWith("native-"));
|
|
347
|
+
const normalizedHasNativeFrontend = normalized.frontend.some((frontend) => frontend.startsWith("native-"));
|
|
348
|
+
const hasNativeFrontend = projectHasNativeFrontend || normalizedHasNativeFrontend;
|
|
349
|
+
if (hasNativeFrontend) for (const field of MOBILE_CONFIG_FIELDS) {
|
|
350
|
+
const projectValue = projectConfig[field];
|
|
351
|
+
const legacyValue = legacyConfig[field];
|
|
352
|
+
if (projectValue !== void 0 && projectValue !== "none" && legacyValue === "none") normalized[field] = projectValue;
|
|
353
|
+
}
|
|
354
|
+
if (!selectedEcosystems.has("react-native") && !hasNativeFrontend) {
|
|
330
355
|
normalized.mobileNavigation = "none";
|
|
331
356
|
normalized.mobileUI = "none";
|
|
332
357
|
normalized.mobileStorage = "none";
|
|
@@ -425,8 +450,16 @@ function buildBtsConfigForPersistence(projectConfig, metadata = {}) {
|
|
|
425
450
|
api: persistedConfig.api,
|
|
426
451
|
webDeploy: persistedConfig.webDeploy,
|
|
427
452
|
serverDeploy: persistedConfig.serverDeploy,
|
|
453
|
+
astroIntegration: persistedConfig.astroIntegration,
|
|
428
454
|
cssFramework: persistedConfig.cssFramework,
|
|
429
455
|
uiLibrary: persistedConfig.uiLibrary,
|
|
456
|
+
shadcnBase: persistedConfig.shadcnBase,
|
|
457
|
+
shadcnStyle: persistedConfig.shadcnStyle,
|
|
458
|
+
shadcnIconLibrary: persistedConfig.shadcnIconLibrary,
|
|
459
|
+
shadcnColorTheme: persistedConfig.shadcnColorTheme,
|
|
460
|
+
shadcnBaseColor: persistedConfig.shadcnBaseColor,
|
|
461
|
+
shadcnFont: persistedConfig.shadcnFont,
|
|
462
|
+
shadcnRadius: persistedConfig.shadcnRadius,
|
|
430
463
|
realtime: persistedConfig.realtime,
|
|
431
464
|
jobQueue: persistedConfig.jobQueue,
|
|
432
465
|
animation: persistedConfig.animation,
|
|
@@ -446,6 +479,7 @@ function buildBtsConfigForPersistence(projectConfig, metadata = {}) {
|
|
|
446
479
|
rateLimit: persistedConfig.rateLimit,
|
|
447
480
|
i18n: persistedConfig.i18n,
|
|
448
481
|
search: persistedConfig.search,
|
|
482
|
+
vectorDb: persistedConfig.vectorDb,
|
|
449
483
|
fileStorage: persistedConfig.fileStorage,
|
|
450
484
|
rustWebFramework: persistedConfig.rustWebFramework,
|
|
451
485
|
rustFrontend: persistedConfig.rustFrontend,
|
|
@@ -537,8 +571,8 @@ function previewBtsConfigUpdate(currentConfig, updates) {
|
|
|
537
571
|
createdAt: currentConfig.createdAt
|
|
538
572
|
});
|
|
539
573
|
}
|
|
540
|
-
async function writeBtsConfig(projectConfig) {
|
|
541
|
-
const btsConfig = buildBtsConfigForPersistence(projectConfig);
|
|
574
|
+
async function writeBtsConfig(projectConfig, metadata = {}) {
|
|
575
|
+
const btsConfig = buildBtsConfigForPersistence(projectConfig, metadata);
|
|
542
576
|
const baseContent = {
|
|
543
577
|
$schema: "https://better-fullstack-web.vercel.app/schema.json",
|
|
544
578
|
version: btsConfig.version,
|
|
@@ -571,8 +605,16 @@ async function writeBtsConfig(projectConfig) {
|
|
|
571
605
|
api: btsConfig.api,
|
|
572
606
|
webDeploy: btsConfig.webDeploy,
|
|
573
607
|
serverDeploy: btsConfig.serverDeploy,
|
|
608
|
+
astroIntegration: btsConfig.astroIntegration,
|
|
574
609
|
cssFramework: btsConfig.cssFramework,
|
|
575
610
|
uiLibrary: btsConfig.uiLibrary,
|
|
611
|
+
shadcnBase: btsConfig.shadcnBase,
|
|
612
|
+
shadcnStyle: btsConfig.shadcnStyle,
|
|
613
|
+
shadcnIconLibrary: btsConfig.shadcnIconLibrary,
|
|
614
|
+
shadcnColorTheme: btsConfig.shadcnColorTheme,
|
|
615
|
+
shadcnBaseColor: btsConfig.shadcnBaseColor,
|
|
616
|
+
shadcnFont: btsConfig.shadcnFont,
|
|
617
|
+
shadcnRadius: btsConfig.shadcnRadius,
|
|
576
618
|
realtime: btsConfig.realtime,
|
|
577
619
|
jobQueue: btsConfig.jobQueue,
|
|
578
620
|
animation: btsConfig.animation,
|
|
@@ -592,6 +634,7 @@ async function writeBtsConfig(projectConfig) {
|
|
|
592
634
|
rateLimit: btsConfig.rateLimit,
|
|
593
635
|
i18n: btsConfig.i18n,
|
|
594
636
|
search: btsConfig.search,
|
|
637
|
+
vectorDb: btsConfig.vectorDb,
|
|
595
638
|
fileStorage: btsConfig.fileStorage,
|
|
596
639
|
rustWebFramework: btsConfig.rustWebFramework,
|
|
597
640
|
rustFrontend: btsConfig.rustFrontend,
|
|
@@ -708,6 +751,27 @@ async function readBtsConfig(projectDir) {
|
|
|
708
751
|
return null;
|
|
709
752
|
}
|
|
710
753
|
}
|
|
754
|
+
async function readBtsConfigFromFile(filePath) {
|
|
755
|
+
try {
|
|
756
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
757
|
+
if (!await fs.pathExists(resolved)) return null;
|
|
758
|
+
const configPath = (await fs.stat(resolved)).isDirectory() ? path.join(resolved, BTS_CONFIG_FILE) : resolved;
|
|
759
|
+
if (!await fs.pathExists(configPath)) return null;
|
|
760
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
761
|
+
const errors = [];
|
|
762
|
+
const config = JSONC.parse(configContent, errors, {
|
|
763
|
+
allowTrailingComma: true,
|
|
764
|
+
disallowComments: false
|
|
765
|
+
});
|
|
766
|
+
if (errors.length > 0 || config === void 0 || typeof config !== "object") return null;
|
|
767
|
+
return buildBtsConfigForPersistence(config, {
|
|
768
|
+
version: config.version,
|
|
769
|
+
createdAt: config.createdAt
|
|
770
|
+
});
|
|
771
|
+
} catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
711
775
|
async function updateBtsConfig(projectDir, updates) {
|
|
712
776
|
try {
|
|
713
777
|
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
|
@@ -734,4 +798,83 @@ async function updateBtsConfig(projectDir, updates) {
|
|
|
734
798
|
}
|
|
735
799
|
|
|
736
800
|
//#endregion
|
|
737
|
-
|
|
801
|
+
//#region src/utils/context.ts
|
|
802
|
+
const cliStorage = new AsyncLocalStorage();
|
|
803
|
+
function defaultContext() {
|
|
804
|
+
return {
|
|
805
|
+
navigation: {
|
|
806
|
+
isFirstPrompt: false,
|
|
807
|
+
lastPromptShownUI: false
|
|
808
|
+
},
|
|
809
|
+
silent: false
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function getContext() {
|
|
813
|
+
const ctx = cliStorage.getStore();
|
|
814
|
+
if (!ctx) return defaultContext();
|
|
815
|
+
return ctx;
|
|
816
|
+
}
|
|
817
|
+
function tryGetContext() {
|
|
818
|
+
return cliStorage.getStore();
|
|
819
|
+
}
|
|
820
|
+
function isSilent() {
|
|
821
|
+
return getContext().silent;
|
|
822
|
+
}
|
|
823
|
+
function isFirstPrompt() {
|
|
824
|
+
return getContext().navigation.isFirstPrompt;
|
|
825
|
+
}
|
|
826
|
+
function didLastPromptShowUI() {
|
|
827
|
+
return getContext().navigation.lastPromptShownUI;
|
|
828
|
+
}
|
|
829
|
+
function setIsFirstPrompt(value) {
|
|
830
|
+
const ctx = tryGetContext();
|
|
831
|
+
if (ctx) ctx.navigation.isFirstPrompt = value;
|
|
832
|
+
}
|
|
833
|
+
function setLastPromptShownUI(value) {
|
|
834
|
+
const ctx = tryGetContext();
|
|
835
|
+
if (ctx) ctx.navigation.lastPromptShownUI = value;
|
|
836
|
+
}
|
|
837
|
+
async function runWithContextAsync(options, fn) {
|
|
838
|
+
const ctx = {
|
|
839
|
+
navigation: {
|
|
840
|
+
isFirstPrompt: false,
|
|
841
|
+
lastPromptShownUI: false
|
|
842
|
+
},
|
|
843
|
+
silent: options.silent ?? false
|
|
844
|
+
};
|
|
845
|
+
return cliStorage.run(ctx, fn);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/utils/errors.ts
|
|
850
|
+
var UserCancelledError = class extends Error {
|
|
851
|
+
constructor(message = "Operation cancelled") {
|
|
852
|
+
super(message);
|
|
853
|
+
this.name = "UserCancelledError";
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
var CLIError = class extends Error {
|
|
857
|
+
constructor(message) {
|
|
858
|
+
super(message);
|
|
859
|
+
this.name = "CLIError";
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
function exitWithError(message) {
|
|
863
|
+
if (isSilent()) throw new CLIError(message);
|
|
864
|
+
consola.error(pc.red(message));
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
function exitCancelled(message = "Operation cancelled") {
|
|
868
|
+
if (isSilent()) throw new UserCancelledError(message);
|
|
869
|
+
cancel(pc.red(message));
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
function handleError(error, fallbackMessage) {
|
|
873
|
+
const message = error instanceof Error ? error.message : fallbackMessage || String(error);
|
|
874
|
+
if (isSilent()) throw error instanceof Error ? error : new Error(message);
|
|
875
|
+
consola.error(pc.red(message));
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
//#endregion
|
|
880
|
+
export { types_exports as A, getEffectiveStack as C, getGraphSummary as D, getGraphPart as E, getPrimaryGraphPart as O, getUserPkgManager as S, getGraphBackendUrl as T, writeBtsConfig as _, handleError as a, DEFAULT_UI_LIBRARY_BY_FRONTEND as b, isSilent as c, setLastPromptShownUI as d, buildBtsConfigForPersistence as f, updateBtsConfig as g, readBtsConfigFromFile as h, exitWithError as i, hasGraphPart as k, runWithContextAsync as l, readBtsConfig as m, UserCancelledError as n, didLastPromptShowUI as o, previewBtsConfigUpdate as p, exitCancelled as r, isFirstPrompt as s, CLIError as t, setIsFirstPrompt as u, getLatestCLIVersion as v, getGraphBackendDeployInstructions as w, getDefaultConfig as x, DEFAULT_CONFIG as y };
|