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.
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { g as getDefaultConfig, n as readBtsConfig, r as updateBtsConfig } from "./bts-config-Bg1Qea9Y.mjs";
3
- import { W as renderTitle, c as applyDependencyVersionChannel, t as installDependencies, u as getAddonsToAdd } from "./install-dependencies-D6-GO3BZ.mjs";
4
- import { a as CLIError, f as isSilent, o as UserCancelledError, p as runWithContextAsync, t as setupAddons } from "./addons-setup-HSghQS7c.mjs";
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
- if (input.install) await installDependencies({
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
- if (!input.install) {
117
- const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
118
- log.info(pc.yellow(`Run '${installCmd}' to install new dependencies.`));
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 {
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import "./bts-config-InNcw1aJ.mjs";
3
+ import "./errors-ns_o2OKg.mjs";
4
+ import { t as setupAddons } from "./addons-setup-uSvqHagW.mjs";
5
+
6
+ export { setupAddons };
@@ -1,92 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { n as readBtsConfig } from "./bts-config-Bg1Qea9Y.mjs";
3
- import { autocompleteMultiselect, cancel, group, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
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 { CLIError as a, exitWithError as c, isFirstPrompt as d, isSilent as f, setLastPromptShownUI as h, canPromptInteractively as i, handleError as l, setIsFirstPrompt as m, getPackageExecutionArgs as n, UserCancelledError as o, runWithContextAsync as p, addPackageDependency as r, exitCancelled as s, setupAddons as t, didLastPromptShowUI as u };
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 { getUserPkgManager as _, getEffectiveStack as a, getGraphPart as c, hasGraphPart as d, types_exports as f, getDefaultConfig as g, DEFAULT_UI_LIBRARY_BY_FRONTEND as h, writeBtsConfig as i, getGraphSummary as l, DEFAULT_CONFIG as m, readBtsConfig as n, getGraphBackendDeployInstructions as o, getLatestCLIVersion as p, updateBtsConfig as r, getGraphBackendUrl as s, previewBtsConfigUpdate as t, getPrimaryGraphPart as u };
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-58r70ZcL.mjs").then((m) => m.startMcpServer());
4
- else import("./run-QRBymn-p.mjs").then((m) => m.createBtsCli().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 };