create-loadout 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +154 -0
  2. package/dist/claude-md.d.ts +3 -0
  3. package/dist/claude-md.js +494 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +186 -0
  6. package/dist/config.d.ts +3 -0
  7. package/dist/config.js +98 -0
  8. package/dist/create-next.d.ts +1 -0
  9. package/dist/create-next.js +17 -0
  10. package/dist/detect.d.ts +4 -0
  11. package/dist/detect.js +60 -0
  12. package/dist/env.d.ts +3 -0
  13. package/dist/env.js +183 -0
  14. package/dist/generate-readme.d.ts +3 -0
  15. package/dist/generate-readme.js +160 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +3 -0
  18. package/dist/instrumentation.d.ts +3 -0
  19. package/dist/instrumentation.js +95 -0
  20. package/dist/integrations/ai-sdk.d.ts +3 -0
  21. package/dist/integrations/ai-sdk.js +20 -0
  22. package/dist/integrations/clerk.d.ts +2 -0
  23. package/dist/integrations/clerk.js +50 -0
  24. package/dist/integrations/firecrawl.d.ts +2 -0
  25. package/dist/integrations/firecrawl.js +26 -0
  26. package/dist/integrations/index.d.ts +4 -0
  27. package/dist/integrations/index.js +64 -0
  28. package/dist/integrations/inngest.d.ts +2 -0
  29. package/dist/integrations/inngest.js +45 -0
  30. package/dist/integrations/neon-drizzle.d.ts +2 -0
  31. package/dist/integrations/neon-drizzle.js +56 -0
  32. package/dist/integrations/posthog.d.ts +2 -0
  33. package/dist/integrations/posthog.js +25 -0
  34. package/dist/integrations/resend.d.ts +2 -0
  35. package/dist/integrations/resend.js +34 -0
  36. package/dist/integrations/sentry.d.ts +2 -0
  37. package/dist/integrations/sentry.js +47 -0
  38. package/dist/integrations/stripe.d.ts +2 -0
  39. package/dist/integrations/stripe.js +45 -0
  40. package/dist/integrations/uploadthing.d.ts +2 -0
  41. package/dist/integrations/uploadthing.js +34 -0
  42. package/dist/landing-page.d.ts +2 -0
  43. package/dist/landing-page.js +97 -0
  44. package/dist/prompts.d.ts +7 -0
  45. package/dist/prompts.js +99 -0
  46. package/dist/setup-shadcn.d.ts +1 -0
  47. package/dist/setup-shadcn.js +27 -0
  48. package/dist/templates/ai-sdk.d.ts +12 -0
  49. package/dist/templates/ai-sdk.js +96 -0
  50. package/dist/templates/clerk.d.ts +6 -0
  51. package/dist/templates/clerk.js +96 -0
  52. package/dist/templates/firecrawl.d.ts +4 -0
  53. package/dist/templates/firecrawl.js +106 -0
  54. package/dist/templates/inngest.d.ts +6 -0
  55. package/dist/templates/inngest.js +91 -0
  56. package/dist/templates/neon-drizzle.d.ts +16 -0
  57. package/dist/templates/neon-drizzle.js +343 -0
  58. package/dist/templates/posthog.d.ts +3 -0
  59. package/dist/templates/posthog.js +10 -0
  60. package/dist/templates/resend.d.ts +5 -0
  61. package/dist/templates/resend.js +102 -0
  62. package/dist/templates/sentry.d.ts +8 -0
  63. package/dist/templates/sentry.js +145 -0
  64. package/dist/templates/stripe.d.ts +6 -0
  65. package/dist/templates/stripe.js +215 -0
  66. package/dist/templates/uploadthing.d.ts +7 -0
  67. package/dist/templates/uploadthing.js +150 -0
  68. package/dist/templates/zustand.d.ts +3 -0
  69. package/dist/templates/zustand.js +26 -0
  70. package/dist/types.d.ts +26 -0
  71. package/dist/types.js +1 -0
  72. package/package.json +46 -0
