@launchframe/cli 0.1.11 → 1.0.0-beta.10

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/README.md CHANGED
@@ -2,15 +2,14 @@
2
2
 
3
3
  > Ship your B2B SaaS to production in hours, not weeks.
4
4
 
5
- LaunchFrame is a production-ready SaaS boilerplate that deploys to a single affordable VPS. Get subscriptions, credits, multi-tenancy, feature gating, and API management out of the box.
5
+ ---
6
6
 
7
- ## Status
7
+ **🚀 We're looking for beta testers!** Get free lifetime access to LaunchFrame by joining our [Discord server](https://discord.gg/mH7Xjfeye2) or emailing us at [support@launchframe.dev](mailto:support@launchframe.dev).
8
8
 
9
- LaunchFrame is currently in **private beta**. This CLI exists but does not yet generate projects.
9
+ ---
10
10
 
11
- **[Join the waitlist at launchframe.dev](https://launchframe.dev)** to get early access, exclusive updates, and founding member perks.
11
+ LaunchFrame is a production-ready SaaS boilerplate that deploys to a single affordable VPS. Get subscriptions, credits, multi-tenancy, feature gating, and API management out of the box.
12
12
 
13
- Here's a sneak peek of the CLI experience:
14
13
  ![LaunchFrame CLI Preview](https://unpkg.com/@launchframe/cli@latest/cli.png)
15
14
 
16
15
  ## What You Get
@@ -30,14 +29,39 @@ Here's a sneak peek of the CLI experience:
30
29
  ## Installation
31
30
 
32
31
  ```bash
33
- npx @launchframe/cli init
32
+ npm install -g @launchframe/cli
34
33
  ```
35
34
 
36
- ## Early Access
35
+ ## Quick Start
37
36
 
38
- LaunchFrame is currently in **private beta**.
37
+ Initialize a new LaunchFrame project:
39
38
 
40
- Join the waitlist at **[launchframe.dev](https://launchframe.dev)** to get early access, exclusive updates, and founding member perks.
39
+ ```bash
40
+ launchframe init
41
+ ```
42
+
43
+ ### Local Development
44
+
45
+ Start the full stack locally with Docker:
46
+
47
+ ```bash
48
+ launchframe docker:up
49
+ ```
50
+
51
+ This spins up all services (backend, admin portal, customer portal, website, database, etc.) with hot-reload enabled. Build your domain logic, customize the UI, and test everything locally.
52
+
53
+ ### Deployment
54
+
55
+ When you're ready to deploy to your VPS:
56
+
57
+ ```bash
58
+ launchframe deploy:configure # Set up deployment configuration
59
+ launchframe deploy:set-env # Configure environment variables
60
+ launchframe deploy:init # Initialize the VPS
61
+ launchframe deploy:up # Deploy to production
62
+ ```
63
+
64
+ **[Get started at launchframe.dev](https://launchframe.dev)** | **[Read the docs at docs.launchframe.dev](https://docs.launchframe.dev)**
41
65
 
42
66
  ## Why LaunchFrame?
43
67
 
@@ -53,6 +77,17 @@ Most SaaS boilerplates give you authentication and a database. LaunchFrame gives
53
77
 
54
78
  All tested in production. All ready to customize.
55
79
 
80
+ ## Documentation
81
+
82
+ Full documentation is available at **[docs.launchframe.dev](https://docs.launchframe.dev)**, including:
83
+
84
+ - Getting started guides
85
+ - Architecture overview
86
+ - Deployment instructions
87
+ - API reference
88
+ - Feature customization guides
89
+ - Multi-tenancy patterns
90
+
56
91
  ## License
57
92
 
58
93
  MIT
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@launchframe/cli",
3
- "version": "0.1.11",
3
+ "version": "1.0.0-beta.10",
4
4
  "description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "launchframe": "./src/index.js"
7
+ "launchframe": "src/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/index.js"
@@ -28,7 +28,7 @@
28
28
  "homepage": "https://launchframe.dev",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "https://github.com/launchframe/cli"
31
+ "url": "git+https://github.com/launchframe/cli.git"
32
32
  },
33
33
  "bugs": {
34
34
  "url": "https://github.com/launchframe/cli/issues"
@@ -40,9 +40,9 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "inquirer": "^8.2.5",
44
43
  "chalk": "^4.1.2",
45
44
  "fs-extra": "^11.1.1",
46
- "glob": "^10.3.10"
45
+ "glob": "^10.3.10",
46
+ "inquirer": "^8.2.5"
47
47
  }
48
48
  }
@@ -0,0 +1,102 @@
1
+ const chalk = require('chalk');
2
+ const { clearCache, getCacheInfo } = require('../utils/service-cache');
3
+
4
+ /**
5
+ * Clear service cache
6
+ */
7
+ async function cacheClear() {
8
+ console.log(chalk.yellow('\n⚠️ This will delete all cached services'));
9
+ console.log(chalk.gray('You will need to re-download on next init or service:add\n'));
10
+
11
+ const inquirer = require('inquirer');
12
+ const { confirmed } = await inquirer.prompt([{
13
+ type: 'confirm',
14
+ name: 'confirmed',
15
+ message: 'Continue with cache clear?',
16
+ default: false
17
+ }]);
18
+
19
+ if (!confirmed) {
20
+ console.log('Cancelled');
21
+ return;
22
+ }
23
+
24
+ await clearCache();
25
+ }
26
+
27
+ /**
28
+ * Show cache information
29
+ */
30
+ async function cacheInfo() {
31
+ const info = await getCacheInfo();
32
+
33
+ console.log(chalk.blue('\n📦 Service Cache Information\n'));
34
+
35
+ console.log(chalk.white('Location:'));
36
+ console.log(chalk.gray(` ${info.path}\n`));
37
+
38
+ if (!info.exists) {
39
+ console.log(chalk.yellow('Status: Not initialized'));
40
+ console.log(chalk.gray('Cache will be created on first use\n'));
41
+ return;
42
+ }
43
+
44
+ console.log(chalk.green('Status: Active\n'));
45
+
46
+ if (info.size !== undefined) {
47
+ const sizeMB = (info.size / 1024 / 1024).toFixed(2);
48
+ console.log(chalk.white('Size:'));
49
+ console.log(chalk.gray(` ${sizeMB} MB\n`));
50
+ }
51
+
52
+ if (info.lastUpdate) {
53
+ console.log(chalk.white('Last Updated:'));
54
+ console.log(chalk.gray(` ${info.lastUpdate.toLocaleString()}\n`));
55
+ }
56
+
57
+ if (info.services && info.services.length > 0) {
58
+ console.log(chalk.white('Cached Services:'));
59
+ info.services.forEach(mod => {
60
+ console.log(chalk.gray(` • ${mod}`));
61
+ });
62
+ console.log('');
63
+ } else {
64
+ console.log(chalk.gray('No services cached yet\n'));
65
+ }
66
+
67
+ console.log(chalk.gray('Commands:'));
68
+ console.log(chalk.gray(' launchframe cache:clear - Delete cache'));
69
+ console.log(chalk.gray(' launchframe cache:update - Force update\n'));
70
+ }
71
+
72
+ /**
73
+ * Force update cache
74
+ */
75
+ async function cacheUpdate() {
76
+ const { ensureCacheReady, getCacheInfo } = require('../utils/service-cache');
77
+
78
+ console.log(chalk.blue('\n🔄 Forcing cache update...\n'));
79
+
80
+ try {
81
+ const info = await getCacheInfo();
82
+ const currentServices = info.services || [];
83
+
84
+ if (currentServices.length === 0) {
85
+ console.log(chalk.yellow('No services in cache yet. Use init or service:add to populate.\n'));
86
+ return;
87
+ }
88
+
89
+ // Update cache with current services
90
+ await ensureCacheReady(currentServices);
91
+ console.log(chalk.green('\n✓ Cache updated successfully\n'));
92
+ } catch (error) {
93
+ console.error(chalk.red(`\n❌ Failed to update cache: ${error.message}\n`));
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ module.exports = {
99
+ cacheClear,
100
+ cacheInfo,
101
+ cacheUpdate
102
+ };
@@ -61,9 +61,35 @@ async function deployConfigure() {
61
61
  // Files that need template variable replacement
62
62
  const filesToUpdate = [
63
63
  'infrastructure/.env',
64
- 'infrastructure/.env.example'
64
+ 'infrastructure/.env.example',
65
+ 'infrastructure/docker-compose.yml',
66
+ 'infrastructure/docker-compose.dev.yml',
67
+ 'infrastructure/docker-compose.prod.yml',
68
+ 'infrastructure/traefik.yml',
69
+ 'backend/src/main.ts',
70
+ 'admin-portal/.env.example',
71
+ 'admin-portal/public/env-config.js',
72
+ 'admin-portal/src/config/runtime.ts',
73
+ 'admin-portal/src/config/pageMetadata.ts',
74
+ 'admin-portal/src/App.tsx',
75
+ 'admin-portal/src/components/common/PageTitle.tsx',
76
+ 'admin-portal/src/sentry.tsx',
65
77
  ];
66
78
 
79
+ if (config.variants.tenancy === 'multi-tenant') {
80
+ filesToUpdate.push(
81
+ 'admin-portal/src/pages/FirstProject.tsx',
82
+ 'admin-portal/src/components/projects/NewProject.tsx',
83
+ );
84
+ }
85
+
86
+ if (config.variants.userModel === 'b2b2c') {
87
+ filesToUpdate.push(
88
+ 'admin-portal/src/components/settings/CustomDomain.tsx',
89
+ 'customers-portal/src/App.tsx'
90
+ )
91
+ }
92
+
67
93
  const projectRoot = process.cwd();
68
94
  let filesUpdated = 0;
69
95
 
@@ -134,14 +160,14 @@ async function deployConfigure() {
134
160
 
135
161
  const updatedConfig = {
136
162
  ...config,
137
- primaryDomain: deployAnswers.primaryDomain,
138
- githubOrg: deployAnswers.githubOrg,
139
- vpsAppFolder: deployAnswers.vpsAppFolder,
140
163
  deployConfigured: true,
141
164
  deployment: {
142
165
  adminEmail: deployAnswers.adminEmail,
143
166
  vpsHost: deployAnswers.vpsHost,
144
167
  vpsUser: deployAnswers.vpsUser,
168
+ vpsAppFolder: deployAnswers.vpsAppFolder,
169
+ primaryDomain: deployAnswers.primaryDomain,
170
+ githubOrg: deployAnswers.githubOrg,
145
171
  ghcrToken: deployAnswers.ghcrToken,
146
172
  configuredAt: new Date().toISOString()
147
173
  }
@@ -9,13 +9,11 @@ const {
9
9
  checkSSHKeys,
10
10
  executeSSH,
11
11
  copyFileToVPS,
12
- copyDirectoryToVPS,
13
- checkRepoPrivacy,
14
- showDeployKeyInstructions
12
+ copyDirectoryToVPS
15
13
  } = require('../utils/ssh-helper');
16
14
 
17
15
  /**
18
- * Initial VPS setup - clone repo and configure environment
16
+ * Initial VPS setup - copy infrastructure files and configure environment
19
17
  */
20
18
  async function deployInit() {
21
19
  requireProject();
@@ -119,8 +117,9 @@ async function deployInit() {
119
117
  // Login to GHCR
120
118
  await loginToGHCR(githubOrg, ghcrToken);
121
119
 
122
- // Build full-app images
123
- await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath);
120
+ // Build full-app images (only for installed services)
121
+ const installedServices = config.installedServices || ['backend', 'admin-portal', 'website'];
122
+ await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath, installedServices);
124
123
 
125
124
  console.log(chalk.green.bold('\n✅ All images built and pushed to GHCR!\n'));
126
125
  } catch (error) {
@@ -137,57 +136,40 @@ async function deployInit() {
137
136
  }
138
137
 
139
138
 
140
- // Step 4: Check repository privacy
141
- console.log(chalk.yellow('🔍 Step 4: Checking repository access...\n'));
139
+ // Step 4: Create app directory and copy infrastructure files
140
+ console.log(chalk.yellow('📦 Step 4: Setting up application on VPS...\n'));
142
141
 
143
- const repoCheck = await checkRepoPrivacy(githubOrg, projectName);
144
- if (repoCheck.isPrivate) {
145
- console.log(chalk.yellow('⚠️ Repository appears to be private or inaccessible\n'));
146
- showDeployKeyInstructions(vpsUser, vpsHost, githubOrg, projectName);
147
- process.exit(1);
148
- }
142
+ const setupSpinner = ora('Creating app directory...').start();
149
143
 
150
- console.log(chalk.green('✓ Repository is accessible (public)\n'));
144
+ try {
145
+ // Create infrastructure directory on VPS
146
+ await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}/infrastructure`);
147
+
148
+ setupSpinner.text = 'Copying infrastructure files to VPS...';
151
149
 
152
- // Step 5: Create app directory and clone repository
153
- console.log(chalk.yellow('📦 Step 5: Setting up application on VPS...\n'));
150
+ // Copy entire infrastructure directory to VPS
151
+ const infrastructurePath = path.join(projectRoot, 'infrastructure');
152
+ await copyDirectoryToVPS(infrastructurePath, vpsUser, vpsHost, `${vpsAppFolder}/infrastructure`);
154
153
 
155
- const cloneSpinner = ora('Creating app directory...').start();
154
+ setupSpinner.succeed('Infrastructure files copied to VPS');
155
+ } catch (error) {
156
+ setupSpinner.fail('Failed to copy infrastructure files');
157
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
158
+ process.exit(1);
159
+ }
156
160
 
157
- try {
158
- // Create directory
159
- await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}`);
160
- cloneSpinner.text = 'Cloning repository...';
161
-
162
- // Check if directory is empty or has .git
163
- const { stdout: lsOutput } = await executeSSH(vpsUser, vpsHost, `ls -A ${vpsAppFolder}`);
164
-
165
- if (lsOutput.trim()) {
166
- // Directory not empty
167
- const { stdout: gitCheck } = await executeSSH(vpsUser, vpsHost, `test -d ${vpsAppFolder}/.git && echo "exists" || echo "missing"`);
168
-
169
- if (gitCheck.trim() === 'exists') {
170
- cloneSpinner.text = 'Repository already cloned, pulling latest changes...';
171
- await executeSSH(vpsUser, vpsHost, `cd ${vpsAppFolder} && git pull origin main || git pull origin master`);
172
- } else {
173
- cloneSpinner.warn('Directory not empty and not a git repository');
174
- console.log(chalk.yellow(`\n⚠️ Warning: ${vpsAppFolder} exists but is not a git repository\n`));
175
- console.log(chalk.gray('Please clean up the directory or choose a different path.\n'));
176
- process.exit(1);
177
- }
178
- } else {
179
- // Clone fresh
180
- await executeSSH(
181
- vpsUser,
182
- vpsHost,
183
- `git clone https://github.com/${githubOrg}/${projectName}.git ${vpsAppFolder}`,
184
- { timeout: 180000 } // 3 minutes for clone
185
- );
186
- }
161
+ // Create symlink for docker-compose.override.yml -> docker-compose.prod.yml
162
+ const symlinkSpinner = ora('Creating docker-compose.override.yml symlink...').start();
187
163
 
188
- cloneSpinner.succeed('Repository setup complete');
164
+ try {
165
+ await executeSSH(
166
+ vpsUser,
167
+ vpsHost,
168
+ `cd ${vpsAppFolder}/infrastructure && ln -sf docker-compose.prod.yml docker-compose.override.yml`
169
+ );
170
+ symlinkSpinner.succeed('Docker Compose override symlink created');
189
171
  } catch (error) {
190
- cloneSpinner.fail('Failed to setup repository');
172
+ symlinkSpinner.fail('Failed to create symlink');
191
173
  console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
192
174
  process.exit(1);
193
175
  }
@@ -223,23 +205,23 @@ async function deployInit() {
223
205
  // If error, waitlist probably not running - continue
224
206
  }
225
207
 
226
- // Step 6: Copy .env.prod to VPS
227
- console.log(chalk.yellow('\n📄 Step 6: Copying production environment...\n'));
208
+ // Step 5: Copy .env.prod to VPS (overwrites .env copied from infrastructure/)
209
+ console.log(chalk.yellow('\n📄 Step 5: Configuring production environment...\n'));
228
210
 
229
211
  const envSpinner = ora('Copying .env.prod to VPS...').start();
230
212
 
231
213
  try {
232
214
  const remoteEnvPath = `${vpsAppFolder}/infrastructure/.env`;
233
215
  await copyFileToVPS(envProdPath, vpsUser, vpsHost, remoteEnvPath);
234
- envSpinner.succeed('.env.prod copied successfully');
216
+ envSpinner.succeed('.env.prod copied as .env successfully');
235
217
  } catch (error) {
236
218
  envSpinner.fail('Failed to copy .env.prod');
237
219
  console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
238
220
  process.exit(1);
239
221
  }
240
222
 
241
- // Step 7: Pull Docker images
242
- console.log(chalk.yellow('\n🐳 Step 7: Pulling Docker images...\n'));
223
+ // Step 6: Pull Docker images
224
+ console.log(chalk.yellow('\n🐳 Step 6: Pulling Docker images...\n'));
243
225
  console.log(chalk.gray('This may take several minutes...\n'));
244
226
 
245
227
  const dockerSpinner = ora('Pulling Docker images...').start();
@@ -266,7 +248,7 @@ async function deployInit() {
266
248
  console.log(chalk.green.bold('\n✅ Initial VPS setup complete!\n'));
267
249
 
268
250
  console.log(chalk.white('Summary:'));
269
- console.log(chalk.gray(` - Repository cloned to: ${vpsAppFolder}`));
251
+ console.log(chalk.gray(` - Infrastructure copied to: ${vpsAppFolder}`));
270
252
  console.log(chalk.gray(' - Production .env configured'));
271
253
  console.log(chalk.gray(' - Docker images pulled\n'));
272
254
 
@@ -75,77 +75,65 @@ async function deploySetEnv() {
75
75
  const bullAdminToken = generateSecret(32);
76
76
 
77
77
  const answers = await inquirer.prompt([
78
- {
79
- type: 'password',
80
- name: 'dbPassword',
81
- message: 'Database password:',
82
- default: dbPassword,
83
- mask: '*'
84
- },
85
- {
86
- type: 'password',
87
- name: 'redisPassword',
88
- message: 'Redis password:',
89
- default: redisPassword,
90
- mask: '*'
91
- },
92
- {
93
- type: 'password',
94
- name: 'jwtSecret',
95
- message: 'JWT secret:',
96
- default: jwtSecret,
97
- mask: '*'
98
- },
99
- {
100
- type: 'password',
101
- name: 'bullAdminToken',
102
- message: 'Bull Admin token:',
103
- default: bullAdminToken,
104
- mask: '*'
105
- },
106
- {
107
- type: 'input',
108
- name: 'resendApiKey',
109
- message: 'Resend API key (email):',
110
- default: ''
111
- },
112
- {
113
- type: 'input',
114
- name: 'openaiApiKey',
115
- message: 'OpenAI API key:',
116
- default: ''
117
- },
118
- {
119
- type: 'input',
120
- name: 'claudeApiKey',
121
- message: 'Claude API key:',
122
- default: ''
123
- },
124
- {
125
- type: 'input',
126
- name: 'polarAccessToken',
127
- message: 'Polar Access Token:',
128
- default: ''
129
- },
130
- {
131
- type: 'input',
132
- name: 'polarWebhookSecret',
133
- message: 'Polar Webhook Secret:',
134
- default: ''
135
- },
136
- {
137
- type: 'input',
138
- name: 'googleClientId',
139
- message: 'Google OAuth Client ID:',
140
- default: ''
141
- },
142
- {
143
- type: 'input',
144
- name: 'googleClientSecret',
145
- message: 'Google OAuth Client Secret:',
146
- default: ''
147
- }
148
- ]);
78
+ {
79
+ type: 'password',
80
+ name: 'dbPassword',
81
+ message: 'Database password:',
82
+ default: dbPassword,
83
+ mask: '*'
84
+ },
85
+ {
86
+ type: 'password',
87
+ name: 'redisPassword',
88
+ message: 'Redis password:',
89
+ default: redisPassword,
90
+ mask: '*'
91
+ },
92
+ {
93
+ type: 'password',
94
+ name: 'jwtSecret',
95
+ message: 'JWT secret:',
96
+ default: jwtSecret,
97
+ mask: '*'
98
+ },
99
+ {
100
+ type: 'password',
101
+ name: 'bullAdminToken',
102
+ message: 'Bull Admin token:',
103
+ default: bullAdminToken,
104
+ mask: '*'
105
+ },
106
+ {
107
+ type: 'input',
108
+ name: 'resendApiKey',
109
+ message: 'Resend API key (email):',
110
+ default: ''
111
+ },
112
+ {
113
+ type: 'input',
114
+ name: 'polarAccessToken',
115
+ message: 'Polar Access Token:',
116
+ default: ''
117
+ },
118
+ {
119
+ type: 'input',
120
+ name: 'polarWebhookSecret',
121
+ message: 'Polar Webhook Secret:',
122
+ default: ''
123
+ },
124
+ {
125
+ type: 'input',
126
+ name: 'googleClientId',
127
+ message: 'Google OAuth Client ID:',
128
+ default: ''
129
+ },
130
+ {
131
+ type: 'input',
132
+ name: 'googleClientSecret',
133
+ message: 'Google OAuth Client Secret:',
134
+ default: ''
135
+ }
136
+ ]);
149
137
 
150
138
  // Read current .env.prod content
151
139
  let envContent = await fs.readFile(envProdPath, 'utf8');
@@ -153,26 +141,15 @@ async function deploySetEnv() {
153
141
  // Replace values based on deployment mode
154
142
  const replacements = {};
155
143
 
156
- if (isWaitlistMode) {
157
- // Waitlist mode: only update Resend API key if provided
158
- // Airtable credentials should already be in .env from component:add
159
- if (answers.resendApiKey) {
160
- replacements['RESEND_API_KEY'] = answers.resendApiKey;
161
- }
162
- } else {
163
- // Full-app mode: update all configuration
164
- replacements['DB_PASSWORD'] = answers.dbPassword;
165
- replacements['REDIS_PASSWORD'] = answers.redisPassword;
166
- replacements['JWT_SECRET'] = answers.jwtSecret;
167
- replacements['BULL_ADMIN_TOKEN'] = answers.bullAdminToken;
168
- replacements['RESEND_API_KEY'] = answers.resendApiKey || 're_your_resend_api_key';
169
- replacements['OPENAI_API_KEY'] = answers.openaiApiKey || 'sk-your_openai_api_key';
170
- replacements['CLAUDE_API_KEY'] = answers.claudeApiKey || 'sk-ant-your_claude_api_key';
171
- replacements['POLAR_ACCESS_TOKEN'] = answers.polarAccessToken || 'polar_oat_your_token';
172
- replacements['POLAR_WEBHOOK_SECRET'] = answers.polarWebhookSecret || 'polar_whs_your_secret';
173
- replacements['GOOGLE_CLIENT_ID'] = answers.googleClientId || 'YOUR_GOOGLE_CLIENT_ID';
174
- replacements['GOOGLE_CLIENT_SECRET'] = answers.googleClientSecret || 'YOUR_GOOGLE_CLIENT_SECRET';
175
- }
144
+ replacements['DB_PASSWORD'] = answers.dbPassword;
145
+ replacements['REDIS_PASSWORD'] = answers.redisPassword;
146
+ replacements['JWT_SECRET'] = answers.jwtSecret;
147
+ replacements['BULL_ADMIN_TOKEN'] = answers.bullAdminToken;
148
+ replacements['RESEND_API_KEY'] = answers.resendApiKey || 're_your_resend_api_key';
149
+ replacements['POLAR_ACCESS_TOKEN'] = answers.polarAccessToken || 'polar_oat_your_token';
150
+ replacements['POLAR_WEBHOOK_SECRET'] = answers.polarWebhookSecret || 'polar_whs_your_secret';
151
+ replacements['GOOGLE_CLIENT_ID'] = answers.googleClientId || 'YOUR_GOOGLE_CLIENT_ID';
152
+ replacements['GOOGLE_CLIENT_SECRET'] = answers.googleClientSecret || 'YOUR_GOOGLE_CLIENT_SECRET';
176
153
 
177
154
  // Update environment variables
178
155
  for (const [key, value] of Object.entries(replacements)) {