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.
@@ -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 };
@@ -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 };