create-better-t-stack 3.7.3-canary.7cbb05fc → 3.7.3-canary.8e4d5716

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.
Files changed (104) hide show
  1. package/package.json +19 -23
  2. package/src/cli.ts +3 -0
  3. package/src/constants.ts +188 -0
  4. package/src/helpers/addons/addons-setup.ts +226 -0
  5. package/src/helpers/addons/examples-setup.ts +104 -0
  6. package/src/helpers/addons/fumadocs-setup.ts +103 -0
  7. package/src/helpers/addons/ruler-setup.ts +139 -0
  8. package/src/helpers/addons/starlight-setup.ts +51 -0
  9. package/src/helpers/addons/tauri-setup.ts +96 -0
  10. package/src/helpers/addons/ultracite-setup.ts +232 -0
  11. package/src/helpers/addons/vite-pwa-setup.ts +59 -0
  12. package/src/helpers/core/add-addons.ts +85 -0
  13. package/src/helpers/core/add-deployment.ts +102 -0
  14. package/src/helpers/core/api-setup.ts +280 -0
  15. package/src/helpers/core/auth-setup.ts +203 -0
  16. package/src/helpers/core/backend-setup.ts +73 -0
  17. package/src/helpers/core/command-handlers.ts +354 -0
  18. package/src/helpers/core/convex-codegen.ts +14 -0
  19. package/src/helpers/core/create-project.ts +133 -0
  20. package/src/helpers/core/create-readme.ts +687 -0
  21. package/src/helpers/core/db-setup.ts +184 -0
  22. package/src/helpers/core/detect-project-config.ts +41 -0
  23. package/src/helpers/core/env-setup.ts +449 -0
  24. package/src/helpers/core/git.ts +31 -0
  25. package/src/helpers/core/install-dependencies.ts +32 -0
  26. package/src/helpers/core/payments-setup.ts +48 -0
  27. package/src/helpers/core/post-installation.ts +383 -0
  28. package/src/helpers/core/project-config.ts +246 -0
  29. package/src/helpers/core/runtime-setup.ts +76 -0
  30. package/src/helpers/core/template-manager.ts +917 -0
  31. package/src/helpers/core/workspace-setup.ts +184 -0
  32. package/src/helpers/database-providers/d1-setup.ts +28 -0
  33. package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
  34. package/src/helpers/database-providers/mongodb-atlas-setup.ts +186 -0
  35. package/src/helpers/database-providers/neon-setup.ts +243 -0
  36. package/src/helpers/database-providers/planetscale-setup.ts +78 -0
  37. package/src/helpers/database-providers/prisma-postgres-setup.ts +196 -0
  38. package/src/helpers/database-providers/supabase-setup.ts +218 -0
  39. package/src/helpers/database-providers/turso-setup.ts +309 -0
  40. package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
  41. package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +51 -0
  42. package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +104 -0
  43. package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +32 -0
  44. package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +32 -0
  45. package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +98 -0
  46. package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +33 -0
  47. package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +98 -0
  48. package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
  49. package/src/helpers/deployment/alchemy/index.ts +7 -0
  50. package/src/helpers/deployment/server-deploy-setup.ts +55 -0
  51. package/src/helpers/deployment/web-deploy-setup.ts +58 -0
  52. package/src/index.ts +253 -0
  53. package/src/prompts/addons.ts +178 -0
  54. package/src/prompts/api.ts +49 -0
  55. package/src/prompts/auth.ts +84 -0
  56. package/src/prompts/backend.ts +83 -0
  57. package/src/prompts/config-prompts.ts +138 -0
  58. package/src/prompts/database-setup.ts +112 -0
  59. package/src/prompts/database.ts +57 -0
  60. package/src/prompts/examples.ts +64 -0
  61. package/src/prompts/frontend.ts +118 -0
  62. package/src/prompts/git.ts +16 -0
  63. package/src/prompts/install.ts +16 -0
  64. package/src/prompts/orm.ts +53 -0
  65. package/src/prompts/package-manager.ts +32 -0
  66. package/src/prompts/payments.ts +50 -0
  67. package/src/prompts/project-name.ts +86 -0
  68. package/src/prompts/runtime.ts +47 -0
  69. package/src/prompts/server-deploy.ts +91 -0
  70. package/src/prompts/web-deploy.ts +107 -0
  71. package/src/types.ts +2 -0
  72. package/src/utils/add-package-deps.ts +57 -0
  73. package/src/utils/analytics.ts +39 -0
  74. package/src/utils/better-auth-plugin-setup.ts +71 -0
  75. package/src/utils/biome-formatter.ts +82 -0
  76. package/src/utils/bts-config.ts +122 -0
  77. package/src/utils/command-exists.ts +16 -0
  78. package/src/utils/compatibility-rules.ts +319 -0
  79. package/src/utils/compatibility.ts +11 -0
  80. package/src/utils/config-processing.ts +130 -0
  81. package/src/utils/config-validation.ts +470 -0
  82. package/src/utils/display-config.ts +96 -0
  83. package/src/utils/docker-utils.ts +70 -0
  84. package/src/utils/errors.ts +32 -0
  85. package/src/utils/generate-reproducible-command.ts +53 -0
  86. package/src/utils/get-latest-cli-version.ts +11 -0
  87. package/src/utils/get-package-manager.ts +13 -0
  88. package/src/utils/open-url.ts +25 -0
  89. package/src/utils/package-runner.ts +23 -0
  90. package/src/utils/project-directory.ts +102 -0
  91. package/src/utils/project-name-validation.ts +43 -0
  92. package/src/utils/render-title.ts +48 -0
  93. package/src/utils/setup-catalogs.ts +192 -0
  94. package/src/utils/sponsors.ts +101 -0
  95. package/src/utils/telemetry.ts +19 -0
  96. package/src/utils/template-processor.ts +64 -0
  97. package/src/utils/templates.ts +94 -0
  98. package/src/utils/ts-morph.ts +26 -0
  99. package/src/validation.ts +117 -0
  100. package/dist/cli.d.mts +0 -1
  101. package/dist/cli.mjs +0 -8
  102. package/dist/index.d.mts +0 -347
  103. package/dist/index.mjs +0 -4
  104. package/dist/src-CxVxLS85.mjs +0 -7077
