create-propelkit 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-propelkit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Initialize a PropelKit SaaS project with AI PM",
5
5
  "bin": "./bin/cli.js",
6
6
  "main": "./src/index.js",
@@ -0,0 +1,68 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Generates vercel.json configuration file for Vercel deployment
6
+ * @param {string} projectDir - Absolute path to project directory
7
+ * @param {object} chalk - Chalk instance for colored output
8
+ */
9
+ function generateVercelJson(projectDir, chalk) {
10
+ const vercelJsonPath = path.join(projectDir, 'vercel.json');
11
+
12
+ // Check if vercel.json already exists
13
+ if (fs.existsSync(vercelJsonPath)) {
14
+ console.log(chalk.dim(' vercel.json exists, skipping'));
15
+ return;
16
+ }
17
+
18
+ const config = {
19
+ "$schema": "https://openapi.vercel.sh/vercel.json",
20
+ "framework": "nextjs",
21
+ "buildCommand": "npm run build",
22
+ "installCommand": "npm ci"
23
+ };
24
+
25
+ fs.writeFileSync(vercelJsonPath, JSON.stringify(config, null, 2) + '\n');
26
+ console.log(chalk.dim(' Created vercel.json'));
27
+ }
28
+
29
+ /**
30
+ * Generates railway.toml configuration file for Railway deployment
31
+ * @param {string} projectDir - Absolute path to project directory
32
+ * @param {object} chalk - Chalk instance for colored output
33
+ */
34
+ function generateRailwayToml(projectDir, chalk) {
35
+ const railwayTomlPath = path.join(projectDir, 'railway.toml');
36
+
37
+ // Check if railway.toml already exists
38
+ if (fs.existsSync(railwayTomlPath)) {
39
+ console.log(chalk.dim(' railway.toml exists, skipping'));
40
+ return;
41
+ }
42
+
43
+ const config = `[build]
44
+ builder = "NIXPACKS"
45
+
46
+ [build.nixpacksPlan.phases.setup]
47
+ nixPkgs = ["nodejs_20"]
48
+
49
+ [build.nixpacksPlan.phases.install]
50
+ cmds = ["npm ci"]
51
+
52
+ [build.nixpacksPlan.phases.build]
53
+ cmds = ["npm run build"]
54
+
55
+ [deploy]
56
+ startCommand = "npm run start"
57
+ restartPolicyType = "on_failure"
58
+ restartPolicyMaxRetries = 3
59
+ `;
60
+
61
+ fs.writeFileSync(railwayTomlPath, config);
62
+ console.log(chalk.dim(' Created railway.toml'));
63
+ }
64
+
65
+ module.exports = {
66
+ generateVercelJson,
67
+ generateRailwayToml
68
+ };
package/src/config.js CHANGED
@@ -9,8 +9,8 @@ const API_ENDPOINTS = {
9
9
  };
10
10
 
11
11
  const LICENSE_PATTERNS = {
12
- // PK-STARTER-2026-XXXXXX or PK-PRO-2026-XXXXXX
13
- format: /^PK-(STARTER|PRO|AGENCY)-\d{4}-[A-Z0-9]{6}$/
12
+ // PK-STARTER-2026-XXXXXX (6 chars) or PK-PRO-2026-75AA835B178345DD (16 chars)
13
+ format: /^PK-(STARTER|PRO|AGENCY)-\d{4}-[A-Z0-9]{6,16}$/
14
14
  };
15
15
 