@@ -0,0 +1,47 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { sentryTemplates } from '../templates/sentry.js';
4
+ export const sentryIntegration = {
5
+ id: 'sentry',
6
+ name: 'Sentry',
7
+ description: 'Error tracking and monitoring',
8
+ packages: ['@sentry/nextjs'],
9
+ envVars: [
10
+ {
11
+ key: 'NEXT_PUBLIC_SENTRY_DSN',
12
+ description: 'Sentry DSN',
13
+ example: 'https://...@sentry.io/...',
14
+ isPublic: true,
15
+ },
16
+ {
17
+ key: 'SENTRY_AUTH_TOKEN',
18
+ description: 'Sentry auth token for source maps',
19
+ example: 'sntrys_...',
20
+ },
21
+ {
22
+ key: 'SENTRY_ORG',
23
+ description: 'Sentry organization slug',
24
+ example: 'your-org',
25
+ },
26
+ {
27
+ key: 'SENTRY_PROJECT',
28
+ description: 'Sentry project slug',
29
+ example: 'your-project',
30
+ },
31
+ ],
32
+ setup: async (projectPath) => {
33
+ // instrumentation.ts and instrumentation-client.ts are generated centrally
34
+ // in cli.ts to handle merging with other integrations (e.g., PostHog)
35
+ // Create server and edge config files (imported by instrumentation.ts)
36
+ await fs.writeFile(path.join(projectPath, 'sentry.server.config.ts'), sentryTemplates.serverConfig);
37
+ await fs.writeFile(path.join(projectPath, 'sentry.edge.config.ts'), sentryTemplates.edgeConfig);
38
+ // Create global-error.tsx
39
+ await fs.writeFile(path.join(projectPath, 'app/global-error.tsx'), sentryTemplates.globalError);
40
+ // Update next.config.ts to wrap with Sentry
41
+ const nextConfigPath = path.join(projectPath, 'next.config.ts');
42
+ await fs.writeFile(nextConfigPath, sentryTemplates.nextConfig);
43
+ // No service needed - use Sentry directly:
44
+ // Server: import * as Sentry from '@sentry/nextjs'; Sentry.captureException(error)
45
+ // Client: Same import, already initialized via instrumentation-client.ts
46
+ },
47
+ };
@@ -0,0 +1,2 @@
1
+ import type { Integration } from '../types.js';
2
+ export declare const stripeIntegration: Integration;
@@ -0,0 +1,45 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { stripeTemplates } from '../templates/stripe.js';
4
+ export const stripeIntegration = {
5
+ id: 'stripe',
6
+ name: 'Stripe',
7
+ description: 'Payments and subscriptions',
8
+ packages: ['stripe'],
9
+ envVars: [
10
+ {
11
+ key: 'STRIPE_SECRET_KEY',
12
+ description: 'Stripe secret key',
13
+ example: 'sk_test_...',
14
+ },
15
+ {
16
+ key: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
17
+ description: 'Stripe publishable key',
18
+ example: 'pk_test_...',
19
+ isPublic: true,
20
+ },
21
+ {
22
+ key: 'STRIPE_WEBHOOK_SECRET',
23
+ description: 'Stripe webhook secret',
24
+ example: 'whsec_...',
25
+ },
26
+ ],
27
+ setup: async (projectPath) => {
28
+ // Create payment service
29
+ await fs.mkdir(path.join(projectPath, 'services'), { recursive: true });
30
+ await fs.writeFile(path.join(projectPath, 'services/payment.service.ts'), stripeTemplates.paymentService);
31
+ // Create API routes
32
+ await fs.mkdir(path.join(projectPath, 'app/api/stripe/checkout'), {
33
+ recursive: true,
34
+ });
35
+ await fs.mkdir(path.join(projectPath, 'app/api/stripe/webhooks'), {
36
+ recursive: true,
37
+ });
38
+ await fs.mkdir(path.join(projectPath, 'app/api/stripe/portal'), {
39
+ recursive: true,
40
+ });
41
+ await fs.writeFile(path.join(projectPath, 'app/api/stripe/checkout/route.ts'), stripeTemplates.checkoutRoute);
42
+ await fs.writeFile(path.join(projectPath, 'app/api/stripe/webhooks/route.ts'), stripeTemplates.webhooksRoute);
43
+ await fs.writeFile(path.join(projectPath, 'app/api/stripe/portal/route.ts'), stripeTemplates.portalRoute);
44
+ },
45
+ };
@@ -0,0 +1,2 @@
1
+ import type { Integration } from '../types.js';
2
+ export declare const uploadthingIntegration: Integration;
@@ -0,0 +1,34 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { uploadthingTemplates } from '../templates/uploadthing.js';
4
+ export const uploadthingIntegration = {
5
+ id: 'uploadthing',
6
+ name: 'UploadThing',
7
+ description: 'File uploads',
8
+ packages: ['uploadthing', '@uploadthing/react'],
9
+ envVars: [
10
+ {
11
+ key: 'UPLOADTHING_TOKEN',
12
+ description: 'UploadThing token',
13
+ example: '...',
14
+ },
15
+ ],
16
+ setup: async (projectPath) => {
17
+ // Create lib directory
18
+ await fs.mkdir(path.join(projectPath, 'lib'), { recursive: true });
19
+ // Create UploadThing client helpers
20
+ await fs.writeFile(path.join(projectPath, 'lib/uploadthing.client.ts'), uploadthingTemplates.uploadthingClient);
21
+ // Create file service
22
+ await fs.mkdir(path.join(projectPath, 'services'), { recursive: true });
23
+ await fs.writeFile(path.join(projectPath, 'services/file.service.ts'), uploadthingTemplates.fileService);
24
+ // Create API routes
25
+ await fs.mkdir(path.join(projectPath, 'app/api/uploadthing'), {
26
+ recursive: true,
27
+ });
28
+ await fs.writeFile(path.join(projectPath, 'app/api/uploadthing/core.ts'), uploadthingTemplates.uploadthingCore);
29
+ await fs.writeFile(path.join(projectPath, 'app/api/uploadthing/route.ts'), uploadthingTemplates.uploadthingRoute);
30
+ // Create upload button component
31
+ await fs.mkdir(path.join(projectPath, 'components'), { recursive: true });
32
+ await fs.writeFile(path.join(projectPath, 'components/upload-button.tsx'), uploadthingTemplates.uploadButton);
33
+ },
34
+ };
@@ -0,0 +1,2 @@
1
+ import type { ProjectConfig } from './types.js';
2
+ export declare function generateLandingPage(projectPath: string, config: ProjectConfig): Promise<void>;
@@ -0,0 +1,97 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ const coreTechnologies = [
4
+ { name: 'Next.js', href: 'https://nextjs.org/docs', description: 'App Router & React 19' },
5
+ { name: 'Tailwind', href: 'https://tailwindcss.com/docs', description: 'Utility-first CSS' },
6
+ { name: 'shadcn/ui', href: 'https://ui.shadcn.com', description: 'Component primitives' },
7
+ { name: 'Zod', href: 'https://zod.dev', description: 'Schema validation' },
8
+ { name: 'Zustand', href: 'https://zustand.docs.pmnd.rs', description: 'State management' },
9
+ { name: 'Luxon', href: 'https://moment.github.io/luxon', description: 'Date/time' },
10
+ ];
11
+ const integrationTechnologies = {
12
+ clerk: { name: 'Clerk', href: 'https://clerk.com/docs', description: 'Authentication' },
13
+ 'neon-drizzle': { name: 'Neon + Drizzle', href: 'https://neon.tech/docs', description: 'Serverless Postgres' },
14
+ 'ai-sdk': { name: 'AI SDK', href: 'https://sdk.vercel.ai/docs', description: 'AI integration' },
15
+ resend: { name: 'Resend', href: 'https://resend.com/docs', description: 'Email API' },
16
+ firecrawl: { name: 'Firecrawl', href: 'https://docs.firecrawl.dev', description: 'Web scraping' },
17
+ inngest: { name: 'Inngest', href: 'https://www.inngest.com/docs', description: 'Background jobs' },
18
+ uploadthing: { name: 'UploadThing', href: 'https://docs.uploadthing.com', description: 'File uploads' },
19
+ stripe: { name: 'Stripe', href: 'https://docs.stripe.com', description: 'Payments' },
20
+ posthog: { name: 'PostHog', href: 'https://posthog.com/docs', description: 'Analytics' },
21
+ sentry: { name: 'Sentry', href: 'https://docs.sentry.io', description: 'Error tracking' },
22
+ };
23
+ export async function generateLandingPage(projectPath, config) {
24
+ const technologies = [
25
+ ...coreTechnologies,
26
+ ...config.integrations.map((id) => integrationTechnologies[id]),
27
+ ];
28
+ const techArrayString = technologies
29
+ .map((tech) => ` { name: '${tech.name}', href: '${tech.href}', description: '${tech.description}' },`)
30
+ .join('\n');
31
+ const content = `import { ExternalLink } from 'lucide-react';
32
+
33
+ export default function Home() {
34
+ const technologies = [
35
+ ${techArrayString}
36
+ ];
37
+
38
+ return (
39
+ <main className="min-h-screen bg-stone-50 text-stone-900 selection:bg-amber-200">
40
+ <div className="max-w-2xl mx-auto px-6 py-24">
41
+ <header className="mb-16 text-center">
42
+ <div className="inline-flex items-center gap-2 px-3 py-1 mb-6 text-xs font-medium tracking-wide text-amber-700 bg-amber-100 rounded-full">
43
+ <span className="w-1.5 h-1.5 bg-amber-500 rounded-full" />
44
+ Setup complete
45
+ </div>
46
+ <h1 className="text-4xl sm:text-5xl font-light tracking-tight mb-4 text-stone-800">
47
+ You&apos;re ready to build.
48
+ </h1>
49
+ <p className="text-stone-500 max-w-md mx-auto">
50
+ Your project is configured and running. Delete this page and start creating.
51
+ </p>
52
+ </header>
53
+
54
+ <section className="mb-16">
55
+ <h2 className="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-6 text-center">
56
+ Your Stack
57
+ </h2>
58
+
59
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
60
+ {technologies.map((tech) => (
61
+ <a
62
+ key={tech.name}
63
+ href={tech.href}
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ className="group p-4 rounded-lg border border-stone-200 bg-white hover:border-stone-300 hover:shadow-sm transition-all duration-200"
67
+ >
68
+ <div className="flex items-center justify-between mb-1">
69
+ <span className="font-medium text-stone-800 text-sm">{tech.name}</span>
70
+ <ExternalLink className="w-3 h-3 text-stone-300 group-hover:text-amber-500 transition-colors" />
71
+ </div>
72
+ <span className="text-xs text-stone-400">{tech.description}</span>
73
+ </a>
74
+ ))}
75
+ </div>
76
+ </section>
77
+
78
+ <footer className="text-center pt-8 border-t border-stone-200">
79
+ <p className="text-xs text-stone-400">
80
+ Scaffolded with{' '}
81
+ <a
82
+ href="https://github.com/KylerD/loadout"
83
+ target="_blank"
84
+ rel="noopener noreferrer"
85
+ className="text-stone-500 hover:text-amber-600 transition-colors"
86
+ >
87
+ create-loadout
88
+ </a>
89
+ </p>
90
+ </footer>
91
+ </div>
92
+ </main>
93
+ );
94
+ }
95
+ `;
96
+ await fs.writeFile(path.join(projectPath, 'app/page.tsx'), content);
97
+ }
@@ -0,0 +1,7 @@
1
+ import type { IntegrationId, ProjectConfig, AIProviderChoice } from './types.js';
2
+ export interface AddIntegrationConfig {
3
+ integrations: IntegrationId[];
4
+ aiProvider?: AIProviderChoice;
5
+ }
6
+ export declare function getProjectConfig(): Promise<ProjectConfig>;
7
+ export declare function getAddIntegrationConfig(available: IntegrationId[]): Promise<AddIntegrationConfig>;
@@ -0,0 +1,99 @@
1
+ import { input, confirm, select } from '@inquirer/prompts';
2
+ export async function getProjectConfig() {
3
+ const name = await input({
4
+ message: 'Project name:',
5
+ default: 'my-app',
6
+ validate: (value) => {
7
+ if (!value.trim())
8
+ return 'Project name is required';
9
+ if (!/^[a-z0-9-]+$/.test(value)) {
10
+ return 'Project name can only contain lowercase letters, numbers, and hyphens';
11
+ }
12
+ return true;
13
+ },
14
+ });
15
+ const integrations = [];
16
+ let aiProvider;
17
+ // Authentication
18
+ if (await confirm({ message: 'Add Clerk for authentication?', default: false })) {
19
+ integrations.push('clerk');
20
+ }
21
+ // Database
22
+ if (await confirm({ message: 'Add Neon + Drizzle for database?', default: false })) {
23
+ integrations.push('neon-drizzle');
24
+ }
25
+ // AI
26
+ if (await confirm({ message: 'Add Vercel AI SDK?', default: false })) {
27
+ integrations.push('ai-sdk');
28
+ aiProvider = await select({
29
+ message: 'Which AI provider?',
30
+ choices: [
31
+ { value: 'openai', name: 'OpenAI (GPT-4o)' },
32
+ { value: 'anthropic', name: 'Anthropic (Claude)' },
33
+ { value: 'google', name: 'Google (Gemini)' },
34
+ ],
35
+ });
36
+ }
37
+ // Email
38
+ if (await confirm({ message: 'Add Resend for email?', default: false })) {
39
+ integrations.push('resend');
40
+ }
41
+ // Scraping
42
+ if (await confirm({ message: 'Add Firecrawl for web scraping?', default: false })) {
43
+ integrations.push('firecrawl');
44
+ }
45
+ // Background Jobs
46
+ if (await confirm({ message: 'Add Inngest for background jobs?', default: false })) {
47
+ integrations.push('inngest');
48
+ }
49
+ // File Uploads
50
+ if (await confirm({ message: 'Add UploadThing for file uploads?', default: false })) {
51
+ integrations.push('uploadthing');
52
+ }
53
+ // Payments
54
+ if (await confirm({ message: 'Add Stripe for payments?', default: false })) {
55
+ integrations.push('stripe');
56
+ }
57
+ // Analytics
58
+ if (await confirm({ message: 'Add PostHog for analytics?', default: false })) {
59
+ integrations.push('posthog');
60
+ }
61
+ // Error Tracking
62
+ if (await confirm({ message: 'Add Sentry for error tracking?', default: false })) {
63
+ integrations.push('sentry');
64
+ }
65
+ return { name, integrations, aiProvider };
66
+ }
67
+ const integrationPrompts = {
68
+ clerk: 'Add Clerk for authentication?',
69
+ 'neon-drizzle': 'Add Neon + Drizzle for database?',
70
+ 'ai-sdk': 'Add Vercel AI SDK?',
71
+ resend: 'Add Resend for email?',
72
+ firecrawl: 'Add Firecrawl for web scraping?',
73
+ inngest: 'Add Inngest for background jobs?',
74
+ uploadthing: 'Add UploadThing for file uploads?',
75
+ stripe: 'Add Stripe for payments?',
76
+ posthog: 'Add PostHog for analytics?',
77
+ sentry: 'Add Sentry for error tracking?',
78
+ };
79
+ export async function getAddIntegrationConfig(available) {
80
+ const integrations = [];
81
+ let aiProvider;
82
+ for (const id of available) {
83
+ const message = integrationPrompts[id];
84
+ if (await confirm({ message, default: false })) {
85
+ integrations.push(id);
86
+ if (id === 'ai-sdk') {
87
+ aiProvider = await select({
88
+ message: 'Which AI provider?',
89
+ choices: [
90
+ { value: 'openai', name: 'OpenAI (GPT-4o)' },
91
+ { value: 'anthropic', name: 'Anthropic (Claude)' },
92
+ { value: 'google', name: 'Google (Gemini)' },
93
+ ],
94
+ });
95
+ }
96
+ }
97
+ }
98
+ return { integrations, aiProvider };
99
+ }
@@ -0,0 +1 @@
1
+ export declare function setupShadcn(projectPath: string): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { execa } from 'execa';
2
+ export async function setupShadcn(projectPath) {
3
+ // Initialize shadcn/ui with defaults
4
+ await execa('npx', [
5
+ 'shadcn@latest',
6
+ 'init',
7
+ '-y',
8
+ '--base-color', 'neutral',
9
+ ], {
10
+ cwd: projectPath,
11
+ stdio: 'pipe',
12
+ });
13
+ // Add commonly used components
14
+ await execa('npx', [
15
+ 'shadcn@latest',
16
+ 'add',
17
+ 'button',
18
+ 'input',
19
+ 'card',
20
+ 'form',
21
+ 'label',
22
+ '-y',
23
+ ], {
24
+ cwd: projectPath,
25
+ stdio: 'pipe',
26
+ });
27
+ }
@@ -0,0 +1,12 @@
1
+ export type AIProvider = 'openai' | 'anthropic' | 'google';
2
+ export declare function getAiServiceTemplate(provider: AIProvider): string;
3
+ export declare function getAiPackages(provider: AIProvider): string[];
4
+ export declare function getAiEnvVar(provider: AIProvider): {
5
+ key: string;
6
+ example: string;
7
+ description: string;
8
+ };
9
+ export declare function getAiConfigVar(provider: AIProvider): {
10
+ name: string;
11
+ envKey: string;
12
+ };
@@ -0,0 +1,96 @@
1
+ const providerConfigs = {
2
+ openai: {
3
+ package: '@ai-sdk/openai',
4
+ import: "import { createOpenAI, type OpenAIProvider } from '@ai-sdk/openai';",
5
+ createFn: 'createOpenAI',
6
+ providerType: 'OpenAIProvider',
7
+ configVar: 'OPENAI_API_KEY',
8
+ defaultModel: 'gpt-4o-mini',
9
+ },
10
+ anthropic: {
11
+ package: '@ai-sdk/anthropic',
12
+ import: "import { createAnthropic, type AnthropicProvider } from '@ai-sdk/anthropic';",
13
+ createFn: 'createAnthropic',
14
+ providerType: 'AnthropicProvider',
15
+ configVar: 'ANTHROPIC_API_KEY',
16
+ defaultModel: 'claude-sonnet-4-5',
17
+ },
18
+ google: {
19
+ package: '@ai-sdk/google',
20
+ import: "import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from '@ai-sdk/google';",
21
+ createFn: 'createGoogleGenerativeAI',
22
+ providerType: 'GoogleGenerativeAIProvider',
23
+ configVar: 'GOOGLE_API_KEY',
24
+ defaultModel: 'gemini-2.0-flash',
25
+ },
26
+ };
27
+ export function getAiServiceTemplate(provider) {
28
+ const config = providerConfigs[provider];
29
+ return `import { generateText, Output } from 'ai';
30
+ ${config.import}
31
+ import { z } from 'zod';
32
+ import { ${config.configVar} } from '@/lib/config';
33
+
34
+ export class AIService {
35
+ private provider: ${config.providerType};
36
+
37
+ constructor(apiKey: string) {
38
+ this.provider = ${config.createFn}({ apiKey });
39
+ }
40
+
41
+ async generateObject() {
42
+ const model = this.provider('${config.defaultModel}');
43
+
44
+ const { output } = await generateText({
45
+ model: model,
46
+ output: Output.object({
47
+ schema: z.object({
48
+ recipe: z.object({
49
+ name: z.string(),
50
+ ingredients: z.array(
51
+ z.object({ name: z.string(), amount: z.string() }),
52
+ ),
53
+ steps: z.array(z.string()),
54
+ }),
55
+ }),
56
+ }),
57
+ prompt: 'Generate a lasagna recipe.',
58
+ });
59
+
60
+ return output;
61
+ }
62
+
63
+ async generateText() {
64
+ const model = this.provider('${config.defaultModel}');
65
+
66
+ const { text } = await generateText({
67
+ model: model,
68
+ prompt: 'Generate a lasagna recipe.',
69
+ });
70
+
71
+ return text;
72
+ }
73
+ }
74
+
75
+ export const aiService = new AIService(${config.configVar});
76
+ `;
77
+ }
78
+ export function getAiPackages(provider) {
79
+ return ['ai', providerConfigs[provider].package];
80
+ }
81
+ export function getAiEnvVar(provider) {
82
+ const vars = {
83
+ openai: { key: 'OPENAI_API_KEY', example: 'sk-...', description: 'OpenAI API key' },
84
+ anthropic: { key: 'ANTHROPIC_API_KEY', example: 'sk-ant-...', description: 'Anthropic API key' },
85
+ google: { key: 'GOOGLE_API_KEY', example: 'AI...', description: 'Google AI API key' },
86
+ };
87
+ return vars[provider];
88
+ }
89
+ export function getAiConfigVar(provider) {
90
+ const vars = {
91
+ openai: { name: 'OPENAI_API_KEY', envKey: 'OPENAI_API_KEY' },
92
+ anthropic: { name: 'ANTHROPIC_API_KEY', envKey: 'ANTHROPIC_API_KEY' },
93
+ google: { name: 'GOOGLE_API_KEY', envKey: 'GOOGLE_API_KEY' },
94
+ };
95
+ return vars[provider];
96
+ }
@@ -0,0 +1,6 @@
1
+ export declare const clerkTemplates: {
2
+ userService: string;
3
+ proxy: string;
4
+ authComponents: string;
5
+ wrapLayout: (content: string) => string;
6
+ };
@@ -0,0 +1,96 @@
1
+ export const clerkTemplates = {
2
+ userService: `import { clerkClient, User } from '@clerk/nextjs/server';
3
+
4
+ export class UserService {
5
+ async getUserById(userId: string): Promise<User> {
6
+ const client = await clerkClient();
7
+ return client.users.getUser(userId);
8
+ }
9
+
10
+ async getUserByEmail(email: string): Promise<User | null> {
11
+ const client = await clerkClient();
12
+ const users = await client.users.getUserList({
13
+ emailAddress: [email],
14
+ });
15
+ return users.data[0] ?? null;
16
+ }
17
+
18
+ async updateUserMetadata(
19
+ userId: string,
20
+ metadata: {
21
+ publicMetadata?: Record<string, unknown>;
22
+ privateMetadata?: Record<string, unknown>;
23
+ }
24
+ ): Promise<User> {
25
+ const client = await clerkClient();
26
+ return client.users.updateUserMetadata(userId, metadata);
27
+ }
28
+
29
+ async deleteUser(userId: string): Promise<User> {
30
+ const client = await clerkClient();
31
+ return client.users.deleteUser(userId);
32
+ }
33
+ }
34
+
35
+ export const userService = new UserService();
36
+ `,
37
+ // proxy.ts for Next.js 16+
38
+ proxy: `import { clerkMiddleware } from '@clerk/nextjs/server';
39
+
40
+ export default clerkMiddleware();
41
+
42
+ export const config = {
43
+ matcher: [
44
+ // Skip Next.js internals and all static files, unless found in search params
45
+ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
46
+ // Always run for API routes
47
+ '/(api|trpc)(.*)',
48
+ ],
49
+ };
50
+ `,
51
+ // Auth components using native Clerk buttons
52
+ authComponents: `'use client';
53
+
54
+ import {
55
+ SignInButton,
56
+ SignUpButton,
57
+ SignedIn,
58
+ SignedOut,
59
+ UserButton,
60
+ } from '@clerk/nextjs';
61
+ import { Button } from '@/components/ui/button';
62
+
63
+ export function AuthButtons() {
64
+ return (
65
+ <div className="flex items-center gap-4">
66
+ <SignedOut>
67
+ <SignInButton mode="modal">
68
+ <Button variant="ghost">Sign In</Button>
69
+ </SignInButton>
70
+ <SignUpButton mode="modal">
71
+ <Button>Sign Up</Button>
72
+ </SignUpButton>
73
+ </SignedOut>
74
+ <SignedIn>
75
+ <UserButton afterSignOutUrl="/" />
76
+ </SignedIn>
77
+ </div>
78
+ );
79
+ }
80
+ `,
81
+ wrapLayout: (content) => {
82
+ const importStatement = "import { ClerkProvider } from '@clerk/nextjs';\n";
83
+ const importMatch = content.match(/^(import[\s\S]*?from\s+['"][^'"]+['"];\n*)+/m);
84
+ let newContent = content;
85
+ if (importMatch) {
86
+ const lastImportIndex = importMatch.index + importMatch[0].length;
87
+ newContent =
88
+ content.slice(0, lastImportIndex) + importStatement + content.slice(lastImportIndex);
89
+ }
90
+ else {
91
+ newContent = importStatement + content;
92
+ }
93
+ newContent = newContent.replace(/(<body[^>]*>)([\s\S]*?)(<\/body>)/, '$1\n <ClerkProvider>$2</ClerkProvider>\n $3');
94
+ return newContent;
95
+ },
96
+ };
@@ -0,0 +1,4 @@
1
+ export declare const firecrawlTemplates: {
2
+ scrapeService: string;
3
+ scrapeRoute: string;
4
+ };