create-better-fullstack 2.0.2 → 2.1.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-BPAZM8Zo.mjs → add-handler-BjTmOwEN.mjs} +11 -9
- package/dist/addons-setup-CSmj5ClW.mjs +6 -0
- package/dist/{addons-setup-HSghQS7c.mjs → addons-setup-uSvqHagW.mjs} +4 -83
- package/dist/{bts-config-Bg1Qea9Y.mjs → bts-config-InNcw1aJ.mjs} +90 -68
- package/dist/cli.mjs +2 -2
- package/dist/doctor-Bm9qvkya.mjs +281 -0
- package/dist/errors-ns_o2OKg.mjs +86 -0
- package/dist/generated-checks-CUhFFfVo.mjs +91 -0
- package/dist/index.d.mts +100 -41
- package/dist/index.mjs +10 -6
- package/dist/{install-dependencies-D6-GO3BZ.mjs → install-dependencies-CDjTNvIV.mjs} +126 -49
- package/dist/{mcp-58r70ZcL.mjs → mcp-BaLHDRdC.mjs} +1 -1
- package/dist/mcp-entry.mjs +601 -137
- package/dist/render-title-zvyKC1ej.mjs +34 -0
- package/dist/{run-DQlymC4z.mjs → run-Cqy-n1iT.mjs} +586 -654
- package/dist/run-DYWSKowD.mjs +10 -0
- package/dist/templates-B351F1yX.mjs +489 -0
- package/package.json +7 -6
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/run-QRBymn-p.mjs +0 -7
- /package/dist/{update-deps-CLebIM70.mjs → update-deps-D5OG0KmJ.mjs} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { a as updateBtsConfig, r as readBtsConfig, u as getDefaultConfig } from "./bts-config-InNcw1aJ.mjs";
|
|
3
|
+
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
+
import { c as isSilent, l as runWithContextAsync, n as UserCancelledError, t as CLIError } from "./errors-ns_o2OKg.mjs";
|
|
5
|
+
import { t as setupAddons } from "./addons-setup-uSvqHagW.mjs";
|
|
6
|
+
import { c as applyDependencyVersionChannel, t as installDependencies, u as getAddonsToAdd } from "./install-dependencies-CDjTNvIV.mjs";
|
|
5
7
|
import { intro, log, outro } from "@clack/prompts";
|
|
6
8
|
import pc from "picocolors";
|
|
7
9
|
import fs from "fs-extra";
|
|
@@ -106,17 +108,17 @@ async function addHandlerInternal(input) {
|
|
|
106
108
|
if (input.webDeploy !== void 0) configUpdates.webDeploy = input.webDeploy;
|
|
107
109
|
if (input.serverDeploy !== void 0) configUpdates.serverDeploy = input.serverDeploy;
|
|
108
110
|
await updateBtsConfig(projectDir, configUpdates);
|
|
109
|
-
|
|
111
|
+
let addonInstallFailed = false;
|
|
112
|
+
if (input.install) addonInstallFailed = !(await installDependencies({
|
|
110
113
|
projectDir,
|
|
111
114
|
packageManager: config.packageManager
|
|
112
|
-
});
|
|
115
|
+
})).success;
|
|
113
116
|
if (!isSilent()) {
|
|
114
117
|
log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
|
|
115
118
|
for (const warning of setupWarnings) log.warn(pc.yellow(warning));
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
119
|
+
const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
|
|
120
|
+
if (!input.install) log.info(pc.yellow(`Run '${installCmd}' to install new dependencies.`));
|
|
121
|
+
else if (addonInstallFailed) log.warn(pc.yellow(`Dependency installation failed. Run '${installCmd}' after resolving the error above.`));
|
|
120
122
|
outro(pc.magenta("Addons added successfully!"));
|
|
121
123
|
}
|
|
122
124
|
return {
|
|
@@ -1,92 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { r as readBtsConfig } from "./bts-config-InNcw1aJ.mjs";
|
|
3
|
+
import { c as isSilent, r as exitCancelled } from "./errors-ns_o2OKg.mjs";
|
|
4
|
+
import { autocompleteMultiselect, group, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
|
|
4
5
|
import pc from "picocolors";
|
|
5
6
|
import fs from "fs-extra";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import consola from "consola";
|
|
8
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
9
|
import { $ } from "execa";
|
|
10
10
|
|
|
11
|
-
//#region src/utils/context.ts
|
|
12
|
-
const cliStorage = new AsyncLocalStorage();
|
|
13
|
-
function defaultContext() {
|
|
14
|
-
return {
|
|
15
|
-
navigation: {
|
|
16
|
-
isFirstPrompt: false,
|
|
17
|
-
lastPromptShownUI: false
|
|
18
|
-
},
|
|
19
|
-
silent: false
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function getContext() {
|
|
23
|
-
const ctx = cliStorage.getStore();
|
|
24
|
-
if (!ctx) return defaultContext();
|
|
25
|
-
return ctx;
|
|
26
|
-
}
|
|
27
|
-
function tryGetContext() {
|
|
28
|
-
return cliStorage.getStore();
|
|
29
|
-
}
|
|
30
|
-
function isSilent() {
|
|
31
|
-
return getContext().silent;
|
|
32
|
-
}
|
|
33
|
-
function isFirstPrompt() {
|
|
34
|
-
return getContext().navigation.isFirstPrompt;
|
|
35
|
-
}
|
|
36
|
-
function didLastPromptShowUI() {
|
|
37
|
-
return getContext().navigation.lastPromptShownUI;
|
|
38
|
-
}
|
|
39
|
-
function setIsFirstPrompt(value) {
|
|
40
|
-
const ctx = tryGetContext();
|
|
41
|
-
if (ctx) ctx.navigation.isFirstPrompt = value;
|
|
42
|
-
}
|
|
43
|
-
function setLastPromptShownUI(value) {
|
|
44
|
-
const ctx = tryGetContext();
|
|
45
|
-
if (ctx) ctx.navigation.lastPromptShownUI = value;
|
|
46
|
-
}
|
|
47
|
-
async function runWithContextAsync(options, fn) {
|
|
48
|
-
const ctx = {
|
|
49
|
-
navigation: {
|
|
50
|
-
isFirstPrompt: false,
|
|
51
|
-
lastPromptShownUI: false
|
|
52
|
-
},
|
|
53
|
-
silent: options.silent ?? false
|
|
54
|
-
};
|
|
55
|
-
return cliStorage.run(ctx, fn);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
//#endregion
|
|
59
|
-
//#region src/utils/errors.ts
|
|
60
|
-
var UserCancelledError = class extends Error {
|
|
61
|
-
constructor(message = "Operation cancelled") {
|
|
62
|
-
super(message);
|
|
63
|
-
this.name = "UserCancelledError";
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
var CLIError = class extends Error {
|
|
67
|
-
constructor(message) {
|
|
68
|
-
super(message);
|
|
69
|
-
this.name = "CLIError";
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
function exitWithError(message) {
|
|
73
|
-
if (isSilent()) throw new CLIError(message);
|
|
74
|
-
consola.error(pc.red(message));
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
function exitCancelled(message = "Operation cancelled") {
|
|
78
|
-
if (isSilent()) throw new UserCancelledError(message);
|
|
79
|
-
cancel(pc.red(message));
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
function handleError(error, fallbackMessage) {
|
|
83
|
-
const message = error instanceof Error ? error.message : fallbackMessage || String(error);
|
|
84
|
-
if (isSilent()) throw error instanceof Error ? error : new Error(message);
|
|
85
|
-
consola.error(pc.red(message));
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
//#endregion
|
|
90
11
|
//#region src/utils/prompt-environment.ts
|
|
91
12
|
function hasOwnProperty(value, property) {
|
|
92
13
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
@@ -1412,4 +1333,4 @@ async function setupLefthook(projectDir) {
|
|
|
1412
1333
|
}
|
|
1413
1334
|
|
|
1414
1335
|
//#endregion
|
|
1415
|
-
export {
|
|
1336
|
+
export { canPromptInteractively as i, getPackageExecutionArgs as n, addPackageDependency as r, setupAddons as t };
|
|
@@ -2,75 +2,10 @@
|
|
|
2
2
|
import { t as __reExport } from "./chunk-CCII7kTE.mjs";
|
|
3
3
|
import fs from "fs-extra";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createCliDefaultProjectConfigBase } from "@better-fullstack/types";
|
|
7
5
|
import * as JSONC from "jsonc-parser";
|
|
6
|
+
import { createCliDefaultProjectConfigBase } from "@better-fullstack/types";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
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
9
|
//#region src/types.ts
|
|
75
10
|
var types_exports = {};
|
|
76
11
|
import * as import__better_fullstack_types from "@better-fullstack/types";
|
|
@@ -228,6 +163,71 @@ function getGraphBackendDeployInstructions(config) {
|
|
|
228
163
|
}
|
|
229
164
|
}
|
|
230
165
|
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/utils/get-package-manager.ts
|
|
168
|
+
const getUserPkgManager = () => {
|
|
169
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
170
|
+
if (userAgent?.startsWith("pnpm")) return "pnpm";
|
|
171
|
+
if (userAgent?.startsWith("bun")) return "bun";
|
|
172
|
+
if (userAgent?.startsWith("yarn")) return "yarn";
|
|
173
|
+
return "npm";
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/constants.ts
|
|
178
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
179
|
+
const distPath = path.dirname(__filename);
|
|
180
|
+
const PKG_ROOT = path.join(distPath, "../");
|
|
181
|
+
const DEFAULT_CONFIG_BASE = createCliDefaultProjectConfigBase(getUserPkgManager());
|
|
182
|
+
function getDefaultConfig() {
|
|
183
|
+
return {
|
|
184
|
+
...DEFAULT_CONFIG_BASE,
|
|
185
|
+
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
|
|
186
|
+
packageManager: getUserPkgManager(),
|
|
187
|
+
frontend: [...DEFAULT_CONFIG_BASE.frontend],
|
|
188
|
+
addons: [...DEFAULT_CONFIG_BASE.addons],
|
|
189
|
+
examples: [...DEFAULT_CONFIG_BASE.examples],
|
|
190
|
+
rustLibraries: [...DEFAULT_CONFIG_BASE.rustLibraries],
|
|
191
|
+
pythonAi: [...DEFAULT_CONFIG_BASE.pythonAi],
|
|
192
|
+
javaLibraries: [...DEFAULT_CONFIG_BASE.javaLibraries],
|
|
193
|
+
javaTestingLibraries: [...DEFAULT_CONFIG_BASE.javaTestingLibraries],
|
|
194
|
+
aiDocs: [...DEFAULT_CONFIG_BASE.aiDocs]
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const DEFAULT_CONFIG = getDefaultConfig();
|
|
198
|
+
/**
|
|
199
|
+
* Default UI library for each frontend framework
|
|
200
|
+
* Falls back based on what's compatible
|
|
201
|
+
*/
|
|
202
|
+
const DEFAULT_UI_LIBRARY_BY_FRONTEND = {
|
|
203
|
+
"tanstack-router": "shadcn-ui",
|
|
204
|
+
"react-router": "shadcn-ui",
|
|
205
|
+
"react-vite": "shadcn-ui",
|
|
206
|
+
"tanstack-start": "shadcn-ui",
|
|
207
|
+
next: "shadcn-ui",
|
|
208
|
+
vinext: "shadcn-ui",
|
|
209
|
+
nuxt: "daisyui",
|
|
210
|
+
svelte: "daisyui",
|
|
211
|
+
solid: "daisyui",
|
|
212
|
+
"solid-start": "daisyui",
|
|
213
|
+
astro: "daisyui",
|
|
214
|
+
qwik: "daisyui",
|
|
215
|
+
angular: "daisyui",
|
|
216
|
+
redwood: "daisyui",
|
|
217
|
+
fresh: "daisyui",
|
|
218
|
+
"native-bare": "none",
|
|
219
|
+
"native-uniwind": "none",
|
|
220
|
+
"native-unistyles": "none",
|
|
221
|
+
none: "none"
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/utils/get-latest-cli-version.ts
|
|
226
|
+
const getLatestCLIVersion = () => {
|
|
227
|
+
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
228
|
+
return fs.readJSONSync(packageJsonPath).version ?? "1.0.0";
|
|
229
|
+
};
|
|
230
|
+
|
|
231
231
|
//#endregion
|
|
232
232
|
//#region src/utils/bts-config.ts
|
|
233
233
|
const BTS_CONFIG_FILE = "bts.jsonc";
|
|
@@ -446,6 +446,7 @@ function buildBtsConfigForPersistence(projectConfig, metadata = {}) {
|
|
|
446
446
|
rateLimit: persistedConfig.rateLimit,
|
|
447
447
|
i18n: persistedConfig.i18n,
|
|
448
448
|
search: persistedConfig.search,
|
|
449
|
+
vectorDb: persistedConfig.vectorDb,
|
|
449
450
|
fileStorage: persistedConfig.fileStorage,
|
|
450
451
|
rustWebFramework: persistedConfig.rustWebFramework,
|
|
451
452
|
rustFrontend: persistedConfig.rustFrontend,
|
|
@@ -708,6 +709,27 @@ async function readBtsConfig(projectDir) {
|
|
|
708
709
|
return null;
|
|
709
710
|
}
|
|
710
711
|
}
|
|
712
|
+
async function readBtsConfigFromFile(filePath) {
|
|
713
|
+
try {
|
|
714
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
715
|
+
if (!await fs.pathExists(resolved)) return null;
|
|
716
|
+
const configPath = (await fs.stat(resolved)).isDirectory() ? path.join(resolved, BTS_CONFIG_FILE) : resolved;
|
|
717
|
+
if (!await fs.pathExists(configPath)) return null;
|
|
718
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
719
|
+
const errors = [];
|
|
720
|
+
const config = JSONC.parse(configContent, errors, {
|
|
721
|
+
allowTrailingComma: true,
|
|
722
|
+
disallowComments: false
|
|
723
|
+
});
|
|
724
|
+
if (errors.length > 0 || config === void 0 || typeof config !== "object") return null;
|
|
725
|
+
return buildBtsConfigForPersistence(config, {
|
|
726
|
+
version: config.version,
|
|
727
|
+
createdAt: config.createdAt
|
|
728
|
+
});
|
|
729
|
+
} catch {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
711
733
|
async function updateBtsConfig(projectDir, updates) {
|
|
712
734
|
try {
|
|
713
735
|
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
|
@@ -734,4 +756,4 @@ async function updateBtsConfig(projectDir, updates) {
|
|
|
734
756
|
}
|
|
735
757
|
|
|
736
758
|
//#endregion
|
|
737
|
-
export {
|
|
759
|
+
export { getPrimaryGraphPart as _, updateBtsConfig as a, DEFAULT_CONFIG as c, getUserPkgManager as d, getEffectiveStack as f, getGraphSummary as g, getGraphPart as h, readBtsConfigFromFile as i, DEFAULT_UI_LIBRARY_BY_FRONTEND as l, getGraphBackendUrl as m, previewBtsConfigUpdate as n, writeBtsConfig as o, getGraphBackendDeployInstructions as p, readBtsConfig as r, getLatestCLIVersion as s, buildBtsConfigForPersistence as t, getDefaultConfig as u, hasGraphPart as v, types_exports as y };
|
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-
|
|
4
|
-
else import("./run-
|
|
3
|
+
if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-BaLHDRdC.mjs").then((m) => m.startMcpServer());
|
|
4
|
+
else import("./run-DYWSKowD.mjs").then((m) => m.createBtsCli().run());
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { r as readBtsConfig } from "./bts-config-InNcw1aJ.mjs";
|
|
3
|
+
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
+
import { a as handleError } from "./errors-ns_o2OKg.mjs";
|
|
5
|
+
import { t as runGeneratedChecks } from "./generated-checks-CUhFFfVo.mjs";
|
|
6
|
+
import { intro, log, spinner } from "@clack/prompts";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
import fs from "fs-extra";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { $ } from "execa";
|
|
11
|
+
|
|
12
|
+
//#region src/commands/doctor.ts
|
|
13
|
+
const NON_TS_BACKEND_ECOSYSTEMS = new Set([
|
|
14
|
+
"go",
|
|
15
|
+
"rust",
|
|
16
|
+
"python",
|
|
17
|
+
"elixir",
|
|
18
|
+
"java",
|
|
19
|
+
"dotnet"
|
|
20
|
+
]);
|
|
21
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
22
|
+
"node_modules",
|
|
23
|
+
".git",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
".next",
|
|
27
|
+
".expo",
|
|
28
|
+
".turbo",
|
|
29
|
+
"target",
|
|
30
|
+
".venv",
|
|
31
|
+
"deps",
|
|
32
|
+
"_build"
|
|
33
|
+
]);
|
|
34
|
+
const JS_LOCKFILES = [
|
|
35
|
+
"bun.lock",
|
|
36
|
+
"bun.lockb",
|
|
37
|
+
"pnpm-lock.yaml",
|
|
38
|
+
"package-lock.json",
|
|
39
|
+
"yarn.lock"
|
|
40
|
+
];
|
|
41
|
+
const NATIVE_LOCKFILES = {
|
|
42
|
+
rust: {
|
|
43
|
+
file: "Cargo.lock",
|
|
44
|
+
hint: "cargo build"
|
|
45
|
+
},
|
|
46
|
+
go: {
|
|
47
|
+
file: "go.sum",
|
|
48
|
+
hint: "go mod tidy"
|
|
49
|
+
},
|
|
50
|
+
python: {
|
|
51
|
+
file: "uv.lock",
|
|
52
|
+
hint: "uv sync"
|
|
53
|
+
},
|
|
54
|
+
elixir: {
|
|
55
|
+
file: "mix.lock",
|
|
56
|
+
hint: "mix deps.get"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function statusIcon(status) {
|
|
60
|
+
switch (status) {
|
|
61
|
+
case "pass": return pc.green("✓");
|
|
62
|
+
case "warn": return pc.yellow("!");
|
|
63
|
+
case "fail": return pc.red("✗");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function hasNativeChecks(config) {
|
|
67
|
+
if (NON_TS_BACKEND_ECOSYSTEMS.has(config.ecosystem)) return true;
|
|
68
|
+
return (config.stackParts ?? []).some((part) => part.source !== "provided" && part.role === "backend" && NON_TS_BACKEND_ECOSYSTEMS.has(part.ecosystem));
|
|
69
|
+
}
|
|
70
|
+
async function checkInstalledDependencies(projectDir, config) {
|
|
71
|
+
const checks = [];
|
|
72
|
+
if (await fs.pathExists(path.join(projectDir, "package.json"))) {
|
|
73
|
+
const lockfile = JS_LOCKFILES.find((name) => fs.existsSync(path.join(projectDir, name)));
|
|
74
|
+
checks.push(lockfile ? {
|
|
75
|
+
label: "Lockfile",
|
|
76
|
+
status: "pass",
|
|
77
|
+
detail: lockfile
|
|
78
|
+
} : {
|
|
79
|
+
label: "Lockfile",
|
|
80
|
+
status: "warn",
|
|
81
|
+
detail: "No JavaScript lockfile found at the project root"
|
|
82
|
+
});
|
|
83
|
+
const nodeModulesExists = await fs.pathExists(path.join(projectDir, "node_modules"));
|
|
84
|
+
checks.push(nodeModulesExists ? {
|
|
85
|
+
label: "node_modules",
|
|
86
|
+
status: "pass"
|
|
87
|
+
} : {
|
|
88
|
+
label: "node_modules",
|
|
89
|
+
status: "fail",
|
|
90
|
+
detail: `Dependencies are not installed. Run \`${config.packageManager ?? "npm"} install\`.`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const native = NATIVE_LOCKFILES[config.ecosystem];
|
|
94
|
+
if (native) {
|
|
95
|
+
const exists = await fs.pathExists(path.join(projectDir, native.file)) || await fs.pathExists(path.join(projectDir, "apps/server", native.file));
|
|
96
|
+
checks.push(exists ? {
|
|
97
|
+
label: native.file,
|
|
98
|
+
status: "pass"
|
|
99
|
+
} : {
|
|
100
|
+
label: native.file,
|
|
101
|
+
status: "warn",
|
|
102
|
+
detail: `Not found. Run \`${native.hint}\` to fetch dependencies.`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return checks;
|
|
106
|
+
}
|
|
107
|
+
async function findEnvExampleFiles(rootDir) {
|
|
108
|
+
const results = [];
|
|
109
|
+
async function walk(dir, depth) {
|
|
110
|
+
if (depth > 5) return;
|
|
111
|
+
let entries;
|
|
112
|
+
try {
|
|
113
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
114
|
+
} catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
118
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
119
|
+
await walk(path.join(dir, entry.name), depth + 1);
|
|
120
|
+
} else if (entry.name === ".env.example") results.push(path.join(dir, entry.name));
|
|
121
|
+
}
|
|
122
|
+
await walk(rootDir, 0);
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
function parseEnvKeys(content) {
|
|
126
|
+
const map = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const rawLine of content.split("\n")) {
|
|
128
|
+
const line = rawLine.trim();
|
|
129
|
+
if (!line || line.startsWith("#")) continue;
|
|
130
|
+
const eq = line.indexOf("=");
|
|
131
|
+
if (eq === -1) continue;
|
|
132
|
+
const key = line.slice(0, eq).trim();
|
|
133
|
+
if (key) map.set(key, line.slice(eq + 1).trim());
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}
|
|
137
|
+
async function checkEnvFiles(projectDir) {
|
|
138
|
+
const checks = [];
|
|
139
|
+
const exampleFiles = await findEnvExampleFiles(projectDir);
|
|
140
|
+
for (const examplePath of exampleFiles) {
|
|
141
|
+
const envPath = examplePath.replace(/\.example$/, "");
|
|
142
|
+
const relExample = path.relative(projectDir, examplePath) || ".env.example";
|
|
143
|
+
const exampleKeys = parseEnvKeys(await fs.readFile(examplePath, "utf-8"));
|
|
144
|
+
if (exampleKeys.size === 0) continue;
|
|
145
|
+
if (!await fs.pathExists(envPath)) {
|
|
146
|
+
checks.push({
|
|
147
|
+
label: relExample,
|
|
148
|
+
status: "warn",
|
|
149
|
+
detail: `Missing ${path.relative(projectDir, envPath)} (copy from .env.example and fill in values)`
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const envKeys = parseEnvKeys(await fs.readFile(envPath, "utf-8"));
|
|
154
|
+
const missing = [];
|
|
155
|
+
for (const key of exampleKeys.keys()) {
|
|
156
|
+
const value = envKeys.get(key);
|
|
157
|
+
if (value === void 0 || value === "") missing.push(key);
|
|
158
|
+
}
|
|
159
|
+
checks.push(missing.length > 0 ? {
|
|
160
|
+
label: path.relative(projectDir, envPath),
|
|
161
|
+
status: "warn",
|
|
162
|
+
detail: `Missing or empty: ${missing.join(", ")}`
|
|
163
|
+
} : {
|
|
164
|
+
label: path.relative(projectDir, envPath),
|
|
165
|
+
status: "pass"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return checks;
|
|
169
|
+
}
|
|
170
|
+
async function runBuildChecks(config, json) {
|
|
171
|
+
const checks = [];
|
|
172
|
+
const projectDir = config.projectDir;
|
|
173
|
+
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
|
174
|
+
if (await fs.pathExists(rootPackageJsonPath)) {
|
|
175
|
+
if ((await fs.readJson(rootPackageJsonPath).catch(() => ({}))).scripts?.["check-types"]) {
|
|
176
|
+
const pm = config.packageManager ?? "npm";
|
|
177
|
+
const s = json ? null : spinner();
|
|
178
|
+
s?.start("Running type checks (check-types)...");
|
|
179
|
+
const result = await $({
|
|
180
|
+
cwd: projectDir,
|
|
181
|
+
reject: false,
|
|
182
|
+
stdout: json ? "ignore" : "inherit",
|
|
183
|
+
stderr: json ? "ignore" : "inherit"
|
|
184
|
+
})`${pm} run check-types`;
|
|
185
|
+
if (result.exitCode === 0) {
|
|
186
|
+
s?.stop("Type checks passed");
|
|
187
|
+
checks.push({
|
|
188
|
+
label: "check-types",
|
|
189
|
+
status: "pass"
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
s?.stop(pc.red("Type checks failed"));
|
|
193
|
+
checks.push({
|
|
194
|
+
label: "check-types",
|
|
195
|
+
status: "fail",
|
|
196
|
+
detail: `\`${pm} run check-types\` exited with code ${result.exitCode ?? `signal ${result.signal}`}`
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (hasNativeChecks(config)) if (json) checks.push({
|
|
202
|
+
label: "ecosystem build checks",
|
|
203
|
+
status: "warn",
|
|
204
|
+
detail: "Skipped in --json mode. Re-run without --json to execute native build checks."
|
|
205
|
+
});
|
|
206
|
+
else try {
|
|
207
|
+
await runGeneratedChecks(config);
|
|
208
|
+
checks.push({
|
|
209
|
+
label: "ecosystem build checks",
|
|
210
|
+
status: "pass"
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
checks.push({
|
|
214
|
+
label: "ecosystem build checks",
|
|
215
|
+
status: "fail",
|
|
216
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return checks;
|
|
220
|
+
}
|
|
221
|
+
async function doctorCommand(input) {
|
|
222
|
+
const projectDir = path.resolve(input.projectDir || process.cwd());
|
|
223
|
+
const json = input.json ?? false;
|
|
224
|
+
const btsConfig = await readBtsConfig(projectDir);
|
|
225
|
+
if (!btsConfig) {
|
|
226
|
+
if (json) {
|
|
227
|
+
console.log(JSON.stringify({
|
|
228
|
+
projectDir,
|
|
229
|
+
ok: false,
|
|
230
|
+
error: "No Better Fullstack project found (bts.jsonc missing or invalid)."
|
|
231
|
+
}, null, 2));
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
handleError(`No Better Fullstack project found in ${projectDir}. Make sure bts.jsonc exists.`);
|
|
235
|
+
}
|
|
236
|
+
const config = {
|
|
237
|
+
...btsConfig,
|
|
238
|
+
projectDir
|
|
239
|
+
};
|
|
240
|
+
if (!json) {
|
|
241
|
+
renderTitle();
|
|
242
|
+
intro(pc.magenta(`Diagnosing ${pc.cyan(path.basename(projectDir))}`));
|
|
243
|
+
log.info(pc.dim(`Path: ${projectDir}`));
|
|
244
|
+
log.info(pc.dim(`Ecosystem: ${btsConfig.ecosystem}`));
|
|
245
|
+
if (btsConfig.graphSummary) log.info(pc.dim(`Stack: ${btsConfig.graphSummary}`));
|
|
246
|
+
}
|
|
247
|
+
const checks = [{
|
|
248
|
+
label: "bts.jsonc",
|
|
249
|
+
status: "pass",
|
|
250
|
+
detail: `version ${btsConfig.version}`
|
|
251
|
+
}];
|
|
252
|
+
checks.push(...await checkInstalledDependencies(projectDir, btsConfig));
|
|
253
|
+
checks.push(...await checkEnvFiles(projectDir));
|
|
254
|
+
if (!input.skipChecks) checks.push(...await runBuildChecks(config, json));
|
|
255
|
+
const counts = {
|
|
256
|
+
pass: 0,
|
|
257
|
+
warn: 0,
|
|
258
|
+
fail: 0
|
|
259
|
+
};
|
|
260
|
+
for (const check of checks) counts[check.status] += 1;
|
|
261
|
+
if (json) console.log(JSON.stringify({
|
|
262
|
+
projectDir,
|
|
263
|
+
ecosystem: btsConfig.ecosystem,
|
|
264
|
+
ok: counts.fail === 0,
|
|
265
|
+
summary: counts,
|
|
266
|
+
checks
|
|
267
|
+
}, null, 2));
|
|
268
|
+
else {
|
|
269
|
+
log.message("");
|
|
270
|
+
for (const check of checks) log.message(`${statusIcon(check.status)} ${check.label}${check.detail ? pc.dim(` — ${check.detail}`) : ""}`);
|
|
271
|
+
log.message("");
|
|
272
|
+
const summaryLine = `${pc.green(`${counts.pass} passed`)}, ${pc.yellow(`${counts.warn} warnings`)}, ${pc.red(`${counts.fail} failed`)}`;
|
|
273
|
+
if (counts.fail > 0) log.error(`Diagnosis complete: ${summaryLine}`);
|
|
274
|
+
else if (counts.warn > 0) log.warn(`Diagnosis complete: ${summaryLine}`);
|
|
275
|
+
else log.success(`Diagnosis complete: ${summaryLine}`);
|
|
276
|
+
}
|
|
277
|
+
if (counts.fail > 0) process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
export { doctorCommand };
|