@aifabrix/builder 2.0.3 → 2.0.5

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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@aifabrix/builder.svg)](https://www.npmjs.com/package/@aifabrix/builder)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
5
6
  Local development infrastructure + Azure deployment tool.
6
7
 
7
8
  ## Install
package/lib/app-run.js CHANGED
@@ -281,11 +281,11 @@ async function startContainer(appName, composePath, port, config = null) {
281
281
  await execAsync(`docker-compose -f "${composePath}" up -d`, { env });
282
282
  logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
283
283
 
284
- // Wait for health check
284
+ // Wait for health check - detect actual mapped port from Docker
285
+ // Don't pass port so waitForHealthCheck will auto-detect it from container
285
286
  const healthCheckPath = config?.healthCheck?.path || '/health';
286
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
287
- logger.log(chalk.blue(`Waiting for application to be healthy at ${healthCheckUrl}...`));
288
- await waitForHealthCheck(appName, 90, port, config);
287
+ logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
288
+ await waitForHealthCheck(appName, 90, null, config);
289
289
  }
290
290
 
291
291
  /**
package/lib/secrets.js CHANGED
@@ -13,9 +13,12 @@ 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
16
  const chalk = require('chalk');
18
17
  const logger = require('./utils/logger');
18
+ const {
19
+ generateMissingSecrets,
20
+ createDefaultSecrets
21
+ } = require('./utils/secrets-generator');
19
22
 
20
23
  /**
21
24
  * Loads environment configuration for docker/local context
@@ -42,13 +45,7 @@ function loadEnvConfig() {
42
45
  * // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
43
46
  */
44
47
  async function loadSecrets(secretsPath) {
45
- let resolvedPath = secretsPath;
46
-
47
- if (!resolvedPath) {
48
- resolvedPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
49
- } else if (secretsPath.startsWith('..')) {
50
- resolvedPath = path.resolve(process.cwd(), secretsPath);
51
- }
48
+ const resolvedPath = resolveSecretsPath(secretsPath);
52
49
 
53
50
  if (!fs.existsSync(resolvedPath)) {
54
51
  throw new Error(`Secrets file not found: ${resolvedPath}`);
@@ -64,6 +61,45 @@ async function loadSecrets(secretsPath) {
64
61
  return secrets;
65
62
  }
66
63
 
64
+ /**
65
+ * Resolves secrets file path (same logic as loadSecrets)
66
+ * Also checks common locations if path is not provided
67
+ * @function resolveSecretsPath
68
+ * @param {string} [secretsPath] - Path to secrets file (optional)
69
+ * @returns {string} Resolved secrets file path
70
+ */
71
+ function resolveSecretsPath(secretsPath) {
72
+ let resolvedPath = secretsPath;
73
+
74
+ if (!resolvedPath) {
75
+ // Check common locations for secrets.local.yaml
76
+ const commonLocations = [
77
+ path.join(process.cwd(), '..', 'aifabrix-setup', 'secrets.local.yaml'),
78
+ path.join(process.cwd(), '..', '..', 'aifabrix-setup', 'secrets.local.yaml'),
79
+ path.join(process.cwd(), 'secrets.local.yaml'),
80
+ path.join(process.cwd(), '..', 'secrets.local.yaml'),
81
+ path.join(os.homedir(), '.aifabrix', 'secrets.yaml')
82
+ ];
83
+
84
+ // Find first existing file
85
+ for (const location of commonLocations) {
86
+ if (fs.existsSync(location)) {
87
+ resolvedPath = location;
88
+ break;
89
+ }
90
+ }
91
+
92
+ // If none found, use default location
93
+ if (!resolvedPath) {
94
+ resolvedPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
95
+ }
96
+ } else if (secretsPath.startsWith('..')) {
97
+ resolvedPath = path.resolve(process.cwd(), secretsPath);
98
+ }
99
+
100
+ return resolvedPath;
101
+ }
102
+
67
103
  /**
68
104
  * Resolves kv:// references in environment template
69
105
  * Replaces kv://keyName with actual values from secrets
@@ -73,6 +109,7 @@ async function loadSecrets(secretsPath) {
73
109
  * @param {string} envTemplate - Environment template content
74
110
  * @param {Object} secrets - Secrets object from loadSecrets()
75
111
  * @param {string} [environment='local'] - Environment context (docker/local)
112
+ * @param {string} [secretsFilePath] - Path to secrets file (for error messages)
76
113
  * @returns {Promise<string>} Resolved environment content
77
114
  * @throws {Error} If kv:// reference cannot be resolved
78
115
  *
@@ -80,7 +117,7 @@ async function loadSecrets(secretsPath) {
80
117
  * const resolved = await resolveKvReferences(template, secrets, 'local');
81
118
  * // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
82
119
  */
83
- async function resolveKvReferences(envTemplate, secrets, environment = 'local') {
120
+ async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePath = null) {
84
121
  const envConfig = loadEnvConfig();
85
122
  const envVars = envConfig.environments[environment] || envConfig.environments.local;
86
123
 
@@ -101,7 +138,8 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local')
101
138
  }
102
139
 
103
140
  if (missingSecrets.length > 0) {
104
- throw new Error(`Missing secrets: ${missingSecrets.join(', ')}`);
141
+ const fileInfo = secretsFilePath ? `\n\nSecrets file location: ${secretsFilePath}` : '';
142
+ throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}`);
105
143
  }
106
144
 
107
145
  // Now replace kv:// references, and handle ${VAR} inside the secret values
@@ -119,146 +157,6 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local')
119
157
  return resolved;
120
158
  }
121
159
 
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
160
  /**
263
161
  * Loads environment template from file
264
162
  * @function loadEnvTemplate
@@ -335,13 +233,15 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
335
233
 
336
234
  const template = loadEnvTemplate(templatePath);
337
235
 
236
+ // Resolve secrets path to show in error messages
237
+ const resolvedSecretsPath = resolveSecretsPath(secretsPath);
238
+
338
239
  if (force) {
339
- const resolvedSecretsPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
340
240
  await generateMissingSecrets(template, resolvedSecretsPath);
341
241
  }
342
242
 
343
243
  const secrets = await loadSecrets(secretsPath);
344
- const resolved = await resolveKvReferences(template, secrets, environment);
244
+ const resolved = await resolveKvReferences(template, secrets, environment, resolvedSecretsPath);
345
245
 
346
246
  fs.writeFileSync(envPath, resolved, { mode: 0o600 });
347
247
  processEnvVariables(envPath, variablesPath);
@@ -434,48 +334,6 @@ function validateSecrets(envTemplate, secrets) {
434
334
  };
435
335
  }
436
336
 
437
- /**
438
- * Creates default secrets file if it doesn't exist
439
- * Generates template with common secrets for local development
440
- *
441
- * @async
442
- * @function createDefaultSecrets
443
- * @param {string} secretsPath - Path where to create secrets file
444
- * @returns {Promise<void>} Resolves when file is created
445
- * @throws {Error} If file creation fails
446
- *
447
- * @example
448
- * await createDefaultSecrets('~/.aifabrix/secrets.yaml');
449
- * // Default secrets file is created
450
- */
451
- async function createDefaultSecrets(secretsPath) {
452
- const resolvedPath = secretsPath.startsWith('~')
453
- ? path.join(os.homedir(), secretsPath.slice(1))
454
- : secretsPath;
455
-
456
- const dir = path.dirname(resolvedPath);
457
- if (!fs.existsSync(dir)) {
458
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
459
- }
460
-
461
- const defaultSecrets = `# Local Development Secrets
462
- # Production uses Azure KeyVault
463
-
464
- # Database Secrets
465
- postgres-passwordKeyVault: "admin123"
466
-
467
- # Redis Secrets
468
- redis-passwordKeyVault: ""
469
- redis-urlKeyVault: "redis://\${REDIS_HOST}:6379"
470
-
471
- # Keycloak Secrets
472
- keycloak-admin-passwordKeyVault: "admin123"
473
- keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:8082"
474
- `;
475
-
476
- fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });
477
- }
478
-
479
337
  module.exports = {
480
338
  loadSecrets,
481
339
  resolveKvReferences,
@@ -107,14 +107,18 @@ function buildRequiresConfig(config) {
107
107
  * @returns {Object} Service configuration
108
108
  */
109
109
  function buildServiceConfig(appName, config, port) {
110
- const containerPort = config.build?.containerPort || config.port || 3000;
110
+ // Host port is the local port (from options.port or build.localPort)
111
+ const hostPort = port;
112
+ // Container port: use build.containerPort if specified, otherwise use port config
113
+ const containerPortValue = config.build?.containerPort || config.port || hostPort;
111
114
 
112
115
  return {
113
116
  app: buildAppConfig(appName, config),
114
117
  image: buildImageConfig(config, appName),
115
- port: containerPort,
118
+ port: containerPortValue, // Container port (for health check and template)
119
+ containerPort: config.build?.containerPort || null, // Explicit containerPort if specified, null otherwise
116
120
  build: {
117
- localPort: port
121
+ localPort: hostPort // Host port
118
122
  },
119
123
  healthCheck: buildHealthCheckConfig(config),
120
124
  ...buildRequiresConfig(config)
@@ -71,10 +71,29 @@ async function waitForDbInit(appName) {
71
71
  */
72
72
  async function getContainerPort(appName) {
73
73
  try {
74
- const { stdout: portMapping } = await execAsync(`docker inspect --format='{{range .NetworkSettings.Ports}}{{range .}}{{.HostPort}}{{end}}{{end}}' aifabrix-${appName}`);
75
- const ports = portMapping.trim().split('\n').filter(p => p);
74
+ // Try to get the actual mapped host port from Docker
75
+ // First try docker inspect for the container port mapping
76
+ const { stdout: portMapping } = await execAsync(`docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`);
77
+ const ports = portMapping.trim().split('\n').filter(p => p && p !== '');
76
78
  if (ports.length > 0) {
77
- return parseInt(ports[0], 10);
79
+ const port = parseInt(ports[0], 10);
80
+ if (!isNaN(port) && port > 0) {
81
+ return port;
82
+ }
83
+ }
84
+
85
+ // Fallback: try docker ps to get port mapping (format: "0.0.0.0:3010->3000/tcp")
86
+ try {
87
+ const { stdout: psOutput } = await execAsync(`docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`);
88
+ const portMatch = psOutput.match(/:(\d+)->/);
89
+ if (portMatch) {
90
+ const port = parseInt(portMatch[1], 10);
91
+ if (!isNaN(port) && port > 0) {
92
+ return port;
93
+ }
94
+ }
95
+ } catch (error) {
96
+ // Fall through
78
97
  }
79
98
  } catch (error) {
80
99
  // Fall through to default
@@ -152,12 +171,12 @@ async function checkHealthEndpoint(healthCheckUrl) {
152
171
  async function waitForHealthCheck(appName, timeout = 90, port = null, config = null) {
153
172
  await waitForDbInit(appName);
154
173
 
155
- if (!port) {
156
- port = await getContainerPort(appName);
157
- }
174
+ // Always detect the actual port from Docker to ensure we use the correct mapped port
175
+ const detectedPort = await getContainerPort(appName);
176
+ const healthCheckPort = port || detectedPort;
158
177
 
159
178
  const healthCheckPath = config?.healthCheck?.path || '/health';
160
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
179
+ const healthCheckUrl = `http://localhost:${healthCheckPort}${healthCheckPath}`;
161
180
  const maxAttempts = timeout / 2;
162
181
 
163
182
  for (let attempts = 0; attempts < maxAttempts; attempts++) {
@@ -0,0 +1,209 @@
1
+ /**
2
+ * AI Fabrix Builder Secrets Generation Utilities
3
+ *
4
+ * This module handles secret generation and file management.
5
+ * Generates default secret values and manages secrets files.
6
+ *
7
+ * @fileoverview Secret generation utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const os = require('os');
16
+ const crypto = require('crypto');
17
+ const logger = require('./logger');
18
+
19
+ /**
20
+ * Finds missing secret keys from template
21
+ * @function findMissingSecretKeys
22
+ * @param {string} envTemplate - Environment template content
23
+ * @param {Object} existingSecrets - Existing secrets object
24
+ * @returns {string[]} Array of missing secret keys
25
+ */
26
+ function findMissingSecretKeys(envTemplate, existingSecrets) {
27
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
28
+ const missingKeys = [];
29
+ const seenKeys = new Set();
30
+
31
+ let match;
32
+ while ((match = kvPattern.exec(envTemplate)) !== null) {
33
+ const secretKey = match[1];
34
+ if (!seenKeys.has(secretKey) && !(secretKey in existingSecrets)) {
35
+ missingKeys.push(secretKey);
36
+ seenKeys.add(secretKey);
37
+ }
38
+ }
39
+
40
+ return missingKeys;
41
+ }
42
+
43
+ /**
44
+ * Generates secret value based on key name
45
+ * @function generateSecretValue
46
+ * @param {string} key - Secret key name
47
+ * @returns {string} Generated secret value
48
+ */
49
+ function generateSecretValue(key) {
50
+ const keyLower = key.toLowerCase();
51
+
52
+ if (keyLower.includes('password')) {
53
+ const dbPasswordMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-passwordKeyVault$/i);
54
+ if (dbPasswordMatch) {
55
+ const appName = dbPasswordMatch[1];
56
+ const dbName = appName.replace(/-/g, '_');
57
+ return `${dbName}_pass123`;
58
+ }
59
+ return crypto.randomBytes(32).toString('base64');
60
+ }
61
+
62
+ if (keyLower.includes('url') || keyLower.includes('uri')) {
63
+ const dbUrlMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-urlKeyVault$/i);
64
+ if (dbUrlMatch) {
65
+ const appName = dbUrlMatch[1];
66
+ const dbName = appName.replace(/-/g, '_');
67
+ return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:5432/${dbName}`;
68
+ }
69
+ return '';
70
+ }
71
+
72
+ if (keyLower.includes('key') || keyLower.includes('secret') || keyLower.includes('token')) {
73
+ return crypto.randomBytes(32).toString('base64');
74
+ }
75
+
76
+ return '';
77
+ }
78
+
79
+ /**
80
+ * Loads existing secrets from file
81
+ * @function loadExistingSecrets
82
+ * @param {string} resolvedPath - Path to secrets file
83
+ * @returns {Object} Existing secrets object
84
+ */
85
+ function loadExistingSecrets(resolvedPath) {
86
+ if (!fs.existsSync(resolvedPath)) {
87
+ return {};
88
+ }
89
+
90
+ try {
91
+ const content = fs.readFileSync(resolvedPath, 'utf8');
92
+ const secrets = yaml.load(content) || {};
93
+ return typeof secrets === 'object' ? secrets : {};
94
+ } catch (error) {
95
+ logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
96
+ return {};
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Saves secrets file
102
+ * @function saveSecretsFile
103
+ * @param {string} resolvedPath - Path to secrets file
104
+ * @param {Object} secrets - Secrets object to save
105
+ * @throws {Error} If save fails
106
+ */
107
+ function saveSecretsFile(resolvedPath, secrets) {
108
+ const dir = path.dirname(resolvedPath);
109
+ if (!fs.existsSync(dir)) {
110
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
111
+ }
112
+
113
+ const yamlContent = yaml.dump(secrets, {
114
+ indent: 2,
115
+ lineWidth: 120,
116
+ noRefs: true,
117
+ sortKeys: false
118
+ });
119
+
120
+ fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
121
+ }
122
+
123
+ /**
124
+ * Generates missing secret keys in secrets file
125
+ * Scans env.template for kv:// references and adds missing keys with secure defaults
126
+ *
127
+ * @async
128
+ * @function generateMissingSecrets
129
+ * @param {string} envTemplate - Environment template content
130
+ * @param {string} secretsPath - Path to secrets file
131
+ * @returns {Promise<string[]>} Array of newly generated secret keys
132
+ * @throws {Error} If generation fails
133
+ *
134
+ * @example
135
+ * const newKeys = await generateMissingSecrets(template, '~/.aifabrix/secrets.yaml');
136
+ * // Returns: ['new-secret-key', 'another-secret']
137
+ */
138
+ async function generateMissingSecrets(envTemplate, secretsPath) {
139
+ const resolvedPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
140
+ const existingSecrets = loadExistingSecrets(resolvedPath);
141
+ const missingKeys = findMissingSecretKeys(envTemplate, existingSecrets);
142
+
143
+ if (missingKeys.length === 0) {
144
+ return [];
145
+ }
146
+
147
+ const newSecrets = {};
148
+ for (const key of missingKeys) {
149
+ newSecrets[key] = generateSecretValue(key);
150
+ }
151
+
152
+ const updatedSecrets = { ...existingSecrets, ...newSecrets };
153
+ saveSecretsFile(resolvedPath, updatedSecrets);
154
+
155
+ logger.log(`✓ Generated ${missingKeys.length} missing secret key(s): ${missingKeys.join(', ')}`);
156
+ return missingKeys;
157
+ }
158
+
159
+ /**
160
+ * Creates default secrets file if it doesn't exist
161
+ * Generates template with common secrets for local development
162
+ *
163
+ * @async
164
+ * @function createDefaultSecrets
165
+ * @param {string} secretsPath - Path where to create secrets file
166
+ * @returns {Promise<void>} Resolves when file is created
167
+ * @throws {Error} If file creation fails
168
+ *
169
+ * @example
170
+ * await createDefaultSecrets('~/.aifabrix/secrets.yaml');
171
+ * // Default secrets file is created
172
+ */
173
+ async function createDefaultSecrets(secretsPath) {
174
+ const resolvedPath = secretsPath.startsWith('~')
175
+ ? path.join(os.homedir(), secretsPath.slice(1))
176
+ : secretsPath;
177
+
178
+ const dir = path.dirname(resolvedPath);
179
+ if (!fs.existsSync(dir)) {
180
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
181
+ }
182
+
183
+ const defaultSecrets = `# Local Development Secrets
184
+ # Production uses Azure KeyVault
185
+
186
+ # Database Secrets
187
+ postgres-passwordKeyVault: "admin123"
188
+
189
+ # Redis Secrets
190
+ redis-passwordKeyVault: ""
191
+ redis-urlKeyVault: "redis://\${REDIS_HOST}:6379"
192
+
193
+ # Keycloak Secrets
194
+ keycloak-admin-passwordKeyVault: "admin123"
195
+ keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:8082"
196
+ `;
197
+
198
+ fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });
199
+ }
200
+
201
+ module.exports = {
202
+ findMissingSecretKeys,
203
+ generateSecretValue,
204
+ loadExistingSecrets,
205
+ saveSecretsFile,
206
+ generateMissingSecrets,
207
+ createDefaultSecrets
208
+ };
209
+
package/lib/validator.js CHANGED
@@ -185,13 +185,17 @@ async function validateEnvTemplate(appName) {
185
185
  const lines = content.split('\n');
186
186
  lines.forEach((line, index) => {
187
187
  const trimmed = line.trim();
188
+ // Skip empty lines and comments
188
189
  if (trimmed && !trimmed.startsWith('#')) {
189
190
  if (!trimmed.includes('=')) {
190
191
  errors.push(`Line ${index + 1}: Invalid environment variable format (missing =)`);
191
192
  } else {
192
- const [key, value] = trimmed.split('=', 2);
193
- if (!key || !value) {
194
- errors.push(`Line ${index + 1}: Invalid environment variable format`);
193
+ const [key, _value] = trimmed.split('=', 2);
194
+ // Trim key to handle whitespace issues
195
+ // Empty values are allowed (_value can be empty string or undefined)
196
+ const trimmedKey = key ? key.trim() : '';
197
+ if (!trimmedKey) {
198
+ errors.push(`Line ${index + 1}: Invalid environment variable format (missing variable name)`);
195
199
  }
196
200
  }
197
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -2,8 +2,6 @@
2
2
  # Shared infrastructure services only
3
3
  # Generated by AI Fabrix Builder SDK
4
4
 
5
- version: "3.9"
6
-
7
5
  services:
8
6
  # PostgreSQL Database with pgvector extension
9
7
  postgres:
@@ -2,8 +2,6 @@
2
2
  # Generated by AI Fabrix Builder SDK
3
3
  # Service definition for local development
4
4
 
5
- version: "3.9"
6
-
7
5
  services:
8
6
  {{app.key}}:
9
7
  image: {{image.name}}:{{image.tag}}
@@ -11,7 +9,11 @@ services:
11
9
  env_file:
12
10
  - {{envFile}}
13
11
  ports:
12
+ {{#if containerPort}}
13
+ - "{{build.localPort}}:{{containerPort}}"
14
+ {{else}}
14
15
  - "{{build.localPort}}:{{port}}"
16
+ {{/if}}
15
17
  networks:
16
18
  - infra_aifabrix-network
17
19
  {{#if requiresStorage}}
@@ -48,23 +50,34 @@ services:
48
50
  command: >
49
51
  sh -c "
50
52
  export PGHOST=postgres PGPORT=5432 PGUSER=pgadmin &&
51
- export PGPASSWORD="${POSTGRES_PASSWORD}" &&
53
+ export PGPASSWORD=\"${POSTGRES_PASSWORD}\" &&
54
+ echo 'Waiting for PostgreSQL to be ready...' &&
55
+ counter=0 &&
56
+ while [ $counter -lt 30 ]; do
57
+ if pg_isready -h postgres -p 5432 -U pgadmin >/dev/null 2>&1; then
58
+ echo 'PostgreSQL is ready!'
59
+ break
60
+ fi
61
+ echo 'Waiting for PostgreSQL...'
62
+ sleep 1
63
+ counter=$((counter + 1))
64
+ done &&
52
65
  {{#if databases}}
53
66
  {{#each databases}}
54
67
  echo 'Creating {{name}} database and user...' &&
55
- psql -d postgres -c 'CREATE DATABASE {{name}};' || echo '{{name}} database exists' &&
56
- psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || echo '{{name}}_user exists' &&
57
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' &&
58
- psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' &&
59
- psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' &&
68
+ (psql -d postgres -c 'CREATE DATABASE {{name}};' || true) &&
69
+ (psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || true) &&
70
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' || true &&
71
+ psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' || true &&
72
+ psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' || true &&
60
73
  {{/each}}
61
74
  {{else}}
62
75
  echo 'Creating {{app.key}} database and user...' &&
63
- psql -d postgres -c 'CREATE DATABASE {{app.key}};' || echo '{{app.key}} database exists' &&
64
- psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || echo '{{app.key}}_user exists' &&
65
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' &&
66
- psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' &&
67
- psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' &&
76
+ (psql -d postgres -c 'CREATE DATABASE {{app.key}};' || true) &&
77
+ (psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || true) &&
78
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' || true &&
79
+ psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' || true &&
80
+ psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' || true &&
68
81
  {{/if}}
69
82
  echo 'Database initialization complete!'
70
83
  "
@@ -2,8 +2,6 @@
2
2
  # Generated by AI Fabrix Builder SDK
3
3
  # Service definition for local development
4
4
 
5
- version: "3.9"
6
-
7
5
  services:
8
6
  {{app.key}}:
9
7
  image: {{image.name}}:{{image.tag}}
@@ -11,7 +9,11 @@ services:
11
9
  env_file:
12
10
  - {{envFile}}
13
11
  ports:
12
+ {{#if containerPort}}
13
+ - "{{build.localPort}}:{{containerPort}}"
14
+ {{else}}
14
15
  - "{{build.localPort}}:{{port}}"
16
+ {{/if}}
15
17
  networks:
16
18
  - infra_aifabrix-network
17
19
  {{#if requiresStorage}}
@@ -48,23 +50,34 @@ services:
48
50
  command: >
49
51
  sh -c "
50
52
  export PGHOST=postgres PGPORT=5432 PGUSER=pgadmin &&
51
- export PGPASSWORD="${POSTGRES_PASSWORD}" &&
53
+ export PGPASSWORD=\"${POSTGRES_PASSWORD}\" &&
54
+ echo 'Waiting for PostgreSQL to be ready...' &&
55
+ counter=0 &&
56
+ while [ $counter -lt 30 ]; do
57
+ if pg_isready -h postgres -p 5432 -U pgadmin >/dev/null 2>&1; then
58
+ echo 'PostgreSQL is ready!'
59
+ break
60
+ fi
61
+ echo 'Waiting for PostgreSQL...'
62
+ sleep 1
63
+ counter=$((counter + 1))
64
+ done &&
52
65
  {{#if databases}}
53
66
  {{#each databases}}
54
67
  echo 'Creating {{name}} database and user...' &&
55
- psql -d postgres -c 'CREATE DATABASE {{name}};' || echo '{{name}} database exists' &&
56
- psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || echo '{{name}}_user exists' &&
57
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' &&
58
- psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' &&
59
- psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' &&
68
+ (psql -d postgres -c 'CREATE DATABASE {{name}};' || true) &&
69
+ (psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || true) &&
70
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' || true &&
71
+ psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' || true &&
72
+ psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' || true &&
60
73
  {{/each}}
61
74
  {{else}}
62
75
  echo 'Creating {{app.key}} database and user...' &&
63
- psql -d postgres -c 'CREATE DATABASE {{app.key}};' || echo '{{app.key}} database exists' &&
64
- psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || echo '{{app.key}}_user exists' &&
65
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' &&
66
- psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' &&
67
- psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' &&
76
+ (psql -d postgres -c 'CREATE DATABASE {{app.key}};' || true) &&
77
+ (psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || true) &&
78
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' || true &&
79
+ psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' || true &&
80
+ psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' || true &&
68
81
  {{/if}}
69
82
  echo 'Database initialization complete!'
70
83
  "