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
package/dist/cli.js ADDED
@@ -0,0 +1,186 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { getProjectConfig, getAddIntegrationConfig } from './prompts.js';
6
+ import { createNextApp } from './create-next.js';
7
+ import { setupShadcn } from './setup-shadcn.js';
8
+ import { installIntegrations } from './integrations/index.js';
9
+ import { generateClaudeMd, appendClaudeMd } from './claude-md.js';
10
+ import { generateEnvFiles, appendEnvFiles } from './env.js';
11
+ import { generateConfig, appendConfig } from './config.js';
12
+ import { generateReadme, generateGitignore } from './generate-readme.js';
13
+ import { generateInstrumentationClient, generateInstrumentation, } from './instrumentation.js';
14
+ import { zustandTemplates } from './templates/zustand.js';
15
+ import { generateLandingPage } from './landing-page.js';
16
+ import { isExistingProject, getInstalledIntegrations, getAvailableIntegrations, } from './detect.js';
17
+ export async function main() {
18
+ console.log();
19
+ console.log(chalk.bold.cyan(' create-loadout'));
20
+ console.log(chalk.gray(' Custom Next.js scaffolding with SaaS integrations'));
21
+ console.log();
22
+ const cwd = process.cwd();
23
+ try {
24
+ if (await isExistingProject(cwd)) {
25
+ await addIntegrationFlow(cwd);
26
+ }
27
+ else {
28
+ await newProjectFlow();
29
+ }
30
+ }
31
+ catch (error) {
32
+ console.error();
33
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
34
+ process.exit(1);
35
+ }
36
+ }
37
+ async function newProjectFlow() {
38
+ const config = await getProjectConfig();
39
+ console.log();
40
+ const spinner = ora('Creating Next.js app...').start();
41
+ const projectPath = await createNextApp(config.name);
42
+ spinner.succeed('Next.js app created');
43
+ spinner.start('Setting up shadcn/ui...');
44
+ await setupShadcn(projectPath);
45
+ spinner.succeed('shadcn/ui configured');
46
+ await extendUtils(projectPath);
47
+ spinner.start('Installing base packages...');
48
+ const { execa } = await import('execa');
49
+ await execa('npm', ['install', 'zod@^3.24', 'zustand@^5', 'luxon@^3'], { cwd: projectPath });
50
+ await execa('npm', ['install', '-D', '@types/luxon'], { cwd: projectPath });
51
+ spinner.succeed('Base packages installed (zod, zustand, luxon)');
52
+ await fs.mkdir(path.join(projectPath, 'lib/stores'), { recursive: true });
53
+ await fs.writeFile(path.join(projectPath, 'lib/stores/counter.store.ts'), zustandTemplates.exampleStore);
54
+ if (config.integrations.length > 0) {
55
+ spinner.start(`Setting up ${config.integrations.length} integration(s)...`);
56
+ await installIntegrations(projectPath, config);
57
+ spinner.succeed('Integrations configured');
58
+ }
59
+ await generateInstrumentationClient(projectPath, config);
60
+ await generateInstrumentation(projectPath, config);
61
+ spinner.start('Generating config and environment files...');
62
+ await generateConfig(projectPath, config);
63
+ await generateEnvFiles(projectPath, config);
64
+ spinner.succeed('Config and environment files created');
65
+ spinner.start('Generating project files...');
66
+ await generateLandingPage(projectPath, config);
67
+ await generateGitignore(projectPath);
68
+ await generateReadme(projectPath, config);
69
+ await generateClaudeMd(projectPath, config);
70
+ spinner.succeed('Project files created');
71
+ console.log();
72
+ console.log(chalk.green.bold(' Success!') + ' Created ' + chalk.cyan(config.name));
73
+ console.log();
74
+ if (config.integrations.length > 0) {
75
+ console.log(chalk.bold(' Installed integrations:'));
76
+ config.integrations.forEach((integration) => {
77
+ console.log(chalk.gray(` - ${integration}`));
78
+ });
79
+ console.log();
80
+ }
81
+ console.log(chalk.bold(' Next steps:'));
82
+ console.log(chalk.gray(` 1. cd ${config.name}`));
83
+ console.log(chalk.gray(' 2. Configure .env.local with your API keys'));
84
+ console.log(chalk.gray(' 3. npm run dev'));
85
+ console.log();
86
+ if (config.integrations.includes('neon-drizzle')) {
87
+ console.log(chalk.yellow(' Database commands:'));
88
+ console.log(chalk.gray(' npm run db:generate - Generate migrations'));
89
+ console.log(chalk.gray(' npm run db:migrate - Run migrations'));
90
+ console.log(chalk.gray(' npm run db:studio - Open Drizzle Studio'));
91
+ console.log();
92
+ }
93
+ if (config.integrations.includes('inngest')) {
94
+ console.log(chalk.yellow(' Inngest commands:'));
95
+ console.log(chalk.gray(' npm run inngest:dev - Start Inngest dev server'));
96
+ console.log();
97
+ }
98
+ }
99
+ async function addIntegrationFlow(projectPath) {
100
+ console.log(chalk.yellow(' Existing Next.js project detected'));
101
+ console.log(chalk.gray(' Running in add integration mode'));
102
+ console.log();
103
+ const installed = await getInstalledIntegrations(projectPath);
104
+ const available = getAvailableIntegrations(installed);
105
+ if (available.length === 0) {
106
+ console.log(chalk.green(' All integrations already installed!'));
107
+ return;
108
+ }
109
+ if (installed.length > 0) {
110
+ console.log(chalk.gray(' Already installed: ') + installed.join(', '));
111
+ console.log();
112
+ }
113
+ const addConfig = await getAddIntegrationConfig(available);
114
+ if (addConfig.integrations.length === 0) {
115
+ console.log();
116
+ console.log(chalk.gray(' No integrations selected'));
117
+ return;
118
+ }
119
+ console.log();
120
+ const spinner = ora(`Installing ${addConfig.integrations.length} integration(s)...`).start();
121
+ const config = {
122
+ name: path.basename(projectPath),
123
+ integrations: addConfig.integrations,
124
+ aiProvider: addConfig.aiProvider,
125
+ };
126
+ await installIntegrations(projectPath, config);
127
+ spinner.succeed('Integrations installed');
128
+ spinner.start('Updating config and environment files...');
129
+ await appendConfig(projectPath, addConfig.integrations, addConfig.aiProvider);
130
+ await appendEnvFiles(projectPath, addConfig.integrations, addConfig.aiProvider);
131
+ spinner.succeed('Config and environment files updated');
132
+ spinner.start('Updating CLAUDE.md...');
133
+ await appendClaudeMd(projectPath, addConfig.integrations);
134
+ spinner.succeed('CLAUDE.md updated');
135
+ console.log();
136
+ console.log(chalk.green.bold(' Success!') + ' Added integrations:');
137
+ addConfig.integrations.forEach((integration) => {
138
+ console.log(chalk.gray(` - ${integration}`));
139
+ });
140
+ console.log();
141
+ console.log(chalk.bold(' Next steps:'));
142
+ console.log(chalk.gray(' 1. Update .env.local with new API keys'));
143
+ console.log(chalk.gray(' 2. npm run dev'));
144
+ console.log();
145
+ if (addConfig.integrations.includes('neon-drizzle')) {
146
+ console.log(chalk.yellow(' Database commands:'));
147
+ console.log(chalk.gray(' npm run db:generate - Generate migrations'));
148
+ console.log(chalk.gray(' npm run db:migrate - Run migrations'));
149
+ console.log(chalk.gray(' npm run db:studio - Open Drizzle Studio'));
150
+ console.log();
151
+ }
152
+ if (addConfig.integrations.includes('inngest')) {
153
+ console.log(chalk.yellow(' Inngest commands:'));
154
+ console.log(chalk.gray(' npm run inngest:dev - Start Inngest dev server'));
155
+ console.log();
156
+ }
157
+ }
158
+ async function extendUtils(projectPath) {
159
+ const utilsPath = path.join(projectPath, 'lib/utils.ts');
160
+ const existingContent = await fs.readFile(utilsPath, 'utf-8');
161
+ const additionalUtils = `
162
+ import { DateTime } from 'luxon';
163
+
164
+ export function formatDate(date: Date | string, format = 'LLL d, yyyy'): string {
165
+ const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
166
+ return dt.toFormat(format);
167
+ }
168
+
169
+ export function formatRelative(date: Date | string): string {
170
+ const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
171
+ return dt.toRelative() ?? dt.toFormat('LLL d, yyyy');
172
+ }
173
+
174
+ export function debounce<P extends unknown[], R>(
175
+ func: (...args: P) => R,
176
+ wait: number
177
+ ): (...args: P) => void {
178
+ let timeout: ReturnType<typeof setTimeout>;
179
+ return (...args: P) => {
180
+ clearTimeout(timeout);
181
+ timeout = setTimeout(() => func(...args), wait);
182
+ };
183
+ }
184
+ `;
185
+ await fs.writeFile(utilsPath, existingContent + additionalUtils);
186
+ }
@@ -0,0 +1,3 @@
1
+ import type { IntegrationId, ProjectConfig, AIProviderChoice } from './types.js';
2
+ export declare function generateConfig(projectPath: string, config: ProjectConfig): Promise<void>;
3
+ export declare function appendConfig(projectPath: string, integrations: IntegrationId[], aiProvider?: AIProviderChoice): Promise<void>;
package/dist/config.js ADDED
@@ -0,0 +1,98 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getAiConfigVar } from './templates/ai-sdk.js';
4
+ const staticConfigVars = {
5
+ core: [],
6
+ clerk: [
7
+ { name: 'CLERK_PUBLISHABLE_KEY', envKey: 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', isPublic: true },
8
+ { name: 'CLERK_SECRET_KEY', envKey: 'CLERK_SECRET_KEY', isPublic: false },
9
+ ],
10
+ 'neon-drizzle': [
11
+ { name: 'DATABASE_URL', envKey: 'DATABASE_URL', isPublic: false },
12
+ ],
13
+ resend: [
14
+ { name: 'RESEND_API_KEY', envKey: 'RESEND_API_KEY', isPublic: false },
15
+ { name: 'RESEND_FROM_EMAIL', envKey: 'RESEND_FROM_EMAIL', isPublic: false, defaultValue: 'onboarding@resend.dev' },
16
+ ],
17
+ firecrawl: [
18
+ { name: 'FIRECRAWL_API_KEY', envKey: 'FIRECRAWL_API_KEY', isPublic: false },
19
+ ],
20
+ inngest: [
21
+ { name: 'INNGEST_EVENT_KEY', envKey: 'INNGEST_EVENT_KEY', isPublic: false },
22
+ { name: 'INNGEST_SIGNING_KEY', envKey: 'INNGEST_SIGNING_KEY', isPublic: false },
23
+ ],
24
+ uploadthing: [
25
+ { name: 'UPLOADTHING_TOKEN', envKey: 'UPLOADTHING_TOKEN', isPublic: false },
26
+ ],
27
+ stripe: [
28
+ { name: 'STRIPE_SECRET_KEY', envKey: 'STRIPE_SECRET_KEY', isPublic: false },
29
+ { name: 'STRIPE_PUBLISHABLE_KEY', envKey: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', isPublic: true },
30
+ { name: 'STRIPE_WEBHOOK_SECRET', envKey: 'STRIPE_WEBHOOK_SECRET', isPublic: false },
31
+ ],
32
+ posthog: [
33
+ { name: 'POSTHOG_KEY', envKey: 'NEXT_PUBLIC_POSTHOG_KEY', isPublic: true },
34
+ { name: 'POSTHOG_HOST', envKey: 'NEXT_PUBLIC_POSTHOG_HOST', isPublic: true, defaultValue: 'https://us.i.posthog.com' },
35
+ ],
36
+ sentry: [
37
+ { name: 'SENTRY_DSN', envKey: 'NEXT_PUBLIC_SENTRY_DSN', isPublic: true },
38
+ { name: 'SENTRY_AUTH_TOKEN', envKey: 'SENTRY_AUTH_TOKEN', isPublic: false },
39
+ { name: 'SENTRY_ORG', envKey: 'SENTRY_ORG', isPublic: false },
40
+ { name: 'SENTRY_PROJECT', envKey: 'SENTRY_PROJECT', isPublic: false },
41
+ ],
42
+ };
43
+ function getConfigVars(id, aiProvider) {
44
+ if (id === 'ai-sdk') {
45
+ const aiVar = getAiConfigVar(aiProvider ?? 'openai');
46
+ return [{ name: aiVar.name, envKey: aiVar.envKey, isPublic: false }];
47
+ }
48
+ return staticConfigVars[id] ?? [];
49
+ }
50
+ export async function generateConfig(projectPath, config) {
51
+ const selectedIds = ['core', ...config.integrations];
52
+ let content = '';
53
+ for (const id of selectedIds) {
54
+ const vars = getConfigVars(id, config.aiProvider);
55
+ if (!vars || vars.length === 0)
56
+ continue;
57
+ for (const v of vars) {
58
+ if (v.defaultValue) {
59
+ content += `export const ${v.name} = process.env.${v.envKey} ?? '${v.defaultValue}';\n`;
60
+ }
61
+ else {
62
+ content += `export const ${v.name} = process.env.${v.envKey} as string;\n`;
63
+ }
64
+ }
65
+ content += '\n';
66
+ }
67
+ await fs.mkdir(path.join(projectPath, 'lib'), { recursive: true });
68
+ await fs.writeFile(path.join(projectPath, 'lib/config.ts'), content.trim() + '\n');
69
+ }
70
+ export async function appendConfig(projectPath, integrations, aiProvider) {
71
+ const configPath = path.join(projectPath, 'lib/config.ts');
72
+ let existing = '';
73
+ try {
74
+ existing = await fs.readFile(configPath, 'utf-8');
75
+ }
76
+ catch {
77
+ existing = '';
78
+ }
79
+ let content = '';
80
+ for (const id of integrations) {
81
+ const vars = getConfigVars(id, aiProvider);
82
+ if (!vars || vars.length === 0)
83
+ continue;
84
+ for (const v of vars) {
85
+ if (v.defaultValue) {
86
+ content += `export const ${v.name} = process.env.${v.envKey} ?? '${v.defaultValue}';\n`;
87
+ }
88
+ else {
89
+ content += `export const ${v.name} = process.env.${v.envKey} as string;\n`;
90
+ }
91
+ }
92
+ content += '\n';
93
+ }
94
+ if (content) {
95
+ const newContent = existing.trimEnd() + '\n\n' + content.trim() + '\n';
96
+ await fs.writeFile(configPath, newContent);
97
+ }
98
+ }
@@ -0,0 +1 @@
1
+ export declare function createNextApp(name: string): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { execa } from 'execa';
2
+ import path from 'path';
3
+ export async function createNextApp(name) {
4
+ await execa('npx', [
5
+ 'create-next-app@latest',
6
+ name,
7
+ '--typescript',
8
+ '--tailwind',
9
+ '--eslint',
10
+ '--app',
11
+ '--import-alias', '@/*',
12
+ '--yes',
13
+ ], {
14
+ stdio: 'inherit',
15
+ });
16
+ return path.resolve(process.cwd(), name);
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { IntegrationId } from './types.js';
2
+ export declare function isExistingProject(cwd: string): Promise<boolean>;
3
+ export declare function getInstalledIntegrations(cwd: string): Promise<IntegrationId[]>;
4
+ export declare function getAvailableIntegrations(installed: IntegrationId[]): IntegrationId[];
package/dist/detect.js ADDED
@@ -0,0 +1,60 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ const integrationPackages = {
4
+ clerk: ['@clerk/nextjs'],
5
+ 'neon-drizzle': ['drizzle-orm', '@neondatabase/serverless'],
6
+ 'ai-sdk': ['ai'],
7
+ resend: ['resend'],
8
+ firecrawl: ['@mendable/firecrawl-js'],
9
+ inngest: ['inngest'],
10
+ uploadthing: ['uploadthing'],
11
+ stripe: ['stripe'],
12
+ posthog: ['posthog-js'],
13
+ sentry: ['@sentry/nextjs'],
14
+ };
15
+ export async function isExistingProject(cwd) {
16
+ try {
17
+ const pkgPath = path.join(cwd, 'package.json');
18
+ const content = await fs.readFile(pkgPath, 'utf-8');
19
+ const pkg = JSON.parse(content);
20
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
21
+ return 'next' in allDeps;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export async function getInstalledIntegrations(cwd) {
28
+ try {
29
+ const pkgPath = path.join(cwd, 'package.json');
30
+ const content = await fs.readFile(pkgPath, 'utf-8');
31
+ const pkg = JSON.parse(content);
32
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
33
+ const installed = [];
34
+ for (const [integrationId, packages] of Object.entries(integrationPackages)) {
35
+ const hasAll = packages.every((pkg) => pkg in allDeps);
36
+ if (hasAll) {
37
+ installed.push(integrationId);
38
+ }
39
+ }
40
+ return installed;
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ export function getAvailableIntegrations(installed) {
47
+ const all = [
48
+ 'clerk',
49
+ 'neon-drizzle',
50
+ 'ai-sdk',
51
+ 'resend',
52
+ 'firecrawl',
53
+ 'inngest',
54
+ 'uploadthing',
55
+ 'stripe',
56
+ 'posthog',
57
+ 'sentry',
58
+ ];
59
+ return all.filter((id) => !installed.includes(id));
60
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { IntegrationId, ProjectConfig, AIProviderChoice } from './types.js';
2
+ export declare function generateEnvFiles(projectPath: string, config: ProjectConfig): Promise<void>;
3
+ export declare function appendEnvFiles(projectPath: string, integrations: IntegrationId[], aiProvider?: AIProviderChoice): Promise<void>;
package/dist/env.js ADDED
@@ -0,0 +1,183 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getAiEnvVar } from './templates/ai-sdk.js';
4
+ const staticEnvSections = {
5
+ core: {
6
+ name: 'CORE',
7
+ vars: [],
8
+ },
9
+ clerk: {
10
+ name: 'CLERK - Authentication',
11
+ url: 'https://dashboard.clerk.com',
12
+ vars: [
13
+ { key: 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', example: 'pk_test_...', description: 'Clerk publishable key' },
14
+ { key: 'CLERK_SECRET_KEY', example: 'sk_test_...', description: 'Clerk secret key' },
15
+ ],
16
+ },
17
+ 'neon-drizzle': {
18
+ name: 'NEON - Database',
19
+ url: 'https://console.neon.tech',
20
+ vars: [
21
+ { key: 'DATABASE_URL', example: 'postgresql://user:pass@host/db?sslmode=require', description: 'Neon database connection string' },
22
+ ],
23
+ },
24
+ resend: {
25
+ name: 'RESEND - Email',
26
+ url: 'https://resend.com/api-keys',
27
+ vars: [
28
+ { key: 'RESEND_API_KEY', example: 're_...', description: 'Resend API key' },
29
+ { key: 'RESEND_FROM_EMAIL', example: 'onboarding@resend.dev', description: 'Default from email address' },
30
+ ],
31
+ },
32
+ firecrawl: {
33
+ name: 'FIRECRAWL - Scraping',
34
+ url: 'https://firecrawl.dev',
35
+ vars: [
36
+ { key: 'FIRECRAWL_API_KEY', example: 'fc-...', description: 'Firecrawl API key' },
37
+ ],
38
+ },
39
+ inngest: {
40
+ name: 'INNGEST - Background Jobs',
41
+ url: 'https://app.inngest.com',
42
+ vars: [
43
+ { key: 'INNGEST_EVENT_KEY', example: '...', description: 'Inngest event key' },
44
+ { key: 'INNGEST_SIGNING_KEY', example: '...', description: 'Inngest signing key' },
45
+ ],
46
+ },
47
+ uploadthing: {
48
+ name: 'UPLOADTHING - File Uploads',
49
+ url: 'https://uploadthing.com/dashboard',
50
+ vars: [
51
+ { key: 'UPLOADTHING_TOKEN', example: '...', description: 'UploadThing token' },
52
+ ],
53
+ },
54
+ stripe: {
55
+ name: 'STRIPE - Payments',
56
+ url: 'https://dashboard.stripe.com/apikeys',
57
+ vars: [
58
+ { key: 'STRIPE_SECRET_KEY', example: 'sk_test_...', description: 'Stripe secret key' },
59
+ { key: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', example: 'pk_test_...', description: 'Stripe publishable key' },
60
+ { key: 'STRIPE_WEBHOOK_SECRET', example: 'whsec_...', description: 'Stripe webhook secret' },
61
+ ],
62
+ },
63
+ posthog: {
64
+ name: 'POSTHOG - Analytics',
65
+ url: 'https://app.posthog.com/project/settings',
66
+ vars: [
67
+ { key: 'NEXT_PUBLIC_POSTHOG_KEY', example: 'phc_...', description: 'PostHog project API key' },
68
+ { key: 'NEXT_PUBLIC_POSTHOG_HOST', example: 'https://us.i.posthog.com', description: 'PostHog host' },
69
+ ],
70
+ },
71
+ sentry: {
72
+ name: 'SENTRY - Error Tracking',
73
+ url: 'https://sentry.io/settings/projects',
74
+ vars: [
75
+ { key: 'NEXT_PUBLIC_SENTRY_DSN', example: 'https://...@sentry.io/...', description: 'Sentry DSN' },
76
+ { key: 'SENTRY_AUTH_TOKEN', example: 'sntrys_...', description: 'Sentry auth token for source maps' },
77
+ { key: 'SENTRY_ORG', example: 'your-org', description: 'Sentry organization slug' },
78
+ { key: 'SENTRY_PROJECT', example: 'your-project', description: 'Sentry project slug' },
79
+ ],
80
+ },
81
+ };
82
+ function getAiEnvSection(provider) {
83
+ const envVar = getAiEnvVar(provider);
84
+ const urls = {
85
+ openai: 'https://platform.openai.com/api-keys',
86
+ anthropic: 'https://console.anthropic.com/settings/keys',
87
+ google: 'https://aistudio.google.com/apikey',
88
+ };
89
+ const names = {
90
+ openai: 'OPENAI - AI',
91
+ anthropic: 'ANTHROPIC - AI',
92
+ google: 'GOOGLE - AI',
93
+ };
94
+ return {
95
+ name: names[provider],
96
+ url: urls[provider],
97
+ vars: [envVar],
98
+ };
99
+ }
100
+ function getEnvSection(id, aiProvider) {
101
+ if (id === 'ai-sdk') {
102
+ return getAiEnvSection(aiProvider ?? 'openai');
103
+ }
104
+ return staticEnvSections[id];
105
+ }
106
+ function generateEnvSection(section, includeExamples) {
107
+ let content = `# ===========================================\n`;
108
+ content += `# ${section.name}\n`;
109
+ if (section.url) {
110
+ content += `# Get keys at: ${section.url}\n`;
111
+ }
112
+ content += `# ===========================================\n`;
113
+ for (const v of section.vars) {
114
+ if (includeExamples) {
115
+ content += `${v.key}=${v.example}\n`;
116
+ }
117
+ else {
118
+ content += `${v.key}=\n`;
119
+ }
120
+ }
121
+ return content;
122
+ }
123
+ export async function generateEnvFiles(projectPath, config) {
124
+ const selectedIds = ['core', ...config.integrations];
125
+ // Generate .env.example with all vars and examples
126
+ let envExample = '';
127
+ for (const id of selectedIds) {
128
+ const section = getEnvSection(id, config.aiProvider);
129
+ envExample += generateEnvSection(section, true);
130
+ envExample += '\n';
131
+ }
132
+ await fs.writeFile(path.join(projectPath, '.env.example'), envExample.trim() + '\n');
133
+ // Generate .env.local with empty values
134
+ let envLocal = '';
135
+ for (const id of selectedIds) {
136
+ const section = getEnvSection(id, config.aiProvider);
137
+ envLocal += generateEnvSection(section, false);
138
+ envLocal += '\n';
139
+ }
140
+ await fs.writeFile(path.join(projectPath, '.env.local'), envLocal.trim() + '\n');
141
+ // Update .gitignore to include .env.local
142
+ const gitignorePath = path.join(projectPath, '.gitignore');
143
+ try {
144
+ let gitignore = await fs.readFile(gitignorePath, 'utf-8');
145
+ if (!gitignore.includes('.env.local')) {
146
+ gitignore += '\n# Environment variables\n.env.local\n.env*.local\n';
147
+ await fs.writeFile(gitignorePath, gitignore);
148
+ }
149
+ }
150
+ catch {
151
+ // .gitignore doesn't exist, create it
152
+ await fs.writeFile(gitignorePath, '# Environment variables\n.env.local\n.env*.local\n');
153
+ }
154
+ }
155
+ export async function appendEnvFiles(projectPath, integrations, aiProvider) {
156
+ const envExamplePath = path.join(projectPath, '.env.example');
157
+ const envLocalPath = path.join(projectPath, '.env.local');
158
+ let envExampleContent = '';
159
+ let envLocalContent = '';
160
+ for (const id of integrations) {
161
+ const section = getEnvSection(id, aiProvider);
162
+ envExampleContent += '\n' + generateEnvSection(section, true);
163
+ envLocalContent += '\n' + generateEnvSection(section, false);
164
+ }
165
+ if (envExampleContent) {
166
+ try {
167
+ const existing = await fs.readFile(envExamplePath, 'utf-8');
168
+ await fs.writeFile(envExamplePath, existing.trimEnd() + '\n' + envExampleContent.trim() + '\n');
169
+ }
170
+ catch {
171
+ await fs.writeFile(envExamplePath, envExampleContent.trim() + '\n');
172
+ }
173
+ }
174
+ if (envLocalContent) {
175
+ try {
176
+ const existing = await fs.readFile(envLocalPath, 'utf-8');
177
+ await fs.writeFile(envLocalPath, existing.trimEnd() + '\n' + envLocalContent.trim() + '\n');
178
+ }
179
+ catch {
180
+ await fs.writeFile(envLocalPath, envLocalContent.trim() + '\n');
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from './types.js';
2
+ export declare function generateReadme(projectPath: string, config: ProjectConfig): Promise<void>;
3
+ export declare function generateGitignore(projectPath: string): Promise<void>;