create-loadout 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,19 +1,16 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import fs from 'fs/promises';
4
3
  import path from 'path';
5
4
  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';
5
+ import { createProject, addIntegrations } from './engine.js';
16
6
  import { isExistingProject, getInstalledIntegrations, getAvailableIntegrations, } from './detect.js';
7
+ function oraReporter() {
8
+ const spinner = ora();
9
+ const callback = (step) => {
10
+ spinner.start(step);
11
+ };
12
+ return { spinner, callback };
13
+ }
17
14
  export async function main() {
18
15
  console.log();
19
16
  console.log(chalk.bold.cyan(' create-loadout'));
@@ -37,36 +34,8 @@ export async function main() {
37
34
  async function newProjectFlow() {
38
35
  const config = await getProjectConfig();
39
36
  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);
37
+ const { spinner, callback } = oraReporter();
38
+ const result = await createProject(config, callback);
70
39
  spinner.succeed('Project files created');
71
40
  console.log();
72
41
  console.log(chalk.green.bold(' Success!') + ' Created ' + chalk.cyan(config.name));
@@ -80,7 +49,7 @@ async function newProjectFlow() {
80
49
  }
81
50
  console.log(chalk.bold(' Next steps:'));
82
51
  console.log(chalk.gray(` 1. cd ${config.name}`));
83
- console.log(chalk.gray(' 2. Configure .env.local with your API keys'));
52
+ console.log(chalk.gray(' 2. Configure .env with your API keys'));
84
53
  console.log(chalk.gray(' 3. npm run dev'));
85
54
  console.log();
86
55
  if (config.integrations.includes('neon-drizzle')) {
@@ -117,21 +86,14 @@ async function addIntegrationFlow(projectPath) {
117
86
  return;
118
87
  }
119
88
  console.log();
120
- const spinner = ora(`Installing ${addConfig.integrations.length} integration(s)...`).start();
121
89
  const config = {
122
90
  name: path.basename(projectPath),
123
91
  integrations: addConfig.integrations,
124
92
  aiProvider: addConfig.aiProvider,
125
93
  };
126
- await installIntegrations(projectPath, config);
94
+ const { spinner, callback } = oraReporter();
95
+ const result = await addIntegrations(projectPath, config, callback);
127
96
  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
97
  console.log();
136
98
  console.log(chalk.green.bold(' Success!') + ' Added integrations:');
137
99
  addConfig.integrations.forEach((integration) => {
@@ -139,7 +101,7 @@ async function addIntegrationFlow(projectPath) {
139
101
  });
140
102
  console.log();
141
103
  console.log(chalk.bold(' Next steps:'));
142
- console.log(chalk.gray(' 1. Update .env.local with new API keys'));
104
+ console.log(chalk.gray(' 1. Update .env with new API keys'));
143
105
  console.log(chalk.gray(' 2. npm run dev'));
144
106
  console.log();
145
107
  if (addConfig.integrations.includes('neon-drizzle')) {
@@ -155,32 +117,3 @@ async function addIntegrationFlow(projectPath) {
155
117
  console.log();
156
118
  }
157
119
  }
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
- }
@@ -11,7 +11,7 @@ export async function createNextApp(name) {
11
11
  '--import-alias', '@/*',
12
12
  '--yes',
13
13
  ], {
14
- stdio: 'inherit',
14
+ stdio: 'pipe',
15
15
  });
16
16
  return path.resolve(process.cwd(), name);
17
17
  }
@@ -0,0 +1,9 @@
1
+ import type { ProjectConfig, IntegrationId } from './types.js';
2
+ export type ProgressCallback = (step: string) => void;
3
+ export interface EngineResult {
4
+ projectPath: string;
5
+ integrations: IntegrationId[];
6
+ envVarsNeeded: string[];
7
+ }
8
+ export declare function createProject(config: ProjectConfig, onProgress?: ProgressCallback): Promise<EngineResult>;
9
+ export declare function addIntegrations(projectPath: string, config: ProjectConfig, onProgress?: ProgressCallback): Promise<EngineResult>;
package/dist/engine.js ADDED
@@ -0,0 +1,84 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { createNextApp } from './create-next.js';
4
+ import { setupShadcn } from './setup-shadcn.js';
5
+ import { installIntegrations, getEnvVars } from './integrations/index.js';
6
+ import { generateClaudeMd, appendClaudeMd } from './claude-md.js';
7
+ import { generateEnvFiles, appendEnvFiles } from './env.js';
8
+ import { generateConfig, appendConfig } from './config.js';
9
+ import { generateReadme, generateGitignore } from './generate-readme.js';
10
+ import { generateInstrumentationClient, generateInstrumentation, } from './instrumentation.js';
11
+ import { zustandTemplates } from './templates/zustand.js';
12
+ import { generateLandingPage } from './landing-page.js';
13
+ export async function createProject(config, onProgress) {
14
+ onProgress?.('Creating Next.js app...');
15
+ const projectPath = await createNextApp(config.name);
16
+ onProgress?.('Setting up shadcn/ui...');
17
+ await setupShadcn(projectPath);
18
+ await extendUtils(projectPath);
19
+ onProgress?.('Installing base packages...');
20
+ const { execa } = await import('execa');
21
+ await execa('npm', ['install', 'zod', 'zustand', 'luxon'], { cwd: projectPath });
22
+ await execa('npm', ['install', '-D', '@types/luxon'], { cwd: projectPath });
23
+ await fs.mkdir(path.join(projectPath, 'lib/stores'), { recursive: true });
24
+ await fs.writeFile(path.join(projectPath, 'lib/stores/counter.store.ts'), zustandTemplates.exampleStore);
25
+ if (config.integrations.length > 0) {
26
+ onProgress?.(`Setting up ${config.integrations.length} integration(s)...`);
27
+ await installIntegrations(projectPath, config);
28
+ }
29
+ await generateInstrumentationClient(projectPath, config);
30
+ await generateInstrumentation(projectPath, config);
31
+ onProgress?.('Generating config and environment files...');
32
+ await generateConfig(projectPath, config);
33
+ await generateEnvFiles(projectPath, config);
34
+ onProgress?.('Generating project files...');
35
+ await generateLandingPage(projectPath, config);
36
+ await generateGitignore(projectPath);
37
+ await generateReadme(projectPath, config);
38
+ await generateClaudeMd(projectPath, config);
39
+ const envVarsNeeded = collectEnvVars(config);
40
+ return { projectPath, integrations: config.integrations, envVarsNeeded };
41
+ }
42
+ export async function addIntegrations(projectPath, config, onProgress) {
43
+ onProgress?.(`Installing ${config.integrations.length} integration(s)...`);
44
+ await installIntegrations(projectPath, config);
45
+ onProgress?.('Updating config and environment files...');
46
+ await appendConfig(projectPath, config.integrations, config.aiProvider);
47
+ await appendEnvFiles(projectPath, config.integrations, config.aiProvider);
48
+ onProgress?.('Updating CLAUDE.md...');
49
+ await appendClaudeMd(projectPath, config.integrations);
50
+ const envVarsNeeded = collectEnvVars(config);
51
+ return { projectPath, integrations: config.integrations, envVarsNeeded };
52
+ }
53
+ async function extendUtils(projectPath) {
54
+ const utilsPath = path.join(projectPath, 'lib/utils.ts');
55
+ const existingContent = await fs.readFile(utilsPath, 'utf-8');
56
+ const additionalUtils = `
57
+ import { DateTime } from 'luxon';
58
+
59
+ export function formatDate(date: Date | string, format = 'LLL d, yyyy'): string {
60
+ const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
61
+ return dt.toFormat(format);
62
+ }
63
+
64
+ export function formatRelative(date: Date | string): string {
65
+ const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
66
+ return dt.toRelative() ?? dt.toFormat('LLL d, yyyy');
67
+ }
68
+
69
+ export function debounce<P extends unknown[], R>(
70
+ func: (...args: P) => R,
71
+ wait: number
72
+ ): (...args: P) => void {
73
+ let timeout: ReturnType<typeof setTimeout>;
74
+ return (...args: P) => {
75
+ clearTimeout(timeout);
76
+ timeout = setTimeout(() => func(...args), wait);
77
+ };
78
+ }
79
+ `;
80
+ await fs.writeFile(utilsPath, existingContent + additionalUtils);
81
+ }
82
+ function collectEnvVars(config) {
83
+ return getEnvVars(config).map((v) => v.key);
84
+ }
package/dist/env.js CHANGED
@@ -138,37 +138,24 @@ export async function generateEnvFiles(projectPath, config) {
138
138
  envExample += '\n';
139
139
  }
140
140
  await fs.writeFile(path.join(projectPath, '.env.example'), envExample.trim() + '\n');
141
- // Generate .env.local with empty values
142
- let envLocal = '';
141
+ // Generate .env with empty values
142
+ let env = '';
143
143
  for (const id of selectedIds) {
144
144
  const section = getEnvSection(id, config.aiProvider);
145
- envLocal += generateEnvSection(section, false);
146
- envLocal += '\n';
147
- }
148
- await fs.writeFile(path.join(projectPath, '.env.local'), envLocal.trim() + '\n');
149
- // Update .gitignore to include .env.local
150
- const gitignorePath = path.join(projectPath, '.gitignore');
151
- try {
152
- let gitignore = await fs.readFile(gitignorePath, 'utf-8');
153
- if (!gitignore.includes('.env.local')) {
154
- gitignore += '\n# Environment variables\n.env.local\n.env*.local\n';
155
- await fs.writeFile(gitignorePath, gitignore);
156
- }
157
- }
158
- catch {
159
- // .gitignore doesn't exist, create it
160
- await fs.writeFile(gitignorePath, '# Environment variables\n.env.local\n.env*.local\n');
145
+ env += generateEnvSection(section, false);
146
+ env += '\n';
161
147
  }
148
+ await fs.writeFile(path.join(projectPath, '.env'), env.trim() + '\n');
162
149
  }
