@l4yercak3/cli 1.0.6 → 1.1.1

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.
@@ -7,9 +7,11 @@ const configManager = require('../config/config-manager');
7
7
  const backendClient = require('../api/backend-client');
8
8
  const projectDetector = require('../detectors');
9
9
  const fileGenerator = require('../generators');
10
+ const { generateProjectPathHash } = require('../utils/file-utils');
10
11
  const inquirer = require('inquirer');
11
12
  const chalk = require('chalk');
12
13
  const path = require('path');
14
+ const pkg = require('../../package.json');
13
15
 
14
16
  /**
15
17
  * Helper function to create an organization
@@ -378,11 +380,15 @@ async function handleSpread() {
378
380
  name: 'features',
379
381
  message: 'Select features to enable:',
380
382
  choices: [
381
- { name: 'CRM (Contacts)', value: 'crm', checked: true },
382
- { name: 'Projects', value: 'projects', checked: true },
383
- { name: 'Invoices', value: 'invoices', checked: true },
383
+ { name: 'CRM (contacts, organizations)', value: 'crm', checked: true },
384
+ { name: 'Events (event management, registrations)', value: 'events', checked: false },
385
+ { name: 'Products (product catalog)', value: 'products', checked: false },
386
+ { name: 'Checkout (payment processing)', value: 'checkout', checked: false },
387
+ { name: 'Tickets (ticket generation)', value: 'tickets', checked: false },
388
+ { name: 'Invoicing (invoice creation)', value: 'invoicing', checked: false },
389
+ { name: 'Forms (dynamic forms)', value: 'forms', checked: false },
390
+ { name: 'Projects (project management)', value: 'projects', checked: false },
384
391
  { name: 'OAuth Authentication', value: 'oauth', checked: false },
385
- { name: 'Stripe Integration', value: 'stripe', checked: false },
386
392
  ],
387
393
  },
388
394
  ]);
@@ -425,17 +431,34 @@ async function handleSpread() {
425
431
  ]);
426
432
 
427
433
  // Step 7: Production domain (for OAuth redirect URIs)
428
- let productionDomain = 'your-domain.com';
434
+ let productionDomain = null;
429
435
  if (features.includes('oauth')) {
430
- const { domain } = await inquirer.prompt([
436
+ console.log(chalk.gray('\n ℹ️ The following settings are written to .env.local for local development.'));
437
+ console.log(chalk.gray(' For production, set NEXTAUTH_URL in your hosting platform (e.g., Vercel).\n'));
438
+
439
+ const { configureNow } = await inquirer.prompt([
431
440
  {
432
- type: 'input',
433
- name: 'domain',
434
- message: 'Production domain (for OAuth redirect URIs):',
435
- default: detection.github.repo ? `${detection.github.repo}.vercel.app` : 'your-domain.com',
441
+ type: 'confirm',
442
+ name: 'configureNow',
443
+ message: 'Configure production domain now? (You can skip and do this later)',
444
+ default: true,
436
445
  },
437
446
  ]);
438
- productionDomain = domain;
447
+
448
+ if (configureNow) {
449
+ const { domain } = await inquirer.prompt([
450
+ {
451
+ type: 'input',
452
+ name: 'domain',
453
+ message: 'Production domain (for OAuth redirect URIs):',
454
+ default: detection.github.repo ? `${detection.github.repo}.vercel.app` : 'your-domain.com',
455
+ },
456
+ ]);
457
+ productionDomain = domain;
458
+ } else {
459
+ console.log(chalk.gray(' Skipping production domain configuration.'));
460
+ console.log(chalk.gray(' Set NEXTAUTH_URL in your hosting platform when deploying.\n'));
461
+ }
439
462
  }
440
463
 
441
464
  // Step 8: Generate files
@@ -481,10 +504,104 @@ async function handleSpread() {
481
504
  console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.gitignore)} (updated)`));
482
505
  }
483
506
 
507
+ // Step 9: Register application with backend
508
+ console.log(chalk.cyan('\n 🔗 Registering with L4YERCAK3...\n'));
509
+
510
+ const projectPathHash = generateProjectPathHash(detection.projectPath);
511
+ let applicationId = null;
512
+ let isUpdate = false;
513
+
514
+ try {
515
+ // Check if application already exists for this project
516
+ const existingApp = await backendClient.checkExistingApplication(organizationId, projectPathHash);
517
+
518
+ if (existingApp.found && existingApp.application) {
519
+ // Application already registered
520
+ console.log(chalk.yellow(` ⚠️ This project is already registered as "${existingApp.application.name}"`));
521
+
522
+ const { updateAction } = await inquirer.prompt([
523
+ {
524
+ type: 'list',
525
+ name: 'updateAction',
526
+ message: 'What would you like to do?',
527
+ choices: [
528
+ { name: 'Update existing registration', value: 'update' },
529
+ { name: 'Skip registration (keep existing)', value: 'skip' },
530
+ ],
531
+ },
532
+ ]);
533
+
534
+ if (updateAction === 'update') {
535
+ // Update existing application
536
+ const updateData = {
537
+ connection: {
538
+ features,
539
+ hasFrontendDatabase: !!detection.framework.metadata?.hasPrisma,
540
+ frontendDatabaseType: detection.framework.metadata?.hasPrisma ? 'prisma' : undefined,
541
+ },
542
+ deployment: {
543
+ productionUrl: productionDomain ? `https://${productionDomain}` : undefined,
544
+ githubRepo: detection.github.isGitHub ? `${detection.github.owner}/${detection.github.repo}` : undefined,
545
+ },
546
+ };
547
+
548
+ await backendClient.updateApplication(existingApp.application.id, updateData);
549
+ applicationId = existingApp.application.id;
550
+ isUpdate = true;
551
+ console.log(chalk.green(` ✅ Application registration updated\n`));
552
+ } else {
553
+ applicationId = existingApp.application.id;
554
+ console.log(chalk.gray(` Skipped registration update\n`));
555
+ }
556
+ } else {
557
+ // Register new application
558
+ const registrationData = {
559
+ organizationId,
560
+ name: detection.github.repo || organizationName || 'My Application',
561
+ description: `Connected via CLI from ${detection.framework.type || 'unknown'} project`,
562
+ source: {
563
+ type: 'cli',
564
+ projectPathHash,
565
+ cliVersion: pkg.version,
566
+ framework: detection.framework.type || 'unknown',
567
+ frameworkVersion: detection.framework.metadata?.version,
568
+ hasTypeScript: detection.framework.metadata?.hasTypeScript || false,
569
+ routerType: detection.framework.metadata?.routerType,
570
+ },
571
+ connection: {
572
+ features,
573
+ hasFrontendDatabase: !!detection.framework.metadata?.hasPrisma,
574
+ frontendDatabaseType: detection.framework.metadata?.hasPrisma ? 'prisma' : undefined,
575
+ },
576
+ deployment: {
577
+ productionUrl: productionDomain ? `https://${productionDomain}` : undefined,
578
+ githubRepo: detection.github.isGitHub ? `${detection.github.owner}/${detection.github.repo}` : undefined,
579
+ githubBranch: detection.github.branch || 'main',
580
+ },
581
+ };
582
+
583
+ const registrationResult = await backendClient.registerApplication(registrationData);
584
+ applicationId = registrationResult.applicationId;
585
+ console.log(chalk.green(` ✅ Application registered with L4YERCAK3\n`));
586
+
587
+ // If backend returned a new API key, use it
588
+ if (registrationResult.apiKey && registrationResult.apiKey.key) {
589
+ console.log(chalk.gray(` API key generated: ${registrationResult.apiKey.prefix}`));
590
+ }
591
+ }
592
+ } catch (regError) {
593
+ // Registration failed but files were generated - warn but don't fail
594
+ console.log(chalk.yellow(` ⚠️ Could not register with backend: ${regError.message}`));
595
+ console.log(chalk.gray(' Your files were generated successfully.'));
596
+ console.log(chalk.gray(' Backend registration will be available in a future update.\n'));
597
+ }
598
+
484
599
  // Save project configuration
485
600
  const projectConfig = {
486
601
  organizationId,
487
602
  organizationName,
603
+ applicationId,
604
+ projectPathHash,
488
605
  apiKey: `${apiKey.substring(0, 10)}...`, // Store partial key for reference only
489
606
  backendUrl,
490
607
  features,
@@ -492,12 +609,20 @@ async function handleSpread() {
492
609
  productionDomain,
493
610
  frameworkType: detection.framework.type,
494
611
  createdAt: Date.now(),
612
+ updatedAt: isUpdate ? Date.now() : undefined,
495
613
  };
496
614
 
497
615
  configManager.saveProjectConfig(detection.projectPath, projectConfig);
498
616
  console.log(chalk.gray(` 📝 Configuration saved to ~/.l4yercak3/config.json\n`));
499
617
 
500
- console.log(chalk.cyan('\n 🎉 Setup complete!\n'));
618
+ // Show appropriate completion message based on registration status
619
+ if (applicationId) {
620
+ console.log(chalk.cyan('\n 🎉 Setup complete!\n'));
621
+ } else {
622
+ console.log(chalk.cyan('\n 🎉 Local setup complete!\n'));
623
+ console.log(chalk.yellow(' ⚠️ Note: Backend registration pending - your app works locally but'));
624
+ console.log(chalk.yellow(' won\'t appear in the L4YERCAK3 dashboard until endpoints are available.\n'));
625
+ }
501
626
 
502
627
  if (features.includes('oauth')) {
503
628
  console.log(chalk.yellow(' 📋 Next steps:\n'));
@@ -5,12 +5,15 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { checkFileOverwrite, writeFileWithBackup, ensureDir } = require('../utils/file-utils');
8
9
 
9
10
  class ApiClientGenerator {
10
11
  /**
11
12
  * Generate API client file
13
+ * @param {Object} options - Generation options
14
+ * @returns {Promise<string|null>} - Path to generated file or null if skipped
12
15
  */
