create-propelkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/bin/cli.js +6 -0
- package/package.json +31 -0
- package/src/cli-detector.js +104 -0
- package/src/config.js +25 -0
- package/src/design-flow.js +442 -0
- package/src/index.js +126 -0
- package/src/launcher.js +42 -0
- package/src/license-validator.js +85 -0
- package/src/messages.js +119 -0
- package/src/orchestrators/existing-designs.ts +176 -0
- package/src/page-mapper.js +247 -0
- package/src/prompt-generator.js +429 -0
- package/src/scenarios.js +217 -0
- package/src/setup-wizard.js +315 -0
- package/src/validators.js +153 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Flow Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Multi-turn conversation flow for collecting design decisions.
|
|
5
|
+
* Handles 4 paths: Lovable, Existing, Manual, Skip
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const { validateHexColor } = require('./validators');
|
|
14
|
+
const { derivePages } = require('./page-mapper');
|
|
15
|
+
const { generateLovablePrompt } = require('./prompt-generator');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse features from project's src/config/features.ts
|
|
19
|
+
* @param {string} projectDir - Path to the project directory
|
|
20
|
+
* @returns {{ multiTenancy: boolean, credits: boolean }}
|
|
21
|
+
*/
|
|
22
|
+
function parseFeaturesFile(projectDir) {
|
|
23
|
+
const featuresPath = path.join(projectDir, 'src', 'config', 'features.ts');
|
|
24
|
+
|
|
25
|
+
// Default features if file doesn't exist
|
|
26
|
+
const defaults = { multiTenancy: false, credits: false };
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(featuresPath)) {
|
|
29
|
+
return defaults;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const content = fs.readFileSync(featuresPath, 'utf-8');
|
|
33
|
+
const features = { ...defaults };
|
|
34
|
+
|
|
35
|
+
// Parse multiTenancy: true/false
|
|
36
|
+
const multiTenancyMatch = content.match(/multiTenancy:\s*(true|false)/);
|
|
37
|
+
if (multiTenancyMatch) {
|
|
38
|
+
features.multiTenancy = multiTenancyMatch[1] === 'true';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parse credits: true/false
|
|
42
|
+
const creditsMatch = content.match(/credits:\s*(true|false)/);
|
|
43
|
+
if (creditsMatch) {
|
|
44
|
+
features.credits = creditsMatch[1] === 'true';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return features;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ask user for design approach choice
|
|
52
|
+
* @returns {Promise<'lovable' | 'existing' | 'manual' | 'skip'>}
|
|
53
|
+
*/
|
|
54
|
+
async function askDesignApproach() {
|
|
55
|
+
const { approach } = await inquirer.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'list',
|
|
58
|
+
name: 'approach',
|
|
59
|
+
message: 'How would you like to handle UI design?',
|
|
60
|
+
choices: [
|
|
61
|
+
{
|
|
62
|
+
name: 'Generate with Lovable - Create beautiful UI with AI',
|
|
63
|
+
value: 'lovable'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'I have existing designs - Import from GitHub/local',
|
|
67
|
+
value: 'existing'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Manual/code-first - I\'ll build UI myself',
|
|
71
|
+
value: 'manual'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Skip for now - Defer design decision',
|
|
75
|
+
value: 'skip'
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
return approach;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Collect design system inputs from user
|
|
86
|
+
* @returns {Promise<{ primaryColor: string, accentColor: string, styleReference: string, brandVoice: string }>}
|
|
87
|
+
*/
|
|
88
|
+
async function askDesignSystem() {
|
|
89
|
+
const answers = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'input',
|
|
92
|
+
name: 'primaryColor',
|
|
93
|
+
message: 'Primary brand color (hex):',
|
|
94
|
+
default: '#3B82F6',
|
|
95
|
+
validate: (input) => {
|
|
96
|
+
const result = validateHexColor(input);
|
|
97
|
+
return result.success || result.error;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'input',
|
|
102
|
+
name: 'accentColor',
|
|
103
|
+
message: 'Accent color (hex):',
|
|
104
|
+
default: '#10B981',
|
|
105
|
+
validate: (input) => {
|
|
106
|
+
const result = validateHexColor(input);
|
|
107
|
+
return result.success || result.error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'list',
|
|
112
|
+
name: 'styleReference',
|
|
113
|
+
message: 'Style reference:',
|
|
114
|
+
choices: [
|
|
115
|
+
{ name: 'Linear - Minimal, dark mode friendly, clean typography', value: 'linear' },
|
|
116
|
+
{ name: 'Notion - Friendly, content-focused, approachable', value: 'notion' },
|
|
117
|
+
{ name: 'Stripe - Professional, elegant, trustworthy', value: 'stripe' },
|
|
118
|
+
{ name: 'Vercel - Modern, geometric, developer-focused', value: 'vercel' },
|
|
119
|
+
{ name: 'Airbnb - Warm, inviting, human-centered', value: 'airbnb' }
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'list',
|
|
124
|
+
name: 'brandVoice',
|
|
125
|
+
message: 'Brand voice:',
|
|
126
|
+
choices: [
|
|
127
|
+
{ name: 'Professional - Formal, authoritative, expert', value: 'Professional' },
|
|
128
|
+
{ name: 'Friendly - Conversational, helpful, approachable', value: 'Friendly' },
|
|
129
|
+
{ name: 'Bold - Confident, direct, impactful', value: 'Bold' },
|
|
130
|
+
{ name: 'Playful - Fun, light-hearted, creative', value: 'Playful' }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
return answers;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Confirm derived pages and allow additions
|
|
140
|
+
* @param {string[]} derivedPages - Pages derived from features
|
|
141
|
+
* @returns {Promise<string[]>} - Final page list
|
|
142
|
+
*/
|
|
143
|
+
async function confirmPages(derivedPages) {
|
|
144
|
+
// Let user select which pages to include
|
|
145
|
+
const { selectedPages } = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: 'checkbox',
|
|
148
|
+
name: 'selectedPages',
|
|
149
|
+
message: 'Select pages to include:',
|
|
150
|
+
choices: derivedPages.map(page => ({
|
|
151
|
+
name: page,
|
|
152
|
+
value: page,
|
|
153
|
+
checked: true
|
|
154
|
+
})),
|
|
155
|
+
pageSize: 15
|
|
156
|
+
}
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// Ask for custom pages
|
|
160
|
+
const { customPages } = await inquirer.prompt([
|
|
161
|
+
{
|
|
162
|
+
type: 'input',
|
|
163
|
+
name: 'customPages',
|
|
164
|
+
message: 'Add custom pages (comma-separated, or press Enter to skip):',
|
|
165
|
+
default: ''
|
|
166
|
+
}
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
// Parse custom pages
|
|
170
|
+
const additionalPages = customPages
|
|
171
|
+
.split(',')
|
|
172
|
+
.map(p => p.trim())
|
|
173
|
+
.filter(p => p.length > 0);
|
|
174
|
+
|
|
175
|
+
// Combine and deduplicate
|
|
176
|
+
const allPages = [...new Set([...selectedPages, ...additionalPages])];
|
|
177
|
+
|
|
178
|
+
return allPages.sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Handle Lovable design approach
|
|
183
|
+
* @param {string} projectName - Name of the project
|
|
184
|
+
* @param {string} projectDir - Path to project directory
|
|
185
|
+
* @returns {Promise<{ approach: 'lovable', promptPath: string }>}
|
|
186
|
+
*/
|
|
187
|
+
async function handleLovablePath(projectName, projectDir) {
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log(chalk.cyan('Collecting design preferences...'));
|
|
190
|
+
console.log('');
|
|
191
|
+
|
|
192
|
+
// Get design system inputs
|
|
193
|
+
const designSystem = await askDesignSystem();
|
|
194
|
+
|
|
195
|
+
// Parse features from project
|
|
196
|
+
const features = parseFeaturesFile(projectDir);
|
|
197
|
+
|
|
198
|
+
// Derive pages based on features
|
|
199
|
+
const derivedPages = derivePages(features);
|
|
200
|
+
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(chalk.cyan('Based on your features, these pages are suggested:'));
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
// Confirm pages
|
|
206
|
+
const finalPages = await confirmPages(derivedPages);
|
|
207
|
+
|
|
208
|
+
// Generate Lovable prompt
|
|
209
|
+
const prompt = generateLovablePrompt(designSystem, finalPages, projectName);
|
|
210
|
+
|
|
211
|
+
// Write to LOVABLE_PROMPT.md
|
|
212
|
+
const promptPath = path.join(projectDir, 'LOVABLE_PROMPT.md');
|
|
213
|
+
fs.writeFileSync(promptPath, prompt, 'utf-8');
|
|
214
|
+
|
|
215
|
+
// Success message
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log(chalk.green.bold('Lovable prompt generated!'));
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(chalk.dim('Saved to:'), chalk.white(promptPath));
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(chalk.cyan('Next steps:'));
|
|
222
|
+
console.log(chalk.dim(' 1. Open lovable.dev and create a new project'));
|
|
223
|
+
console.log(chalk.dim(' 2. Copy the contents of LOVABLE_PROMPT.md'));
|
|
224
|
+
console.log(chalk.dim(' 3. Paste into Lovable and generate your UI'));
|
|
225
|
+
console.log(chalk.dim(' 4. Export from Lovable and use /propelkit:import'));
|
|
226
|
+
console.log('');
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
approach: 'lovable',
|
|
230
|
+
promptPath,
|
|
231
|
+
designSystem,
|
|
232
|
+
pages: finalPages
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Spawn TypeScript orchestrator to execute import → translation → integration.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} projectDir - Target project directory
|
|
240
|
+
* @param {object} source - Import source ({ type: 'github'|'local', url?, path? })
|
|
241
|
+
* @returns {Promise<{ approach: 'existing', source: object }>}
|
|
242
|
+
*/
|
|
243
|
+
function spawnOrchestrator(projectDir, source) {
|
|
244
|
+
const orchestratorPath = path.join(__dirname, 'orchestrators', 'existing-designs.ts');
|
|
245
|
+
const sourceJson = Buffer.from(JSON.stringify(source)).toString('base64');
|
|
246
|
+
const monorepoRoot = path.resolve(__dirname, '../../..');
|
|
247
|
+
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const child = spawn(
|
|
250
|
+
'npx',
|
|
251
|
+
['tsx', orchestratorPath, '--project-dir', projectDir, '--source-base64', sourceJson],
|
|
252
|
+
{
|
|
253
|
+
stdio: 'inherit', // User sees orchestrator's console.log
|
|
254
|
+
shell: true, // Windows compatibility
|
|
255
|
+
cwd: monorepoRoot // Run from root for tsconfig path resolution
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
child.on('error', (err) => {
|
|
260
|
+
console.error('');
|
|
261
|
+
console.error(chalk.red('Failed to start import pipeline'));
|
|
262
|
+
console.error(chalk.dim(`Error: ${err.message}`));
|
|
263
|
+
console.error('');
|
|
264
|
+
reject(err);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
child.on('exit', (code) => {
|
|
268
|
+
if (code === 0) {
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(chalk.green.bold('✓ Import pipeline completed successfully!'));
|
|
271
|
+
console.log('');
|
|
272
|
+
resolve({ approach: 'existing', source });
|
|
273
|
+
} else {
|
|
274
|
+
console.error('');
|
|
275
|
+
console.error(chalk.red(`✗ Import pipeline failed (exit code ${code})`));
|
|
276
|
+
console.error('');
|
|
277
|
+
reject(new Error(`Pipeline failed with exit code ${code}`));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Handle existing designs import approach
|
|
285
|
+
* @param {string} projectDir - Path to project directory
|
|
286
|
+
* @returns {Promise<{ approach: 'existing', source: { type: 'github' | 'local', url?: string, path?: string } }>}
|
|
287
|
+
*/
|
|
288
|
+
async function handleExistingPath(projectDir) {
|
|
289
|
+
console.log('');
|
|
290
|
+
console.log(chalk.cyan('Import existing designs into PropelKit...'));
|
|
291
|
+
console.log('');
|
|
292
|
+
|
|
293
|
+
// Ask source type
|
|
294
|
+
const { sourceType } = await inquirer.prompt([
|
|
295
|
+
{
|
|
296
|
+
type: 'list',
|
|
297
|
+
name: 'sourceType',
|
|
298
|
+
message: 'Where are your designs?',
|
|
299
|
+
choices: [
|
|
300
|
+
{ name: 'GitHub repository', value: 'github' },
|
|
301
|
+
{ name: 'Local folder', value: 'local' }
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
let source;
|
|
307
|
+
|
|
308
|
+
if (sourceType === 'github') {
|
|
309
|
+
const { url } = await inquirer.prompt([
|
|
310
|
+
{
|
|
311
|
+
type: 'input',
|
|
312
|
+
name: 'url',
|
|
313
|
+
message: 'GitHub repository URL:',
|
|
314
|
+
validate: (input) => {
|
|
315
|
+
if (!input.includes('github.com')) {
|
|
316
|
+
return 'Please enter a valid GitHub URL';
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
source = { type: 'github', url };
|
|
324
|
+
} else {
|
|
325
|
+
const { localPath } = await inquirer.prompt([
|
|
326
|
+
{
|
|
327
|
+
type: 'input',
|
|
328
|
+
name: 'localPath',
|
|
329
|
+
message: 'Path to local folder:',
|
|
330
|
+
validate: (input) => {
|
|
331
|
+
const resolvedPath = path.resolve(input);
|
|
332
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
333
|
+
return `Path does not exist: ${resolvedPath}`;
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
source = { type: 'local', path: path.resolve(localPath) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log(chalk.green.bold('Starting import pipeline...'));
|
|
345
|
+
console.log('');
|
|
346
|
+
|
|
347
|
+
// NEW: Spawn TypeScript orchestrator
|
|
348
|
+
return spawnOrchestrator(projectDir, source);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Handle manual/code-first approach
|
|
353
|
+
* @param {string} projectDir - Path to project directory
|
|
354
|
+
* @returns {Promise<{ approach: 'manual', suggestedPages: string[] }>}
|
|
355
|
+
*/
|
|
356
|
+
async function handleManualPath(projectDir) {
|
|
357
|
+
// Parse features and derive suggested pages
|
|
358
|
+
const features = parseFeaturesFile(projectDir);
|
|
359
|
+
const suggestedPages = derivePages(features);
|
|
360
|
+
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(chalk.cyan('Building UI manually? Here\'s your suggested page list:'));
|
|
363
|
+
console.log('');
|
|
364
|
+
|
|
365
|
+
suggestedPages.forEach(page => {
|
|
366
|
+
console.log(chalk.dim(' -'), chalk.white(page));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
console.log('');
|
|
370
|
+
console.log(chalk.cyan('Guidance:'));
|
|
371
|
+
console.log(chalk.dim(' - Use shadcn/ui components from src/components/ui/'));
|
|
372
|
+
console.log(chalk.dim(' - Follow the PropelKit color scheme from src/config/theme.ts'));
|
|
373
|
+
console.log(chalk.dim(' - See existing pages in src/app/ for patterns'));
|
|
374
|
+
console.log(chalk.dim(' - Run /propelkit:design later if you want AI assistance'));
|
|
375
|
+
console.log('');
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
approach: 'manual',
|
|
379
|
+
suggestedPages
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Handle skip/defer approach
|
|
385
|
+
* @returns {{ approach: 'skip' }}
|
|
386
|
+
*/
|
|
387
|
+
function handleSkipPath() {
|
|
388
|
+
console.log('');
|
|
389
|
+
console.log(chalk.yellow('Design decision deferred.'));
|
|
390
|
+
console.log('');
|
|
391
|
+
console.log(chalk.dim('You can revisit this later by running:'));
|
|
392
|
+
console.log(chalk.cyan(' /propelkit:design'));
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(chalk.dim('The AI PM can also help with design at any time.'));
|
|
395
|
+
console.log('');
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
approach: 'skip'
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Run the complete design flow
|
|
404
|
+
* @param {string} projectName - Name of the project
|
|
405
|
+
* @param {string} projectDir - Path to project directory
|
|
406
|
+
* @returns {Promise<{ approach: string, [key: string]: any }>}
|
|
407
|
+
*/
|
|
408
|
+
async function runDesignFlow(projectName, projectDir) {
|
|
409
|
+
console.log('');
|
|
410
|
+
console.log(chalk.bold.blue('Design Approach'));
|
|
411
|
+
console.log(chalk.dim('Choose how you want to create your UI'));
|
|
412
|
+
console.log('');
|
|
413
|
+
|
|
414
|
+
// Get approach choice
|
|
415
|
+
const approach = await askDesignApproach();
|
|
416
|
+
|
|
417
|
+
// Route to appropriate handler
|
|
418
|
+
switch (approach) {
|
|
419
|
+
case 'lovable':
|
|
420
|
+
return handleLovablePath(projectName, projectDir);
|
|
421
|
+
case 'existing':
|
|
422
|
+
return handleExistingPath(projectDir);
|
|
423
|
+
case 'manual':
|
|
424
|
+
return handleManualPath(projectDir);
|
|
425
|
+
case 'skip':
|
|
426
|
+
return handleSkipPath();
|
|
427
|
+
default:
|
|
428
|
+
return handleSkipPath();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
module.exports = {
|
|
433
|
+
runDesignFlow,
|
|
434
|
+
askDesignApproach,
|
|
435
|
+
askDesignSystem,
|
|
436
|
+
confirmPages,
|
|
437
|
+
handleLovablePath,
|
|
438
|
+
handleExistingPath,
|
|
439
|
+
handleManualPath,
|
|
440
|
+
handleSkipPath,
|
|
441
|
+
parseFeaturesFile
|
|
442
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { detectCLIs, displayCLIStatus, getCLIByName, askGitHubConnection } = require('./cli-detector');
|
|
6
|
+
const { handleScenario } = require('./scenarios');
|
|
7
|
+
const { runDesignFlow } = require('./design-flow');
|
|
8
|
+
const { runSetupWizard } = require('./setup-wizard');
|
|
9
|
+
const { launchClaude } = require('./launcher');
|
|
10
|
+
const { validateLicense } = require('./license-validator');
|
|
11
|
+
const messages = require('./messages');
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
// Step 1: Welcome
|
|
15
|
+
messages.welcome();
|
|
16
|
+
|
|
17
|
+
// Step 2: LICENSE VALIDATION (first thing after welcome)
|
|
18
|
+
console.log('');
|
|
19
|
+
const { licenseKey } = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'input',
|
|
22
|
+
name: 'licenseKey',
|
|
23
|
+
message: 'License key:',
|
|
24
|
+
validate: input => input.trim() ? true : 'License key is required'
|
|
25
|
+
}
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
console.log(chalk.dim('Validating license...'));
|
|
29
|
+
const validation = await validateLicense(licenseKey.trim());
|
|
30
|
+
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
messages.licenseInvalid(validation.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tier = validation.tier; // 'starter' or 'pro'
|
|
37
|
+
|
|
38
|
+
if (tier === 'starter') {
|
|
39
|
+
messages.starterLicenseValid();
|
|
40
|
+
} else {
|
|
41
|
+
messages.proLicenseValid();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 3: Detect CLIs
|
|
45
|
+
messages.detectingCLIs();
|
|
46
|
+
const clis = await detectCLIs();
|
|
47
|
+
displayCLIStatus(clis, chalk);
|
|
48
|
+
|
|
49
|
+
// Step 4: Check for Claude Code (required for PRO only)
|
|
50
|
+
const claudeCli = getCLIByName(clis, 'claude');
|
|
51
|
+
if (tier === 'pro' && !claudeCli.installed) {
|
|
52
|
+
messages.claudeRequired(claudeCli.installUrl);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Step 5: Show benefits for missing optional CLIs
|
|
57
|
+
const supabaseCli = getCLIByName(clis, 'supabase');
|
|
58
|
+
const ghCli = getCLIByName(clis, 'gh');
|
|
59
|
+
|
|
60
|
+
if (!supabaseCli.installed) {
|
|
61
|
+
messages.supabaseBenefits();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!ghCli.installed) {
|
|
65
|
+
messages.githubBenefits();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 6: GitHub connection (for Pro tier, or if user wants it for Starter)
|
|
69
|
+
const useGitHub = tier === 'pro' ? await askGitHubConnection(ghCli, chalk) : false;
|
|
70
|
+
|
|
71
|
+
// Step 7: Clone scenario (pass tier first)
|
|
72
|
+
let projectDir;
|
|
73
|
+
try {
|
|
74
|
+
projectDir = await handleScenario(tier, clis, useGitHub);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.message === 'Aborted by user') {
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(chalk.dim('Goodbye!'));
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 8: TIER-SPECIFIC POST-CLONE FLOW
|
|
85
|
+
if (tier === 'starter') {
|
|
86
|
+
// Starter: Run setup wizard, then done
|
|
87
|
+
await runSetupWizard(projectDir);
|
|
88
|
+
messages.starterWizardComplete();
|
|
89
|
+
} else {
|
|
90
|
+
// Pro: Run design flow, then launch Claude Code
|
|
91
|
+
console.log('');
|
|
92
|
+
const designResult = await runDesignFlow(
|
|
93
|
+
path.basename(projectDir),
|
|
94
|
+
projectDir
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Store design result in config.json
|
|
98
|
+
const configPath = path.join(projectDir, '.planning', 'config.json');
|
|
99
|
+
let config = {};
|
|
100
|
+
if (fs.existsSync(configPath)) {
|
|
101
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
102
|
+
}
|
|
103
|
+
config.designFlow = designResult;
|
|
104
|
+
|
|
105
|
+
const planningDir = path.dirname(configPath);
|
|
106
|
+
if (!fs.existsSync(planningDir)) {
|
|
107
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
110
|
+
|
|
111
|
+
// Launch Claude Code
|
|
112
|
+
messages.proLaunchingAIPM(projectDir);
|
|
113
|
+
try {
|
|
114
|
+
await launchClaude(projectDir);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.message === 'CLAUDE_NOT_FOUND') {
|
|
117
|
+
const claudeInfo = getCLIByName(clis, 'claude');
|
|
118
|
+
messages.claudeRequired(claudeInfo.installUrl);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { main };
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const messages = require('./messages');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Launch Claude Code in the specified directory
|
|
7
|
+
* @param {string} projectDir - Absolute path to project directory
|
|
8
|
+
* @param {object} options - Launch options
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
function launchClaude(projectDir, _options = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const resolvedDir = path.resolve(projectDir);
|
|
14
|
+
|
|
15
|
+
messages.launchingClaude(resolvedDir);
|
|
16
|
+
|
|
17
|
+
const claude = spawn('claude', [], {
|
|
18
|
+
cwd: resolvedDir,
|
|
19
|
+
stdio: 'inherit', // User sees Claude's I/O directly
|
|
20
|
+
shell: true // Required for Windows compatibility
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
claude.on('error', (err) => {
|
|
24
|
+
if (err.code === 'ENOENT') {
|
|
25
|
+
reject(new Error('CLAUDE_NOT_FOUND'));
|
|
26
|
+
} else {
|
|
27
|
+
reject(new Error(`Failed to launch Claude Code: ${err.message}`));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
claude.on('exit', (code) => {
|
|
32
|
+
if (code === 0) {
|
|
33
|
+
resolve();
|
|
34
|
+
} else {
|
|
35
|
+
// Non-zero exit is fine - user may have quit normally
|
|
36
|
+
resolve();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { launchClaude };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const { URL } = require('url');
|
|
3
|
+
const { API_ENDPOINTS, LICENSE_PATTERNS, TIMEOUTS } = require('./config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate license key against propelkit.dev API
|
|
7
|
+
* @param {string} key - License key (e.g., PK-STARTER-2026-ABC123)
|
|
8
|
+
* @returns {Promise<{valid: boolean, tier?: 'starter'|'pro', message?: string}>}
|
|
9
|
+
*/
|
|
10
|
+
async function validateLicense(key) {
|
|
11
|
+
// Client-side format check (quick fail for obviously invalid keys)
|
|
12
|
+
if (!LICENSE_PATTERNS.format.test(key)) {
|
|
13
|
+
return { valid: false, message: 'Invalid license key format' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const url = new URL(API_ENDPOINTS.licenseValidation);
|
|
18
|
+
const data = JSON.stringify({ licenseKey: key });
|
|
19
|
+
|
|
20
|
+
const options = {
|
|
21
|
+
hostname: url.hostname,
|
|
22
|
+
port: 443,
|
|
23
|
+
path: url.pathname,
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'Content-Length': Buffer.byteLength(data)
|
|
28
|
+
},
|
|
29
|
+
timeout: TIMEOUTS.licenseValidation
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const req = https.request(options, (res) => {
|
|
33
|
+
let body = '';
|
|
34
|
+
res.on('data', chunk => body += chunk);
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
try {
|
|
37
|
+
if (res.statusCode === 200) {
|
|
38
|
+
const result = JSON.parse(body);
|
|
39
|
+
resolve({
|
|
40
|
+
valid: true,
|
|
41
|
+
tier: result.tier // 'starter' or 'pro'
|
|
42
|
+
});
|
|
43
|
+
} else if (res.statusCode === 400) {
|
|
44
|
+
resolve({ valid: false, message: 'Invalid license key format' });
|
|
45
|
+
} else if (res.statusCode === 404) {
|
|
46
|
+
resolve({ valid: false, message: 'License key not found' });
|
|
47
|
+
} else if (res.statusCode === 403) {
|
|
48
|
+
resolve({ valid: false, message: 'License is not active' });
|
|
49
|
+
} else {
|
|
50
|
+
resolve({ valid: false, message: 'License validation failed' });
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
resolve({ valid: false, message: 'Invalid response from license server' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
req.on('timeout', () => {
|
|
59
|
+
req.destroy();
|
|
60
|
+
resolve({
|
|
61
|
+
valid: false,
|
|
62
|
+
message: 'License server timeout. Check your internet connection.'
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
req.on('error', (err) => {
|
|
67
|
+
if (err.code === 'ENOTFOUND') {
|
|
68
|
+
resolve({
|
|
69
|
+
valid: false,
|
|
70
|
+
message: 'Cannot reach license server. Check your internet connection.'
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
resolve({
|
|
74
|
+
valid: false,
|
|
75
|
+
message: `Network error: ${err.message}`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
req.write(data);
|
|
81
|
+
req.end();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { validateLicense };
|