163
150
  export async function appendEnvFiles(projectPath, integrations, aiProvider) {
164
151
  const envExamplePath = path.join(projectPath, '.env.example');
165
- const envLocalPath = path.join(projectPath, '.env.local');
152
+ const envPath = path.join(projectPath, '.env');
166
153
  let envExampleContent = '';
167
- let envLocalContent = '';
154
+ let envContent = '';
168
155
  for (const id of integrations) {
169
156
  const section = getEnvSection(id, aiProvider);
170
157
  envExampleContent += '\n' + generateEnvSection(section, true);
171
- envLocalContent += '\n' + generateEnvSection(section, false);
158
+ envContent += '\n' + generateEnvSection(section, false);
172
159
  }
173
160
  if (envExampleContent) {
174
161
  try {
@@ -179,13 +166,13 @@ export async function appendEnvFiles(projectPath, integrations, aiProvider) {
179
166
  await fs.writeFile(envExamplePath, envExampleContent.trim() + '\n');
180
167
  }
181
168
  }
182
- if (envLocalContent) {
169
+ if (envContent) {
183
170
  try {
184
- const existing = await fs.readFile(envLocalPath, 'utf-8');
185
- await fs.writeFile(envLocalPath, existing.trimEnd() + '\n' + envLocalContent.trim() + '\n');
171
+ const existing = await fs.readFile(envPath, 'utf-8');
172
+ await fs.writeFile(envPath, existing.trimEnd() + '\n' + envContent.trim() + '\n');
186
173
  }
187
174
  catch {
188
- await fs.writeFile(envLocalPath, envLocalContent.trim() + '\n');
175
+ await fs.writeFile(envPath, envContent.trim() + '\n');
189
176
  }
190
177
  }
191
178
  }
@@ -14,36 +14,36 @@ const integrationNames = {
14
14
  sentry: 'Sentry',
15
15
  };
16
16
  export async function generateReadme(projectPath, config) {
17
- let content = `# ${config.name}
18
-
19
- A Next.js application scaffolded with [create-loadout](https://github.com/your-org/create-loadout).
20
-
21
- ## Getting Started
22
-
23
- 1. Install dependencies:
24
- \`\`\`bash
25
- npm install
26
- \`\`\`
27
-
28
- 2. Copy the environment file and configure your API keys:
29
- \`\`\`bash
30
- cp .env.example .env.local
31
- \`\`\`
32
-
33
- 3. Start the development server:
34
- \`\`\`bash
35
- npm run dev
36
- \`\`\`
37
-
38
- 4. Open [http://localhost:3000](http://localhost:3000) in your browser.
39
-
40
- ## Tech Stack
41
-
42
- - [Next.js](https://nextjs.org/) - React framework
43
- - [TypeScript](https://www.typescriptlang.org/) - Type safety
44
- - [Tailwind CSS](https://tailwindcss.com/) - Styling
45
- - [shadcn/ui](https://ui.shadcn.com/) - UI components
46
- - [Zod](https://zod.dev/) - Schema validation
17
+ let content = `# ${config.name}
18
+
19
+ A Next.js application scaffolded with [create-loadout](https://github.com/your-org/create-loadout).
20
+
21
+ ## Getting Started
22
+
23
+ 1. Install dependencies:
24
+ \`\`\`bash
25
+ npm install
26
+ \`\`\`
27
+
28
+ 2. Copy the environment file and configure your API keys:
29
+ \`\`\`bash
30
+ cp .env.example .env
31
+ \`\`\`
32
+
33
+ 3. Start the development server:
34
+ \`\`\`bash
35
+ npm run dev
36
+ \`\`\`
37
+
38
+ 4. Open [http://localhost:3000](http://localhost:3000) in your browser.
39
+
40
+ ## Tech Stack
41
+
42
+ - [Next.js](https://nextjs.org/) - React framework
43
+ - [TypeScript](https://www.typescriptlang.org/) - Type safety
44
+ - [Tailwind CSS](https://tailwindcss.com/) - Styling
45
+ - [shadcn/ui](https://ui.shadcn.com/) - UI components
46
+ - [Zod](https://zod.dev/) - Schema validation
47
47
  `;
48
48
  if (config.integrations.length > 0) {
49
49
  content += `\n### Integrations\n\n`;
@@ -51,111 +51,114 @@ A Next.js application scaffolded with [create-loadout](https://github.com/your-o
51
51
  content += `- ${integrationNames[id]}\n`;
52
52
  }
53
53
  }
54
- content += `
55
- ## Scripts
56
-
57
- \`\`\`bash
58
- npm run dev # Start development server
59
- npm run build # Build for production
60
- npm run start # Start production server
61
- npm run lint # Run ESLint
62
- \`\`\`
54
+ content += `
55
+ ## Scripts
56
+
57
+ \`\`\`bash
58
+ npm run dev # Start development server
59
+ npm run build # Build for production
60
+ npm run start # Start production server
61
+ npm run lint # Run ESLint
62
+ \`\`\`
63
63
  `;
64
64
  if (config.integrations.includes('neon-drizzle')) {
65
- content += `
66
- ### Database
67
-
68
- \`\`\`bash
69
- npm run db:generate # Generate migrations
70
- npm run db:migrate # Run migrations
71
- npm run db:studio # Open Drizzle Studio
72
- \`\`\`
65
+ content += `
66
+ ### Database
67
+
68
+ \`\`\`bash
69
+ npm run db:generate # Generate migrations
70
+ npm run db:migrate # Run migrations
71
+ npm run db:studio # Open Drizzle Studio
72
+ \`\`\`
73
73
  `;
74
74
  }
75
75
  if (config.integrations.includes('inngest')) {
76
- content += `
77
- ### Background Jobs
78
-
79
- \`\`\`bash
80
- npm run inngest:dev # Start Inngest dev server
81
- \`\`\`
76
+ content += `
77
+ ### Background Jobs
78
+
79
+ \`\`\`bash
80
+ npm run inngest:dev # Start Inngest dev server
81
+ \`\`\`
82
82
  `;
83
83
  }
84
- content += `
85
- ## Project Structure
86
-
87
- \`\`\`
88
- ├── app/ # Next.js App Router
89
- ├── components/ # React components
90
- ├── lib/ # Utilities and clients
91
- ├── services/ # Business logic
84
+ content += `
85
+ ## Project Structure
86
+
87
+ \`\`\`
88
+ ├── app/ # Next.js App Router
89
+ ├── components/ # React components
90
+ ├── lib/ # Utilities and clients
91
+ ├── services/ # Business logic
92
92
  `;
93
93
  if (config.integrations.includes('resend') || config.integrations.includes('postmark')) {
94
94
  content += `├── emails/ # Email templates\n`;
95
95
  }
96
- content += `└── public/ # Static assets
97
- \`\`\`
98
-
99
- ## Environment Variables
100
-
101
- See \`.env.example\` for all required environment variables.
102
-
103
- ## Learn More
104
-
105
- - [Next.js Documentation](https://nextjs.org/docs)
106
- - [CLAUDE.md](./CLAUDE.md) - AI assistant context file
96
+ content += `└── public/ # Static assets
97
+ \`\`\`
98
+
99
+ ## Environment Variables
100
+
101
+ See \`.env.example\` for all required environment variables.
102
+
103
+ ## Learn More
104
+
105
+ - [Next.js Documentation](https://nextjs.org/docs)
106
+ - [CLAUDE.md](./CLAUDE.md) - AI assistant context file
107
107
  `;
108
108
  await fs.writeFile(path.join(projectPath, 'README.md'), content);
109
109
  }
110
110
  export async function generateGitignore(projectPath) {
111
- const content = `# Dependencies
112
- node_modules/
113
- .pnp/
114
- .pnp.js
115
-
116
- # Build
117
- .next/
118
- out/
119
- build/
120
- dist/
121
-
122
- # Environment
123
- .env
124
- .env.local
125
- .env.development.local
126
- .env.test.local
127
- .env.production.local
128
-
129
- # Testing
130
- coverage/
131
-
132
- # IDE
133
- .vscode/
134
- .idea/
135
- *.swp
136
- *.swo
137
-
138
- # OS
139
- .DS_Store
140
- Thumbs.db
141
-
142
- # Debug
143
- npm-debug.log*
144
- yarn-debug.log*
145
- yarn-error.log*
146
-
147
- # Vercel
148
- .vercel
149
-
150
- # TypeScript
151
- *.tsbuildinfo
152
- next-env.d.ts
153
-
154
- # Drizzle
155
- drizzle/
156
-
157
- # Sentry
158
- .sentryclirc
111
+ const content = `# Dependencies
112
+ node_modules/
113
+ .pnp/
114
+ .pnp.js
115
+
116
+ # Build
117
+ .next/
118
+ out/
119
+ build/
120
+ dist/
121
+
122
+ # Environment
123
+ .env
124
+ .env.local
125
+ .env.development.local
126
+ .env.test.local
127
+ .env.production.local
128
+
129
+ # Testing
130
+ coverage/
131
+
132
+ # IDE
133
+ .vscode/
134
+ .idea/
135
+ *.swp
136
+ *.swo
137
+
138
+ # Claude Code
139
+ .claude/
140
+
141
+ # OS
142
+ .DS_Store
143
+ Thumbs.db
144
+
145
+ # Debug
146
+ npm-debug.log*
147
+ yarn-debug.log*
148
+ yarn-error.log*
149
+
150
+ # Vercel
151
+ .vercel
152
+
153
+ # TypeScript
154
+ *.tsbuildinfo
155
+ next-env.d.ts
156
+
157
+ # Drizzle
158
+ drizzle/
159
+
160
+ # Sentry
161
+ .sentryclirc
159
162
  `;
160
163
  await fs.writeFile(path.join(projectPath, '.gitignore'), content);
161
164
  }