create-better-fullstack 1.5.1 → 1.5.3

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.
@@ -0,0 +1,1373 @@
1
+ #!/usr/bin/env node
2
+ import { s as dependencyVersionMap, t as readBtsConfig } from "./bts-config-BYD8mHt-.mjs";
3
+ import { autocompleteMultiselect, cancel, group, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
4
+ import pc from "picocolors";
5
+ import fs from "fs-extra";
6
+ import path from "node:path";
7
+ import { getLocalWebDevPort } from "@better-fullstack/types";
8
+ import consola, { consola as consola$1 } from "consola";
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ import { $ } from "execa";
11
+
12
+ //#region src/utils/context.ts
13
+ const cliStorage = new AsyncLocalStorage();
14
+ function defaultContext() {
15
+ return {
16
+ navigation: {
17
+ isFirstPrompt: false,
18
+ lastPromptShownUI: false
19
+ },
20
+ silent: false,
21
+ verbose: false
22
+ };
23
+ }
24
+ function getContext() {
25
+ const ctx = cliStorage.getStore();
26
+ if (!ctx) return defaultContext();
27
+ return ctx;
28
+ }
29
+ function tryGetContext() {
30
+ return cliStorage.getStore();
31
+ }
32
+ function isSilent() {
33
+ return getContext().silent;
34
+ }
35
+ function isFirstPrompt() {
36
+ return getContext().navigation.isFirstPrompt;
37
+ }
38
+ function didLastPromptShowUI() {
39
+ return getContext().navigation.lastPromptShownUI;
40
+ }
41
+ function setIsFirstPrompt(value) {
42
+ const ctx = tryGetContext();
43
+ if (ctx) ctx.navigation.isFirstPrompt = value;
44
+ }
45
+ function setLastPromptShownUI(value) {
46
+ const ctx = tryGetContext();
47
+ if (ctx) ctx.navigation.lastPromptShownUI = value;
48
+ }
49
+ async function runWithContextAsync(options, fn) {
50
+ const ctx = {
51
+ navigation: {
52
+ isFirstPrompt: false,
53
+ lastPromptShownUI: false
54
+ },
55
+ silent: options.silent ?? false,
56
+ verbose: options.verbose ?? false,
57
+ projectDir: options.projectDir,
58
+ projectName: options.projectName,
59
+ packageManager: options.packageManager
60
+ };
61
+ return cliStorage.run(ctx, fn);
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/utils/errors.ts
66
+ var UserCancelledError = class extends Error {
67
+ constructor(message = "Operation cancelled") {
68
+ super(message);
69
+ this.name = "UserCancelledError";
70
+ }
71
+ };
72
+ var CLIError = class extends Error {
73
+ constructor(message) {
74
+ super(message);
75
+ this.name = "CLIError";
76
+ }
77
+ };
78
+ function exitWithError(message) {
79
+ if (isSilent()) throw new CLIError(message);
80
+ consola.error(pc.red(message));
81
+ process.exit(1);
82
+ }
83
+ function exitCancelled(message = "Operation cancelled") {
84
+ if (isSilent()) throw new UserCancelledError(message);
85
+ cancel(pc.red(message));
86
+ process.exit(1);
87
+ }
88
+ function handleError(error, fallbackMessage) {
89
+ const message = error instanceof Error ? error.message : fallbackMessage || String(error);
90
+ if (isSilent()) throw error instanceof Error ? error : new Error(message);
91
+ consola.error(pc.red(message));
92
+ process.exit(1);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/utils/add-package-deps.ts
97
+ const addPackageDependency = async (opts) => {
98
+ const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
99
+ const pkgJsonPath = path.join(projectDir, "package.json");
100
+ const pkgJson = await fs.readJson(pkgJsonPath);
101
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
102
+ if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
103
+ for (const pkgName of dependencies) {
104
+ const version = dependencyVersionMap[pkgName];
105
+ if (version) pkgJson.dependencies[pkgName] = version;
106
+ else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
107
+ }
108
+ for (const pkgName of devDependencies) {
109
+ const version = dependencyVersionMap[pkgName];
110
+ if (version) pkgJson.devDependencies[pkgName] = version;
111
+ else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
112
+ }
113
+ for (const [pkgName, version] of Object.entries(customDependencies)) pkgJson.dependencies[pkgName] = version;
114
+ for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
115
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
116
+ };
117
+
118
+ //#endregion
119
+ //#region src/utils/package-runner.ts
120
+ /**
121
+ * Returns the appropriate command for running a package without installing it globally,
122
+ * based on the selected package manager.
123
+ *
124
+ * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
125
+ * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
126
+ * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
127
+ */
128
+ function getPackageExecutionCommand(packageManager, commandWithArgs) {
129
+ switch (packageManager) {
130
+ case "pnpm": return `pnpm dlx ${commandWithArgs}`;
131
+ case "bun": return `bunx ${commandWithArgs}`;
132
+ case "yarn": return `yarn dlx ${commandWithArgs}`;
133
+ default: return `npx ${commandWithArgs}`;
134
+ }
135
+ }
136
+ /**
137
+ * Returns the command and arguments as an array for use with execa's $ template syntax.
138
+ * This avoids the need for shell: true and provides better escaping.
139
+ *
140
+ * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
141
+ * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate").
142
+ * @returns An array of [command, ...args] (e.g., ["npx", "prisma", "generate"]).
143
+ */
144
+ function getPackageExecutionArgs(packageManager, commandWithArgs) {
145
+ const args = commandWithArgs.split(" ");
146
+ switch (packageManager) {
147
+ case "pnpm": return [
148
+ "pnpm",
149
+ "dlx",
150
+ ...args
151
+ ];
152
+ case "bun": return ["bunx", ...args];
153
+ case "yarn": return [
154
+ "yarn",
155
+ "dlx",
156
+ ...args
157
+ ];
158
+ default: return ["npx", ...args];
159
+ }
160
+ }
161
+ /**
162
+ * Returns just the runner prefix as an array, for when you already have args built.
163
+ * Use this when you have complex arguments that shouldn't be split by spaces.
164
+ *
165
+ * @param packageManager - The selected package manager.
166
+ * @returns The runner prefix as an array (e.g., ["npx"] or ["pnpm", "dlx"]).
167
+ *
168
+ * @example
169
+ * const prefix = getPackageRunnerPrefix("bun");
170
+ * const args = ["@tauri-apps/cli@latest", "init", "--app-name=foo"];
171
+ * await $`${[...prefix, ...args]}`;
172
+ */
173
+ function getPackageRunnerPrefix(packageManager) {
174
+ switch (packageManager) {
175
+ case "pnpm": return ["pnpm", "dlx"];
176
+ case "bun": return ["bunx"];
177
+ case "yarn": return ["yarn", "dlx"];
178
+ default: return ["npx"];
179
+ }
180
+ }
181
+
182
+ //#endregion
183
+ //#region src/helpers/addons/fumadocs-setup.ts
184
+ const TEMPLATES$2 = {
185
+ "next-mdx": {
186
+ label: "Next.js: Fumadocs MDX",
187
+ hint: "Recommended template with MDX support",
188
+ value: "+next+fuma-docs-mdx"
189
+ },
190
+ waku: {
191
+ label: "Waku: Content Collections",
192
+ hint: "Template using Waku with content collections",
193
+ value: "waku"
194
+ },
195
+ "react-router": {
196
+ label: "React Router: MDX Remote",
197
+ hint: "Template for React Router with MDX remote",
198
+ value: "react-router"
199
+ },
200
+ "react-router-spa": {
201
+ label: "React Router: SPA",
202
+ hint: "Template for React Router SPA",
203
+ value: "react-router-spa"
204
+ },
205
+ "tanstack-start": {
206
+ label: "Tanstack Start: MDX Remote",
207
+ hint: "Template for Tanstack Start with MDX remote",
208
+ value: "tanstack-start"
209
+ }
210
+ };
211
+ async function setupFumadocs(config) {
212
+ const { packageManager, projectDir } = config;
213
+ try {
214
+ log.info("Setting up Fumadocs...");
215
+ const template = await select({
216
+ message: "Choose a template",
217
+ options: Object.entries(TEMPLATES$2).map(([key, template$1]) => ({
218
+ value: key,
219
+ label: template$1.label,
220
+ hint: template$1.hint
221
+ })),
222
+ initialValue: "next-mdx"
223
+ });
224
+ if (isCancel(template)) return exitCancelled("Operation cancelled");
225
+ const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs --template ${TEMPLATES$2[template].value} --src --pm ${packageManager} --no-git`);
226
+ const appsDir = path.join(projectDir, "apps");
227
+ await fs.ensureDir(appsDir);
228
+ const s = spinner();
229
+ s.start("Running Fumadocs create command...");
230
+ await $({
231
+ cwd: appsDir,
232
+ env: { CI: "true" }
233
+ })`${args}`;
234
+ const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
235
+ const packageJsonPath = path.join(fumadocsDir, "package.json");
236
+ if (await fs.pathExists(packageJsonPath)) {
237
+ const packageJson = await fs.readJson(packageJsonPath);
238
+ packageJson.name = "fumadocs";
239
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
240
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
241
+ }
242
+ s.stop("Fumadocs setup complete!");
243
+ } catch (error) {
244
+ log.error(pc.red("Failed to set up Fumadocs"));
245
+ if (error instanceof Error) consola.error(pc.red(error.message));
246
+ }
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/utils/external-commands.ts
251
+ function shouldSkipExternalCommands() {
252
+ const value = process.env.BFS_SKIP_EXTERNAL_COMMANDS;
253
+ if (!value) return false;
254
+ const normalized = value.toLowerCase().trim();
255
+ return normalized === "1" || normalized === "true" || normalized === "yes";
256
+ }
257
+
258
+ //#endregion
259
+ //#region src/helpers/addons/retry-install.ts
260
+ const DEFAULT_MAX_ATTEMPTS = 3;
261
+ const DEFAULT_INITIAL_DELAY_MS = 750;
262
+ const RETRY_BACKOFF_MULTIPLIER = 2;
263
+ function defaultSleep(ms) {
264
+ return new Promise((resolve) => setTimeout(resolve, ms));
265
+ }
266
+ function formatDelay(ms) {
267
+ return ms >= 1e3 ? `${(ms / 1e3).toFixed(ms % 1e3 === 0 ? 0 : 1)}s` : `${ms}ms`;
268
+ }
269
+ async function runInstallWithRetries({ description, run, maxAttempts = DEFAULT_MAX_ATTEMPTS, initialDelayMs = DEFAULT_INITIAL_DELAY_MS, sleep = defaultSleep }) {
270
+ let delayMs = initialDelayMs;
271
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
272
+ await run();
273
+ return true;
274
+ } catch (error) {
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ if (attempt === maxAttempts) {
277
+ log.warn(pc.yellow(`Warning: Could not ${description} after ${maxAttempts} attempts: ${message}`));
278
+ return false;
279
+ }
280
+ log.warn(pc.yellow(`Warning: Failed to ${description} (attempt ${attempt}/${maxAttempts}): ${message}. Retrying in ${formatDelay(delayMs)}...`));
281
+ await sleep(delayMs);
282
+ delayMs *= RETRY_BACKOFF_MULTIPLIER;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ //#endregion
288
+ //#region src/helpers/addons/mcp-setup.ts
289
+ const MCP_AGENTS = [
290
+ {
291
+ value: "cursor",
292
+ label: "Cursor",
293
+ scope: "both"
294
+ },
295
+ {
296
+ value: "claude-code",
297
+ label: "Claude Code",
298
+ scope: "both"
299
+ },
300
+ {
301
+ value: "codex",
302
+ label: "Codex",
303
+ scope: "both"
304
+ },
305
+ {
306
+ value: "opencode",
307
+ label: "OpenCode",
308
+ scope: "both"
309
+ },
310
+ {
311
+ value: "gemini-cli",
312
+ label: "Gemini CLI",
313
+ scope: "both"
314
+ },
315
+ {
316
+ value: "vscode",
317
+ label: "VS Code (GitHub Copilot)",
318
+ scope: "both"
319
+ },
320
+ {
321
+ value: "zed",
322
+ label: "Zed",
323
+ scope: "both"
324
+ },
325
+ {
326
+ value: "claude-desktop",
327
+ label: "Claude Desktop",
328
+ scope: "global"
329
+ },
330
+ {
331
+ value: "goose",
332
+ label: "Goose",
333
+ scope: "global"
334
+ }
335
+ ];
336
+ function uniqueValues$1(values) {
337
+ return Array.from(new Set(values));
338
+ }
339
+ function hasReactBasedFrontend$1(frontend) {
340
+ return frontend.includes("react-router") || frontend.includes("react-vite") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
341
+ }
342
+ function hasNativeFrontend$1(frontend) {
343
+ return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
344
+ }
345
+ function getRecommendedMcpServers(config) {
346
+ const servers = [];
347
+ servers.push({
348
+ key: "context7",
349
+ label: "Context7",
350
+ name: "context7",
351
+ target: "@upstash/context7-mcp"
352
+ });
353
+ if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") servers.push({
354
+ key: "cloudflare-docs",
355
+ label: "Cloudflare Docs",
356
+ name: "cloudflare-docs",
357
+ target: "https://docs.mcp.cloudflare.com/sse",
358
+ transport: "sse"
359
+ });
360
+ if (config.backend === "convex") servers.push({
361
+ key: "convex",
362
+ label: "Convex",
363
+ name: "convex",
364
+ target: "npx -y convex@latest mcp start"
365
+ });
366
+ if (hasReactBasedFrontend$1(config.frontend)) servers.push({
367
+ key: "shadcn",
368
+ label: "shadcn/ui",
369
+ name: "shadcn",
370
+ target: "npx -y shadcn@latest mcp"
371
+ });
372
+ if (config.frontend.includes("next")) servers.push({
373
+ key: "next-devtools",
374
+ label: "Next Devtools",
375
+ name: "next-devtools",
376
+ target: "npx -y next-devtools-mcp@latest"
377
+ });
378
+ if (config.frontend.includes("nuxt")) servers.push({
379
+ key: "nuxt-docs",
380
+ label: "Nuxt Docs",
381
+ name: "nuxt",
382
+ target: "https://nuxt.com/mcp"
383
+ }, {
384
+ key: "nuxt-ui-docs",
385
+ label: "Nuxt UI Docs",
386
+ name: "nuxt-ui",
387
+ target: "https://ui.nuxt.com/mcp"
388
+ });
389
+ if (config.frontend.includes("svelte")) servers.push({
390
+ key: "svelte-docs",
391
+ label: "Svelte Docs",
392
+ name: "svelte",
393
+ target: "https://mcp.svelte.dev/mcp"
394
+ });
395
+ if (config.frontend.includes("astro")) servers.push({
396
+ key: "astro-docs",
397
+ label: "Astro Docs",
398
+ name: "astro-docs",
399
+ target: "https://mcp.docs.astro.build/mcp"
400
+ });
401
+ if (config.dbSetup === "planetscale") servers.push({
402
+ key: "planetscale",
403
+ label: "PlanetScale",
404
+ name: "planetscale",
405
+ target: "https://mcp.pscale.dev/mcp/planetscale"
406
+ });
407
+ if (config.dbSetup === "neon") servers.push({
408
+ key: "neon",
409
+ label: "Neon",
410
+ name: "neon",
411
+ target: "https://mcp.neon.tech/mcp"
412
+ });
413
+ if (config.dbSetup === "supabase") servers.push({
414
+ key: "supabase",
415
+ label: "Supabase",
416
+ name: "supabase",
417
+ target: "https://mcp.supabase.com/mcp"
418
+ });
419
+ if (config.auth === "better-auth") servers.push({
420
+ key: "better-auth",
421
+ label: "Better Auth",
422
+ name: "better-auth",
423
+ target: "https://mcp.inkeep.com/better-auth/mcp"
424
+ });
425
+ if (config.auth === "clerk") servers.push({
426
+ key: "clerk",
427
+ label: "Clerk",
428
+ name: "clerk",
429
+ target: "https://mcp.clerk.com/mcp"
430
+ });
431
+ if (hasNativeFrontend$1(config.frontend)) servers.push({
432
+ key: "expo",
433
+ label: "Expo",
434
+ name: "expo-mcp",
435
+ target: "https://mcp.expo.dev/mcp"
436
+ });
437
+ if (config.payments === "polar") servers.push({
438
+ key: "polar",
439
+ label: "Polar",
440
+ name: "polar",
441
+ target: "https://mcp.polar.sh/mcp/polar-mcp"
442
+ });
443
+ return servers;
444
+ }
445
+ function filterAgentsForScope(scope) {
446
+ return MCP_AGENTS.filter((a) => a.scope === "both" || a.scope === scope);
447
+ }
448
+ async function setupMcp(config) {
449
+ if (shouldSkipExternalCommands()) return;
450
+ const { packageManager, projectDir } = config;
451
+ log.info("Setting up MCP servers...");
452
+ const scope = await select({
453
+ message: "Where should MCP servers be installed?",
454
+ options: [{
455
+ value: "project",
456
+ label: "Project",
457
+ hint: "Writes to project config files (recommended for teams)"
458
+ }, {
459
+ value: "global",
460
+ label: "Global",
461
+ hint: "Writes to user-level config files (personal machine)"
462
+ }],
463
+ initialValue: "project"
464
+ });
465
+ if (isCancel(scope)) return;
466
+ const recommendedServers = getRecommendedMcpServers(config);
467
+ if (recommendedServers.length === 0) return;
468
+ const selectedServerKeys = await multiselect({
469
+ message: "Select MCP servers to install",
470
+ options: recommendedServers.map((server) => ({
471
+ value: server.key,
472
+ label: server.label,
473
+ hint: server.target
474
+ })),
475
+ required: false,
476
+ initialValues: recommendedServers.map((server) => server.key)
477
+ });
478
+ if (isCancel(selectedServerKeys) || selectedServerKeys.length === 0) return;
479
+ const agentOptions = filterAgentsForScope(scope).map((a) => ({
480
+ value: a.value,
481
+ label: a.label
482
+ }));
483
+ const selectedAgents = await multiselect({
484
+ message: "Select agents to install MCP servers to",
485
+ options: agentOptions,
486
+ required: false,
487
+ initialValues: uniqueValues$1([
488
+ "cursor",
489
+ "claude-code",
490
+ "vscode"
491
+ ].filter((agent) => agentOptions.some((option) => option.value === agent)))
492
+ });
493
+ if (isCancel(selectedAgents) || selectedAgents.length === 0) return;
494
+ const serversByKey = new Map(recommendedServers.map((server) => [server.key, server]));
495
+ const selectedServers = [];
496
+ for (const key of selectedServerKeys) {
497
+ const server = serversByKey.get(key);
498
+ if (server) selectedServers.push(server);
499
+ }
500
+ if (selectedServers.length === 0) return;
501
+ const installSpinner = spinner();
502
+ installSpinner.start("Installing MCP servers...");
503
+ const runner = getPackageRunnerPrefix(packageManager);
504
+ const globalFlags = scope === "global" ? ["-g"] : [];
505
+ for (const server of selectedServers) {
506
+ const transportFlags = server.transport ? ["-t", server.transport] : [];
507
+ const headerFlags = (server.headers ?? []).flatMap((header) => ["--header", header]);
508
+ const agentFlags = selectedAgents.flatMap((agent) => ["-a", agent]);
509
+ const args = [
510
+ ...runner,
511
+ "add-mcp@latest",
512
+ server.target,
513
+ "--name",
514
+ server.name,
515
+ ...transportFlags,
516
+ ...headerFlags,
517
+ ...agentFlags,
518
+ ...globalFlags,
519
+ "-y"
520
+ ];
521
+ await runInstallWithRetries({
522
+ description: `install MCP server '${server.name}'`,
523
+ run: async () => {
524
+ await $({
525
+ cwd: projectDir,
526
+ env: { CI: "true" }
527
+ })`${args}`;
528
+ }
529
+ });
530
+ }
531
+ installSpinner.stop("MCP servers installed");
532
+ }
533
+
534
+ //#endregion
535
+ //#region src/helpers/addons/oxlint-setup.ts
536
+ async function setupOxlint(projectDir, packageManager) {
537
+ await addPackageDependency({
538
+ devDependencies: ["oxlint", "oxfmt"],
539
+ projectDir
540
+ });
541
+ const packageJsonPath = path.join(projectDir, "package.json");
542
+ if (await fs.pathExists(packageJsonPath)) {
543
+ const packageJson = await fs.readJson(packageJsonPath);
544
+ packageJson.scripts = {
545
+ ...packageJson.scripts,
546
+ check: "oxlint && oxfmt --write"
547
+ };
548
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
549
+ }
550
+ const s = spinner();
551
+ const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
552
+ s.start("Initializing oxlint and oxfmt...");
553
+ await $({
554
+ cwd: projectDir,
555
+ env: { CI: "true" }
556
+ })`${oxlintArgs}`;
557
+ const oxfmtArgs = getPackageExecutionArgs(packageManager, "oxfmt@latest --init");
558
+ await $({
559
+ cwd: projectDir,
560
+ env: { CI: "true" }
561
+ })`${oxfmtArgs}`;
562
+ s.stop("oxlint and oxfmt initialized successfully!");
563
+ }
564
+
565
+ //#endregion
566
+ //#region src/helpers/addons/ruler-setup.ts
567
+ async function setupRuler(config) {
568
+ const { packageManager, projectDir } = config;
569
+ try {
570
+ log.info("Setting up Ruler...");
571
+ const rulerDir = path.join(projectDir, ".ruler");
572
+ if (!await fs.pathExists(rulerDir)) {
573
+ log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
574
+ return;
575
+ }
576
+ const selectedEditors = await autocompleteMultiselect({
577
+ message: "Select AI assistants for Ruler",
578
+ options: Object.entries({
579
+ agentsmd: { label: "Agents.md" },
580
+ aider: { label: "Aider" },
581
+ amazonqcli: { label: "Amazon Q CLI" },
582
+ amp: { label: "AMP" },
583
+ antigravity: { label: "Antigravity" },
584
+ augmentcode: { label: "AugmentCode" },
585
+ claude: { label: "Claude Code" },
586
+ cline: { label: "Cline" },
587
+ codex: { label: "OpenAI Codex CLI" },
588
+ copilot: { label: "GitHub Copilot" },
589
+ crush: { label: "Crush" },
590
+ cursor: { label: "Cursor" },
591
+ firebase: { label: "Firebase Studio" },
592
+ firebender: { label: "Firebender" },
593
+ "gemini-cli": { label: "Gemini CLI" },
594
+ goose: { label: "Goose" },
595
+ jules: { label: "Jules" },
596
+ junie: { label: "Junie" },
597
+ kilocode: { label: "Kilo Code" },
598
+ kiro: { label: "Kiro" },
599
+ mistral: { label: "Mistral" },
600
+ opencode: { label: "OpenCode" },
601
+ openhands: { label: "Open Hands" },
602
+ qwen: { label: "Qwen" },
603
+ roo: { label: "RooCode" },
604
+ trae: { label: "Trae AI" },
605
+ warp: { label: "Warp" },
606
+ windsurf: { label: "Windsurf" },
607
+ zed: { label: "Zed" }
608
+ }).map(([key, v]) => ({
609
+ value: key,
610
+ label: v.label
611
+ })),
612
+ required: false
613
+ });
614
+ if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
615
+ if (selectedEditors.length === 0) {
616
+ log.info("No AI assistants selected. To apply rules later, run:");
617
+ log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
618
+ return;
619
+ }
620
+ const configFile = path.join(rulerDir, "ruler.toml");
621
+ let updatedConfig = await fs.readFile(configFile, "utf-8");
622
+ const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
623
+ updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
624
+ await fs.writeFile(configFile, updatedConfig);
625
+ await addRulerScriptToPackageJson(projectDir, packageManager);
626
+ const s = spinner();
627
+ s.start("Applying rules with Ruler...");
628
+ try {
629
+ const rulerApplyArgs = getPackageExecutionArgs(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
630
+ await $({
631
+ cwd: projectDir,
632
+ env: { CI: "true" }
633
+ })`${rulerApplyArgs}`;
634
+ s.stop("Applied rules with Ruler");
635
+ } catch {
636
+ s.stop(pc.red("Failed to apply rules"));
637
+ }
638
+ } catch (error) {
639
+ log.error(pc.red("Failed to set up Ruler"));
640
+ if (error instanceof Error) console.error(pc.red(error.message));
641
+ }
642
+ }
643
+ async function addRulerScriptToPackageJson(projectDir, packageManager) {
644
+ const rootPackageJsonPath = path.join(projectDir, "package.json");
645
+ if (!await fs.pathExists(rootPackageJsonPath)) {
646
+ log.warn("Root package.json not found, skipping ruler:apply script addition");
647
+ return;
648
+ }
649
+ const packageJson = await fs.readJson(rootPackageJsonPath);
650
+ if (!packageJson.scripts) packageJson.scripts = {};
651
+ const rulerApplyCommand = getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only");
652
+ packageJson.scripts["ruler:apply"] = rulerApplyCommand;
653
+ await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
654
+ }
655
+
656
+ //#endregion
657
+ //#region src/helpers/addons/skills-setup.ts
658
+ const SKILL_SOURCES = {
659
+ "vercel-labs/agent-skills": { label: "Vercel Agent Skills" },
660
+ "vercel/ai": { label: "Vercel AI SDK" },
661
+ "vercel/turborepo": { label: "Turborepo" },
662
+ "yusukebe/hono-skill": { label: "Hono Backend" },
663
+ "vercel-labs/next-skills": { label: "Next.js Best Practices" },
664
+ "nuxt/ui": { label: "Nuxt UI" },
665
+ "heroui-inc/heroui": { label: "HeroUI Native" },
666
+ "better-auth/skills": { label: "Better Auth" },
667
+ "neondatabase/agent-skills": { label: "Neon Database" },
668
+ "supabase/agent-skills": { label: "Supabase" },
669
+ "planetscale/database-skills": { label: "PlanetScale" },
670
+ "expo/skills": { label: "Expo" },
671
+ "prisma/skills": { label: "Prisma" },
672
+ "elysiajs/skills": { label: "ElysiaJS" },
673
+ "waynesutton/convexskills": { label: "Convex" },
674
+ "msmps/opentui-skill": { label: "OpenTUI Platform" },
675
+ "haydenbleasel/ultracite": { label: "Ultracite" }
676
+ };
677
+ const AVAILABLE_AGENTS = [
678
+ {
679
+ value: "cursor",
680
+ label: "Cursor"
681
+ },
682
+ {
683
+ value: "claude-code",
684
+ label: "Claude Code"
685
+ },
686
+ {
687
+ value: "cline",
688
+ label: "Cline"
689
+ },
690
+ {
691
+ value: "github-copilot",
692
+ label: "GitHub Copilot"
693
+ },
694
+ {
695
+ value: "codex",
696
+ label: "Codex"
697
+ },
698
+ {
699
+ value: "opencode",
700
+ label: "OpenCode"
701
+ },
702
+ {
703
+ value: "windsurf",
704
+ label: "Windsurf"
705
+ },
706
+ {
707
+ value: "goose",
708
+ label: "Goose"
709
+ },
710
+ {
711
+ value: "roo",
712
+ label: "Roo Code"
713
+ },
714
+ {
715
+ value: "kilo",
716
+ label: "Kilo Code"
717
+ },
718
+ {
719
+ value: "gemini-cli",
720
+ label: "Gemini CLI"
721
+ },
722
+ {
723
+ value: "antigravity",
724
+ label: "Antigravity"
725
+ },
726
+ {
727
+ value: "openhands",
728
+ label: "OpenHands"
729
+ },
730
+ {
731
+ value: "trae",
732
+ label: "Trae"
733
+ },
734
+ {
735
+ value: "amp",
736
+ label: "Amp"
737
+ },
738
+ {
739
+ value: "pi",
740
+ label: "Pi"
741
+ },
742
+ {
743
+ value: "qoder",
744
+ label: "Qoder"
745
+ },
746
+ {
747
+ value: "qwen-code",
748
+ label: "Qwen Code"
749
+ },
750
+ {
751
+ value: "kiro-cli",
752
+ label: "Kiro CLI"
753
+ },
754
+ {
755
+ value: "droid",
756
+ label: "Droid"
757
+ },
758
+ {
759
+ value: "command-code",
760
+ label: "Command Code"
761
+ },
762
+ {
763
+ value: "clawdbot",
764
+ label: "Clawdbot"
765
+ },
766
+ {
767
+ value: "zencoder",
768
+ label: "Zencoder"
769
+ },
770
+ {
771
+ value: "neovate",
772
+ label: "Neovate"
773
+ },
774
+ {
775
+ value: "mcpjam",
776
+ label: "MCPJam"
777
+ }
778
+ ];
779
+ function hasReactBasedFrontend(frontend) {
780
+ return frontend.includes("react-router") || frontend.includes("react-vite") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
781
+ }
782
+ function hasNativeFrontend(frontend) {
783
+ return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
784
+ }
785
+ function uniqueValues(values) {
786
+ return Array.from(new Set(values));
787
+ }
788
+ function getRecommendedSourceKeys(config) {
789
+ const sources = [];
790
+ const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
791
+ if (hasReactBasedFrontend(frontend)) sources.push("vercel-labs/agent-skills");
792
+ if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
793
+ if (frontend.includes("nuxt")) sources.push("nuxt/ui");
794
+ if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
795
+ if (hasNativeFrontend(frontend)) sources.push("expo/skills");
796
+ if (auth === "better-auth") sources.push("better-auth/skills");
797
+ if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
798
+ if (dbSetup === "supabase") sources.push("supabase/agent-skills");
799
+ if (dbSetup === "planetscale") sources.push("planetscale/database-skills");
800
+ if (orm === "prisma" || dbSetup === "prisma-postgres") sources.push("prisma/skills");
801
+ if (examples.includes("ai")) sources.push("vercel/ai");
802
+ if (addons.includes("turborepo")) sources.push("vercel/turborepo");
803
+ if (backend === "hono") sources.push("yusukebe/hono-skill");
804
+ if (backend === "elysia") sources.push("elysiajs/skills");
805
+ if (backend === "convex") sources.push("waynesutton/convexskills");
806
+ if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
807
+ if (addons.includes("ultracite")) sources.push("haydenbleasel/ultracite");
808
+ return sources;
809
+ }
810
+ const CURATED_SKILLS_BY_SOURCE = {
811
+ "vercel-labs/agent-skills": (config) => {
812
+ const skills = [
813
+ "web-design-guidelines",
814
+ "vercel-composition-patterns",
815
+ "vercel-react-best-practices"
816
+ ];
817
+ if (hasNativeFrontend(config.frontend)) skills.push("vercel-react-native-skills");
818
+ return skills;
819
+ },
820
+ "vercel/ai": () => ["ai-sdk"],
821
+ "vercel/turborepo": () => ["turborepo"],
822
+ "yusukebe/hono-skill": () => ["hono"],
823
+ "vercel-labs/next-skills": () => ["next-best-practices", "next-cache-components"],
824
+ "nuxt/ui": () => ["nuxt-ui"],
825
+ "heroui-inc/heroui": () => ["heroui-native"],
826
+ "better-auth/skills": () => ["better-auth-best-practices"],
827
+ "neondatabase/agent-skills": () => ["neon-postgres"],
828
+ "supabase/agent-skills": () => ["supabase-postgres-best-practices"],
829
+ "planetscale/database-skills": (config) => {
830
+ if (config.dbSetup !== "planetscale") return [];
831
+ if (config.database === "postgres") return ["postgres", "neki"];
832
+ if (config.database === "mysql") return ["mysql", "vitess"];
833
+ return [];
834
+ },
835
+ "expo/skills": (config) => {
836
+ const skills = [
837
+ "expo-dev-client",
838
+ "building-native-ui",
839
+ "native-data-fetching",
840
+ "expo-deployment",
841
+ "upgrading-expo",
842
+ "expo-cicd-workflows"
843
+ ];
844
+ if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
845
+ return skills;
846
+ },
847
+ "prisma/skills": (config) => {
848
+ const skills = [];
849
+ if (config.orm === "prisma") skills.push("prisma-cli", "prisma-client-api", "prisma-database-setup");
850
+ if (config.dbSetup === "prisma-postgres") skills.push("prisma-postgres");
851
+ return skills;
852
+ },
853
+ "elysiajs/skills": () => ["elysiajs"],
854
+ "waynesutton/convexskills": () => [
855
+ "convex-best-practices",
856
+ "convex-functions",
857
+ "convex-schema-validator",
858
+ "convex-realtime",
859
+ "convex-http-actions",
860
+ "convex-cron-jobs",
861
+ "convex-file-storage",
862
+ "convex-migrations",
863
+ "convex-security-check"
864
+ ],
865
+ "msmps/opentui-skill": () => ["opentui"],
866
+ "haydenbleasel/ultracite": () => ["ultracite"]
867
+ };
868
+ function getCuratedSkillNamesForSourceKey(sourceKey, config) {
869
+ return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
870
+ }
871
+ async function setupSkills(config) {
872
+ if (shouldSkipExternalCommands()) return;
873
+ const { packageManager, projectDir } = config;
874
+ const btsConfig = await readBtsConfig(projectDir);
875
+ const fullConfig = btsConfig ? {
876
+ ...config,
877
+ addons: btsConfig.addons ?? config.addons
878
+ } : config;
879
+ const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
880
+ if (recommendedSourceKeys.length === 0) return;
881
+ const skillOptions = uniqueValues(recommendedSourceKeys).flatMap((sourceKey) => {
882
+ const source = SKILL_SOURCES[sourceKey];
883
+ return getCuratedSkillNamesForSourceKey(sourceKey, fullConfig).map((skillName) => ({
884
+ value: `${sourceKey}::${skillName}`,
885
+ label: skillName,
886
+ hint: source.label
887
+ }));
888
+ });
889
+ if (skillOptions.length === 0) return;
890
+ const scope = await select({
891
+ message: "Where should skills be installed?",
892
+ options: [{
893
+ value: "project",
894
+ label: "Project",
895
+ hint: "Writes to project config files (recommended for teams)"
896
+ }, {
897
+ value: "global",
898
+ label: "Global",
899
+ hint: "Writes to user-level config files (personal machine)"
900
+ }],
901
+ initialValue: "project"
902
+ });
903
+ if (isCancel(scope)) return;
904
+ const selectedSkills = await multiselect({
905
+ message: "Select skills to install",
906
+ options: skillOptions,
907
+ required: false,
908
+ initialValues: skillOptions.map((option) => option.value)
909
+ });
910
+ if (isCancel(selectedSkills) || selectedSkills.length === 0) return;
911
+ const selectedAgents = await multiselect({
912
+ message: "Select agents to install skills to",
913
+ options: AVAILABLE_AGENTS,
914
+ required: false,
915
+ initialValues: [
916
+ "cursor",
917
+ "claude-code",
918
+ "github-copilot"
919
+ ]
920
+ });
921
+ if (isCancel(selectedAgents) || selectedAgents.length === 0) return;
922
+ const skillsBySource = {};
923
+ for (const skillKey of selectedSkills) {
924
+ const [source, skillName] = skillKey.split("::");
925
+ if (!skillsBySource[source]) skillsBySource[source] = [];
926
+ skillsBySource[source].push(skillName);
927
+ }
928
+ const installSpinner = spinner();
929
+ installSpinner.start("Installing skills...");
930
+ const runner = getPackageRunnerPrefix(packageManager);
931
+ const agentFlags = selectedAgents.flatMap((agent) => ["-a", agent]);
932
+ const globalFlag = scope === "global" ? ["-g"] : [];
933
+ for (const [source, skills] of Object.entries(skillsBySource)) {
934
+ const skillFlags = skills.flatMap((skill) => ["-s", skill]);
935
+ const args = [
936
+ ...runner,
937
+ "skills@latest",
938
+ "add",
939
+ source,
940
+ ...globalFlag,
941
+ ...skillFlags,
942
+ ...agentFlags,
943
+ "-y"
944
+ ];
945
+ await runInstallWithRetries({
946
+ description: `install skills from ${source}`,
947
+ run: async () => {
948
+ await $({
949
+ cwd: projectDir,
950
+ env: { CI: "true" }
951
+ })`${args}`;
952
+ }
953
+ });
954
+ }
955
+ installSpinner.stop("Skills installed");
956
+ }
957
+
958
+ //#endregion
959
+ //#region src/helpers/addons/starlight-setup.ts
960
+ async function setupStarlight(config) {
961
+ const { packageManager, projectDir } = config;
962
+ const s = spinner();
963
+ try {
964
+ s.start("Setting up Starlight docs...");
965
+ const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
966
+ "docs",
967
+ "--template",
968
+ "starlight",
969
+ "--no-install",
970
+ "--add",
971
+ "tailwind",
972
+ "--no-git",
973
+ "--skip-houston"
974
+ ].join(" ")}`);
975
+ const appsDir = path.join(projectDir, "apps");
976
+ await fs.ensureDir(appsDir);
977
+ await $({
978
+ cwd: appsDir,
979
+ env: { CI: "true" }
980
+ })`${args}`;
981
+ s.stop("Starlight docs setup successfully!");
982
+ } catch (error) {
983
+ s.stop(pc.red("Failed to set up Starlight docs"));
984
+ if (error instanceof Error) consola.error(pc.red(error.message));
985
+ }
986
+ }
987
+
988
+ //#endregion
989
+ //#region src/helpers/addons/tauri-setup.ts
990
+ async function setupTauri(config) {
991
+ const { packageManager, frontend, projectDir } = config;
992
+ const s = spinner();
993
+ const clientPackageDir = path.join(projectDir, "apps/web");
994
+ if (!await fs.pathExists(clientPackageDir)) return;
995
+ try {
996
+ s.start("Setting up Tauri desktop app support...");
997
+ const hasNuxt = frontend.includes("nuxt");
998
+ const hasSvelte = frontend.includes("svelte");
999
+ const hasNext = frontend.includes("next");
1000
+ const devUrl = `http://localhost:${getLocalWebDevPort(frontend)}`;
1001
+ const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : frontend.includes("react-router") ? "../build/client" : frontend.includes("react-vite") ? "../dist" : "../dist";
1002
+ const tauriArgs = [
1003
+ "@tauri-apps/cli@latest",
1004
+ "init",
1005
+ `--app-name=${path.basename(projectDir)}`,
1006
+ `--window-title=${path.basename(projectDir)}`,
1007
+ `--frontend-dist=${frontendDist}`,
1008
+ `--dev-url=${devUrl}`,
1009
+ `--before-dev-command=${packageManager} run dev`,
1010
+ `--before-build-command=${packageManager} run build`
1011
+ ];
1012
+ const prefix = getPackageRunnerPrefix(packageManager);
1013
+ await $({
1014
+ cwd: clientPackageDir,
1015
+ env: { CI: "true" }
1016
+ })`${[...prefix, ...tauriArgs]}`;
1017
+ s.stop("Tauri desktop app support configured successfully!");
1018
+ } catch (error) {
1019
+ s.stop(pc.red("Failed to set up Tauri"));
1020
+ if (error instanceof Error) consola$1.error(pc.red(error.message));
1021
+ }
1022
+ }
1023
+
1024
+ //#endregion
1025
+ //#region src/helpers/addons/tui-setup.ts
1026
+ const TEMPLATES$1 = {
1027
+ core: {
1028
+ label: "Core",
1029
+ hint: "Basic OpenTUI template"
1030
+ },
1031
+ react: {
1032
+ label: "React",
1033
+ hint: "React-based OpenTUI template"
1034
+ },
1035
+ solid: {
1036
+ label: "Solid",
1037
+ hint: "SolidJS-based OpenTUI template"
1038
+ }
1039
+ };
1040
+ async function setupTui(config) {
1041
+ const { packageManager, projectDir } = config;
1042
+ try {
1043
+ log.info("Setting up OpenTUI...");
1044
+ const template = await select({
1045
+ message: "Choose a template",
1046
+ options: Object.entries(TEMPLATES$1).map(([key, template$1]) => ({
1047
+ value: key,
1048
+ label: template$1.label,
1049
+ hint: template$1.hint
1050
+ })),
1051
+ initialValue: "core"
1052
+ });
1053
+ if (isCancel(template)) return exitCancelled("Operation cancelled");
1054
+ const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
1055
+ const appsDir = path.join(projectDir, "apps");
1056
+ await fs.ensureDir(appsDir);
1057
+ const s = spinner();
1058
+ s.start("Running OpenTUI create command...");
1059
+ await $({
1060
+ cwd: appsDir,
1061
+ env: { CI: "true" }
1062
+ })`${args}`;
1063
+ s.stop("OpenTUI setup complete!");
1064
+ } catch (error) {
1065
+ log.error(pc.red("Failed to set up OpenTUI"));
1066
+ if (error instanceof Error) console.error(pc.red(error.message));
1067
+ }
1068
+ }
1069
+
1070
+ //#endregion
1071
+ //#region src/helpers/addons/ultracite-setup.ts
1072
+ const LINTERS = {
1073
+ biome: {
1074
+ label: "Biome",
1075
+ hint: "Fast formatter and linter"
1076
+ },
1077
+ eslint: {
1078
+ label: "ESLint",
1079
+ hint: "Traditional JavaScript linter"
1080
+ },
1081
+ oxlint: {
1082
+ label: "Oxlint",
1083
+ hint: "Oxidation compiler linter"
1084
+ }
1085
+ };
1086
+ const EDITORS = {
1087
+ vscode: { label: "VS Code" },
1088
+ cursor: { label: "Cursor" },
1089
+ windsurf: { label: "Windsurf" },
1090
+ antigravity: { label: "Antigravity" },
1091
+ kiro: { label: "Kiro" },
1092
+ trae: { label: "Trae" },
1093
+ void: { label: "Void" },
1094
+ zed: { label: "Zed" }
1095
+ };
1096
+ const AGENTS = {
1097
+ claude: { label: "Claude" },
1098
+ codex: { label: "Codex" },
1099
+ jules: { label: "Jules" },
1100
+ copilot: { label: "GitHub Copilot" },
1101
+ cline: { label: "Cline" },
1102
+ amp: { label: "Amp" },
1103
+ aider: { label: "Aider" },
1104
+ "firebase-studio": { label: "Firebase Studio" },
1105
+ "open-hands": { label: "Open Hands" },
1106
+ gemini: { label: "Gemini" },
1107
+ junie: { label: "Junie" },
1108
+ augmentcode: { label: "AugmentCode" },
1109
+ "kilo-code": { label: "Kilo Code" },
1110
+ goose: { label: "Goose" },
1111
+ "roo-code": { label: "Roo Code" },
1112
+ warp: { label: "Warp" },
1113
+ droid: { label: "Droid" },
1114
+ opencode: { label: "OpenCode" },
1115
+ crush: { label: "Crush" },
1116
+ qwen: { label: "Qwen" },
1117
+ "amazon-q-cli": { label: "Amazon Q CLI" },
1118
+ firebender: { label: "Firebender" },
1119
+ "cursor-cli": { label: "Cursor CLI" },
1120
+ "mistral-vibe": { label: "Mistral Vibe" },
1121
+ vercel: { label: "Vercel" }
1122
+ };
1123
+ const HOOKS = {
1124
+ cursor: { label: "Cursor" },
1125
+ windsurf: { label: "Windsurf" },
1126
+ claude: { label: "Claude" }
1127
+ };
1128
+ function getFrameworksFromFrontend(frontend) {
1129
+ const frameworkMap = {
1130
+ "tanstack-router": "react",
1131
+ "react-router": "react",
1132
+ "react-vite": "react",
1133
+ "tanstack-start": "react",
1134
+ next: "next",
1135
+ nuxt: "vue",
1136
+ "native-bare": "react",
1137
+ "native-uniwind": "react",
1138
+ "native-unistyles": "react",
1139
+ svelte: "svelte",
1140
+ solid: "solid"
1141
+ };
1142
+ const frameworks = /* @__PURE__ */ new Set();
1143
+ for (const f of frontend) if (f !== "none" && frameworkMap[f]) frameworks.add(frameworkMap[f]);
1144
+ return Array.from(frameworks);
1145
+ }
1146
+ async function setupUltracite(config, gitHooks) {
1147
+ const { packageManager, projectDir, frontend } = config;
1148
+ try {
1149
+ log.info("Setting up Ultracite...");
1150
+ const result = await group({
1151
+ linter: () => select({
1152
+ message: "Choose linter/formatter",
1153
+ options: Object.entries(LINTERS).map(([key, linter$1]) => ({
1154
+ value: key,
1155
+ label: linter$1.label,
1156
+ hint: linter$1.hint
1157
+ })),
1158
+ initialValue: "biome"
1159
+ }),
1160
+ editors: () => multiselect({
1161
+ message: "Choose editors",
1162
+ options: Object.entries(EDITORS).map(([key, editor]) => ({
1163
+ value: key,
1164
+ label: editor.label
1165
+ })),
1166
+ required: true
1167
+ }),
1168
+ agents: () => multiselect({
1169
+ message: "Choose agents",
1170
+ options: Object.entries(AGENTS).map(([key, agent]) => ({
1171
+ value: key,
1172
+ label: agent.label
1173
+ })),
1174
+ required: true
1175
+ }),
1176
+ hooks: () => multiselect({
1177
+ message: "Choose hooks",
1178
+ options: Object.entries(HOOKS).map(([key, hook]) => ({
1179
+ value: key,
1180
+ label: hook.label
1181
+ }))
1182
+ })
1183
+ }, { onCancel: () => {
1184
+ exitCancelled("Operation cancelled");
1185
+ } });
1186
+ const linter = result.linter;
1187
+ const editors = result.editors;
1188
+ const agents = result.agents;
1189
+ const hooks = result.hooks;
1190
+ const frameworks = getFrameworksFromFrontend(frontend);
1191
+ const ultraciteArgs = [
1192
+ "init",
1193
+ "--pm",
1194
+ packageManager,
1195
+ "--linter",
1196
+ linter
1197
+ ];
1198
+ if (frameworks.length > 0) ultraciteArgs.push("--frameworks", ...frameworks);
1199
+ if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
1200
+ if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
1201
+ if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
1202
+ if (gitHooks.length > 0) {
1203
+ const integrations = [...gitHooks];
1204
+ if (gitHooks.includes("husky")) integrations.push("lint-staged");
1205
+ ultraciteArgs.push("--integrations", ...integrations);
1206
+ }
1207
+ const args = getPackageExecutionArgs(packageManager, `ultracite@latest ${ultraciteArgs.join(" ")} --skip-install`);
1208
+ const s = spinner();
1209
+ s.start("Running Ultracite init command...");
1210
+ await $({
1211
+ cwd: projectDir,
1212
+ env: { CI: "true" }
1213
+ })`${args}`;
1214
+ s.stop("Ultracite setup successfully!");
1215
+ } catch (error) {
1216
+ log.error(pc.red("Failed to set up Ultracite"));
1217
+ if (error instanceof Error) console.error(pc.red(error.message));
1218
+ }
1219
+ }
1220
+
1221
+ //#endregion
1222
+ //#region src/helpers/addons/wxt-setup.ts
1223
+ const TEMPLATES = {
1224
+ vanilla: {
1225
+ label: "Vanilla",
1226
+ hint: "Vanilla JavaScript template"
1227
+ },
1228
+ vue: {
1229
+ label: "Vue",
1230
+ hint: "Vue.js template"
1231
+ },
1232
+ react: {
1233
+ label: "React",
1234
+ hint: "React template"
1235
+ },
1236
+ solid: {
1237
+ label: "Solid",
1238
+ hint: "SolidJS template"
1239
+ },
1240
+ svelte: {
1241
+ label: "Svelte",
1242
+ hint: "Svelte template"
1243
+ }
1244
+ };
1245
+ async function setupWxt(config) {
1246
+ const { packageManager, projectDir } = config;
1247
+ try {
1248
+ log.info("Setting up WXT...");
1249
+ const template = await select({
1250
+ message: "Choose a template",
1251
+ options: Object.entries(TEMPLATES).map(([key, template$1]) => ({
1252
+ value: key,
1253
+ label: template$1.label,
1254
+ hint: template$1.hint
1255
+ })),
1256
+ initialValue: "react"
1257
+ });
1258
+ if (isCancel(template)) return exitCancelled("Operation cancelled");
1259
+ const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
1260
+ const appsDir = path.join(projectDir, "apps");
1261
+ await fs.ensureDir(appsDir);
1262
+ const s = spinner();
1263
+ s.start("Running WXT init command...");
1264
+ await $({
1265
+ cwd: appsDir,
1266
+ env: { CI: "true" }
1267
+ })`${args}`;
1268
+ const extensionDir = path.join(projectDir, "apps", "extension");
1269
+ const packageJsonPath = path.join(extensionDir, "package.json");
1270
+ if (await fs.pathExists(packageJsonPath)) {
1271
+ const packageJson = await fs.readJson(packageJsonPath);
1272
+ packageJson.name = "extension";
1273
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port 5555`;
1274
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1275
+ }
1276
+ s.stop("WXT setup complete!");
1277
+ } catch (error) {
1278
+ log.error(pc.red("Failed to set up WXT"));
1279
+ if (error instanceof Error) console.error(pc.red(error.message));
1280
+ }
1281
+ }
1282
+
1283
+ //#endregion
1284
+ //#region src/helpers/addons/addons-setup.ts
1285
+ async function setupAddons(config) {
1286
+ const warnings = [];
1287
+ const { addons, frontend, projectDir } = config;
1288
+ const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("react-vite") || frontend.includes("tanstack-router") || frontend.includes("next");
1289
+ const hasNuxtFrontend = frontend.includes("nuxt");
1290
+ const hasSvelteFrontend = frontend.includes("svelte");
1291
+ const hasSolidFrontend = frontend.includes("solid");
1292
+ const hasNextFrontend = frontend.includes("next");
1293
+ if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
1294
+ const hasUltracite = addons.includes("ultracite");
1295
+ const hasBiome = addons.includes("biome");
1296
+ const hasHusky = addons.includes("husky");
1297
+ const hasLefthook = addons.includes("lefthook");
1298
+ const hasOxlint = addons.includes("oxlint");
1299
+ if (hasUltracite) {
1300
+ const gitHooks = [];
1301
+ if (hasHusky) gitHooks.push("husky");
1302
+ if (hasLefthook) gitHooks.push("lefthook");
1303
+ await setupUltracite(config, gitHooks);
1304
+ } else {
1305
+ if (hasBiome) await setupBiome(projectDir);
1306
+ if (hasOxlint) await setupOxlint(projectDir, config.packageManager);
1307
+ if (hasHusky || hasLefthook) {
1308
+ let linter;
1309
+ if (hasOxlint) linter = "oxlint";
1310
+ else if (hasBiome) linter = "biome";
1311
+ if (hasHusky) await setupHusky(projectDir, linter);
1312
+ if (hasLefthook) await setupLefthook(projectDir);
1313
+ }
1314
+ }
1315
+ if (addons.includes("starlight")) await setupStarlight(config);
1316
+ if (addons.includes("fumadocs")) await setupFumadocs(config);
1317
+ if (addons.includes("opentui")) await setupTui(config);
1318
+ if (addons.includes("wxt")) await setupWxt(config);
1319
+ if (addons.includes("ruler")) await setupRuler(config);
1320
+ if (addons.includes("mcp")) try {
1321
+ await setupMcp(config);
1322
+ } catch (error) {
1323
+ warnings.push(`MCP setup failed: ${error instanceof Error ? error.message : String(error)}`);
1324
+ }
1325
+ if (addons.includes("skills")) try {
1326
+ await setupSkills(config);
1327
+ } catch (error) {
1328
+ warnings.push(`Skills setup failed: ${error instanceof Error ? error.message : String(error)}`);
1329
+ }
1330
+ return warnings;
1331
+ }
1332
+ async function setupBiome(projectDir) {
1333
+ await addPackageDependency({
1334
+ devDependencies: ["@biomejs/biome"],
1335
+ projectDir
1336
+ });
1337
+ const packageJsonPath = path.join(projectDir, "package.json");
1338
+ if (await fs.pathExists(packageJsonPath)) {
1339
+ const packageJson = await fs.readJson(packageJsonPath);
1340
+ packageJson.scripts = {
1341
+ ...packageJson.scripts,
1342
+ check: "biome check --write ."
1343
+ };
1344
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1345
+ }
1346
+ }
1347
+ async function setupHusky(projectDir, linter) {
1348
+ await addPackageDependency({
1349
+ devDependencies: ["husky", "lint-staged"],
1350
+ projectDir
1351
+ });
1352
+ const packageJsonPath = path.join(projectDir, "package.json");
1353
+ if (await fs.pathExists(packageJsonPath)) {
1354
+ const packageJson = await fs.readJson(packageJsonPath);
1355
+ packageJson.scripts = {
1356
+ ...packageJson.scripts,
1357
+ prepare: "husky"
1358
+ };
1359
+ if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
1360
+ else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
1361
+ else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
1362
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1363
+ }
1364
+ }
1365
+ async function setupLefthook(projectDir) {
1366
+ await addPackageDependency({
1367
+ devDependencies: ["lefthook"],
1368
+ projectDir
1369
+ });
1370
+ }
1371
+
1372
+ //#endregion
1373
+ export { setLastPromptShownUI as _, getPackageExecutionArgs as a, UserCancelledError as c, handleError as d, didLastPromptShowUI as f, setIsFirstPrompt as g, runWithContextAsync as h, setupLefthook as i, exitCancelled as l, isSilent as m, setupBiome as n, addPackageDependency as o, isFirstPrompt as p, setupHusky as r, CLIError as s, setupAddons as t, exitWithError as u };