@aifabrix/builder 2.0.0 → 2.0.3
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 +5 -3
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/env-config.yaml +9 -1
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +14 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +214 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- package/templates/typescript/tsconfig.json +24 -0
package/lib/secrets.js
CHANGED
|
@@ -13,6 +13,9 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
const logger = require('./utils/logger');
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Loads environment configuration for docker/local context
|
|
@@ -81,12 +84,16 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local')
|
|
|
81
84
|
const envConfig = loadEnvConfig();
|
|
82
85
|
const envVars = envConfig.environments[environment] || envConfig.environments.local;
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
// First, replace ${VAR} references in the template itself (for variables like DB_HOST=${DB_HOST})
|
|
88
|
+
let resolved = envTemplate.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
|
|
89
|
+
return envVars[envVar] || match;
|
|
90
|
+
});
|
|
91
|
+
|
|
85
92
|
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
86
93
|
const missingSecrets = [];
|
|
87
94
|
|
|
88
95
|
let match;
|
|
89
|
-
while ((match = kvPattern.exec(
|
|
96
|
+
while ((match = kvPattern.exec(resolved)) !== null) {
|
|
90
97
|
const secretKey = match[1];
|
|
91
98
|
if (!(secretKey in secrets)) {
|
|
92
99
|
missingSecrets.push(`kv://${secretKey}`);
|
|
@@ -97,9 +104,11 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local')
|
|
|
97
104
|
throw new Error(`Missing secrets: ${missingSecrets.join(', ')}`);
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
// Now replace kv:// references, and handle ${VAR} inside the secret values
|
|
100
108
|
resolved = resolved.replace(kvPattern, (match, secretKey) => {
|
|
101
109
|
let value = secrets[secretKey];
|
|
102
110
|
if (typeof value === 'string') {
|
|
111
|
+
// Replace ${VAR} references inside the secret value
|
|
103
112
|
value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
|
|
104
113
|
return envVars[envVar] || m;
|
|
105
114
|
});
|
|
@@ -110,6 +119,197 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local')
|
|
|
110
119
|
return resolved;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Finds missing secret keys from template
|
|
124
|
+
* @function findMissingSecretKeys
|
|
125
|
+
* @param {string} envTemplate - Environment template content
|
|
126
|
+
* @param {Object} existingSecrets - Existing secrets object
|
|
127
|
+
* @returns {string[]} Array of missing secret keys
|
|
128
|
+
*/
|
|
129
|
+
function findMissingSecretKeys(envTemplate, existingSecrets) {
|
|
130
|
+
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
131
|
+
const missingKeys = [];
|
|
132
|
+
const seenKeys = new Set();
|
|
133
|
+
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = kvPattern.exec(envTemplate)) !== null) {
|
|
136
|
+
const secretKey = match[1];
|
|
137
|
+
if (!seenKeys.has(secretKey) && !(secretKey in existingSecrets)) {
|
|
138
|
+
missingKeys.push(secretKey);
|
|
139
|
+
seenKeys.add(secretKey);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return missingKeys;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generates secret value based on key name
|
|
148
|
+
* @function generateSecretValue
|
|
149
|
+
* @param {string} key - Secret key name
|
|
150
|
+
* @returns {string} Generated secret value
|
|
151
|
+
*/
|
|
152
|
+
function generateSecretValue(key) {
|
|
153
|
+
const keyLower = key.toLowerCase();
|
|
154
|
+
|
|
155
|
+
if (keyLower.includes('password')) {
|
|
156
|
+
const dbPasswordMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-passwordKeyVault$/i);
|
|
157
|
+
if (dbPasswordMatch) {
|
|
158
|
+
const appName = dbPasswordMatch[1];
|
|
159
|
+
const dbName = appName.replace(/-/g, '_');
|
|
160
|
+
return `${dbName}_pass123`;
|
|
161
|
+
}
|
|
162
|
+
return crypto.randomBytes(32).toString('base64');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (keyLower.includes('url') || keyLower.includes('uri')) {
|
|
166
|
+
const dbUrlMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-urlKeyVault$/i);
|
|
167
|
+
if (dbUrlMatch) {
|
|
168
|
+
const appName = dbUrlMatch[1];
|
|
169
|
+
const dbName = appName.replace(/-/g, '_');
|
|
170
|
+
return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:5432/${dbName}`;
|
|
171
|
+
}
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (keyLower.includes('key') || keyLower.includes('secret') || keyLower.includes('token')) {
|
|
176
|
+
return crypto.randomBytes(32).toString('base64');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Loads existing secrets from file
|
|
184
|
+
* @function loadExistingSecrets
|
|
185
|
+
* @param {string} resolvedPath - Path to secrets file
|
|
186
|
+
* @returns {Object} Existing secrets object
|
|
187
|
+
*/
|
|
188
|
+
function loadExistingSecrets(resolvedPath) {
|
|
189
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
195
|
+
const secrets = yaml.load(content) || {};
|
|
196
|
+
return typeof secrets === 'object' ? secrets : {};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Saves secrets file
|
|
205
|
+
* @function saveSecretsFile
|
|
206
|
+
* @param {string} resolvedPath - Path to secrets file
|
|
207
|
+
* @param {Object} secrets - Secrets object to save
|
|
208
|
+
* @throws {Error} If save fails
|
|
209
|
+
*/
|
|
210
|
+
function saveSecretsFile(resolvedPath, secrets) {
|
|
211
|
+
const dir = path.dirname(resolvedPath);
|
|
212
|
+
if (!fs.existsSync(dir)) {
|
|
213
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const yamlContent = yaml.dump(secrets, {
|
|
217
|
+
indent: 2,
|
|
218
|
+
lineWidth: 120,
|
|
219
|
+
noRefs: true,
|
|
220
|
+
sortKeys: false
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generates missing secret keys in secrets file
|
|
228
|
+
* Scans env.template for kv:// references and adds missing keys with secure defaults
|
|
229
|
+
*
|
|
230
|
+
* @async
|
|
231
|
+
* @function generateMissingSecrets
|
|
232
|
+
* @param {string} envTemplate - Environment template content
|
|
233
|
+
* @param {string} secretsPath - Path to secrets file
|
|
234
|
+
* @returns {Promise<string[]>} Array of newly generated secret keys
|
|
235
|
+
* @throws {Error} If generation fails
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* const newKeys = await generateMissingSecrets(template, '~/.aifabrix/secrets.yaml');
|
|
239
|
+
* // Returns: ['new-secret-key', 'another-secret']
|
|
240
|
+
*/
|
|
241
|
+
async function generateMissingSecrets(envTemplate, secretsPath) {
|
|
242
|
+
const resolvedPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
243
|
+
const existingSecrets = loadExistingSecrets(resolvedPath);
|
|
244
|
+
const missingKeys = findMissingSecretKeys(envTemplate, existingSecrets);
|
|
245
|
+
|
|
246
|
+
if (missingKeys.length === 0) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const newSecrets = {};
|
|
251
|
+
for (const key of missingKeys) {
|
|
252
|
+
newSecrets[key] = generateSecretValue(key);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const updatedSecrets = { ...existingSecrets, ...newSecrets };
|
|
256
|
+
saveSecretsFile(resolvedPath, updatedSecrets);
|
|
257
|
+
|
|
258
|
+
logger.log(`✓ Generated ${missingKeys.length} missing secret key(s): ${missingKeys.join(', ')}`);
|
|
259
|
+
return missingKeys;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Loads environment template from file
|
|
264
|
+
* @function loadEnvTemplate
|
|
265
|
+
* @param {string} templatePath - Path to env.template
|
|
266
|
+
* @returns {string} Template content
|
|
267
|
+
* @throws {Error} If file not found
|
|
268
|
+
*/
|
|
269
|
+
function loadEnvTemplate(templatePath) {
|
|
270
|
+
if (!fs.existsSync(templatePath)) {
|
|
271
|
+
throw new Error(`env.template not found: ${templatePath}`);
|
|
272
|
+
}
|
|
273
|
+
return fs.readFileSync(templatePath, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Processes environment variables and copies to output path if needed
|
|
278
|
+
* @function processEnvVariables
|
|
279
|
+
* @param {string} envPath - Path to generated .env file
|
|
280
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
281
|
+
* @throws {Error} If processing fails
|
|
282
|
+
*/
|
|
283
|
+
function processEnvVariables(envPath, variablesPath) {
|
|
284
|
+
if (!fs.existsSync(variablesPath)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
289
|
+
const variables = yaml.load(variablesContent);
|
|
290
|
+
|
|
291
|
+
if (!variables?.build?.envOutputPath || variables.build.envOutputPath === null) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let outputPath = path.resolve(process.cwd(), variables.build.envOutputPath);
|
|
296
|
+
if (!outputPath.endsWith('.env')) {
|
|
297
|
+
if (fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) {
|
|
298
|
+
outputPath = path.join(outputPath, '.env');
|
|
299
|
+
} else {
|
|
300
|
+
outputPath = path.join(outputPath, '.env');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const outputDir = path.dirname(outputPath);
|
|
305
|
+
if (!fs.existsSync(outputDir)) {
|
|
306
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fs.copyFileSync(envPath, outputPath);
|
|
310
|
+
logger.log(chalk.green(`✓ Copied .env to: ${variables.build.envOutputPath}`));
|
|
311
|
+
}
|
|
312
|
+
|
|
113
313
|
/**
|
|
114
314
|
* Generates .env file from template and secrets
|
|
115
315
|
* Creates environment file for local development
|
|
@@ -119,44 +319,32 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local')
|
|
|
119
319
|
* @param {string} appName - Name of the application
|
|
120
320
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
121
321
|
* @param {string} [environment='local'] - Environment context
|
|
322
|
+
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
122
323
|
* @returns {Promise<string>} Path to generated .env file
|
|
123
324
|
* @throws {Error} If generation fails
|
|
124
325
|
*
|
|
125
326
|
* @example
|
|
126
|
-
* const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml');
|
|
327
|
+
* const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml', 'local', true);
|
|
127
328
|
* // Returns: './builder/myapp/.env'
|
|
128
329
|
*/
|
|
129
|
-
async function generateEnvFile(appName, secretsPath, environment = 'local') {
|
|
330
|
+
async function generateEnvFile(appName, secretsPath, environment = 'local', force = false) {
|
|
130
331
|
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
131
332
|
const templatePath = path.join(builderPath, 'env.template');
|
|
132
333
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
133
334
|
const envPath = path.join(builderPath, '.env');
|
|
134
335
|
|
|
135
|
-
|
|
136
|
-
|
|
336
|
+
const template = loadEnvTemplate(templatePath);
|
|
337
|
+
|
|
338
|
+
if (force) {
|
|
339
|
+
const resolvedSecretsPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
340
|
+
await generateMissingSecrets(template, resolvedSecretsPath);
|
|
137
341
|
}
|
|
138
342
|
|
|
139
|
-
const template = fs.readFileSync(templatePath, 'utf8');
|
|
140
343
|
const secrets = await loadSecrets(secretsPath);
|
|
141
344
|
const resolved = await resolveKvReferences(template, secrets, environment);
|
|
142
345
|
|
|
143
346
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
144
|
-
|
|
145
|
-
if (fs.existsSync(variablesPath)) {
|
|
146
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
147
|
-
const variables = yaml.load(variablesContent);
|
|
148
|
-
|
|
149
|
-
if (variables?.build?.envOutputPath) {
|
|
150
|
-
const outputPath = path.resolve(builderPath, variables.build.envOutputPath);
|
|
151
|
-
const outputDir = path.dirname(outputPath);
|
|
152
|
-
|
|
153
|
-
if (!fs.existsSync(outputDir)) {
|
|
154
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fs.copyFileSync(envPath, outputPath);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
347
|
+
processEnvVariables(envPath, variablesPath);
|
|
160
348
|
|
|
161
349
|
return envPath;
|
|
162
350
|
}
|
|
@@ -176,7 +364,23 @@ async function generateEnvFile(appName, secretsPath, environment = 'local') {
|
|
|
176
364
|
* // Returns: '~/.aifabrix/admin-secrets.env'
|
|
177
365
|
*/
|
|
178
366
|
async function generateAdminSecretsEnv(secretsPath) {
|
|
179
|
-
|
|
367
|
+
let secrets;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
secrets = await loadSecrets(secretsPath);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
// If secrets file doesn't exist, create default secrets
|
|
373
|
+
const defaultSecretsPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
374
|
+
|
|
375
|
+
if (!fs.existsSync(defaultSecretsPath)) {
|
|
376
|
+
logger.log('Creating default secrets file...');
|
|
377
|
+
await createDefaultSecrets(defaultSecretsPath);
|
|
378
|
+
secrets = await loadSecrets(secretsPath);
|
|
379
|
+
} else {
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
180
384
|
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
181
385
|
const adminEnvPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
182
386
|
|
|
@@ -276,6 +480,7 @@ module.exports = {
|
|
|
276
480
|
loadSecrets,
|
|
277
481
|
resolveKvReferences,
|
|
278
482
|
generateEnvFile,
|
|
483
|
+
generateMissingSecrets,
|
|
279
484
|
generateAdminSecretsEnv,
|
|
280
485
|
validateSecrets,
|
|
281
486
|
createDefaultSecrets
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Template Validation and Management
|
|
3
|
+
*
|
|
4
|
+
* Validates template folders and copies template files to application directories
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Template validation and file copying utilities
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const fsSync = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validates that a template exists and contains files
|
|
17
|
+
* @param {string} templateName - Template name to validate
|
|
18
|
+
* @returns {Promise<boolean>} True if template is valid
|
|
19
|
+
* @throws {Error} If template folder doesn't exist or is empty
|
|
20
|
+
*/
|
|
21
|
+
async function validateTemplate(templateName) {
|
|
22
|
+
if (!templateName || typeof templateName !== 'string') {
|
|
23
|
+
throw new Error('Template name is required and must be a string');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'applications', templateName);
|
|
27
|
+
|
|
28
|
+
// Check if template folder exists
|
|
29
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
30
|
+
throw new Error(`Template '${templateName}' not found. Expected folder: templates/applications/${templateName}/`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if it's a directory
|
|
34
|
+
const stats = fsSync.statSync(templatePath);
|
|
35
|
+
if (!stats.isDirectory()) {
|
|
36
|
+
throw new Error(`Template '${templateName}' exists but is not a directory`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if folder contains at least one file
|
|
40
|
+
const entries = await fs.readdir(templatePath);
|
|
41
|
+
const files = entries.filter(entry => {
|
|
42
|
+
const entryPath = path.join(templatePath, entry);
|
|
43
|
+
const entryStats = fsSync.statSync(entryPath);
|
|
44
|
+
return entryStats.isFile() && !entry.startsWith('.');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (files.length === 0) {
|
|
48
|
+
throw new Error(`Template '${templateName}' folder exists but contains no files`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Copies all files from template folder to application directory
|
|
56
|
+
* Preserves directory structure and skips hidden files
|
|
57
|
+
* @param {string} templateName - Template name to copy
|
|
58
|
+
* @param {string} appPath - Target application directory path
|
|
59
|
+
* @returns {Promise<string[]>} Array of copied file paths
|
|
60
|
+
* @throws {Error} If template validation fails or copying fails
|
|
61
|
+
*/
|
|
62
|
+
async function copyTemplateFiles(templateName, appPath) {
|
|
63
|
+
// Validate template first
|
|
64
|
+
await validateTemplate(templateName);
|
|
65
|
+
|
|
66
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'applications', templateName);
|
|
67
|
+
const copiedFiles = [];
|
|
68
|
+
|
|
69
|
+
async function copyDirectory(sourceDir, targetDir) {
|
|
70
|
+
// Ensure target directory exists
|
|
71
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const entries = await fs.readdir(sourceDir);
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
// Skip hidden files and directories
|
|
77
|
+
if (entry.startsWith('.')) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sourcePath = path.join(sourceDir, entry);
|
|
82
|
+
const targetPath = path.join(targetDir, entry);
|
|
83
|
+
|
|
84
|
+
const stats = await fs.stat(sourcePath);
|
|
85
|
+
|
|
86
|
+
if (stats.isDirectory()) {
|
|
87
|
+
// Recursively copy subdirectories
|
|
88
|
+
await copyDirectory(sourcePath, targetPath);
|
|
89
|
+
} else if (stats.isFile()) {
|
|
90
|
+
// Copy file
|
|
91
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
92
|
+
copiedFiles.push(targetPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await copyDirectory(templatePath, appPath);
|
|
98
|
+
|
|
99
|
+
return copiedFiles;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Copies application files from language template directory
|
|
104
|
+
* Copies files like package.json, index.ts, requirements.txt, main.py from templates/{language}/
|
|
105
|
+
* @param {string} language - Language name (typescript or python)
|
|
106
|
+
* @param {string} appPath - Target application directory path
|
|
107
|
+
* @returns {Promise<string[]>} Array of copied file paths
|
|
108
|
+
* @throws {Error} If language template doesn't exist or copying fails
|
|
109
|
+
*/
|
|
110
|
+
async function copyAppFiles(language, appPath) {
|
|
111
|
+
if (!language || typeof language !== 'string') {
|
|
112
|
+
throw new Error('Language is required and must be a string');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalizedLanguage = language.toLowerCase();
|
|
116
|
+
const languageTemplatePath = path.join(__dirname, '..', 'templates', normalizedLanguage);
|
|
117
|
+
|
|
118
|
+
// Check if language template folder exists
|
|
119
|
+
if (!fsSync.existsSync(languageTemplatePath)) {
|
|
120
|
+
throw new Error(`Language template '${normalizedLanguage}' not found. Expected folder: templates/${normalizedLanguage}/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stats = fsSync.statSync(languageTemplatePath);
|
|
124
|
+
if (!stats.isDirectory()) {
|
|
125
|
+
throw new Error(`Language template '${normalizedLanguage}' exists but is not a directory`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const copiedFiles = [];
|
|
129
|
+
const entries = await fs.readdir(languageTemplatePath);
|
|
130
|
+
|
|
131
|
+
// Copy only application files, skip Dockerfile and docker-compose templates
|
|
132
|
+
const appFiles = entries.filter(entry => {
|
|
133
|
+
const lowerEntry = entry.toLowerCase();
|
|
134
|
+
// Include .gitignore, exclude .hbs files and docker-related files
|
|
135
|
+
if (entry === '.gitignore') {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (lowerEntry.endsWith('.hbs')) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (entry.startsWith('.')) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
for (const entry of appFiles) {
|
|
151
|
+
const sourcePath = path.join(languageTemplatePath, entry);
|
|
152
|
+
const targetPath = path.join(appPath, entry);
|
|
153
|
+
|
|
154
|
+
const entryStats = await fs.stat(sourcePath);
|
|
155
|
+
if (entryStats.isFile()) {
|
|
156
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
157
|
+
copiedFiles.push(targetPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return copiedFiles;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Lists available templates
|
|
166
|
+
* @returns {Promise<string[]>} Array of available template names
|
|
167
|
+
*/
|
|
168
|
+
async function listAvailableTemplates() {
|
|
169
|
+
const templatesDir = path.join(__dirname, '..', 'templates', 'applications');
|
|
170
|
+
|
|
171
|
+
if (!fsSync.existsSync(templatesDir)) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const entries = await fs.readdir(templatesDir);
|
|
176
|
+
const templates = [];
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const entryPath = path.join(templatesDir, entry);
|
|
180
|
+
const stats = fsSync.statSync(entryPath);
|
|
181
|
+
|
|
182
|
+
if (stats.isDirectory()) {
|
|
183
|
+
// Check if directory contains at least one file
|
|
184
|
+
const subEntries = await fs.readdir(entryPath);
|
|
185
|
+
const hasFiles = subEntries.some(subEntry => {
|
|
186
|
+
const subPath = path.join(entryPath, subEntry);
|
|
187
|
+
const subStats = fsSync.statSync(subPath);
|
|
188
|
+
return subStats.isFile() && !subEntry.startsWith('.');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (hasFiles) {
|
|
192
|
+
templates.push(entry);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return templates.sort();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
validateTemplate,
|
|
202
|
+
copyTemplateFiles,
|
|
203
|
+
copyAppFiles,
|
|
204
|
+
listAvailableTemplates
|
|
205
|
+
};
|