create-better-t-stack 3.11.0-bun-compile-opentui.37fc326 → 3.11.0-bun-compile-opentui.9db6a48
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/package.json +7 -7
- package/src/helpers/addons/addons-setup.ts +1 -2
- package/src/helpers/addons/oxlint-setup.ts +4 -4
- package/src/helpers/addons/starlight-setup.ts +4 -5
- package/src/helpers/addons/tauri-setup.ts +4 -5
- package/src/helpers/core/add-addons.ts +2 -3
- package/src/helpers/core/add-deployment.ts +5 -6
- package/src/helpers/core/create-project.ts +1 -1
- package/src/helpers/core/git.ts +1 -2
- package/src/helpers/core/install-dependencies.ts +4 -6
- package/src/helpers/core/project-config.ts +1 -2
- package/src/tui/app.tsx +42 -3
- package/src/tui/use-logger.ts +44 -0
- package/src/utils/logger.ts +80 -0
- package/src/utils/open-url.ts +1 -2
- package/src/prompts/addons.ts +0 -200
- package/src/prompts/api.ts +0 -49
- package/src/prompts/auth.ts +0 -84
- package/src/prompts/backend.ts +0 -83
- package/src/prompts/config-prompts.ts +0 -138
- package/src/prompts/database-setup.ts +0 -112
- package/src/prompts/database.ts +0 -57
- package/src/prompts/examples.ts +0 -60
- package/src/prompts/frontend.ts +0 -118
- package/src/prompts/git.ts +0 -16
- package/src/prompts/install.ts +0 -16
- package/src/prompts/orm.ts +0 -53
- package/src/prompts/package-manager.ts +0 -32
- package/src/prompts/payments.ts +0 -50
- package/src/prompts/project-name.ts +0 -86
- package/src/prompts/runtime.ts +0 -47
- package/src/prompts/server-deploy.ts +0 -91
- package/src/prompts/web-deploy.ts +0 -107
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "3.11.0-bun-compile-opentui.
|
|
3
|
+
"version": "3.11.0-bun-compile-opentui.9db6a48",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-auth",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"prepublishOnly": "echo 'Build binaries separately before publishing'"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@better-t-stack/types": "3.11.0-bun-compile-opentui.
|
|
71
|
+
"@better-t-stack/types": "3.11.0-bun-compile-opentui.9db6a48",
|
|
72
72
|
"@opentui/core": "^0.1.62",
|
|
73
73
|
"@opentui/react": "^0.1.62",
|
|
74
74
|
"commander": "^14.0.2",
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
"typescript": "^5.9.3"
|
|
91
91
|
},
|
|
92
92
|
"optionalDependencies": {
|
|
93
|
-
"@better-t-stack/cli-darwin-arm64": "3.11.0-bun-compile-opentui.
|
|
94
|
-
"@better-t-stack/cli-darwin-x64": "3.11.0-bun-compile-opentui.
|
|
95
|
-
"@better-t-stack/cli-linux-arm64": "3.11.0-bun-compile-opentui.
|
|
96
|
-
"@better-t-stack/cli-linux-x64": "3.11.0-bun-compile-opentui.
|
|
97
|
-
"@better-t-stack/cli-windows-x64": "3.11.0-bun-compile-opentui.
|
|
93
|
+
"@better-t-stack/cli-darwin-arm64": "3.11.0-bun-compile-opentui.9db6a48",
|
|
94
|
+
"@better-t-stack/cli-darwin-x64": "3.11.0-bun-compile-opentui.9db6a48",
|
|
95
|
+
"@better-t-stack/cli-linux-arm64": "3.11.0-bun-compile-opentui.9db6a48",
|
|
96
|
+
"@better-t-stack/cli-linux-x64": "3.11.0-bun-compile-opentui.9db6a48",
|
|
97
|
+
"@better-t-stack/cli-windows-x64": "3.11.0-bun-compile-opentui.9db6a48",
|
|
98
98
|
"@opentui/core-darwin-arm64": "0.1.63",
|
|
99
99
|
"@opentui/core-darwin-x64": "0.1.63",
|
|
100
100
|
"@opentui/core-linux-arm64": "0.1.63",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { log } from "@clack/prompts";
|
|
3
2
|
import fs from "fs-extra";
|
|
4
3
|
import pc from "picocolors";
|
|
5
4
|
import type { Frontend, ProjectConfig } from "../../types";
|
|
@@ -32,7 +31,7 @@ export async function setupAddons(config: ProjectConfig, isAddCommand = false) {
|
|
|
32
31
|
});
|
|
33
32
|
|
|
34
33
|
if (isAddCommand) {
|
|
35
|
-
log
|
|
34
|
+
console.log(`ℹ ${pc.yellow("Update your package.json scripts:")}
|
|
36
35
|
|
|
37
36
|
${pc.dim("Replace:")} ${pc.yellow('"pnpm -r dev"')} ${pc.dim("→")} ${pc.green('"turbo dev"')}
|
|
38
37
|
${pc.dim("Replace:")} ${pc.yellow('"pnpm --filter web dev"')} ${pc.dim(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { spinner } from "@clack/prompts";
|
|
3
2
|
import { $ } from "bun";
|
|
4
3
|
import fs from "fs-extra";
|
|
5
4
|
import type { PackageManager } from "../../types";
|
|
6
5
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
|
7
6
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
|
7
|
+
import { log } from "../../utils/logger";
|
|
8
8
|
|
|
9
9
|
export async function setupOxlint(projectDir: string, packageManager: PackageManager) {
|
|
10
10
|
await addPackageDependency({
|
|
@@ -24,13 +24,13 @@ export async function setupOxlint(projectDir: string, packageManager: PackageMan
|
|
|
24
24
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
log.step("Initializing oxlint and oxfmt...");
|
|
28
28
|
|
|
29
29
|
const oxlintInitCommand = getPackageExecutionCommand(packageManager, "oxlint@latest --init");
|
|
30
|
-
s.start("Initializing oxlint and oxfmt...");
|
|
31
30
|
await $`${{ raw: oxlintInitCommand }}`.cwd(projectDir).env({ CI: "true" });
|
|
32
31
|
|
|
33
32
|
const oxfmtInitCommand = getPackageExecutionCommand(packageManager, "oxfmt@latest --init");
|
|
34
33
|
await $`${{ raw: oxfmtInitCommand }}`.cwd(projectDir).env({ CI: "true" });
|
|
35
|
-
|
|
34
|
+
|
|
35
|
+
log.success("oxlint and oxfmt initialized successfully!");
|
|
36
36
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { spinner } from "@clack/prompts";
|
|
3
2
|
import consola from "consola";
|
|
4
3
|
import { $ } from "bun";
|
|
5
4
|
import fs from "fs-extra";
|
|
6
5
|
import pc from "picocolors";
|
|
7
6
|
import type { ProjectConfig } from "../../types";
|
|
8
7
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
|
8
|
+
import { log } from "../../utils/logger";
|
|
9
9
|
|
|
10
10
|
export async function setupStarlight(config: ProjectConfig) {
|
|
11
11
|
const { packageManager, projectDir } = config;
|
|
12
|
-
const s = spinner();
|
|
13
12
|
|
|
14
13
|
try {
|
|
15
|
-
|
|
14
|
+
log.step("Setting up Starlight docs...");
|
|
16
15
|
|
|
17
16
|
const starlightArgs = [
|
|
18
17
|
"docs",
|
|
@@ -35,9 +34,9 @@ export async function setupStarlight(config: ProjectConfig) {
|
|
|
35
34
|
|
|
36
35
|
await $`${{ raw: starlightInitCommand }}`.cwd(appsDir).env({ CI: "true" });
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
log.success("Starlight docs setup successfully!");
|
|
39
38
|
} catch (error) {
|
|
40
|
-
|
|
39
|
+
log.error("Failed to set up Starlight docs");
|
|
41
40
|
if (error instanceof Error) {
|
|
42
41
|
consola.error(pc.red(error.message));
|
|
43
42
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { spinner } from "@clack/prompts";
|
|
3
2
|
import { consola } from "consola";
|
|
4
3
|
import { $ } from "bun";
|
|
5
4
|
import fs from "fs-extra";
|
|
@@ -7,10 +6,10 @@ import pc from "picocolors";
|
|
|
7
6
|
import type { ProjectConfig } from "../../types";
|
|
8
7
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
|
9
8
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
|
9
|
+
import { log } from "../../utils/logger";
|
|
10
10
|
|
|
11
11
|
export async function setupTauri(config: ProjectConfig) {
|
|
12
12
|
const { packageManager, frontend, projectDir } = config;
|
|
13
|
-
const s = spinner();
|
|
14
13
|
const clientPackageDir = path.join(projectDir, "apps/web");
|
|
15
14
|
|
|
16
15
|
if (!(await fs.pathExists(clientPackageDir))) {
|
|
@@ -18,7 +17,7 @@ export async function setupTauri(config: ProjectConfig) {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
try {
|
|
21
|
-
|
|
20
|
+
log.step("Setting up Tauri desktop app support...");
|
|
22
21
|
|
|
23
22
|
await addPackageDependency({
|
|
24
23
|
devDependencies: ["@tauri-apps/cli"],
|
|
@@ -80,9 +79,9 @@ export async function setupTauri(config: ProjectConfig) {
|
|
|
80
79
|
|
|
81
80
|
await $`${{ raw: tauriInitCommand }}`.cwd(clientPackageDir).env({ CI: "true" });
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
log.success("Tauri desktop app support configured successfully!");
|
|
84
83
|
} catch (error) {
|
|
85
|
-
|
|
84
|
+
log.error("Failed to set up Tauri");
|
|
86
85
|
if (error instanceof Error) {
|
|
87
86
|
consola.error(pc.red(error.message));
|
|
88
87
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { log } from "@clack/prompts";
|
|
3
2
|
import pc from "picocolors";
|
|
4
3
|
import type { AddInput, Addons, ProjectConfig } from "../../types";
|
|
5
4
|
import { updateBtsConfig } from "../../utils/bts-config";
|
|
@@ -74,8 +73,8 @@ export async function addAddonsToProject(
|
|
|
74
73
|
packageManager: config.packageManager,
|
|
75
74
|
});
|
|
76
75
|
} else if (!input.suppressInstallMessage) {
|
|
77
|
-
log
|
|
78
|
-
pc.yellow(
|
|
76
|
+
console.log(
|
|
77
|
+
pc.yellow(`ℹ Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`),
|
|
79
78
|
);
|
|
80
79
|
}
|
|
81
80
|
} catch (error) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { log } from "@clack/prompts";
|
|
3
2
|
import pc from "picocolors";
|
|
4
3
|
import type { AddInput, ProjectConfig, ServerDeploy, WebDeploy } from "../../types";
|
|
5
4
|
import { updateBtsConfig } from "../../utils/bts-config";
|
|
@@ -67,13 +66,13 @@ export async function addDeploymentToProject(
|
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
if (input.webDeploy && input.webDeploy !== "none") {
|
|
70
|
-
log
|
|
71
|
-
pc.green(
|
|
69
|
+
console.log(
|
|
70
|
+
pc.green(`ℹ Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`),
|
|
72
71
|
);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
if (input.serverDeploy && input.serverDeploy !== "none") {
|
|
76
|
-
log
|
|
75
|
+
console.log(pc.green(`ℹ Adding ${input.serverDeploy} server deployment`));
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
await setupDeploymentTemplates(projectDir, config);
|
|
@@ -91,8 +90,8 @@ export async function addDeploymentToProject(
|
|
|
91
90
|
packageManager: config.packageManager,
|
|
92
91
|
});
|
|
93
92
|
} else if (!input.suppressInstallMessage) {
|
|
94
|
-
log
|
|
95
|
-
pc.yellow(
|
|
93
|
+
console.log(
|
|
94
|
+
pc.yellow(`ℹ Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`),
|
|
96
95
|
);
|
|
97
96
|
}
|
|
98
97
|
} catch (error) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { log } from "@clack/prompts";
|
|
2
1
|
import fs from "fs-extra";
|
|
3
2
|
import type { ProjectConfig } from "../../types";
|
|
3
|
+
import { log } from "../../utils/logger";
|
|
4
4
|
import { writeBtsConfig } from "../../utils/bts-config";
|
|
5
5
|
import { exitWithError } from "../../utils/errors";
|
|
6
6
|
import { setupCatalogs } from "../../utils/setup-catalogs";
|
package/src/helpers/core/git.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { log } from "@clack/prompts";
|
|
2
1
|
import { $ } from "bun";
|
|
3
2
|
import pc from "picocolors";
|
|
4
3
|
|
|
@@ -8,7 +7,7 @@ export async function initializeGit(projectDir: string, useGit: boolean) {
|
|
|
8
7
|
const gitVersionResult = await $`git --version`.cwd(projectDir).nothrow().quiet();
|
|
9
8
|
|
|
10
9
|
if (gitVersionResult.exitCode !== 0) {
|
|
11
|
-
|
|
10
|
+
console.warn(pc.yellow("⚠ Git is not installed"));
|
|
12
11
|
return;
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { spinner } from "@clack/prompts";
|
|
2
1
|
import consola from "consola";
|
|
3
2
|
import { $ } from "bun";
|
|
4
3
|
import pc from "picocolors";
|
|
5
4
|
import type { Addons, PackageManager } from "../../types";
|
|
5
|
+
import { log } from "../../utils/logger";
|
|
6
6
|
|
|
7
7
|
export async function installDependencies({
|
|
8
8
|
projectDir,
|
|
@@ -12,16 +12,14 @@ export async function installDependencies({
|
|
|
12
12
|
packageManager: PackageManager;
|
|
13
13
|
addons?: Addons[];
|
|
14
14
|
}) {
|
|
15
|
-
const s = spinner();
|
|
16
|
-
|
|
17
15
|
try {
|
|
18
|
-
|
|
16
|
+
log.step(`Running ${packageManager} install...`);
|
|
19
17
|
|
|
20
18
|
await $`${packageManager} install`.cwd(projectDir);
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
log.success("Dependencies installed successfully");
|
|
23
21
|
} catch (error) {
|
|
24
|
-
|
|
22
|
+
log.error("Failed to install dependencies");
|
|
25
23
|
if (error instanceof Error) {
|
|
26
24
|
consola.error(pc.red(`Installation error: ${error.message}`));
|
|
27
25
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { log } from "@clack/prompts";
|
|
3
2
|
import { $ } from "bun";
|
|
4
3
|
import fs from "fs-extra";
|
|
5
4
|
import type { ProjectConfig } from "../../types";
|
|
@@ -96,7 +95,7 @@ async function updateRootPackageJson(projectDir: string, options: ProjectConfig)
|
|
|
96
95
|
const stdout = await $`${packageManager} -v`.cwd(projectDir).text();
|
|
97
96
|
packageJson.packageManager = `${packageManager}@${stdout.trim()}`;
|
|
98
97
|
} catch {
|
|
99
|
-
|
|
98
|
+
console.warn(`⚠ Could not determine ${packageManager} version.`);
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
if (backend === "convex") {
|
package/src/tui/app.tsx
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { createCliRenderer } from "@opentui/core";
|
|
6
6
|
import { createRoot, useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
7
7
|
import { useState, useCallback, useEffect } from "react";
|
|
8
|
+
import { useLogger } from "./use-logger";
|
|
9
|
+
import type { LogEntry } from "../utils/logger";
|
|
8
10
|
import type {
|
|
9
11
|
ProjectConfig,
|
|
10
12
|
Frontend,
|
|
@@ -558,6 +560,40 @@ function Spinner(props: { text: string }) {
|
|
|
558
560
|
);
|
|
559
561
|
}
|
|
560
562
|
|
|
563
|
+
// LogDisplay component - shows real-time logs during project creation
|
|
564
|
+
function LogDisplay() {
|
|
565
|
+
const logs = useLogger();
|
|
566
|
+
|
|
567
|
+
const getLogIcon = (level: LogEntry["level"]) => {
|
|
568
|
+
switch (level) {
|
|
569
|
+
case "success":
|
|
570
|
+
return { icon: "✓", color: theme.success };
|
|
571
|
+
case "error":
|
|
572
|
+
return { icon: "✗", color: theme.error };
|
|
573
|
+
case "warn":
|
|
574
|
+
return { icon: "⚠", color: "#f59e0b" };
|
|
575
|
+
case "step":
|
|
576
|
+
return { icon: "→", color: theme.primary };
|
|
577
|
+
default:
|
|
578
|
+
return { icon: "ℹ", color: theme.muted };
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
return (
|
|
583
|
+
<box style={{ flexDirection: "column" }}>
|
|
584
|
+
{logs.slice(-15).map((log) => {
|
|
585
|
+
const { icon, color } = getLogIcon(log.level);
|
|
586
|
+
return (
|
|
587
|
+
<text key={log.id}>
|
|
588
|
+
<span fg={color}>{icon}</span>
|
|
589
|
+
<span fg={theme.text}> {log.message}</span>
|
|
590
|
+
</text>
|
|
591
|
+
);
|
|
592
|
+
})}
|
|
593
|
+
</box>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
561
597
|
type Phase = "prompts" | "creating" | "done";
|
|
562
598
|
|
|
563
599
|
function App(props: {
|
|
@@ -788,17 +824,20 @@ function App(props: {
|
|
|
788
824
|
</>
|
|
789
825
|
)}
|
|
790
826
|
|
|
791
|
-
{/* Creating Phase - Show spinner */}
|
|
827
|
+
{/* Creating Phase - Show spinner and logs */}
|
|
792
828
|
{phase === "creating" && (
|
|
793
|
-
<box style={{ flexDirection: "column", marginTop: 2 }}>
|
|
829
|
+
<box style={{ flexDirection: "column", marginTop: 2, flexGrow: 1 }}>
|
|
794
830
|
<Spinner text={creationStatus} />
|
|
795
|
-
<box style={{ marginTop:
|
|
831
|
+
<box style={{ marginTop: 1 }}>
|
|
796
832
|
<text>
|
|
797
833
|
<span fg={theme.muted}>Creating </span>
|
|
798
834
|
<span fg={theme.primary}>{config.projectName}</span>
|
|
799
835
|
<span fg={theme.muted}>...</span>
|
|
800
836
|
</text>
|
|
801
837
|
</box>
|
|
838
|
+
<box style={{ marginTop: 2, flexGrow: 1, overflow: "scroll" }}>
|
|
839
|
+
<LogDisplay />
|
|
840
|
+
</box>
|
|
802
841
|
</box>
|
|
803
842
|
)}
|
|
804
843
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for subscribing to the global logger
|
|
3
|
+
* Updates component state when new logs are emitted
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from "react";
|
|
6
|
+
import { logger, type LogEntry } from "../utils/logger";
|
|
7
|
+
|
|
8
|
+
export function useLogger() {
|
|
9
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
// Get existing logs
|
|
13
|
+
setLogs(logger.getLogs());
|
|
14
|
+
|
|
15
|
+
// Subscribe to new logs
|
|
16
|
+
const unsubscribe = logger.subscribe((entry) => {
|
|
17
|
+
setLogs((prev) => [...prev, entry]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return unsubscribe;
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return logs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Hook to get only the latest log entry
|
|
27
|
+
export function useLatestLog() {
|
|
28
|
+
const [latestLog, setLatestLog] = useState<LogEntry | null>(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const existing = logger.getLogs();
|
|
32
|
+
if (existing.length > 0) {
|
|
33
|
+
setLatestLog(existing[existing.length - 1]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const unsubscribe = logger.subscribe((entry) => {
|
|
37
|
+
setLatestLog(entry);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return unsubscribe;
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return latestLog;
|
|
44
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-integrated logging system
|
|
3
|
+
* Provides a global logger that emits events for the TUI to display in real-time
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = "info" | "success" | "warn" | "error" | "step";
|
|
7
|
+
|
|
8
|
+
export interface LogEntry {
|
|
9
|
+
id: number;
|
|
10
|
+
level: LogLevel;
|
|
11
|
+
message: string;
|
|
12
|
+
timestamp: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type LogListener = (entry: LogEntry) => void;
|
|
16
|
+
|
|
17
|
+
class Logger {
|
|
18
|
+
private listeners: Set<LogListener> = new Set();
|
|
19
|
+
private logs: LogEntry[] = [];
|
|
20
|
+
private idCounter = 0;
|
|
21
|
+
|
|
22
|
+
subscribe(listener: LogListener): () => void {
|
|
23
|
+
this.listeners.add(listener);
|
|
24
|
+
return () => this.listeners.delete(listener);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private emit(level: LogLevel, message: string) {
|
|
28
|
+
const entry: LogEntry = {
|
|
29
|
+
id: this.idCounter++,
|
|
30
|
+
level,
|
|
31
|
+
message,
|
|
32
|
+
timestamp: new Date(),
|
|
33
|
+
};
|
|
34
|
+
this.logs.push(entry);
|
|
35
|
+
for (const listener of this.listeners) {
|
|
36
|
+
listener(entry);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
info(message: string) {
|
|
41
|
+
this.emit("info", message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
success(message: string) {
|
|
45
|
+
this.emit("success", message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
warn(message: string) {
|
|
49
|
+
this.emit("warn", message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
error(message: string) {
|
|
53
|
+
this.emit("error", message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
step(message: string) {
|
|
57
|
+
this.emit("step", message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getLogs(): LogEntry[] {
|
|
61
|
+
return [...this.logs];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clear() {
|
|
65
|
+
this.logs = [];
|
|
66
|
+
this.idCounter = 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Global logger instance
|
|
71
|
+
export const logger = new Logger();
|
|
72
|
+
|
|
73
|
+
// Convenience exports for easy importing
|
|
74
|
+
export const log = {
|
|
75
|
+
info: (msg: string) => logger.info(msg),
|
|
76
|
+
success: (msg: string) => logger.success(msg),
|
|
77
|
+
warn: (msg: string) => logger.warn(msg),
|
|
78
|
+
error: (msg: string) => logger.error(msg),
|
|
79
|
+
step: (msg: string) => logger.step(msg),
|
|
80
|
+
};
|
package/src/utils/open-url.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { log } from "@clack/prompts";
|
|
2
1
|
import { $ } from "bun";
|
|
3
2
|
|
|
4
3
|
export async function openUrl(url: string) {
|
|
@@ -13,6 +12,6 @@ export async function openUrl(url: string) {
|
|
|
13
12
|
await $`xdg-open ${url}`.quiet();
|
|
14
13
|
}
|
|
15
14
|
} catch {
|
|
16
|
-
log
|
|
15
|
+
console.log(`Please open ${url} in your browser.`);
|
|
17
16
|
}
|
|
18
17
|
}
|
package/src/prompts/addons.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { groupMultiselect, isCancel } from "@clack/prompts";
|
|
2
|
-
import { DEFAULT_CONFIG } from "../constants";
|
|
3
|
-
import { type Addons, AddonsSchema, type Auth, type Frontend } from "../types";
|
|
4
|
-
import { getCompatibleAddons, validateAddonCompatibility } from "../utils/compatibility-rules";
|
|
5
|
-
import { exitCancelled } from "../utils/errors";
|
|
6
|
-
|
|
7
|
-
type AddonOption = {
|
|
8
|
-
value: Addons;
|
|
9
|
-
label: string;
|
|
10
|
-
hint: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
function getAddonDisplay(addon: Addons): { label: string; hint: string } {
|
|
14
|
-
let label: string;
|
|
15
|
-
let hint: string;
|
|
16
|
-
|
|
17
|
-
switch (addon) {
|
|
18
|
-
case "turborepo":
|
|
19
|
-
label = "Turborepo";
|
|
20
|
-
hint = "High-performance build system";
|
|
21
|
-
break;
|
|
22
|
-
case "pwa":
|
|
23
|
-
label = "PWA";
|
|
24
|
-
hint = "Make your app installable and work offline";
|
|
25
|
-
break;
|
|
26
|
-
case "tauri":
|
|
27
|
-
label = "Tauri";
|
|
28
|
-
hint = "Build native desktop apps from your web frontend";
|
|
29
|
-
break;
|
|
30
|
-
case "biome":
|
|
31
|
-
label = "Biome";
|
|
32
|
-
hint = "Format, lint, and more";
|
|
33
|
-
break;
|
|
34
|
-
case "oxlint":
|
|
35
|
-
label = "Oxlint";
|
|
36
|
-
hint = "Oxlint + Oxfmt (linting & formatting)";
|
|
37
|
-
break;
|
|
38
|
-
case "ultracite":
|
|
39
|
-
label = "Ultracite";
|
|
40
|
-
hint = "Zero-config Biome preset with AI integration";
|
|
41
|
-
break;
|
|
42
|
-
case "ruler":
|
|
43
|
-
label = "Ruler";
|
|
44
|
-
hint = "Centralize your AI rules";
|
|
45
|
-
break;
|
|
46
|
-
case "husky":
|
|
47
|
-
label = "Husky";
|
|
48
|
-
hint = "Modern native Git hooks made easy";
|
|
49
|
-
break;
|
|
50
|
-
case "starlight":
|
|
51
|
-
label = "Starlight";
|
|
52
|
-
hint = "Build stellar docs with astro";
|
|
53
|
-
break;
|
|
54
|
-
case "fumadocs":
|
|
55
|
-
label = "Fumadocs";
|
|
56
|
-
hint = "Build excellent documentation site";
|
|
57
|
-
break;
|
|
58
|
-
case "opentui":
|
|
59
|
-
label = "OpenTUI";
|
|
60
|
-
hint = "Build terminal user interfaces";
|
|
61
|
-
break;
|
|
62
|
-
case "wxt":
|
|
63
|
-
label = "WXT";
|
|
64
|
-
hint = "Build browser extensions";
|
|
65
|
-
break;
|
|
66
|
-
default:
|
|
67
|
-
label = addon;
|
|
68
|
-
hint = `Add ${addon}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { label, hint };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const ADDON_GROUPS = {
|
|
75
|
-
Documentation: ["starlight", "fumadocs"],
|
|
76
|
-
Linting: ["biome", "oxlint", "ultracite"],
|
|
77
|
-
Other: ["ruler", "pwa", "tauri", "husky", "opentui", "wxt", "turborepo"],
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[], auth?: Auth) {
|
|
81
|
-
if (addons !== undefined) return addons;
|
|
82
|
-
|
|
83
|
-
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
|
|
84
|
-
const groupedOptions: Record<string, AddonOption[]> = {
|
|
85
|
-
Documentation: [],
|
|
86
|
-
Linting: [],
|
|
87
|
-
Other: [],
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const frontendsArray = frontends || [];
|
|
91
|
-
|
|
92
|
-
for (const addon of allAddons) {
|
|
93
|
-
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
|
|
94
|
-
if (!isCompatible) continue;
|
|
95
|
-
|
|
96
|
-
const { label, hint } = getAddonDisplay(addon);
|
|
97
|
-
const option = { value: addon, label, hint };
|
|
98
|
-
|
|
99
|
-
if (ADDON_GROUPS.Documentation.includes(addon)) {
|
|
100
|
-
groupedOptions.Documentation.push(option);
|
|
101
|
-
} else if (ADDON_GROUPS.Linting.includes(addon)) {
|
|
102
|
-
groupedOptions.Linting.push(option);
|
|
103
|
-
} else if (ADDON_GROUPS.Other.includes(addon)) {
|
|
104
|
-
groupedOptions.Other.push(option);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
Object.keys(groupedOptions).forEach((group) => {
|
|
109
|
-
if (groupedOptions[group].length === 0) {
|
|
110
|
-
delete groupedOptions[group];
|
|
111
|
-
} else {
|
|
112
|
-
const groupOrder = ADDON_GROUPS[group as keyof typeof ADDON_GROUPS] || [];
|
|
113
|
-
groupedOptions[group].sort((a, b) => {
|
|
114
|
-
const indexA = groupOrder.indexOf(a.value);
|
|
115
|
-
const indexB = groupOrder.indexOf(b.value);
|
|
116
|
-
return indexA - indexB;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) =>
|
|
122
|
-
Object.values(groupedOptions).some((options) =>
|
|
123
|
-
options.some((opt) => opt.value === addonValue),
|
|
124
|
-
),
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const response = await groupMultiselect<Addons>({
|
|
128
|
-
message: "Select addons",
|
|
129
|
-
options: groupedOptions,
|
|
130
|
-
initialValues: initialValues,
|
|
131
|
-
required: false,
|
|
132
|
-
selectableGroups: false,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
136
|
-
|
|
137
|
-
return response;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export async function getAddonsToAdd(
|
|
141
|
-
frontend: Frontend[],
|
|
142
|
-
existingAddons: Addons[] = [],
|
|
143
|
-
auth?: Auth,
|
|
144
|
-
) {
|
|
145
|
-
const groupedOptions: Record<string, AddonOption[]> = {
|
|
146
|
-
Documentation: [],
|
|
147
|
-
Linting: [],
|
|
148
|
-
Other: [],
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const frontendArray = frontend || [];
|
|
152
|
-
|
|
153
|
-
const compatibleAddons = getCompatibleAddons(
|
|
154
|
-
AddonsSchema.options.filter((addon) => addon !== "none"),
|
|
155
|
-
frontendArray,
|
|
156
|
-
existingAddons,
|
|
157
|
-
auth,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
for (const addon of compatibleAddons) {
|
|
161
|
-
const { label, hint } = getAddonDisplay(addon);
|
|
162
|
-
const option = { value: addon, label, hint };
|
|
163
|
-
|
|
164
|
-
if (ADDON_GROUPS.Documentation.includes(addon)) {
|
|
165
|
-
groupedOptions.Documentation.push(option);
|
|
166
|
-
} else if (ADDON_GROUPS.Linting.includes(addon)) {
|
|
167
|
-
groupedOptions.Linting.push(option);
|
|
168
|
-
} else if (ADDON_GROUPS.Other.includes(addon)) {
|
|
169
|
-
groupedOptions.Other.push(option);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
Object.keys(groupedOptions).forEach((group) => {
|
|
174
|
-
if (groupedOptions[group].length === 0) {
|
|
175
|
-
delete groupedOptions[group];
|
|
176
|
-
} else {
|
|
177
|
-
const groupOrder = ADDON_GROUPS[group as keyof typeof ADDON_GROUPS] || [];
|
|
178
|
-
groupedOptions[group].sort((a, b) => {
|
|
179
|
-
const indexA = groupOrder.indexOf(a.value);
|
|
180
|
-
const indexB = groupOrder.indexOf(b.value);
|
|
181
|
-
return indexA - indexB;
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (Object.keys(groupedOptions).length === 0) {
|
|
187
|
-
return [];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const response = await groupMultiselect<Addons>({
|
|
191
|
-
message: "Select addons to add",
|
|
192
|
-
options: groupedOptions,
|
|
193
|
-
required: false,
|
|
194
|
-
selectableGroups: false,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
198
|
-
|
|
199
|
-
return response;
|
|
200
|
-
}
|