@aifabrix/builder 2.11.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.cursor/rules/project-rules.mdc +194 -0
  2. package/README.md +12 -0
  3. package/lib/api/applications.api.js +164 -0
  4. package/lib/api/auth.api.js +303 -0
  5. package/lib/api/datasources-core.api.js +87 -0
  6. package/lib/api/datasources-extended.api.js +117 -0
  7. package/lib/api/datasources.api.js +13 -0
  8. package/lib/api/deployments.api.js +126 -0
  9. package/lib/api/environments.api.js +245 -0
  10. package/lib/api/external-systems.api.js +251 -0
  11. package/lib/api/index.js +236 -0
  12. package/lib/api/pipeline.api.js +234 -0
  13. package/lib/api/types/applications.types.js +136 -0
  14. package/lib/api/types/auth.types.js +218 -0
  15. package/lib/api/types/datasources.types.js +272 -0
  16. package/lib/api/types/deployments.types.js +184 -0
  17. package/lib/api/types/environments.types.js +197 -0
  18. package/lib/api/types/external-systems.types.js +244 -0
  19. package/lib/api/types/pipeline.types.js +125 -0
  20. package/lib/app-list.js +5 -7
  21. package/lib/app-rotate-secret.js +4 -10
  22. package/lib/cli.js +30 -0
  23. package/lib/commands/login.js +41 -12
  24. package/lib/datasource-deploy.js +7 -30
  25. package/lib/datasource-list.js +9 -6
  26. package/lib/deployer.js +103 -135
  27. package/lib/environment-deploy.js +15 -26
  28. package/lib/external-system-deploy.js +12 -39
  29. package/lib/external-system-download.js +5 -13
  30. package/lib/external-system-test.js +9 -12
  31. package/lib/generator-split.js +342 -0
  32. package/lib/generator.js +94 -5
  33. package/lib/utils/app-register-api.js +5 -10
  34. package/lib/utils/deployment-errors.js +88 -6
  35. package/package.json +1 -1
  36. package/tatus +0 -181
@@ -14,7 +14,13 @@ const fsSync = require('fs');
14
14
  const path = require('path');
15
15
  const yaml = require('js-yaml');
16
16
  const chalk = require('chalk');
17
- const { authenticatedApiCall } = require('./utils/api');
17
+ const {
18
+ deployExternalSystemViaPipeline,
19
+ deployDatasourceViaPipeline,
20
+ uploadApplicationViaPipeline,
21
+ validateUploadViaPipeline,
22
+ publishUploadViaPipeline
23
+ } = require('./api/pipeline.api');
18
24
  const { getDeploymentAuth } = require('./utils/token-manager');
19
25
  const { getConfig } = require('./config');
20
26
  const logger = require('./utils/logger');
