create-propelkit 1.0.5 → 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 +1 -1
- package/src/config-generator.js +68 -0
- package/src/config.js +2 -2
- package/src/deploy-command.js +220 -0
- package/src/env-mapper.js +140 -0
- package/src/index.js +22 -1
- package/src/lovable-flow.js +54 -49
- package/src/messages.js +2 -2
- package/src/page-mapper.js +3 -3
- package/src/payment-config.js +1 -1
- package/src/platform-detector.js +51 -0
- package/src/railway-deployer.js +65 -0
- package/src/vercel-deployer.js +115 -0
- package/src/webhook-guidance.js +84 -0
package/package.json
CHANGED
|
@@ -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-
|
|
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
|
@@ -9,6 +9,13 @@ const { launchClaude } = require('./launcher');
|
|
|
9
9
|
const { validateLicense } = require('./license-validator');
|
|
10
10
|
const { askPaymentProcessor, askPaymentKeys } = require('./payment-config');
|
|
11
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');
|
|
12
19
|
const messages = require('./messages');
|
|
13
20
|
|
|
14
21
|
async function main() {
|
|
@@ -134,4 +141,18 @@ async function main() {
|
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
module.exports = {
|
|
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
|
+
};
|
package/src/lovable-flow.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles Lovable UI design approach prompts for CLI wizard.
|
|
5
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.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
const inquirer = require('inquirer');
|
|
@@ -17,26 +21,26 @@ const { updateBlueprint } = require('./blueprint-manager');
|
|
|
17
21
|
*/
|
|
18
22
|
async function askLovableChoice() {
|
|
19
23
|
console.log('');
|
|
20
|
-
console.log(chalk.cyan.bold('UI Design
|
|
21
|
-
console.log(chalk.dim('
|
|
24
|
+
console.log(chalk.cyan.bold('UI Design'));
|
|
25
|
+
console.log(chalk.dim('PropelKit can generate a tailored Lovable prompt for your project'));
|
|
22
26
|
console.log('');
|
|
23
27
|
|
|
24
28
|
const { choice } = await inquirer.prompt([
|
|
25
29
|
{
|
|
26
30
|
type: 'list',
|
|
27
31
|
name: 'choice',
|
|
28
|
-
message: 'How would you like to handle UI
|
|
32
|
+
message: 'How would you like to handle UI?',
|
|
29
33
|
choices: [
|
|
30
34
|
{
|
|
31
|
-
name: '
|
|
35
|
+
name: 'Use Lovable - AI PM will craft a perfect prompt after understanding your project',
|
|
32
36
|
value: 'make-new'
|
|
33
37
|
},
|
|
34
38
|
{
|
|
35
|
-
name: '
|
|
39
|
+
name: 'I already have Lovable designs - Paste into lovable-designs/ folder',
|
|
36
40
|
value: 'have-existing'
|
|
37
41
|
},
|
|
38
42
|
{
|
|
39
|
-
name: 'Skip for now -
|
|
43
|
+
name: 'Skip for now - Decide later (you can always add UI anytime)',
|
|
40
44
|
value: 'skip'
|
|
41
45
|
}
|
|
42
46
|
]
|
|
@@ -54,80 +58,81 @@ function displayLovableGuidance(choice) {
|
|
|
54
58
|
console.log('');
|
|
55
59
|
|
|
56
60
|
if (choice === 'make-new') {
|
|
57
|
-
console.log(chalk.cyan('
|
|
61
|
+
console.log(chalk.cyan('Great choice! Here\'s what will happen:'));
|
|
58
62
|
console.log('');
|
|
59
|
-
console.log(chalk.dim('
|
|
60
|
-
console.log(chalk.dim('
|
|
61
|
-
console.log(chalk.dim('
|
|
62
|
-
console.log(chalk.dim('
|
|
63
|
-
console.log(chalk.dim('
|
|
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'));
|
|
64
68
|
console.log('');
|
|
65
|
-
console.log(chalk.dim('
|
|
69
|
+
console.log(chalk.dim('Pro tip: Have inspiration screenshots ready - the AI PM will ask for them!'));
|
|
66
70
|
} else if (choice === 'have-existing') {
|
|
67
|
-
console.log(chalk.cyan('Paste your Lovable
|
|
71
|
+
console.log(chalk.cyan('Paste your Lovable output into lovable-designs/ when ready.'));
|
|
68
72
|
console.log('');
|
|
69
|
-
console.log(chalk.dim('The AI PM will
|
|
70
|
-
console.log(chalk.dim('
|
|
71
|
-
console.log(chalk.dim(' -
|
|
72
|
-
console.log(chalk.dim(' -
|
|
73
|
-
console.log(chalk.dim(' -
|
|
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'));
|
|
74
78
|
console.log('');
|
|
75
79
|
console.log(chalk.dim('You can paste code before or after AI PM starts.'));
|
|
76
80
|
} else {
|
|
77
|
-
console.log(chalk.yellow('
|
|
81
|
+
console.log(chalk.yellow('No problem! You can add UI anytime.'));
|
|
78
82
|
console.log('');
|
|
79
|
-
console.log(chalk.dim('
|
|
80
|
-
console.log(chalk.dim(' -
|
|
81
|
-
console.log(chalk.dim(' -
|
|
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'));
|
|
82
86
|
console.log('');
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/**
|
|
87
|
-
* Create lovable-
|
|
91
|
+
* Create lovable-designs directory in project
|
|
88
92
|
* @param {string} projectDir - Project directory path
|
|
89
93
|
*/
|
|
90
|
-
function
|
|
91
|
-
const lovableDir = path.join(projectDir, 'lovable-
|
|
94
|
+
function createLovableDesignsDir(projectDir) {
|
|
95
|
+
const lovableDir = path.join(projectDir, 'lovable-designs');
|
|
92
96
|
if (!fs.existsSync(lovableDir)) {
|
|
93
97
|
fs.mkdirSync(lovableDir, { recursive: true });
|
|
94
98
|
|
|
95
99
|
// Add README to explain the folder
|
|
96
100
|
const readmePath = path.join(lovableDir, 'README.md');
|
|
97
|
-
const readmeContent = `# Lovable
|
|
101
|
+
const readmeContent = `# Lovable Designs
|
|
102
|
+
|
|
103
|
+
Paste your Lovable-generated UI code here.
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
## How it works
|
|
100
106
|
|
|
101
|
-
|
|
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
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
The AI PM will automatically:
|
|
112
|
+
## What the auto-wiring does
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
119
|
|
|
111
120
|
## Folder structure
|
|
112
121
|
|
|
113
122
|
Paste your Lovable export maintaining the original structure:
|
|
114
123
|
\`\`\`
|
|
115
|
-
lovable-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
lovable-designs/
|
|
125
|
+
+-- src/
|
|
126
|
+
| +-- components/
|
|
127
|
+
| +-- pages/
|
|
128
|
+
| +-- ...
|
|
129
|
+
+-- package.json
|
|
130
|
+
+-- ...
|
|
122
131
|
\`\`\`
|
|
123
|
-
|
|
124
|
-
## Need help?
|
|
125
|
-
|
|
126
|
-
The AI PM will guide you through the integration process.
|
|
127
132
|
`;
|
|
128
133
|
fs.writeFileSync(readmePath, readmeContent);
|
|
129
134
|
|
|
130
|
-
console.log(chalk.dim(`Created lovable-
|
|
135
|
+
console.log(chalk.dim(`Created lovable-designs/ directory`));
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -148,7 +153,7 @@ function persistLovableChoice(projectDir, choice) {
|
|
|
148
153
|
async function runLovableFlow(projectDir) {
|
|
149
154
|
const choice = await askLovableChoice();
|
|
150
155
|
displayLovableGuidance(choice);
|
|
151
|
-
|
|
156
|
+
createLovableDesignsDir(projectDir);
|
|
152
157
|
persistLovableChoice(projectDir, choice);
|
|
153
158
|
return choice;
|
|
154
159
|
}
|
|
@@ -156,7 +161,7 @@ async function runLovableFlow(projectDir) {
|
|
|
156
161
|
module.exports = {
|
|
157
162
|
askLovableChoice,
|
|
158
163
|
displayLovableGuidance,
|
|
159
|
-
|
|
164
|
+
createLovableDesignsDir,
|
|
160
165
|
persistLovableChoice,
|
|
161
166
|
runLovableFlow
|
|
162
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
|
|
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
|
|
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
|
},
|
package/src/page-mapper.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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 (
|
|
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)
|
package/src/payment-config.js
CHANGED
|
@@ -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
|
+
};
|