13
- generate(options) {
16
+ async generate(options) {
14
17
  const {
15
18
  projectPath,
16
19
  apiKey,
@@ -25,13 +28,18 @@ class ApiClientGenerator {
25
28
  : path.join(projectPath, 'lib');
26
29
 
27
30
  // Ensure lib directory exists
28
- if (!fs.existsSync(libDir)) {
29
- fs.mkdirSync(libDir, { recursive: true });
30
- }
31
+ ensureDir(libDir);
31
32
 
32
33
  const extension = isTypeScript ? 'ts' : 'js';
33
34
  const outputPath = path.join(libDir, `api-client.${extension}`);
34
35
 
36
+ // Check if file exists and get action
37
+ const action = await checkFileOverwrite(outputPath);
38
+
39
+ if (action === 'skip') {
40
+ return null;
41
+ }
42
+
35
43
  // Generate API client code
36
44
  const code = this.generateCode({
37
45
  apiKey,
@@ -40,8 +48,7 @@ class ApiClientGenerator {
40
48
  isTypeScript,
41
49
  });
42
50
 
43
- fs.writeFileSync(outputPath, code, 'utf8');
44
- return outputPath;
51
+ return writeFileWithBackup(outputPath, code, action);
45
52
  }
46
53
 
47
54
  /**
@@ -105,6 +105,10 @@ class EnvGenerator {
105
105
  let content = `# L4YERCAK3 Configuration
106
106
  # Auto-generated by @l4yercak3/cli
107
107
  # DO NOT commit this file to git - it contains sensitive credentials
108
+ #
109
+ # This file is for LOCAL DEVELOPMENT. For production:
110
+ # - Set these variables in your hosting platform (Vercel, Netlify, etc.)
111
+ # - NEXTAUTH_URL should be your production domain
108
112
 
109
113
  # Core API Configuration
110
114
  L4YERCAK3_API_KEY=${envVars.L4YERCAK3_API_KEY}
@@ -117,6 +121,11 @@ NEXT_PUBLIC_L4YERCAK3_BACKEND_URL=${envVars.NEXT_PUBLIC_L4YERCAK3_BACKEND_URL}
117
121
  // Add OAuth section if present
118
122
  if (envVars.GOOGLE_CLIENT_ID || envVars.AZURE_CLIENT_ID || envVars.GITHUB_CLIENT_ID) {
119
123
  content += `# OAuth Configuration
124
+ # Get your OAuth credentials from:
125
+ # - Google: https://console.cloud.google.com/apis/credentials
126
+ # - Microsoft: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
127
+ # - GitHub: https://github.com/settings/developers
128
+
120
129
  `;
121
130
  if (envVars.GOOGLE_CLIENT_ID) {
122
131
  content += `GOOGLE_CLIENT_ID=${envVars.GOOGLE_CLIENT_ID}
@@ -138,7 +147,11 @@ GITHUB_CLIENT_SECRET=${envVars.GITHUB_CLIENT_SECRET}
138
147
  `;
139
148
  }
140
149
  if (envVars.NEXTAUTH_URL) {
141
- content += `NEXTAUTH_URL=${envVars.NEXTAUTH_URL}
150
+ content += `# NextAuth.js Configuration
151
+ # NEXTAUTH_URL: Set to http://localhost:3000 for development
152
+ # For production, set this in your hosting platform (Vercel auto-sets this)
153
+ NEXTAUTH_URL=${envVars.NEXTAUTH_URL}
154
+ # Generate with: openssl rand -base64 32
142
155
  NEXTAUTH_SECRET=${envVars.NEXTAUTH_SECRET}
143
156
 
144
157
  `;
@@ -22,17 +22,17 @@ class FileGenerator {
22
22
  gitignore: null,
23
23
  };
24
24
 
25
- // Generate API client
25
+ // Generate API client (async - checks for file overwrites)
26
26
  if (options.features && options.features.length > 0) {
27
- results.apiClient = apiClientGenerator.generate(options);
27
+ results.apiClient = await apiClientGenerator.generate(options);
28
28
  }
29
29
 
30
30
  // Generate environment file
31
31
  results.envFile = envGenerator.generate(options);
32
32
 
33
- // Generate NextAuth.js config if OAuth is enabled
33
+ // Generate NextAuth.js config if OAuth is enabled (async - checks for file overwrites)
34
34
  if (options.features && options.features.includes('oauth') && options.oauthProviders) {
35
- results.nextauth = nextauthGenerator.generate(options);
35
+ results.nextauth = await nextauthGenerator.generate(options);
36
36
  }
37
37
 
38
38
  // Generate OAuth guide if OAuth is enabled
@@ -5,12 +5,15 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { checkFileOverwrite, writeFileWithBackup, ensureDir } = require('../utils/file-utils');
8
9
 
9
10
  class NextAuthGenerator {
10
11
  /**
11
12
  * Generate NextAuth.js configuration
13
+ * @param {Object} options - Generation options
14
+ * @returns {Promise<string|null>} - Path to generated file or null if skipped
12
15
  */
13
- generate(options) {
16
+ async generate(options) {
14
17
  const {
15
18
  projectPath,
16
19
  backendUrl,
@@ -25,9 +28,7 @@ class NextAuthGenerator {
25
28
  : path.join(projectPath, 'pages', 'api', 'auth');
26
29
 
27
30
  // Ensure directory exists
28
- if (!fs.existsSync(apiDir)) {
29
- fs.mkdirSync(apiDir, { recursive: true });
30
- }
31
+ ensureDir(apiDir);
31
32
 
32
33
  const extension = isTypeScript ? 'ts' : 'js';
33
34
  const routePath = routerType === 'app'
@@ -37,9 +38,14 @@ class NextAuthGenerator {
37
38
  // Ensure [...nextauth] directory exists for App Router
38
39
  if (routerType === 'app') {
39
40
  const nextauthDir = path.join(apiDir, '[...nextauth]');
40
- if (!fs.existsSync(nextauthDir)) {
41
- fs.mkdirSync(nextauthDir, { recursive: true });
42
- }
41
+ ensureDir(nextauthDir);
42
+ }
43
+
44
+ // Check if file exists and get action
45
+ const action = await checkFileOverwrite(routePath);
46
+
47
+ if (action === 'skip') {
48
+ return null;
43
49
  }
44
50
 
45
51
  // Generate NextAuth configuration
@@ -50,8 +56,7 @@ class NextAuthGenerator {
50
56
  isTypeScript,
51
57
  });
52
58
 
53
- fs.writeFileSync(routePath, code, 'utf8');
54
- return routePath;
59
+ return writeFileWithBackup(routePath, code, action);
55
60
  }
56
61
 
57
62
  /**
@@ -0,0 +1,117 @@
1
+ /**
2
+ * File Utilities
3
+ * Helper functions for safe file operations
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+ const inquirer = require('inquirer');
10
+ const chalk = require('chalk');
11
+
12
+ const GENERATED_HEADER = 'Auto-generated by @l4yercak3/cli';
13
+
14
+ /**
15
+ * Check if a file was generated by our CLI
16
+ */
17
+ function isGeneratedFile(filePath) {
18
+ if (!fs.existsSync(filePath)) {
19
+ return false;
20
+ }
21
+
22
+ try {
23
+ const content = fs.readFileSync(filePath, 'utf8');
24
+ // Check first 500 chars for our header
25
+ return content.substring(0, 500).includes(GENERATED_HEADER);
26
+ } catch (error) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Check if file exists and prompt for action if needed
33
+ * Returns: 'write' | 'skip' | 'backup'
34
+ */
35
+ async function checkFileOverwrite(filePath, options = {}) {
36
+ const { silent = false, defaultAction = 'prompt' } = options;
37
+
38
+ if (!fs.existsSync(filePath)) {
39
+ return 'write';
40
+ }
41
+
42
+ // If it's our generated file, safe to overwrite
43
+ if (isGeneratedFile(filePath)) {
44
+ if (!silent) {
45
+ console.log(chalk.gray(` Updating ${path.basename(filePath)} (previously generated)`));
46
+ }
47
+ return 'write';
48
+ }
49
+
50
+ // File exists and wasn't generated by us - prompt user
51
+ if (defaultAction === 'skip') {
52
+ return 'skip';
53
+ }
54
+
55
+ const relativePath = path.basename(filePath);
56
+ console.log(chalk.yellow(`\n ⚠️ ${relativePath} already exists and appears to be modified`));
57
+
58
+ const { action } = await inquirer.prompt([
59
+ {
60
+ type: 'list',
61
+ name: 'action',
62
+ message: `What would you like to do with ${relativePath}?`,
63
+ choices: [
64
+ { name: 'Overwrite (replace with generated code)', value: 'write' },
65
+ { name: 'Backup and overwrite (save old file as .backup)', value: 'backup' },
66
+ { name: 'Skip (keep existing file)', value: 'skip' },
67
+ ],
68
+ },
69
+ ]);
70
+
71
+ return action;
72
+ }
73
+
74
+ /**
75
+ * Write file with optional backup
76
+ */
77
+ function writeFileWithBackup(filePath, content, action) {
78
+ if (action === 'skip') {
79
+ return null;
80
+ }
81
+
82
+ if (action === 'backup' && fs.existsSync(filePath)) {
83
+ const backupPath = `${filePath}.backup`;
84
+ fs.copyFileSync(filePath, backupPath);
85
+ console.log(chalk.gray(` Backed up to ${path.basename(backupPath)}`));
86
+ }
87
+
88
+ fs.writeFileSync(filePath, content, 'utf8');
89
+ return filePath;
90
+ }
91
+
92
+ /**
93
+ * Ensure directory exists
94
+ */
95
+ function ensureDir(dirPath) {
96
+ if (!fs.existsSync(dirPath)) {
97
+ fs.mkdirSync(dirPath, { recursive: true });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Generate SHA256 hash of project path for identification
103
+ * This is used to identify returning projects when registering with backend
104
+ */
105
+ function generateProjectPathHash(projectPath) {
106
+ const absolutePath = path.resolve(projectPath);
107
+ return crypto.createHash('sha256').update(absolutePath).digest('hex');
108
+ }
109
+
110
+ module.exports = {
111
+ GENERATED_HEADER,
112
+ isGeneratedFile,
113
+ checkFileOverwrite,
114
+ writeFileWithBackup,
115
+ ensureDir,
116
+ generateProjectPathHash,
117
+ };
@@ -6,8 +6,17 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
8
  jest.mock('fs');
9
+ jest.mock('../src/utils/file-utils', () => ({
10
+ checkFileOverwrite: jest.fn().mockResolvedValue('write'),
11
+ writeFileWithBackup: jest.fn((filePath, content, action) => {
12
+ if (action === 'skip') return null;
13
+ return filePath;
14
+ }),
15
+ ensureDir: jest.fn(),
16
+ }));
9
17
 
10
18
  const ApiClientGenerator = require('../src/generators/api-client-generator');
19
+ const { checkFileOverwrite, writeFileWithBackup, ensureDir } = require('../src/utils/file-utils');
11
20
 
12
21
  describe('ApiClientGenerator', () => {
13
22
  const mockProjectPath = '/test/project';
@@ -17,10 +26,11 @@ describe('ApiClientGenerator', () => {
17
26
  fs.existsSync.mockReturnValue(false);
18
27
  fs.mkdirSync.mockReturnValue(undefined);
19
28
  fs.writeFileSync.mockReturnValue(undefined);
29
+ checkFileOverwrite.mockResolvedValue('write');
20
30
  });
21
31
 
22
32
  describe('generate', () => {
23
- it('creates api-client.js in lib folder when no src exists', () => {
33
+ it('creates api-client.js in lib folder when no src exists', async () => {
24
34
  fs.existsSync.mockReturnValue(false);
25
35
 
26
36
  const options = {
@@ -31,17 +41,14 @@ describe('ApiClientGenerator', () => {
31
41
  isTypeScript: false,
32
42
  };
33
43
 
34
- const result = ApiClientGenerator.generate(options);
44
+ const result = await ApiClientGenerator.generate(options);
35
45
 
36
46
  expect(result).toBe(path.join(mockProjectPath, 'lib', 'api-client.js'));
37
- expect(fs.mkdirSync).toHaveBeenCalledWith(
38
- path.join(mockProjectPath, 'lib'),
39
- { recursive: true }
40
- );
41
- expect(fs.writeFileSync).toHaveBeenCalled();
47
+ expect(ensureDir).toHaveBeenCalledWith(path.join(mockProjectPath, 'lib'));
48
+ expect(writeFileWithBackup).toHaveBeenCalled();
42
49
  });
43
50
 
44
- it('creates api-client.ts in src/lib folder when src exists', () => {
51
+ it('creates api-client.ts in src/lib folder when src exists', async () => {
45
52
  fs.existsSync.mockImplementation((p) =>
46
53
  p === path.join(mockProjectPath, 'src')
47
54
  );
@@ -54,13 +61,13 @@ describe('ApiClientGenerator', () => {
54
61
  isTypeScript: true,
55
62
  };
56
63
 
57
- const result = ApiClientGenerator.generate(options);
64
+ const result = await ApiClientGenerator.generate(options);
58
65
 
59
66
  expect(result).toBe(path.join(mockProjectPath, 'src', 'lib', 'api-client.ts'));
60
67
  });
61
68
 
62
- it('does not create lib dir if it exists', () => {
63
- fs.existsSync.mockReturnValue(true);
69
+ it('returns null when user skips overwrite', async () => {
70
+ checkFileOverwrite.mockResolvedValue('skip');
64
71
 
65
72
  const options = {
66
73
  projectPath: mockProjectPath,
@@ -70,9 +77,9 @@ describe('ApiClientGenerator', () => {
70
77
  isTypeScript: false,
71
78
  };
72
79
 
73
- ApiClientGenerator.generate(options);
80
+ const result = await ApiClientGenerator.generate(options);
74
81
 
75
- expect(fs.mkdirSync).not.toHaveBeenCalled();
82
+ expect(result).toBeNull();
76
83
  });
77
84
  });
78
85