@@ -155,14 +161,7 @@ async function buildExternalSystem(appName, options = {}) {
155
161
  const systemContent = await fs.readFile(systemFiles[0], 'utf8');
156
162
  const systemJson = JSON.parse(systemContent);
157
163
 
158
- const systemResponse = await authenticatedApiCall(
159
- `${dataplaneUrl}/api/v1/pipeline/deploy`,
160
- {
161
- method: 'POST',
162
- body: JSON.stringify(systemJson)
163
- },
164
- authConfig.token
165
- );
164
+ const systemResponse = await deployExternalSystemViaPipeline(dataplaneUrl, authConfig, systemJson);
166
165
 
167
166
  if (!systemResponse.success) {
168
167
  throw new Error(`Failed to deploy external system: ${systemResponse.error || systemResponse.formattedError}`);
@@ -178,14 +177,7 @@ async function buildExternalSystem(appName, options = {}) {
178
177
  const datasourceContent = await fs.readFile(datasourceFile, 'utf8');
179
178
  const datasourceJson = JSON.parse(datasourceContent);
180
179
 
181
- const datasourceResponse = await authenticatedApiCall(
182
- `${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`,
183
- {
184
- method: 'POST',
185
- body: JSON.stringify(datasourceJson)
186
- },
187
- authConfig.token
188
- );
180
+ const datasourceResponse = await deployDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceJson);
189
181
 
190
182
  if (!datasourceResponse.success) {
191
183
  throw new Error(`Failed to deploy datasource ${datasourceName}: ${datasourceResponse.error || datasourceResponse.formattedError}`);
@@ -245,14 +237,7 @@ async function deployExternalSystem(appName, options = {}) {
245
237
 
246
238
  // Step 1: Upload application
247
239
  logger.log(chalk.blue('📤 Uploading application configuration...'));
248
- const uploadResponse = await authenticatedApiCall(
249
- `${dataplaneUrl}/api/v1/pipeline/upload`,
250
- {
251
- method: 'POST',
252
- body: JSON.stringify(applicationSchema)
253
- },
254
- authConfig.token
255
- );
240
+ const uploadResponse = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicationSchema);
256
241
 
257
242
  if (!uploadResponse.success || !uploadResponse.data) {
258
243
  throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
@@ -270,13 +255,7 @@ async function deployExternalSystem(appName, options = {}) {
270
255
  // Step 2: Validate upload (optional, can be skipped)
271
256
  if (!options.skipValidation) {
272
257
  logger.log(chalk.blue('🔍 Validating upload...'));
273
- const validateResponse = await authenticatedApiCall(
274
- `${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/validate`,
275
- {
276
- method: 'POST'
277
- },
278
- authConfig.token
279
- );
258
+ const validateResponse = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
280
259
 
281
260
  if (!validateResponse.success || !validateResponse.data) {
282
261
  throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
@@ -308,13 +287,7 @@ async function deployExternalSystem(appName, options = {}) {
308
287
  const generateMcpContract = options.generateMcpContract !== false; // Default to true
309
288
  logger.log(chalk.blue(`📢 Publishing application (MCP contract: ${generateMcpContract ? 'enabled' : 'disabled'})...`));
310
289
 
311
- const publishResponse = await authenticatedApiCall(
312
- `${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/publish?generateMcpContract=${generateMcpContract}`,
313
- {
314
- method: 'POST'
315
- },
316
- authConfig.token
317
- );
290
+ const publishResponse = await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig, { generateMcpContract });
318
291
 
319
292
  if (!publishResponse.success || !publishResponse.data) {
320
293
  throw new Error(`Failed to publish application: ${publishResponse.error || publishResponse.formattedError || 'Unknown error'}`);
@@ -14,7 +14,7 @@ const path = require('path');
14
14
  const os = require('os');
15
15
  const yaml = require('js-yaml');
16
16
  const chalk = require('chalk');
17
- const { authenticatedApiCall } = require('./utils/api');
17
+ const { getExternalSystemConfig } = require('./api/external-systems.api');
18
18
  const { getDeploymentAuth } = require('./utils/token-manager');
19
19
  const { getDataplaneUrl } = require('./datasource-deploy');
20
20
  const { getConfig } = require('./config');
@@ -284,14 +284,12 @@ async function downloadExternalSystem(systemKey, options = {}) {
284
284
  const dataplaneUrl = await getDataplaneUrl(controllerUrl, systemKey, environment, authConfig);
285
285
  logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
286
286
 
287
- // Download system configuration
288
- // Note: Verify this endpoint exists. Alternative: GET /api/v1/pipeline/{systemIdOrKey}
289
- const downloadEndpoint = `${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`;
290
- logger.log(chalk.blue(`📡 Downloading from: ${downloadEndpoint}`));
287
+ // Download system configuration using centralized API client
288
+ logger.log(chalk.blue(`📡 Downloading system configuration: ${systemKey}`));
291
289
 
292
290
  if (options.dryRun) {
293
291
  logger.log(chalk.yellow('🔍 Dry run mode - would download from:'));
294
- logger.log(chalk.gray(` ${downloadEndpoint}`));
292
+ logger.log(chalk.gray(` ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
295
293
  logger.log(chalk.yellow('\nWould create:'));
296
294
  logger.log(chalk.gray(` integration/${systemKey}/`));
297
295
  logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
@@ -301,13 +299,7 @@ async function downloadExternalSystem(systemKey, options = {}) {
301
299
  return;
302
300
  }
303
301
 
304
- const response = await authenticatedApiCall(
305
- downloadEndpoint,
306
- {
307
- method: 'GET'
308
- },
309
- authConfig.token
310
- );
302
+ const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
311
303
 
312
304
  if (!response.success || !response.data) {
313
305
  throw new Error(`Failed to download system configuration: ${response.error || response.formattedError || 'Unknown error'}`);
@@ -14,7 +14,7 @@ const fsSync = require('fs');
14
14
  const path = require('path');
15
15
  const yaml = require('js-yaml');
16
16
  const chalk = require('chalk');
17
- const { authenticatedApiCall } = require('./utils/api');
17
+ const { testDatasourceViaPipeline } = require('./api/pipeline.api');
18
18
  const { getDeploymentAuth } = require('./utils/token-manager');
19
19
  const { getDataplaneUrl } = require('./datasource-deploy');
20
20
  const { getConfig } = require('./config');
@@ -270,7 +270,7 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
270
270
  }
271
271
 
272
272
  /**
273
- * Calls pipeline test endpoint
273
+ * Calls pipeline test endpoint using centralized API client
274
274
  * @async
275
275
  * @param {string} systemKey - System key
276
276
  * @param {string} datasourceKey - Datasource key
@@ -281,17 +281,14 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
281
281
  * @returns {Promise<Object>} Test response
282
282
  */
283
283
  async function callPipelineTestEndpoint(systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000) {
284
- const endpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/${datasourceKey}/test`;
285
-
286
284
  const response = await retryApiCall(async() => {
287
- return await authenticatedApiCall(
288
- endpoint,
289
- {
290
- method: 'POST',
291
- body: JSON.stringify({ payloadTemplate }),
292
- timeout
293
- },
294
- authConfig.token
285
+ return await testDatasourceViaPipeline(
286
+ dataplaneUrl,
287
+ systemKey,
288
+ datasourceKey,
289
+ authConfig,
290
+ { payloadTemplate },
291
+ { timeout }
295
292
  );
296
293
  });
297
294
 
@@ -0,0 +1,342 @@
1
+ /**
2
+ * AI Fabrix Builder Deployment JSON Split Functions
3
+ *
4
+ * Helper functions for splitting deployment JSON files into component files
5
+ *
6
+ * @fileoverview Split functions for deployment JSON reverse conversion
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const yaml = require('js-yaml');
14
+
15
+ /**
16
+ * Converts configuration array back to env.template format
17
+ * @function extractEnvTemplate
18
+ * @param {Array} configuration - Configuration array from deployment JSON
19
+ * @returns {string} env.template content
20
+ */
21
+ function extractEnvTemplate(configuration) {
22
+ if (!Array.isArray(configuration) || configuration.length === 0) {
23
+ return '';
24
+ }
25
+
26
+ const lines = [];
27
+
28
+ // Generate env.template lines
29
+ for (const config of configuration) {
30
+ if (!config.name || !config.value) {
31
+ continue;
32
+ }
33
+
34
+ let value = config.value;
35
+ // Add kv:// prefix if location is keyvault
36
+ if (config.location === 'keyvault') {
37
+ value = `kv://${value}`;
38
+ }
39
+
40
+ lines.push(`${config.name}=${value}`);
41
+ }
42
+
43
+ return lines.join('\n');
44
+ }
45
+
46
+ /**
47
+ * Parses image reference string into components
48
+ * @function parseImageReference
49
+ * @param {string} imageString - Full image string (e.g., "registry/name:tag")
50
+ * @returns {Object} Object with registry, name, and tag
51
+ */
52
+ function parseImageReference(imageString) {
53
+ if (!imageString || typeof imageString !== 'string') {
54
+ return { registry: null, name: null, tag: 'latest' };
55
+ }
56
+
57
+ // Handle format: registry/name:tag or name:tag or registry/name
58
+ const parts = imageString.split('/');
59
+ let registry = null;
60
+ let nameAndTag = imageString;
61
+
62
+ if (parts.length > 1) {
63
+ // Check if first part looks like a registry (contains .)
64
+ if (parts[0].includes('.')) {
65
+ registry = parts[0];
66
+ nameAndTag = parts.slice(1).join('/');
67
+ } else {
68
+ // No registry, just name:tag
69
+ nameAndTag = imageString;
70
+ }
71
+ }
72
+
73
+ // Split name and tag
74
+ const tagIndex = nameAndTag.lastIndexOf(':');
75
+ let name = nameAndTag;
76
+ let tag = 'latest';
77
+
78
+ if (tagIndex !== -1) {
79
+ name = nameAndTag.substring(0, tagIndex);
80
+ tag = nameAndTag.substring(tagIndex + 1);
81
+ }
82
+
83
+ return { registry, name, tag };
84
+ }
85
+
86
+ /**
87
+ * Extracts deployment JSON into variables.yaml structure
88
+ * @function extractVariablesYaml
89
+ * @param {Object} deployment - Deployment JSON object
90
+ * @returns {Object} Variables YAML structure
91
+ */
92
+ function extractVariablesYaml(deployment) {
93
+ if (!deployment || typeof deployment !== 'object') {
94
+ throw new Error('Deployment object is required');
95
+ }
96
+
97
+ const variables = {};
98
+
99
+ // Extract app information
100
+ if (deployment.key || deployment.displayName || deployment.description || deployment.type) {
101
+ variables.app = {};
102
+ if (deployment.key) variables.app.key = deployment.key;
103
+ if (deployment.displayName) variables.app.displayName = deployment.displayName;
104
+ if (deployment.description) variables.app.description = deployment.description;
105
+ if (deployment.type) variables.app.type = deployment.type;
106
+ }
107
+
108
+ // Extract image information
109
+ if (deployment.image) {
110
+ const imageParts = parseImageReference(deployment.image);
111
+ variables.image = {};
112
+ if (imageParts.name) variables.image.name = imageParts.name;
113
+ if (imageParts.registry) variables.image.registry = imageParts.registry;
114
+ if (imageParts.tag) variables.image.tag = imageParts.tag;
115
+ if (deployment.registryMode) variables.image.registryMode = deployment.registryMode;
116
+ }
117
+
118
+ // Extract port
119
+ if (deployment.port !== undefined) {
120
+ variables.port = deployment.port;
121
+ }
122
+
123
+ // Extract requirements
124
+ if (deployment.requiresDatabase || deployment.requiresRedis || deployment.requiresStorage || deployment.databases) {
125
+ variables.requires = {};
126
+ if (deployment.requiresDatabase !== undefined) variables.requires.database = deployment.requiresDatabase;
127
+ if (deployment.requiresRedis !== undefined) variables.requires.redis = deployment.requiresRedis;
128
+ if (deployment.requiresStorage !== undefined) variables.requires.storage = deployment.requiresStorage;
129
+ if (deployment.databases) variables.requires.databases = deployment.databases;
130
+ }
131
+
132
+ // Extract healthCheck
133
+ if (deployment.healthCheck) {
134
+ variables.healthCheck = deployment.healthCheck;
135
+ }
136
+
137
+ // Extract authentication
138
+ if (deployment.authentication) {
139
+ variables.authentication = { ...deployment.authentication };
140
+ }
141
+
142
+ // Extract build
143
+ if (deployment.build) {
144
+ variables.build = deployment.build;
145
+ }
146
+
147
+ // Extract repository
148
+ if (deployment.repository) {
149
+ variables.repository = deployment.repository;
150
+ }
151
+
152
+ // Extract deployment config
153
+ if (deployment.deployment) {
154
+ variables.deployment = deployment.deployment;
155
+ }
156
+
157
+ // Extract other optional fields
158
+ if (deployment.startupCommand) {
159
+ variables.startupCommand = deployment.startupCommand;
160
+ }
161
+ if (deployment.runtimeVersion) {
162
+ variables.runtimeVersion = deployment.runtimeVersion;
163
+ }
164
+ if (deployment.scaling) {
165
+ variables.scaling = deployment.scaling;
166
+ }
167
+ if (deployment.frontDoorRouting) {
168
+ variables.frontDoorRouting = deployment.frontDoorRouting;
169
+ }
170
+
171
+ // Extract roles and permissions (if present)
172
+ if (deployment.roles) {
173
+ variables.roles = deployment.roles;
174
+ }
175
+ if (deployment.permissions) {
176
+ variables.permissions = deployment.permissions;
177
+ }
178
+
179
+ return variables;
180
+ }
181
+
182
+ /**
183
+ * Extracts roles and permissions from deployment JSON
184
+ * @function extractRbacYaml
185
+ * @param {Object} deployment - Deployment JSON object
186
+ * @returns {Object|null} RBAC YAML structure or null if no roles/permissions
187
+ */
188
+ function extractRbacYaml(deployment) {
189
+ if (!deployment || typeof deployment !== 'object') {
190
+ return null;
191
+ }
192
+
193
+ const hasRoles = deployment.roles && Array.isArray(deployment.roles) && deployment.roles.length > 0;
194
+ const hasPermissions = deployment.permissions && Array.isArray(deployment.permissions) && deployment.permissions.length > 0;
195
+
196
+ if (!hasRoles && !hasPermissions) {
197
+ return null;
198
+ }
199
+
200
+ const rbac = {};
201
+ if (hasRoles) {
202
+ rbac.roles = deployment.roles;
203
+ }
204
+ if (hasPermissions) {
205
+ rbac.permissions = deployment.permissions;
206
+ }
207
+
208
+ return rbac;
209
+ }
210
+
211
+ /**
212
+ * Generates README.md content from deployment JSON
213
+ * @function generateReadmeFromDeployJson
214
+ * @param {Object} deployment - Deployment JSON object
215
+ * @returns {string} README.md content
216
+ */
217
+ function generateReadmeFromDeployJson(deployment) {
218
+ if (!deployment || typeof deployment !== 'object') {
219
+ throw new Error('Deployment object is required');
220
+ }
221
+
222
+ const appName = deployment.key || 'application';
223
+ const displayName = deployment.displayName || appName;
224
+ const description = deployment.description || 'Application deployment configuration';
225
+ const port = deployment.port || 3000;
226
+ const image = deployment.image || 'unknown';
227
+
228
+ const lines = [
229
+ `# ${displayName}`,
230
+ '',
231
+ description,
232
+ '',
233
+ '## Quick Start',
234
+ '',
235
+ 'This application is configured via deployment JSON and component files.',
236
+ '',
237
+ '## Configuration',
238
+ '',
239
+ `- **Application Key**: \`${appName}\``,
240
+ `- **Port**: \`${port}\``,
241
+ `- **Image**: \`${image}\``,
242
+ '',
243
+ '## Files',
244
+ '',
245
+ '- `variables.yaml` - Application configuration',
246
+ '- `env.template` - Environment variables template',
247
+ '- `rbac.yml` - Roles and permissions (if applicable)',
248
+ '- `README.md` - This file',
249
+ '',
250
+ '## Documentation',
251
+ '',
252
+ 'For more information, see the [AI Fabrix Builder documentation](../../docs/README.md).'
253
+ ];
254
+
255
+ return lines.join('\n');
256
+ }
257
+
258
+ /**
259
+ * Splits a deployment JSON file into component files
260
+ * @async
261
+ * @function splitDeployJson
262
+ * @param {string} deployJsonPath - Path to deployment JSON file
263
+ * @param {string} [outputDir] - Directory to write component files (defaults to same directory as JSON)
264
+ * @returns {Promise<Object>} Object with paths to generated files
265
+ * @throws {Error} If JSON file not found or invalid
266
+ */
267
+ async function splitDeployJson(deployJsonPath, outputDir = null) {
268
+ if (!deployJsonPath || typeof deployJsonPath !== 'string') {
269
+ throw new Error('Deployment JSON path is required and must be a string');
270
+ }
271
+
272
+ // Validate file exists
273
+ const fsSync = require('fs');
274
+ if (!fsSync.existsSync(deployJsonPath)) {
275
+ throw new Error(`Deployment JSON file not found: ${deployJsonPath}`);
276
+ }
277
+
278
+ // Determine output directory
279
+ const finalOutputDir = outputDir || path.dirname(deployJsonPath);
280
+
281
+ // Ensure output directory exists
282
+ if (!fsSync.existsSync(finalOutputDir)) {
283
+ await fs.mkdir(finalOutputDir, { recursive: true });
284
+ }
285
+
286
+ // Load and parse deployment JSON
287
+ let deployment;
288
+ try {
289
+ const jsonContent = await fs.readFile(deployJsonPath, 'utf8');
290
+ deployment = JSON.parse(jsonContent);
291
+ } catch (error) {
292
+ if (error.code === 'ENOENT') {
293
+ throw new Error(`Deployment JSON file not found: ${deployJsonPath}`);
294
+ }
295
+ throw new Error(`Invalid JSON syntax in deployment file: ${error.message}`);
296
+ }
297
+
298
+ // Extract components
299
+ const envTemplate = extractEnvTemplate(deployment.configuration || []);
300
+ const variables = extractVariablesYaml(deployment);
301
+ const rbac = extractRbacYaml(deployment);
302
+ const readme = generateReadmeFromDeployJson(deployment);
303
+
304
+ // Write component files
305
+ const results = {};
306
+
307
+ // Write env.template
308
+ const envTemplatePath = path.join(finalOutputDir, 'env.template');
309
+ await fs.writeFile(envTemplatePath, envTemplate, { mode: 0o644, encoding: 'utf8' });
310
+ results.envTemplate = envTemplatePath;
311
+
312
+ // Write variables.yaml
313
+ const variablesPath = path.join(finalOutputDir, 'variables.yaml');
314
+ const variablesYaml = yaml.dump(variables, { indent: 2, lineWidth: -1 });
315
+ await fs.writeFile(variablesPath, variablesYaml, { mode: 0o644, encoding: 'utf8' });
316
+ results.variables = variablesPath;
317
+
318
+ // Write rbac.yml (only if roles/permissions exist)
319
+ if (rbac) {
320
+ const rbacPath = path.join(finalOutputDir, 'rbac.yml');
321
+ const rbacYaml = yaml.dump(rbac, { indent: 2, lineWidth: -1 });
322
+ await fs.writeFile(rbacPath, rbacYaml, { mode: 0o644, encoding: 'utf8' });
323
+ results.rbac = rbacPath;
324
+ }
325
+
326
+ // Write README.md
327
+ const readmePath = path.join(finalOutputDir, 'README.md');
328
+ await fs.writeFile(readmePath, readme, { mode: 0o644, encoding: 'utf8' });
329
+ results.readme = readmePath;
330
+
331
+ return results;
332
+ }
333
+
334
+ module.exports = {
335
+ splitDeployJson,
336
+ extractEnvTemplate,
337
+ extractVariablesYaml,
338
+ extractRbacYaml,
339
+ parseImageReference,
340
+ generateReadmeFromDeployJson
341
+ };
342
+
package/lib/generator.js CHANGED
@@ -18,6 +18,7 @@ const _keyGenerator = require('./key-generator');
18
18
  const _validator = require('./validator');
19
19
  const builders = require('./generator-builders');
20
20
  const { detectAppType, getDeployJsonPath } = require('./utils/paths');
21
+ const splitFunctions = require('./generator-split');
21
22
 
22
23
  /**
23
24
  * Loads variables.yaml file
@@ -161,8 +162,8 @@ async function generateDeployJson(appName) {
161
162
  const envTemplate = loadEnvTemplate(templatePath);
162
163
  const rbac = loadRbac(rbacPath);
163
164
 
164
- // Parse environment variables from template
165
- const configuration = parseEnvironmentVariables(envTemplate);
165
+ // Parse environment variables from template and merge portalInput from variables.yaml
166
+ const configuration = parseEnvironmentVariables(envTemplate, variables);
166
167
 
167
168
  // Build deployment manifest WITHOUT deploymentKey initially
168
169
  const deployment = builders.buildManifestStructure(appName, variables, null, configuration, rbac);
@@ -187,10 +188,85 @@ async function generateDeployJson(appName) {
187
188
  return jsonPath;
188
189
  }
189
190
 
190
- function parseEnvironmentVariables(envTemplate) {
191
+ /**
192
+ * Validates portalInput structure against schema requirements
193
+ * @param {Object} portalInput - Portal input configuration to validate
194
+ * @param {string} variableName - Variable name for error messages
195
+ * @throws {Error} If portalInput structure is invalid
196
+ */
197
+ function validatePortalInput(portalInput, variableName) {
198
+ if (!portalInput || typeof portalInput !== 'object') {
199
+ throw new Error(`Invalid portalInput for variable '${variableName}': must be an object`);
200
+ }
201
+
202
+ // Check required fields
203
+ if (!portalInput.field || typeof portalInput.field !== 'string') {
204
+ throw new Error(`Invalid portalInput for variable '${variableName}': field is required and must be a string`);
205
+ }
206
+
207
+ if (!portalInput.label || typeof portalInput.label !== 'string') {
208
+ throw new Error(`Invalid portalInput for variable '${variableName}': label is required and must be a string`);
209
+ }
210
+
211
+ // Validate field type
212
+ const validFieldTypes = ['password', 'text', 'textarea', 'select'];
213
+ if (!validFieldTypes.includes(portalInput.field)) {
214
+ throw new Error(`Invalid portalInput for variable '${variableName}': field must be one of: ${validFieldTypes.join(', ')}`);
215
+ }
216
+
217
+ // Validate select field requires options
218
+ if (portalInput.field === 'select') {
219
+ if (!portalInput.options || !Array.isArray(portalInput.options) || portalInput.options.length === 0) {
220
+ throw new Error(`Invalid portalInput for variable '${variableName}': select field requires a non-empty options array`);
221
+ }
222
+ }
223
+
224
+ // Validate optional fields
225
+ if (portalInput.placeholder !== undefined && typeof portalInput.placeholder !== 'string') {
226
+ throw new Error(`Invalid portalInput for variable '${variableName}': placeholder must be a string`);
227
+ }
228
+
229
+ if (portalInput.masked !== undefined && typeof portalInput.masked !== 'boolean') {
230
+ throw new Error(`Invalid portalInput for variable '${variableName}': masked must be a boolean`);
231
+ }
232
+
233
+ if (portalInput.validation !== undefined) {
234
+ if (typeof portalInput.validation !== 'object' || Array.isArray(portalInput.validation)) {
235
+ throw new Error(`Invalid portalInput for variable '${variableName}': validation must be an object`);
236
+ }
237
+ }
238
+
239
+ if (portalInput.options !== undefined && portalInput.field !== 'select') {
240
+ // Options should only be present for select fields
241
+ if (Array.isArray(portalInput.options) && portalInput.options.length > 0) {
242
+ throw new Error(`Invalid portalInput for variable '${variableName}': options can only be used with select field type`);
243
+ }
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Parses environment variables from env.template and merges portalInput from variables.yaml
249
+ * @param {string} envTemplate - Content of env.template file
250
+ * @param {Object|null} [variablesConfig=null] - Optional configuration from variables.yaml
251
+ * @returns {Array<Object>} Configuration array with merged portalInput
252
+ * @throws {Error} If portalInput structure is invalid
253
+ */
254
+ function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
191
255
  const configuration = [];
192
256
  const lines = envTemplate.split('\n');
193
257
 
258
+ // Create a map of portalInput configurations by variable name
259
+ const portalInputMap = new Map();
260
+ if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
261
+ for (const configItem of variablesConfig.configuration) {
262
+ if (configItem.name && configItem.portalInput) {
263
+ // Validate portalInput before adding to map
264
+ validatePortalInput(configItem.portalInput, configItem.name);
265
+ portalInputMap.set(configItem.name, configItem.portalInput);
266
+ }
267
+ }
268
+ }
269
+
194
270
  for (const line of lines) {
195
271
  const trimmed = line.trim();
196
272
 
@@ -227,12 +303,19 @@ function parseEnvironmentVariables(envTemplate) {
227
303
  required = true;
228
304
  }
229
305
 
230
- configuration.push({
306
+ const configItem = {
231
307
  name: key,
232
308
  value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
233
309
  location,
234
310
  required
235
- });
311
+ };
312
+
313
+ // Merge portalInput if it exists in variables.yaml
314
+ if (portalInputMap.has(key)) {
315
+ configItem.portalInput = portalInputMap.get(key);
316
+ }
317
+
318
+ configuration.push(configItem);
236
319
  }
237
320
 
238
321
  return configuration;
@@ -399,6 +482,12 @@ module.exports = {
399
482
  generateDeployJsonWithValidation,
400
483
  generateExternalSystemApplicationSchema,
401
484
  parseEnvironmentVariables,
485
+ splitDeployJson: splitFunctions.splitDeployJson,
486
+ extractEnvTemplate: splitFunctions.extractEnvTemplate,
487
+ extractVariablesYaml: splitFunctions.extractVariablesYaml,
488
+ extractRbacYaml: splitFunctions.extractRbacYaml,
489
+ parseImageReference: splitFunctions.parseImageReference,
490
+ generateReadmeFromDeployJson: splitFunctions.generateReadmeFromDeployJson,
402
491
  buildImageReference: builders.buildImageReference,
403
492
  buildHealthCheck: builders.buildHealthCheck,
404
493
  buildRequirements: builders.buildRequirements,