@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.
- package/bin/cli.js +6 -0
- package/docs/microsass_production_machine/CLI_API_REFERENCE.md +1197 -0
- package/docs/microsass_production_machine/CLI_PRODUCT_VISION.md +676 -0
- package/docs/microsass_production_machine/CLI_REQUIREMENTS.md +606 -0
- package/docs/microsass_production_machine/CONNECTED_APPLICATIONS_SPEC.md +390 -0
- package/docs/microsass_production_machine/IMPLEMENTATION_ROADMAP.md +725 -0
- package/docs/microsass_production_machine/OBJECT_MAPPINGS.md +808 -0
- package/docs/microsass_production_machine/REFERENCE_IMPLEMENTATION.md +532 -0
- package/package.json +1 -1
- package/src/api/backend-client.js +62 -0
- package/src/commands/spread.js +137 -12
- package/src/generators/api-client-generator.js +13 -6
- package/src/generators/env-generator.js +14 -1
- package/src/generators/index.js +4 -4
- package/src/generators/nextauth-generator.js +14 -9
- package/src/utils/file-utils.js +117 -0
- package/tests/api-client-generator.test.js +20 -13
- package/tests/backend-client.test.js +167 -0
- package/tests/file-utils.test.js +194 -0
- package/tests/generators-index.test.js +8 -0
- package/tests/nextauth-generator.test.js +38 -14
package/src/commands/spread.js
CHANGED
|
@@ -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 (
|
|
382
|
-
{ name: '
|
|
383
|
-
{ name: '
|
|
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 =
|
|
434
|
+
let productionDomain = null;
|
|
429
435
|
if (features.includes('oauth')) {
|
|
430
|
-
|
|
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: '
|
|
433
|
-
name: '
|
|
434
|
-
message: '
|
|
435
|
-
default:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 +=
|
|
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
|
`;
|
package/src/generators/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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(
|
|
38
|
-
|
|
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('
|
|
63
|
-
|
|
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(
|
|
82
|
+
expect(result).toBeNull();
|
|
76
83
|
});
|
|
77
84
|
});
|
|
78
85
|
|