@@ -0,0 +1,103 @@
1
+ import path from "node:path";
2
+ import { isCancel, log, select, spinner } from "@clack/prompts";
3
+ import consola from "consola";
4
+ import { execa } from "execa";
5
+ import fs from "fs-extra";
6
+ import pc from "picocolors";
7
+ import type { ProjectConfig } from "../../types";
8
+ import { exitCancelled } from "../../utils/errors";
9
+ import { getPackageExecutionCommand } from "../../utils/package-runner";
10
+
11
+ type FumadocsTemplate =
12
+ | "next-mdx"
13
+ | "waku"
14
+ | "react-router"
15
+ | "react-router-spa"
16
+ | "tanstack-start";
17
+
18
+ const TEMPLATES = {
19
+ "next-mdx": {
20
+ label: "Next.js: Fumadocs MDX",
21
+ hint: "Recommended template with MDX support",
22
+ value: "+next+fuma-docs-mdx",
23
+ },
24
+ waku: {
25
+ label: "Waku: Content Collections",
26
+ hint: "Template using Waku with content collections",
27
+ value: "waku",
28
+ },
29
+ "react-router": {
30
+ label: "React Router: MDX Remote",
31
+ hint: "Template for React Router with MDX remote",
32
+ value: "react-router",
33
+ },
34
+ "react-router-spa": {
35
+ label: "React Router: SPA",
36
+ hint: "Template for React Router SPA",
37
+ value: "react-router-spa",
38
+ },
39
+ "tanstack-start": {
40
+ label: "Tanstack Start: MDX Remote",
41
+ hint: "Template for Tanstack Start with MDX remote",
42
+ value: "tanstack-start",
43
+ },
44
+ } as const;
45
+
46
+ export async function setupFumadocs(config: ProjectConfig) {
47
+ const { packageManager, projectDir } = config;
48
+
49
+ try {
50
+ log.info("Setting up Fumadocs...");
51
+
52
+ const template = await select<FumadocsTemplate>({
53
+ message: "Choose a template",
54
+ options: Object.entries(TEMPLATES).map(([key, template]) => ({
55
+ value: key as FumadocsTemplate,
56
+ label: template.label,
57
+ hint: template.hint,
58
+ })),
59
+ initialValue: "next-mdx",
60
+ });
61
+
62
+ if (isCancel(template)) return exitCancelled("Operation cancelled");
63
+
64
+ const templateArg = TEMPLATES[template].value;
65
+
66
+ const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --pm ${packageManager} --no-git`;
67
+
68
+ const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
69
+
70
+ const appsDir = path.join(projectDir, "apps");
71
+ await fs.ensureDir(appsDir);
72
+
73
+ const s = spinner();
74
+ s.start("Running Fumadocs create command...");
75
+
76
+ await execa(fumadocsInitCommand, {
77
+ cwd: appsDir,
78
+ env: { CI: "true" },
79
+ shell: true,
80
+ });
81
+
82
+ const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
83
+ const packageJsonPath = path.join(fumadocsDir, "package.json");
84
+
85
+ if (await fs.pathExists(packageJsonPath)) {
86
+ const packageJson = await fs.readJson(packageJsonPath);
87
+ packageJson.name = "fumadocs";
88
+
89
+ if (packageJson.scripts?.dev) {
90
+ packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
91
+ }
92
+
93
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
94
+ }
95
+
96
+ s.stop("Fumadocs setup complete!");
97
+ } catch (error) {
98
+ log.error(pc.red("Failed to set up Fumadocs"));
99
+ if (error instanceof Error) {
100
+ consola.error(pc.red(error.message));
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,139 @@
1
+ import path from "node:path";
2
+ import { autocompleteMultiselect, isCancel, log, spinner } from "@clack/prompts";
3
+ import { execa } from "execa";
4
+ import fs from "fs-extra";
5
+ import pc from "picocolors";
6
+ import type { ProjectConfig } from "../../types";
7
+ import { exitCancelled } from "../../utils/errors";
8
+ import { getPackageExecutionCommand } from "../../utils/package-runner";
9
+
10
+ export async function setupRuler(config: ProjectConfig) {
11
+ const { packageManager, projectDir } = config;
12
+
13
+ try {
14
+ log.info("Setting up Ruler...");
15
+
16
+ const rulerDir = path.join(projectDir, ".ruler");
17
+
18
+ if (!(await fs.pathExists(rulerDir))) {
19
+ log.error(
20
+ pc.red(
21
+ "Ruler template directory not found. Please ensure ruler addon is properly installed.",
22
+ ),
23
+ );
24
+ return;
25
+ }
26
+
27
+ const EDITORS = {
28
+ amp: { label: "AMP" },
29
+ copilot: { label: "GitHub Copilot" },
30
+ claude: { label: "Claude Code" },
31
+ codex: { label: "OpenAI Codex CLI" },
32
+ cursor: { label: "Cursor" },
33
+ windsurf: { label: "Windsurf" },
34
+ cline: { label: "Cline" },
35
+ aider: { label: "Aider" },
36
+ firebase: { label: "Firebase Studio" },
37
+ "gemini-cli": { label: "Gemini CLI" },
38
+ junie: { label: "Junie" },
39
+ kilocode: { label: "Kilo Code" },
40
+ opencode: { label: "OpenCode" },
41
+ crush: { label: "Crush" },
42
+ zed: { label: "Zed" },
43
+ qwen: { label: "Qwen" },
44
+ amazonqcli: { label: "Amazon Q CLI" },
45
+ augmentcode: { label: "AugmentCode" },
46
+ firebender: { label: "Firebender" },
47
+ goose: { label: "Goose" },
48
+ jules: { label: "Jules" },
49
+ kiro: { label: "Kiro" },
50
+ openhands: { label: "Open Hands" },
51
+ roo: { label: "RooCode" },
52
+ trae: { label: "Trae AI" },
53
+ warp: { label: "Warp" },
54
+ } as const;
55
+
56
+ const selectedEditors = await autocompleteMultiselect({
57
+ message: "Select AI assistants for Ruler",
58
+ options: Object.entries(EDITORS).map(([key, v]) => ({
59
+ value: key,
60
+ label: v.label,
61
+ })),
62
+ required: false,
63
+ });
64
+
65
+ if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
66
+
67
+ if (selectedEditors.length === 0) {
68
+ log.info("No AI assistants selected. To apply rules later, run:");
69
+ log.info(
70
+ pc.cyan(
71
+ `${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`,
72
+ ),
73
+ );
74
+ return;
75
+ }
76
+
77
+ const configFile = path.join(rulerDir, "ruler.toml");
78
+ const currentConfig = await fs.readFile(configFile, "utf-8");
79
+
80
+ let updatedConfig = currentConfig;
81
+
82
+ const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
83
+ updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
84
+
85
+ await fs.writeFile(configFile, updatedConfig);
86
+
87
+ await addRulerScriptToPackageJson(projectDir, packageManager);
88
+
89
+ const s = spinner();
90
+ s.start("Applying rules with Ruler...");
91
+
92
+ try {
93
+ const rulerApplyCmd = getPackageExecutionCommand(
94
+ packageManager,
95
+ `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`,
96
+ );
97
+ await execa(rulerApplyCmd, {
98
+ cwd: projectDir,
99
+ env: { CI: "true" },
100
+ shell: true,
101
+ });
102
+
103
+ s.stop("Applied rules with Ruler");
104
+ } catch {
105
+ s.stop(pc.red("Failed to apply rules"));
106
+ }
107
+ } catch (error) {
108
+ log.error(pc.red("Failed to set up Ruler"));
109
+ if (error instanceof Error) {
110
+ console.error(pc.red(error.message));
111
+ }
112
+ }
113
+ }
114
+
115
+ async function addRulerScriptToPackageJson(
116
+ projectDir: string,
117
+ packageManager: ProjectConfig["packageManager"],
118
+ ) {
119
+ const rootPackageJsonPath = path.join(projectDir, "package.json");
120
+
121
+ if (!(await fs.pathExists(rootPackageJsonPath))) {
122
+ log.warn("Root package.json not found, skipping ruler:apply script addition");
123
+ return;
124
+ }
125
+
126
+ const packageJson = await fs.readJson(rootPackageJsonPath);
127
+
128
+ if (!packageJson.scripts) {
129
+ packageJson.scripts = {};
130
+ }
131
+
132
+ const rulerApplyCommand = getPackageExecutionCommand(
133
+ packageManager,
134
+ "@intellectronica/ruler@latest apply --local-only",
135
+ );
136
+ packageJson.scripts["ruler:apply"] = rulerApplyCommand;
137
+
138
+ await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
139
+ }
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { spinner } from "@clack/prompts";
3
+ import consola from "consola";
4
+ import { execa } from "execa";
5
+ import fs from "fs-extra";
6
+ import pc from "picocolors";
7
+ import type { ProjectConfig } from "../../types";
8
+ import { getPackageExecutionCommand } from "../../utils/package-runner";
9
+
10
+ export async function setupStarlight(config: ProjectConfig) {
11
+ const { packageManager, projectDir } = config;
12
+ const s = spinner();
13
+
14
+ try {
15
+ s.start("Setting up Starlight docs...");
16
+
17
+ const starlightArgs = [
18
+ "docs",
19
+ "--template",
20
+ "starlight",
21
+ "--no-install",
22
+ "--add",
23
+ "tailwind",
24
+ "--no-git",
25
+ "--skip-houston",
26
+ ];
27
+ const starlightArgsString = starlightArgs.join(" ");
28
+
29
+ const commandWithArgs = `create-astro@latest ${starlightArgsString}`;
30
+
31
+ const starlightInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
32
+
33
+ const appsDir = path.join(projectDir, "apps");
34
+ await fs.ensureDir(appsDir);
35
+
36
+ await execa(starlightInitCommand, {
37
+ cwd: appsDir,
38
+ env: {
39
+ CI: "true",
40
+ },
41
+ shell: true,
42
+ });
43
+
44
+ s.stop("Starlight docs setup successfully!");
45
+ } catch (error) {
46
+ s.stop(pc.red("Failed to set up Starlight docs"));
47
+ if (error instanceof Error) {
48
+ consola.error(pc.red(error.message));
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,96 @@
1
+ import path from "node:path";
2
+ import { spinner } from "@clack/prompts";
3
+ import { consola } from "consola";
4
+ import { execa } from "execa";
5
+ import fs from "fs-extra";
6
+ import pc from "picocolors";
7
+ import type { ProjectConfig } from "../../types";
8
+ import { addPackageDependency } from "../../utils/add-package-deps";
9
+ import { getPackageExecutionCommand } from "../../utils/package-runner";
10
+
11
+ export async function setupTauri(config: ProjectConfig) {
12
+ const { packageManager, frontend, projectDir } = config;
13
+ const s = spinner();
14
+ const clientPackageDir = path.join(projectDir, "apps/web");
15
+
16
+ if (!(await fs.pathExists(clientPackageDir))) {
17
+ return;
18
+ }
19
+
20
+ try {
21
+ s.start("Setting up Tauri desktop app support...");
22
+
23
+ await addPackageDependency({
24
+ devDependencies: ["@tauri-apps/cli"],
25
+ projectDir: clientPackageDir,
26
+ });
27
+
28
+ const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
29
+ if (await fs.pathExists(clientPackageJsonPath)) {
30
+ const packageJson = await fs.readJson(clientPackageJsonPath);
31
+
32
+ packageJson.scripts = {
33
+ ...packageJson.scripts,
34
+ tauri: "tauri",
35
+ "desktop:dev": "tauri dev",
36
+ "desktop:build": "tauri build",
37
+ };
38
+
39
+ await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
40
+ }
41
+
42
+ const _hasTanstackRouter = frontend.includes("tanstack-router");
43
+ const hasReactRouter = frontend.includes("react-router");
44
+ const hasNuxt = frontend.includes("nuxt");
45
+ const hasSvelte = frontend.includes("svelte");
46
+ const _hasSolid = frontend.includes("solid");
47
+ const hasNext = frontend.includes("next");
48
+
49
+ const devUrl =
50
+ hasReactRouter || hasSvelte
51
+ ? "http://localhost:5173"
52
+ : hasNext
53
+ ? "http://localhost:3001"
54
+ : "http://localhost:3001";
55
+
56
+ const frontendDist = hasNuxt
57
+ ? "../.output/public"
58
+ : hasSvelte
59
+ ? "../build"
60
+ : hasNext
61
+ ? "../.next"
62
+ : hasReactRouter
63
+ ? "../build/client"
64
+ : "../dist";
65
+
66
+ const tauriArgs = [
67
+ "init",
68
+ `--app-name=${path.basename(projectDir)}`,
69
+ `--window-title=${path.basename(projectDir)}`,
70
+ `--frontend-dist=${frontendDist}`,
71
+ `--dev-url=${devUrl}`,
72
+ `--before-dev-command="${packageManager} run dev"`,
73
+ `--before-build-command="${packageManager} run build"`,
74
+ ];
75
+ const tauriArgsString = tauriArgs.join(" ");
76
+
77
+ const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
78
+
79
+ const tauriInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
80
+
81
+ await execa(tauriInitCommand, {
82
+ cwd: clientPackageDir,
83
+ env: {
84
+ CI: "true",
85
+ },
86
+ shell: true,
87
+ });
88
+
89
+ s.stop("Tauri desktop app support configured successfully!");
90
+ } catch (error) {
91
+ s.stop(pc.red("Failed to set up Tauri"));
92
+ if (error instanceof Error) {
93
+ consola.error(pc.red(error.message));
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,232 @@
1
+ import { autocompleteMultiselect, group, log, multiselect, spinner } from "@clack/prompts";
2
+ import { execa } from "execa";
3
+ import pc from "picocolors";
4
+ import type { ProjectConfig } from "../../types";
5
+ import { addPackageDependency } from "../../utils/add-package-deps";
6
+ import { exitCancelled } from "../../utils/errors";
7
+ import { getPackageExecutionCommand } from "../../utils/package-runner";
8
+ import { setupBiome } from "./addons-setup";
9
+
10
+ type UltraciteEditor = "vscode" | "zed";
11
+ type UltraciteAgent =
12
+ | "vscode-copilot"
13
+ | "cursor"
14
+ | "windsurf"
15
+ | "zed"
16
+ | "claude"
17
+ | "codex"
18
+ | "kiro"
19
+ | "cline"
20
+ | "amp"
21
+ | "aider"
22
+ | "firebase-studio"
23
+ | "open-hands"
24
+ | "gemini-cli"
25
+ | "junie"
26
+ | "augmentcode"
27
+ | "kilo-code"
28
+ | "goose"
29
+ | "roo-code";
30
+
31
+ type UltraciteHook = "cursor" | "claude";
32
+
33
+ const EDITORS = {
34
+ vscode: {
35
+ label: "VSCode / Cursor / Windsurf",
36
+ },
37
+ zed: {
38
+ label: "Zed",
39
+ },
40
+ } as const;
41
+
42
+ const AGENTS = {
43
+ "vscode-copilot": {
44
+ label: "VS Code Copilot",
45
+ },
46
+ cursor: {
47
+ label: "Cursor",
48
+ },
49
+ windsurf: {
50
+ label: "Windsurf",
51
+ },
52
+ zed: {
53
+ label: "Zed",
54
+ },
55
+ claude: {
56
+ label: "Claude",
57
+ },
58
+ codex: {
59
+ label: "Codex",
60
+ },
61
+ kiro: {
62
+ label: "Kiro",
63
+ },
64
+ cline: {
65
+ label: "Cline",
66
+ },
67
+ amp: {
68
+ label: "Amp",
69
+ },
70
+ aider: {
71
+ label: "Aider",
72
+ },
73
+ "firebase-studio": {
74
+ label: "Firebase Studio",
75
+ },
76
+ "open-hands": {
77
+ label: "Open Hands",
78
+ },
79
+ "gemini-cli": {
80
+ label: "Gemini CLI",
81
+ },
82
+ junie: {
83
+ label: "Junie",
84
+ },
85
+ augmentcode: {
86
+ label: "AugmentCode",
87
+ },
88
+ "kilo-code": {
89
+ label: "Kilo Code",
90
+ },
91
+ goose: {
92
+ label: "Goose",
93
+ },
94
+ "roo-code": {
95
+ label: "Roo Code",
96
+ },
97
+ } as const;
98
+
99
+ const HOOKS = {
100
+ cursor: {
101
+ label: "Cursor",
102
+ },
103
+ claude: {
104
+ label: "Claude",
105
+ },
106
+ } as const;
107
+
108
+ function getFrameworksFromFrontend(frontend: string[]): string[] {
109
+ const frameworkMap: Record<string, string> = {
110
+ "tanstack-router": "react",
111
+ "react-router": "react",
112
+ "tanstack-start": "react",
113
+ next: "next",
114
+ nuxt: "vue",
115
+ "native-bare": "react",
116
+ "native-uniwind": "react",
117
+ "native-unistyles": "react",
118
+ svelte: "svelte",
119
+ solid: "solid",
120
+ };
121
+
122
+ const frameworks = new Set<string>();
123
+
124
+ for (const f of frontend) {
125
+ if (f !== "none" && frameworkMap[f]) {
126
+ frameworks.add(frameworkMap[f]);
127
+ }
128
+ }
129
+
130
+ return Array.from(frameworks);
131
+ }
132
+
133
+ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
134
+ const { packageManager, projectDir, frontend } = config;
135
+
136
+ try {
137
+ log.info("Setting up Ultracite...");
138
+
139
+ await setupBiome(projectDir);
140
+
141
+ const result = await group(
142
+ {
143
+ editors: () =>
144
+ multiselect<UltraciteEditor>({
145
+ message: "Choose editors",
146
+ options: Object.entries(EDITORS).map(([key, editor]) => ({
147
+ value: key as UltraciteEditor,
148
+ label: editor.label,
149
+ })),
150
+ required: true,
151
+ }),
152
+ agents: () =>
153
+ autocompleteMultiselect<UltraciteAgent>({
154
+ message: "Choose agents",
155
+ options: Object.entries(AGENTS).map(([key, agent]) => ({
156
+ value: key as UltraciteAgent,
157
+ label: agent.label,
158
+ })),
159
+ required: true,
160
+ }),
161
+ hooks: () =>
162
+ autocompleteMultiselect<UltraciteHook>({
163
+ message: "Choose hooks",
164
+ options: Object.entries(HOOKS).map(([key, hook]) => ({
165
+ value: key as UltraciteHook,
166
+ label: hook.label,
167
+ })),
168
+ }),
169
+ },
170
+ {
171
+ onCancel: () => {
172
+ exitCancelled("Operation cancelled");
173
+ },
174
+ },
175
+ );
176
+
177
+ const editors = result.editors as UltraciteEditor[];
178
+ const agents = result.agents as UltraciteAgent[];
179
+ const hooks = result.hooks as UltraciteHook[];
180
+ const frameworks = getFrameworksFromFrontend(frontend);
181
+
182
+ const ultraciteArgs = ["init", "--pm", packageManager];
183
+
184
+ if (frameworks.length > 0) {
185
+ ultraciteArgs.push("--frameworks", ...frameworks);
186
+ }
187
+
188
+ if (editors.length > 0) {
189
+ ultraciteArgs.push("--editors", ...editors);
190
+ }
191
+
192
+ if (agents.length > 0) {
193
+ ultraciteArgs.push("--agents", ...agents);
194
+ }
195
+
196
+ if (hooks.length > 0) {
197
+ ultraciteArgs.push("--hooks", ...hooks);
198
+ }
199
+
200
+ if (hasHusky) {
201
+ ultraciteArgs.push("--integrations", "husky", "lint-staged");
202
+ }
203
+
204
+ const ultraciteArgsString = ultraciteArgs.join(" ");
205
+ const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
206
+
207
+ const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
208
+
209
+ const s = spinner();
210
+ s.start("Running Ultracite init command...");
211
+
212
+ await execa(ultraciteInitCommand, {
213
+ cwd: projectDir,
214
+ env: { CI: "true" },
215
+ shell: true,
216
+ });
217
+
218
+ if (hasHusky) {
219
+ await addPackageDependency({
220
+ devDependencies: ["husky", "lint-staged"],
221
+ projectDir,
222
+ });
223
+ }
224
+
225
+ s.stop("Ultracite setup successfully!");
226
+ } catch (error) {
227
+ log.error(pc.red("Failed to set up Ultracite"));
228
+ if (error instanceof Error) {
229
+ console.error(pc.red(error.message));
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,59 @@
1
+ import { type CallExpression, Node, type ObjectLiteralExpression, SyntaxKind } from "ts-morph";
2
+ import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
3
+
4
+ export async function addPwaToViteConfig(viteConfigPath: string, projectName: string) {
5
+ const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
6
+ if (!sourceFile) {
7
+ throw new Error("vite config not found");
8
+ }
9
+
10
+ const hasImport = sourceFile
11
+ .getImportDeclarations()
12
+ .some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa");
13
+
14
+ if (!hasImport) {
15
+ sourceFile.insertImportDeclaration(0, {
16
+ namedImports: ["VitePWA"],
17
+ moduleSpecifier: "vite-plugin-pwa",
18
+ });
19
+ }
20
+
21
+ const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
22
+ const expression = expr.getExpression();
23
+ return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
24
+ });
25
+
26
+ if (!defineCall) {
27
+ throw new Error("Could not find defineConfig call in vite config");
28
+ }
29
+
30
+ const callExpr = defineCall as CallExpression;
31
+ const configObject = callExpr.getArguments()[0] as ObjectLiteralExpression | undefined;
32
+ if (!configObject) {
33
+ throw new Error("defineConfig argument is not an object literal");
34
+ }
35
+
36
+ const pluginsArray = ensureArrayProperty(configObject, "plugins");
37
+
38
+ const alreadyPresent = pluginsArray
39
+ .getElements()
40
+ .some((el) => el.getText().startsWith("VitePWA("));
41
+
42
+ if (!alreadyPresent) {
43
+ pluginsArray.addElement(
44
+ `VitePWA({
45
+ registerType: "autoUpdate",
46
+ manifest: {
47
+ name: "${projectName}",
48
+ short_name: "${projectName}",
49
+ description: "${projectName} - PWA Application",
50
+ theme_color: "#0c0c0c",
51
+ },
52
+ pwaAssets: { disabled: false, config: true },
53
+ devOptions: { enabled: true },
54
+ })`,
55
+ );
56
+ }
57
+
58
+ await tsProject.save();
59
+ }