16
16
  const TIMEOUTS = {
@@ -0,0 +1,220 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { detectPlatformCLI } = require('./platform-detector');
6
+ const { readEnvFile, getRequiredEnvVars, promptMissingEnvVars } = require('./env-mapper');
7
+ const { generateVercelJson, generateRailwayToml } = require('./config-generator');
8
+ const { checkVercelAuth, deployToVercel } = require('./vercel-deployer');
9
+ const { deployToRailway } = require('./railway-deployer');
10
+ const { displayWebhookGuidance } = require('./webhook-guidance');
11
+
12
+ /**
13
+ * Main deployment command orchestrator
14
+ * Handles platform selection, CLI detection, env vars, and deployment
15
+ */
16
+ async function deployCommand() {
17
+ // Step 1: Determine project directory
18
+ const projectDir = process.cwd();
19
+
20
+ // Step 2: Verify PropelKit project
21
+ const featuresPath = path.join(projectDir, 'src', 'config', 'features.ts');
22
+ if (!fs.existsSync(featuresPath)) {
23
+ throw new Error('Not a PropelKit project (src/config/features.ts not found)');
24
+ }
25
+
26
+ // Step 3: Read payment processor from features.ts
27
+ const featuresContent = fs.readFileSync(featuresPath, 'utf-8');
28
+ const processorMatch = featuresContent.match(/paymentProcessor:\s*['"](\w+)['"]/);
29
+ const paymentProcessor = processorMatch ? processorMatch[1] : 'razorpay';
30
+
31
+ // Step 4: Display header
32
+ console.log('');
33
+ console.log(chalk.cyan.bold('🚀 PropelKit Deployment'));
34
+ console.log(chalk.dim('Deploy your SaaS to the cloud'));
35
+ console.log('');
36
+
37
+ // Step 5: Platform selection prompt
38
+ const { platform } = await inquirer.prompt([
39
+ {
40
+ type: 'list',
41
+ name: 'platform',
42
+ message: 'Select deployment platform:',
43
+ choices: [
44
+ { name: 'Vercel (Recommended)', value: 'vercel' },
45
+ { name: 'Railway', value: 'railway' },
46
+ { name: 'Manual (config files only)', value: 'manual' }
47
+ ]
48
+ }
49
+ ]);
50
+
51
+ // Step 6: Handle manual deployment
52
+ if (platform === 'manual') {
53
+ await handleManualDeploy(projectDir, paymentProcessor, chalk);
54
+ return;
55
+ }
56
+
57
+ // Step 7: CLI detection
58
+ const cliDetection = await detectPlatformCLI(platform);
59
+ if (!cliDetection.installed) {
60
+ console.log('');
61
+ console.log(chalk.red(`${cliDetection.config.label} is not installed.`));
62
+ console.log('');
63
+ console.log(chalk.yellow('Installation options:'));
64
+ Object.entries(cliDetection.config.installCommand).forEach(([method, command]) => {
65
+ console.log(chalk.dim(` ${method}: ${command}`));
66
+ });
67
+ console.log('');
68
+ console.log(chalk.dim(`Documentation: ${cliDetection.config.docs}`));
69
+ console.log('');
70
+ process.exit(1);
71
+ }
72
+
73
+ // Step 8: Vercel authentication check
74
+ if (platform === 'vercel') {
75
+ const authCheck = await checkVercelAuth();
76
+ if (!authCheck.authenticated) {
77
+ console.log('');
78
+ console.log(chalk.red('Not authenticated with Vercel CLI.'));
79
+ console.log('');
80
+ console.log(chalk.yellow('Please run:'));
81
+ console.log(chalk.bold(' vercel login'));
82
+ console.log('');
83
+ process.exit(1);
84
+ }
85
+ console.log('');
86
+ console.log(chalk.green(`✓ Authenticated as ${authCheck.user}`));
87
+ console.log('');
88
+ }
89
+
90
+ // Step 9: Environment variables
91
+ const existingEnvVars = readEnvFile(projectDir) || {};
92
+ const requiredEnvVars = getRequiredEnvVars(paymentProcessor);
93
+ const envVars = await promptMissingEnvVars(existingEnvVars, requiredEnvVars, chalk);
94
+
95
+ // Step 10: Production confirmation prompt
96
+ let isProduction = false;
97
+ if (platform === 'vercel') {
98
+ const { production } = await inquirer.prompt([
99
+ {
100
+ type: 'confirm',
101
+ name: 'production',
102
+ message: 'Deploy to production? (No = preview/staging)',
103
+ default: false
104
+ }
105
+ ]);
106
+ isProduction = production;
107
+ }
108
+
109
+ // Step 11: Execute deployment
110
+ let deploymentResult;
111
+ try {
112
+ if (platform === 'vercel') {
113
+ deploymentResult = await deployToVercel(projectDir, envVars, isProduction, chalk);
114
+ } else if (platform === 'railway') {
115
+ deploymentResult = await deployToRailway(projectDir, envVars, chalk);
116
+ }
117
+ } catch (error) {
118
+ console.log('');
119
+ console.log(chalk.red('Deployment failed:'));
120
+ console.log(chalk.red(error.message));
121
+ console.log('');
122
+ process.exit(1);
123
+ }
124
+
125
+ // Step 12: Prompt for deployed URL
126
+ console.log('');
127
+ const { deployedUrl } = await inquirer.prompt([
128
+ {
129
+ type: 'input',
130
+ name: 'deployedUrl',
131
+ message: 'Enter your deployment URL:',
132
+ default: 'https://your-app.vercel.app',
133
+ validate: input => {
134
+ if (!input.trim()) return 'URL is required';
135
+ if (!input.startsWith('http')) return 'URL must start with http:// or https://';
136
+ return true;
137
+ }
138
+ }
139
+ ]);
140
+
141
+ // Step 13: Display webhook guidance
142
+ displayWebhookGuidance(
143
+ deployedUrl.trim(),
144
+ paymentProcessor,
145
+ platform,
146
+ Object.keys(envVars),
147
+ chalk
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Handle manual deployment (config file generation only)
153
+ * @param {string} projectDir - Absolute path to project directory
154
+ * @param {string} paymentProcessor - Payment processor from features.ts
155
+ * @param {object} chalk - Chalk instance for colored output
156
+ */
157
+ async function handleManualDeploy(projectDir, paymentProcessor, chalk) {
158
+ console.log('');
159
+ console.log(chalk.cyan('Manual Deployment Setup'));
160
+ console.log('');
161
+
162
+ // Checkbox prompt for config files
163
+ const { platforms } = await inquirer.prompt([
164
+ {
165
+ type: 'checkbox',
166
+ name: 'platforms',
167
+ message: 'Generate config files for:',
168
+ choices: [
169
+ { name: 'Vercel', value: 'vercel', checked: true },
170
+ { name: 'Railway', value: 'railway', checked: true }
171
+ ],
172
+ validate: input => input.length > 0 ? true : 'Select at least one platform'
173
+ }
174
+ ]);
175
+
176
+ // Generate selected config files
177
+ console.log('');
178
+ console.log(chalk.cyan('Generating configuration files...'));
179
+ console.log('');
180
+
181
+ if (platforms.includes('vercel')) {
182
+ generateVercelJson(projectDir, chalk);
183
+ }
184
+
185
+ if (platforms.includes('railway')) {
186
+ generateRailwayToml(projectDir, chalk);
187
+ }
188
+
189
+ // Display manual deployment instructions
190
+ console.log('');
191
+ console.log(chalk.yellow.bold('📋 Manual Deployment Steps:'));
192
+ console.log('');
193
+ console.log(chalk.white('1. Push your code to GitHub:'));
194
+ console.log(chalk.dim(' git add .'));
195
+ console.log(chalk.dim(' git commit -m "Add deployment config"'));
196
+ console.log(chalk.dim(' git push'));
197
+ console.log('');
198
+ console.log(chalk.white('2. Import project in platform dashboard:'));
199
+ if (platforms.includes('vercel')) {
200
+ console.log(chalk.dim(' Vercel: https://vercel.com/new'));
201
+ }
202
+ if (platforms.includes('railway')) {
203
+ console.log(chalk.dim(' Railway: https://railway.app/new'));
204
+ }
205
+ console.log('');
206
+ console.log(chalk.white('3. Set environment variables in dashboard:'));
207
+ const requiredEnvVars = getRequiredEnvVars(paymentProcessor);
208
+ requiredEnvVars.forEach(key => {
209
+ console.log(chalk.dim(` • ${key}`));
210
+ });
211
+ console.log('');
212
+ console.log(chalk.white('4. Deploy from platform dashboard'));
213
+ console.log('');
214
+ console.log(chalk.green('✓ Config files generated successfully!'));
215
+ console.log('');
216
+ }
217
+
218
+ module.exports = {
219
+ deployCommand
220
+ };
@@ -0,0 +1,140 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const inquirer = require('inquirer');
4
+
5
+ /**
6
+ * Reads environment variables from .env.local or .env
7
+ * @param {string} projectDir - Absolute path to project directory
8
+ * @returns {object|null} Object with env vars, or null if no file exists
9
+ */
10
+ function readEnvFile(projectDir) {
11
+ const envLocalPath = path.join(projectDir, '.env.local');
12
+ const envPath = path.join(projectDir, '.env');
13
+
14
+ let envFilePath = null;
15
+ if (fs.existsSync(envLocalPath)) {
16
+ envFilePath = envLocalPath;
17
+ } else if (fs.existsSync(envPath)) {
18
+ envFilePath = envPath;
19
+ } else {
20
+ return null;
21
+ }
22
+
23
+ const content = fs.readFileSync(envFilePath, 'utf-8');
24
+ const envVars = {};
25
+
26
+ // Simple parser: KEY=VALUE lines
27
+ content.split('\n').forEach(line => {
28
+ const trimmed = line.trim();
29
+
30
+ // Skip comments and empty lines
31
+ if (!trimmed || trimmed.startsWith('#')) {
32
+ return;
33
+ }
34
+
35
+ const equalsIndex = trimmed.indexOf('=');
36
+ if (equalsIndex === -1) {
37
+ return;
38
+ }
39
+
40
+ const key = trimmed.substring(0, equalsIndex).trim();
41
+ const value = trimmed.substring(equalsIndex + 1).trim();
42
+
43
+ if (key) {
44
+ envVars[key] = value;
45
+ }
46
+ });
47
+
48
+ return envVars;
49
+ }
50
+
51
+ /**
52
+ * Gets list of required environment variables based on payment processor
53
+ * @param {string} paymentProcessor - 'stripe', 'razorpay', or 'both'
54
+ * @returns {string[]} Array of required env var names
55
+ */
56
+ function getRequiredEnvVars(paymentProcessor) {
57
+ const baseVars = [
58
+ 'NEXT_PUBLIC_APP_URL',
59
+ 'NEXT_PUBLIC_SUPABASE_URL',
60
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
61
+ 'SUPABASE_SERVICE_ROLE_KEY',
62
+ 'RESEND_API_KEY',
63
+ 'INNGEST_EVENT_KEY',
64
+ 'INNGEST_SIGNING_KEY'
65
+ ];
66
+
67
+ const stripeVars = [
68
+ 'STRIPE_SECRET_KEY',
69
+ 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
70
+ 'STRIPE_WEBHOOK_SECRET'
71
+ ];
72
+
73
+ const razorpayVars = [
74
+ 'NEXT_PUBLIC_RAZORPAY_KEY_ID',
75
+ 'RAZORPAY_KEY_SECRET',
76
+ 'RAZORPAY_WEBHOOK_SECRET'
77
+ ];
78
+
79
+ let required = [...baseVars];
80
+
81
+ if (paymentProcessor === 'stripe' || paymentProcessor === 'both') {
82
+ required = required.concat(stripeVars);
83
+ }
84
+
85
+ if (paymentProcessor === 'razorpay' || paymentProcessor === 'both') {
86
+ required = required.concat(razorpayVars);
87
+ }
88
+
89
+ return required;
90
+ }
91
+
92
+ /**
93
+ * Prompts user for missing environment variables
94
+ * @param {object} existing - Existing env vars from file
95
+ * @param {string[]} required - Array of required env var names
96
+ * @param {object} chalk - Chalk instance for colored output
97
+ * @returns {Promise<object>} Merged object with existing and new env vars
98
+ */
99
+ async function promptMissingEnvVars(existing, required, chalk) {
100
+ // Filter for missing vars or vars with placeholder values
101
+ const missing = required.filter(key => {
102
+ if (!existing[key]) return true;
103
+ const value = existing[key];
104
+ // Check for placeholder values starting with 'your-'
105
+ if (value.startsWith('your-')) return true;
106
+ return false;
107
+ });
108
+
109
+ // No missing vars
110
+ if (missing.length === 0) {
111
+ return existing;
112
+ }
113
+
114
+ // Display missing vars
115
+ console.log('');
116
+ console.log(chalk.yellow('Missing required environment variables:'));
117
+ missing.forEach(key => {
118
+ console.log(chalk.yellow(` - ${key}`));
119
+ });
120
+ console.log('');
121
+
122
+ // Prompt for each missing var
123
+ const questions = missing.map(key => ({
124
+ type: 'input',
125
+ name: key,
126
+ message: `${key}:`,
127
+ validate: input => input.trim() ? true : `${key} is required`
128
+ }));
129
+
130
+ const answers = await inquirer.prompt(questions);
131
+
132
+ // Merge existing with new answers
133
+ return { ...existing, ...answers };
134
+ }
135
+
136
+ module.exports = {
137
+ readEnvFile,
138
+ getRequiredEnvVars,
139
+ promptMissingEnvVars
140
+ };
package/src/index.js CHANGED
@@ -8,6 +8,14 @@ const { runSetupWizard } = require('./setup-wizard');
8
8
  const { launchClaude } = require('./launcher');
9
9
  const { validateLicense } = require('./license-validator');
10
10
  const { askPaymentProcessor, askPaymentKeys } = require('./payment-config');
11
+ const { runLovableFlow } = require('./lovable-flow');
12
+ const { generateVercelJson, generateRailwayToml } = require('./config-generator');
13
+ const { readEnvFile, getRequiredEnvVars, promptMissingEnvVars } = require('./env-mapper');
14
+ const { detectPlatformCLI, PLATFORM_CLIS } = require('./platform-detector');
15
+ const { checkVercelAuth, deployToVercel } = require('./vercel-deployer');
16
+ const { deployToRailway } = require('./railway-deployer');
17
+ const { deployCommand } = require('./deploy-command');
18
+ const { displayWebhookGuidance } = require('./webhook-guidance');
11
19
  const messages = require('./messages');
12
20
 
13
21
  async function main() {
@@ -105,7 +113,10 @@ async function main() {
105
113
  await runSetupWizard(projectDir);
106
114
  messages.starterWizardComplete();
107
115
  } else {
108
- // Pro: Launch Claude Code directly - AI PM handles everything
116
+ // Pro: Step 6.1 - Lovable design choice
117
+ await runLovableFlow(projectDir);
118
+
119
+ // Pro: Step 6.2 - Launch Claude Code directly - AI PM handles everything
109
120
  messages.proLaunchingAIPM(projectDir);
110
121
 
111
122
  // Give user time to read the instructions
@@ -130,4 +141,18 @@ async function main() {
130
141
  }
131
142
  }
132
143
 
133
- module.exports = { main };
144
+ module.exports = {
145
+ main,
146
+ generateVercelJson,
147
+ generateRailwayToml,
148
+ readEnvFile,
149
+ getRequiredEnvVars,
150
+ promptMissingEnvVars,
151
+ detectPlatformCLI,
152
+ PLATFORM_CLIS,
153
+ checkVercelAuth,
154
+ deployToVercel,
155
+ deployToRailway,
156
+ deployCommand,
157
+ displayWebhookGuidance
158
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Lovable Flow Module
3
+ *
4
+ * Handles Lovable UI design approach prompts for CLI wizard.
5
+ * Called during PRO tier setup, before launching AI PM.
6
+ *
7
+ * The CLI only asks a simple question here. The intelligent Lovable prompt
8
+ * generation happens AFTER the AI PM understands the full project scope
9
+ * (post-roadmap), inside new-project.md Phase 10.5.
10
+ */
11
+
12
+ const inquirer = require('inquirer');
13
+ const chalk = require('chalk');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { updateBlueprint } = require('./blueprint-manager');
17
+
18
+ /**
19
+ * Ask user about Lovable UI design approach
20
+ * @returns {Promise<'make-new'|'have-existing'|'skip'>}
21
+ */
22
+ async function askLovableChoice() {
23
+ console.log('');
24
+ console.log(chalk.cyan.bold('UI Design'));
25
+ console.log(chalk.dim('PropelKit can generate a tailored Lovable prompt for your project'));
26
+ console.log('');
27
+
28
+ const { choice } = await inquirer.prompt([
29
+ {
30
+ type: 'list',
31
+ name: 'choice',
32
+ message: 'How would you like to handle UI?',
33
+ choices: [
34
+ {
35
+ name: 'Use Lovable - AI PM will craft a perfect prompt after understanding your project',
36
+ value: 'make-new'
37
+ },
38
+ {
39
+ name: 'I already have Lovable designs - Paste into lovable-designs/ folder',
40
+ value: 'have-existing'
41
+ },
42
+ {
43
+ name: 'Skip for now - Decide later (you can always add UI anytime)',
44
+ value: 'skip'
45
+ }
46
+ ]
47
+ }
48
+ ]);
49
+
50
+ return choice;
51
+ }
52
+
53
+ /**
54
+ * Display guidance based on Lovable choice
55
+ * @param {string} choice - 'make-new' | 'have-existing' | 'skip'
56
+ */
57
+ function displayLovableGuidance(choice) {
58
+ console.log('');
59
+
60
+ if (choice === 'make-new') {
61
+ console.log(chalk.cyan('Great choice! Here\'s what will happen:'));
62
+ console.log('');
63
+ console.log(chalk.dim(' 1. AI PM will deeply understand your project first'));
64
+ console.log(chalk.dim(' 2. After planning all features, it generates an intelligent Lovable prompt'));
65
+ console.log(chalk.dim(' 3. The prompt includes every page, feature, and style tailored to your project'));
66
+ console.log(chalk.dim(' 4. You paste it into Lovable, generate UI, then paste output back'));
67
+ console.log(chalk.dim(' 5. AI PM auto-wires everything into your Next.js app'));
68
+ console.log('');
69
+ console.log(chalk.dim('Pro tip: Have inspiration screenshots ready - the AI PM will ask for them!'));
70
+ } else if (choice === 'have-existing') {
71
+ console.log(chalk.cyan('Paste your Lovable output into lovable-designs/ when ready.'));
72
+ console.log('');
73
+ console.log(chalk.dim('The AI PM will auto-wire your designs:'));
74
+ console.log(chalk.dim(' - Translate React Router to Next.js App Router'));
75
+ console.log(chalk.dim(' - Connect to Supabase auth and database'));
76
+ console.log(chalk.dim(' - Wire up payment and email flows'));
77
+ console.log(chalk.dim(' - Apply your brand config'));
78
+ console.log('');
79
+ console.log(chalk.dim('You can paste code before or after AI PM starts.'));
80
+ } else {
81
+ console.log(chalk.yellow('No problem! You can add UI anytime.'));
82
+ console.log('');
83
+ console.log(chalk.dim('Options when you\'re ready:'));
84
+ console.log(chalk.dim(' - Run /propelkit:wire-ui to import and wire Lovable designs'));
85
+ console.log(chalk.dim(' - AI PM can generate a Lovable prompt during any session'));
86
+ console.log('');
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Create lovable-designs directory in project
92
+ * @param {string} projectDir - Project directory path
93
+ */
94
+ function createLovableDesignsDir(projectDir) {
95
+ const lovableDir = path.join(projectDir, 'lovable-designs');
96
+ if (!fs.existsSync(lovableDir)) {
97
+ fs.mkdirSync(lovableDir, { recursive: true });
98
+
99
+ // Add README to explain the folder
100
+ const readmePath = path.join(lovableDir, 'README.md');
101
+ const readmeContent = `# Lovable Designs
102
+
103
+ Paste your Lovable-generated UI code here.
104
+
105
+ ## How it works
106
+
107
+ 1. AI PM generates a tailored prompt for your project (after understanding it fully)
108
+ 2. You paste the prompt into lovable.dev and generate your UI
109
+ 3. Export/download from Lovable and paste the output here
110
+ 4. Run \`/propelkit:wire-ui\` to auto-wire everything into your Next.js app
111
+
112
+ ## What the auto-wiring does
113
+
114
+ - Translates React Router to Next.js App Router
115
+ - Connects components to Supabase auth and database
116
+ - Wires up payment flows (Stripe/Razorpay)
117
+ - Applies your brand config from \`src/config/brand.ts\`
118
+ - Generates an integration report
119
+
120
+ ## Folder structure
121
+
122
+ Paste your Lovable export maintaining the original structure:
123
+ \`\`\`
124
+ lovable-designs/
125
+ +-- src/
126
+ | +-- components/
127
+ | +-- pages/
128
+ | +-- ...
129
+ +-- package.json
130
+ +-- ...
131
+ \`\`\`
132
+ `;
133
+ fs.writeFileSync(readmePath, readmeContent);
134
+
135
+ console.log(chalk.dim(`Created lovable-designs/ directory`));
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Persist Lovable choice to blueprint.json
141
+ * @param {string} projectDir - Project directory path
142
+ * @param {string} choice - 'make-new' | 'have-existing' | 'skip'
143
+ */
144
+ function persistLovableChoice(projectDir, choice) {
145
+ updateBlueprint(projectDir, { lovableChoice: choice });
146
+ }
147
+
148
+ /**
149
+ * Run complete Lovable flow in CLI wizard
150
+ * @param {string} projectDir - Project directory path
151
+ * @returns {Promise<string>} - The choice made ('make-new' | 'have-existing' | 'skip')
152
+ */
153
+ async function runLovableFlow(projectDir) {
154
+ const choice = await askLovableChoice();
155
+ displayLovableGuidance(choice);
156
+ createLovableDesignsDir(projectDir);
157
+ persistLovableChoice(projectDir, choice);
158
+ return choice;
159
+ }
160
+
161
+ module.exports = {
162
+ askLovableChoice,
163
+ displayLovableGuidance,
164
+ createLovableDesignsDir,
165
+ persistLovableChoice,
166
+ runLovableFlow
167
+ };
package/src/messages.js CHANGED
@@ -4,7 +4,7 @@ const messages = {
4
4
  welcome: () => {
5
5
  console.log('');
6
6
  console.log(chalk.cyan.bold('PropelKit'));
7
- console.log(chalk.dim('AI-powered SaaS boilerplate for India'));
7
+ console.log(chalk.dim('AI-powered SaaS boilerplate'));
8
8
  console.log('');
9
9
  },
10
10
 
@@ -122,7 +122,7 @@ const messages = {
122
122
  console.log('');
123
123
  console.log(chalk.dim('PropelKit supports multiple payment processors:'));
124
124
  console.log(chalk.dim(' - Stripe for international payments'));
125
- console.log(chalk.dim(' - Razorpay for India (INR + UPI)'));
125
+ console.log(chalk.dim(' - Razorpay (INR + UPI)'));
126
126
  console.log(chalk.dim(' - Both for global SaaS with auto-routing'));
127
127
  console.log('');
128
128
  },
@@ -23,7 +23,7 @@ const PAGE_TEMPLATES = {
23
23
  // Credits pages (optional - based on features.credits)
24
24
  credits: ['Usage Dashboard', 'Purchase Credits'],
25
25
 
26
- // GST pages (always included for India)
26
+ // GST pages (optional - based on features.gst)
27
27
  gst: ['Invoice Settings'],
28
28
 
29
29
  // Admin pages (always included)
@@ -143,7 +143,7 @@ const PAGE_DETAILS = {
143
143
 
144
144
  // GST pages
145
145
  'Invoice Settings': {
146
- purpose: 'Configure GST and invoice details for Indian billing',
146
+ purpose: 'Configure GST and invoice details for tax billing',
147
147
  elements: ['GST number input', 'Business name', 'Business address', 'State selector', 'Invoice preferences'],
148
148
  components: ['Form', 'Input', 'Select', 'Button', 'Alert'],
149
149
  userFlow: 'User enters GST details -> Saves settings -> Future invoices use these details'
@@ -175,7 +175,7 @@ function derivePages(featuresConfig) {
175
175
  // Payment pages (always included)
176
176
  PAGE_TEMPLATES.payments.forEach(page => pages.add(page));
177
177
 
178
- // GST pages (always included for India-first SaaS)
178
+ // GST pages (included when GST feature is enabled)
179
179
  PAGE_TEMPLATES.gst.forEach(page => pages.add(page));
180
180
 
181
181
  // Admin pages (always included)
@@ -17,7 +17,7 @@ async function askPaymentProcessor() {
17
17
  value: 'stripe'
18
18
  },
19
19
  {
20
- name: `${chalk.cyan('Razorpay')} - India (INR, UPI)`,
20
+ name: `${chalk.cyan('Razorpay')} - (INR, UPI)`,
21
21
  value: 'razorpay'
22
22
  },
23
23
  {
@@ -0,0 +1,51 @@
1
+ const commandExists = require('command-exists');
2
+
3
+ const PLATFORM_CLIS = {
4
+ vercel: {
5
+ name: 'vercel',
6
+ label: 'Vercel CLI',
7
+ installCommand: {
8
+ global: 'npm install -g vercel',
9
+ npx: 'npx vercel'
10
+ },
11
+ docs: 'https://vercel.com/docs/cli'
12
+ },
13
+ railway: {
14
+ name: 'railway',
15
+ label: 'Railway CLI',
16
+ installCommand: {
17
+ global: 'npm install -g @railway/cli',
18
+ macos: 'brew install railway',
19
+ windows: 'scoop install railway'
20
+ },
21
+ docs: 'https://docs.railway.app/reference/cli-api'
22
+ }
23
+ };
24
+
25
+ /**
26
+ * Detect if a platform's CLI is installed
27
+ * @param {string} platform - 'vercel' or 'railway'
28
+ * @returns {Promise<{required: boolean, installed: boolean, config: object}>}
29
+ */
30
+ async function detectPlatformCLI(platform) {
31
+ const config = PLATFORM_CLIS[platform];
32
+
33
+ if (!config) {
34
+ throw new Error(`Unknown platform: ${platform}`);
35
+ }
36
+
37
+ const installed = await commandExists(config.name)
38
+ .then(() => true)
39
+ .catch(() => false);
40
+
41
+ return {
42
+ required: true,
43
+ installed,
44
+ config
45
+ };
46
+ }
47
+
48
+ module.exports = {
49
+ detectPlatformCLI,
50
+ PLATFORM_CLIS
51
+ };
@@ -0,0 +1,65 @@
1
+ const { spawn } = require('child_process');
2
+ const path = require('path');
3
+ const { generateRailwayToml } = require('./config-generator');
4
+
5
+ /**
6
+ * Deploys project to Railway using Railway CLI
7
+ * @param {string} projectDir - Absolute path to project directory
8
+ * @param {object} envVars - Environment variables (keys returned for guidance, not passed to CLI)
9
+ * @param {object} chalk - Chalk instance for colored output
10
+ * @returns {Promise<{success: boolean, envVars: string[]}>}
11
+ */
12
+ async function deployToRailway(projectDir, envVars, chalk) {
13
+ return new Promise((resolve, reject) => {
14
+ // Step 1: Generate railway.toml before deployment
15
+ generateRailwayToml(projectDir, chalk);
16
+
17
+ console.log('');
18
+ console.log(chalk.cyan('🚀 Deploying to Railway...'));
19
+ console.log('');
20
+ console.log(chalk.dim('Note: Set environment variables in Railway dashboard after deployment'));
21
+ console.log('');
22
+
23
+ // Step 2: Build args for railway CLI
24
+ const args = ['up', '--detach'];
25
+
26
+ // Step 3: Spawn railway command
27
+ const railwayProcess = spawn('railway', args, {
28
+ cwd: projectDir,
29
+ stdio: 'inherit',
30
+ shell: true // Required for Windows cross-platform compatibility
31
+ });
32
+
33
+ // Step 4: Set 5-minute timeout
34
+ const timeout = setTimeout(() => {
35
+ railwayProcess.kill();
36
+ reject(new Error('Railway deployment timed out after 5 minutes'));
37
+ }, 300000); // 300000ms = 5 minutes
38
+
39
+ // Step 5: Handle process completion
40
+ railwayProcess.on('error', (error) => {
41
+ clearTimeout(timeout);
42
+ if (error.code === 'ENOENT') {
43
+ reject(new Error('Railway CLI not found. Install it from: https://docs.railway.app/develop/cli'));
44
+ } else {
45
+ reject(new Error(`Railway deployment failed: ${error.message}`));
46
+ }
47
+ });
48
+
49
+ railwayProcess.on('exit', (code) => {
50
+ clearTimeout(timeout);
51
+ if (code === 0) {
52
+ resolve({
53
+ success: true,
54
+ envVars: Object.keys(envVars) // Return env var keys for post-deploy guidance
55
+ });
56
+ } else {
57
+ reject(new Error(`Railway deployment failed with exit code ${code}`));
58
+ }
59
+ });
60
+ });
61
+ }
62
+
63
+ module.exports = {
64
+ deployToRailway
65
+ };
@@ -0,0 +1,115 @@
1
+ const { spawn } = require('child_process');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Check if user is authenticated with Vercel CLI
6
+ * @returns {Promise<{authenticated: boolean, user?: string}>}
7
+ */
8
+ async function checkVercelAuth() {
9
+ return new Promise((resolve) => {
10
+ const vercel = spawn('vercel', ['whoami'], {
11
+ stdio: 'pipe',
12
+ shell: true
13
+ });
14
+
15
+ let stdout = '';
16
+ let stderr = '';
17
+
18
+ vercel.stdout.on('data', (data) => {
19
+ stdout += data.toString();
20
+ });
21
+
22
+ vercel.stderr.on('data', (data) => {
23
+ stderr += data.toString();
24
+ });
25
+
26
+ vercel.on('close', (code) => {
27
+ if (code === 0 && stdout.trim()) {
28
+ resolve({ authenticated: true, user: stdout.trim() });
29
+ } else {
30
+ resolve({ authenticated: false });
31
+ }
32
+ });
33
+
34
+ vercel.on('error', (error) => {
35
+ resolve({ authenticated: false });
36
+ });
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Deploy project to Vercel with environment variables
42
+ * @param {string} projectDir - Absolute path to project directory
43
+ * @param {object} envVars - Environment variables object
44
+ * @param {boolean} isProduction - Whether to deploy to production (--prod flag)
45
+ * @param {object} chalk - Chalk instance for colored output
46
+ * @returns {Promise<{success: boolean}>}
47
+ */
48
+ async function deployToVercel(projectDir, envVars, isProduction, chalk) {
49
+ return new Promise((resolve, reject) => {
50
+ // Build args array
51
+ const args = ['--yes'];
52
+
53
+ if (isProduction) {
54
+ args.push('--prod');
55
+ }
56
+
57
+ // Allowed secret env vars (never exposed to browser)
58
+ const allowedSecrets = [
59
+ 'STRIPE_SECRET_KEY',
60
+ 'STRIPE_WEBHOOK_SECRET',
61
+ 'RAZORPAY_KEY_SECRET',
62
+ 'RAZORPAY_WEBHOOK_SECRET',
63
+ 'SUPABASE_SERVICE_ROLE_KEY',
64
+ 'RESEND_API_KEY',
65
+ 'INNGEST_EVENT_KEY',
66
+ 'INNGEST_SIGNING_KEY'
67
+ ];
68
+
69
+ // Filter and add env vars
70
+ Object.entries(envVars).forEach(([key, value]) => {
71
+ // Include if starts with NEXT_PUBLIC_ OR is in allowed secrets list
72
+ if (key.startsWith('NEXT_PUBLIC_') || allowedSecrets.includes(key)) {
73
+ args.push('--env', `${key}=${value}`);
74
+ }
75
+ });
76
+
77
+ console.log('');
78
+ console.log(chalk.cyan(`Deploying to Vercel ${isProduction ? '(production)' : '(preview)'}...`));
79
+ console.log(chalk.dim(`Environment variables: ${Object.keys(envVars).filter(key => key.startsWith('NEXT_PUBLIC_') || allowedSecrets.includes(key)).length} passed`));
80
+ console.log('');
81
+
82
+ const vercel = spawn('vercel', args, {
83
+ cwd: projectDir,
84
+ stdio: 'inherit', // Show real-time logs
85
+ shell: true
86
+ });
87
+
88
+ // 5-minute timeout
89
+ const timeout = setTimeout(() => {
90
+ vercel.kill();
91
+ reject(new Error('Deployment timed out after 5 minutes'));
92
+ }, 300000);
93
+
94
+ vercel.on('close', (code) => {
95
+ clearTimeout(timeout);
96
+ if (code === 0) {
97
+ console.log('');
98
+ console.log(chalk.green('Deployment successful!'));
99
+ resolve({ success: true });
100
+ } else {
101
+ reject(new Error(`Deployment failed with exit code ${code}`));
102
+ }
103
+ });
104
+
105
+ vercel.on('error', (error) => {
106
+ clearTimeout(timeout);
107
+ reject(new Error(`Deployment error: ${error.message}`));
108
+ });
109
+ });
110
+ }
111
+
112
+ module.exports = {
113
+ checkVercelAuth,
114
+ deployToVercel
115
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Display post-deployment webhook configuration guidance
3
+ * @param {string} deployedUrl - The deployed application URL
4
+ * @param {string} paymentProcessor - 'stripe', 'razorpay', or 'both'
5
+ * @param {string} platform - 'vercel' or 'railway'
6
+ * @param {string[]} envVarKeys - Array of environment variable keys
7
+ * @param {object} chalk - Chalk instance for colored output
8
+ */
9
+ function displayWebhookGuidance(deployedUrl, paymentProcessor, platform, envVarKeys, chalk) {
10
+ console.log('');
11
+ console.log(chalk.green.bold('✓ Deployment successful!'));
12
+ console.log('');
13
+ console.log(chalk.cyan('Your app is deployed at:'));
14
+ console.log(chalk.bold(deployedUrl));
15
+ console.log('');
16
+
17
+ // Section 2: Configure webhook URLs based on payment processor
18
+ console.log(chalk.yellow.bold('📡 Next Steps: Configure Webhooks'));
19
+ console.log('');
20
+
21
+ if (paymentProcessor === 'razorpay' || paymentProcessor === 'both') {
22
+ console.log(chalk.cyan('Razorpay Webhooks:'));
23
+ console.log(chalk.dim(' Dashboard: https://dashboard.razorpay.com/app/webhooks'));
24
+ console.log('');
25
+ console.log(chalk.white(' Webhook URL:'));
26
+ console.log(chalk.bold(` ${deployedUrl}/api/payments/webhooks/razorpay`));
27
+ console.log('');
28
+ console.log(chalk.dim(' Events to subscribe:'));
29
+ console.log(chalk.dim(' • payment.captured'));
30
+ console.log(chalk.dim(' • subscription.activated'));
31
+ console.log(chalk.dim(' • subscription.charged'));
32
+ console.log('');
33
+ }
34
+
35
+ if (paymentProcessor === 'stripe' || paymentProcessor === 'both') {
36
+ console.log(chalk.cyan('Stripe Webhooks:'));
37
+ console.log(chalk.dim(' Dashboard: https://dashboard.stripe.com/webhooks'));
38
+ console.log('');
39
+ console.log(chalk.white(' Webhook URL:'));
40
+ console.log(chalk.bold(` ${deployedUrl}/api/payments/webhooks/stripe`));
41
+ console.log('');
42
+ console.log(chalk.dim(' Events to subscribe:'));
43
+ console.log(chalk.dim(' • checkout.session.completed'));
44
+ console.log(chalk.dim(' • invoice.paid'));
45
+ console.log(chalk.dim(' • invoice.payment_failed'));
46
+ console.log(chalk.dim(' • customer.subscription.updated'));
47
+ console.log(chalk.dim(' • customer.subscription.deleted'));
48
+ console.log('');
49
+ }
50
+
51
+ // Section 3: Railway-specific env var setup
52
+ if (platform === 'railway') {
53
+ console.log(chalk.yellow.bold('⚙️ Railway Environment Variables'));
54
+ console.log('');
55
+ console.log(chalk.white('Set these in your Railway dashboard:'));
56
+ console.log(chalk.dim(' Dashboard: https://railway.app/dashboard'));
57
+ console.log('');
58
+ envVarKeys.forEach(key => {
59
+ console.log(chalk.dim(` • ${key}`));
60
+ });
61
+ console.log('');
62
+ }
63
+
64
+ // Section 4: Test your deployment checklist
65
+ console.log(chalk.yellow.bold('✅ Testing Checklist'));
66
+ console.log('');
67
+ console.log(chalk.dim(' 1. Visit your app and sign up'));
68
+ console.log(chalk.dim(' 2. Test authentication flow'));
69
+ console.log(chalk.dim(' 3. Verify payment webhook (test mode)'));
70
+ console.log('');
71
+
72
+ // Section 5: Production warning
73
+ console.log(chalk.red.bold('⚠️ Important: Production Setup'));
74
+ console.log('');
75
+ console.log(chalk.yellow('For production deployments:'));
76
+ console.log(chalk.dim(' • Generate NEW webhook secrets (never reuse test mode secrets)'));
77
+ console.log(chalk.dim(' • Update webhook secrets in your deployment platform'));
78
+ console.log(chalk.dim(' • Test webhooks in production before launching'));
79
+ console.log('');
80
+ }
81
+
82
+ module.exports = {
83
+ displayWebhookGuidance
84
+ };