@aifabrix/builder 2.31.0 → 2.32.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.
Files changed (119) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +123 -37
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +34 -3
  111. package/scripts/install-local.js +210 -0
  112. package/templates/external-system/deploy.ps1.hbs +34 -0
  113. package/templates/external-system/deploy.sh.hbs +34 -0
  114. package/templates/external-system/external-datasource.json.hbs +31 -12
  115. package/lib/app.js +0 -467
  116. package/lib/datasource-list.js +0 -141
  117. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  118. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  119. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -11,8 +11,8 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const Ajv = require('ajv');
14
- const { detectAppType, getDeployJsonPath } = require('./utils/paths');
15
- const { loadVariables, loadRbac } = require('./generator-helpers');
14
+ const { detectAppType, getDeployJsonPath } = require('../utils/paths');
15
+ const { loadVariables, loadRbac } = require('./helpers');
16
16
 
17
17
  /**
18
18
  * Generates external system <app-name>-deploy.json by loading the system JSON file
@@ -24,56 +24,86 @@ const { loadVariables, loadRbac } = require('./generator-helpers');
24
24
  * @returns {Promise<string>} Path to generated <app-name>-deploy.json file
25
25
  * @throws {Error} If generation fails
26
26
  */
27
- async function generateExternalSystemDeployJson(appName, appPath) {
28
- if (!appName || typeof appName !== 'string') {
29
- throw new Error('App name is required and must be a string');
30
- }
31
-
32
- const variablesPath = path.join(appPath, 'variables.yaml');
33
- const { parsed: variables } = loadVariables(variablesPath);
34
-
35
- if (!variables.externalIntegration) {
36
- throw new Error('externalIntegration block not found in variables.yaml');
37
- }
38
-
39
- // For external systems, the system JSON file should be in the same folder
40
- // Check if it already exists (should be <app-name>-deploy.json)
41
- const deployJsonPath = getDeployJsonPath(appName, 'external', true);
27
+ /**
28
+ * Resolves system file path from variables
29
+ * @function resolveSystemFilePath
30
+ * @param {Object} variables - Variables configuration
31
+ * @param {string} appPath - Application path
32
+ * @param {string} appName - Application name
33
+ * @returns {string} System file path
34
+ * @throws {Error} If file not found
35
+ */
36
+ function resolveSystemFilePath(variables, appPath, appName) {
42
37
  const systemFileName = variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0
43
38
  ? variables.externalIntegration.systems[0]
44
39
  : `${appName}-deploy.json`;
45
40
 
46
- // Resolve system file path (schemaBasePath is usually './' for same folder)
47
41
  const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
48
42
  const systemFilePath = path.isAbsolute(schemaBasePath)
49
43
  ? path.join(schemaBasePath, systemFileName)
50
44
  : path.join(appPath, schemaBasePath, systemFileName);
51
45
 
52
- // If system file doesn't exist, throw error (it should be created manually or via external-system-generator)
53
46
  if (!fs.existsSync(systemFilePath)) {
54
47
  throw new Error(`External system file not found: ${systemFilePath}. Please create it first.`);
55
48
  }
56
49
 
57
- // Read the system JSON file
50
+ return systemFilePath;
51
+ }
52
+
53
+ /**
54
+ * Merges RBAC into system JSON
55
+ * @function mergeRbacIntoSystemJson
56
+ * @param {Object} systemJson - System JSON object
57
+ * @param {Object|null} rbac - RBAC configuration
58
+ */
59
+ function mergeRbacIntoSystemJson(systemJson, rbac) {
60
+ if (!rbac) {
61
+ return;
62
+ }
63
+
64
+ // Priority: roles/permissions in system JSON > rbac.yaml (if both exist, prefer JSON)
65
+ if (rbac.roles && (!systemJson.roles || systemJson.roles.length === 0)) {
66
+ systemJson.roles = rbac.roles;
67
+ }
68
+ if (rbac.permissions && (!systemJson.permissions || systemJson.permissions.length === 0)) {
69
+ systemJson.permissions = rbac.permissions;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Loads and parses system file
75
+ * @async
76
+ * @function loadSystemFileContent
77
+ * @param {string} systemFilePath - System file path
78
+ * @returns {Promise<Object>} Parsed system JSON
79
+ */
80
+ async function loadSystemFileContent(systemFilePath) {
58
81
  const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
59
- const systemJson = JSON.parse(systemContent);
82
+ return JSON.parse(systemContent);
83
+ }
84
+
85
+ async function generateExternalSystemDeployJson(appName, appPath) {
86
+ if (!appName || typeof appName !== 'string') {
87
+ throw new Error('App name is required and must be a string');
88
+ }
89
+
90
+ const variablesPath = path.join(appPath, 'variables.yaml');
91
+ const { parsed: variables } = loadVariables(variablesPath);
92
+
93
+ if (!variables.externalIntegration) {
94
+ throw new Error('externalIntegration block not found in variables.yaml');
95
+ }
96
+
97
+ const systemFilePath = resolveSystemFilePath(variables, appPath, appName);
98
+ const systemJson = await loadSystemFileContent(systemFilePath);
60
99
 
61
100
  // Load rbac.yaml from app directory (similar to regular apps)
62
101
  const rbacPath = path.join(appPath, 'rbac.yaml');
63
102
  const rbac = loadRbac(rbacPath);
64
-
65
- // Merge rbac into systemJson if present
66
- // Priority: roles/permissions in system JSON > rbac.yaml (if both exist, prefer JSON)
67
- if (rbac) {
68
- if (rbac.roles && (!systemJson.roles || systemJson.roles.length === 0)) {
69
- systemJson.roles = rbac.roles;
70
- }
71
- if (rbac.permissions && (!systemJson.permissions || systemJson.permissions.length === 0)) {
72
- systemJson.permissions = rbac.permissions;
73
- }
74
- }
103
+ mergeRbacIntoSystemJson(systemJson, rbac);
75
104
 
76
105
  // Write it as <app-name>-deploy.json (consistent naming)
106
+ const deployJsonPath = getDeployJsonPath(appName, 'external', true);
77
107
  const jsonContent = JSON.stringify(systemJson, null, 2);
78
108
  await fs.promises.writeFile(deployJsonPath, jsonContent, { mode: 0o644, encoding: 'utf8' });
79
109
 
@@ -242,22 +272,22 @@ function validateDatasourceSchemas(datasourceJsons, externalDatasourceSchema, aj
242
272
  * @returns {Promise<Object>} Application schema object
243
273
  * @throws {Error} If generation fails
244
274
  */
245
- async function generateExternalSystemApplicationSchema(appName) {
246
- if (!appName || typeof appName !== 'string') {
247
- throw new Error('App name is required and must be a string');
248
- }
249
-
250
- const { appPath } = await detectAppType(appName);
275
+ /**
276
+ * Loads and validates external integration configuration
277
+ * @async
278
+ * @function loadExternalIntegrationConfig
279
+ * @param {string} appPath - Application path
280
+ * @returns {Promise<Object>} Variables and schema base path
281
+ * @throws {Error} If configuration is invalid
282
+ */
283
+ async function loadExternalIntegrationConfig(appPath) {
251
284
  const variablesPath = path.join(appPath, 'variables.yaml');
252
-
253
- // Load variables.yaml
254
285
  const { parsed: variables } = loadVariables(variablesPath);
255
286
 
256
287
  if (!variables.externalIntegration) {
257
288
  throw new Error('externalIntegration block not found in variables.yaml');
258
289
  }
259
290
 
260
- // Load system file and merge RBAC
261
291
  const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
262
292
  const systemFiles = variables.externalIntegration.systems || [];
263
293
 
@@ -265,18 +295,17 @@ async function generateExternalSystemApplicationSchema(appName) {
265
295
  throw new Error('No system files specified in externalIntegration.systems');
266
296
  }
267
297
 
268
- const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFiles[0]);
269
-
270
- // Load datasource files
271
- const datasourceFiles = variables.externalIntegration.dataSources || [];
272
- const datasourceJsons = await loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles);
273
-
274
- // Build application-schema.json structure
275
- const applicationSchema = buildBaseSchema(systemJson, datasourceJsons, variables.externalIntegration.version);
298
+ return { variables, schemaBasePath, systemFiles };
299
+ }
276
300
 
277
- // Validate individual components against their schemas
278
- const externalSystemSchema = require('./schema/external-system.schema.json');
279
- const externalDatasourceSchema = require('./schema/external-datasource.schema.json');
301
+ /**
302
+ * Prepares schemas for validation
303
+ * @function prepareSchemasForValidation
304
+ * @returns {Object} Prepared schemas
305
+ */
306
+ function prepareSchemasForValidation() {
307
+ const externalSystemSchema = require('../schema/external-system.schema.json');
308
+ const externalDatasourceSchema = require('../schema/external-datasource.schema.json');
280
309
 
281
310
  // For draft-2020-12 schemas, remove $schema to avoid AJV issues
282
311
  const datasourceSchemaToAdd = { ...externalDatasourceSchema };
@@ -284,6 +313,18 @@ async function generateExternalSystemApplicationSchema(appName) {
284
313
  delete datasourceSchemaToAdd.$schema;
285
314
  }
286
315
 
316
+ return { externalSystemSchema, datasourceSchemaToAdd };
317
+ }
318
+
319
+ /**
320
+ * Validates application schema components
321
+ * @function validateApplicationSchemaComponents
322
+ * @param {Object} systemJson - System JSON
323
+ * @param {Object[]} datasourceJsons - Array of datasource JSONs
324
+ * @param {Object} externalSystemSchema - External system schema
325
+ * @param {Object} datasourceSchemaToAdd - Datasource schema
326
+ */
327
+ function validateApplicationSchemaComponents(systemJson, datasourceJsons, externalSystemSchema, datasourceSchemaToAdd) {
287
328
  const ajv = new Ajv({ allErrors: true, strict: false, removeAdditional: false });
288
329
 
289
330
  // Validate system against external-system schema
@@ -291,6 +332,28 @@ async function generateExternalSystemApplicationSchema(appName) {
291
332
 
292
333
  // Validate datasources against external-datasource schema
293
334
  validateDatasourceSchemas(datasourceJsons, datasourceSchemaToAdd, ajv);
335
+ }
336
+
337
+ async function generateExternalSystemApplicationSchema(appName) {
338
+ if (!appName || typeof appName !== 'string') {
339
+ throw new Error('App name is required and must be a string');
340
+ }
341
+
342
+ const { appPath } = await detectAppType(appName);
343
+ const { variables, schemaBasePath, systemFiles } = await loadExternalIntegrationConfig(appPath);
344
+
345
+ const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFiles[0]);
346
+
347
+ // Load datasource files
348
+ const datasourceFiles = variables.externalIntegration.dataSources || [];
349
+ const datasourceJsons = await loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles);
350
+
351
+ // Build application-schema.json structure
352
+ const applicationSchema = buildBaseSchema(systemJson, datasourceJsons, variables.externalIntegration.version);
353
+
354
+ // Validate individual components against their schemas
355
+ const { externalSystemSchema, datasourceSchemaToAdd } = prepareSchemasForValidation();
356
+ validateApplicationSchemaComponents(systemJson, datasourceJsons, externalSystemSchema, datasourceSchemaToAdd);
294
357
 
295
358
  return applicationSchema;
296
359
  }
@@ -21,7 +21,7 @@ Handlebars.registerHelper('contains', (array, value) => {
21
21
  * @returns {Promise<Object>} Object mapping step names to their compiled content
22
22
  */
23
23
  async function loadStepTemplates(stepNames = []) {
24
- const stepsDir = path.join(__dirname, '..', 'templates', 'github', 'steps');
24
+ const stepsDir = path.join(__dirname, '..', '..', 'templates', 'github', 'steps');
25
25
  const stepTemplates = {};
26
26
 
27
27
  for (const stepName of stepNames) {
@@ -64,7 +64,7 @@ async function generateGithubWorkflows(appPath, config, options = {}) {
64
64
  const templateContext = await getTemplateContext(config, options, stepTemplates);
65
65
 
66
66
  // Load and compile templates
67
- const templatesDir = path.join(__dirname, '..', 'templates', 'github');
67
+ const templatesDir = path.join(__dirname, '..', '..', 'templates', 'github');
68
68
  const templateFiles = await fs.readdir(templatesDir);
69
69
 
70
70
  const generatedFiles = [];
@@ -103,17 +103,48 @@ async function generateGithubWorkflows(appPath, config, options = {}) {
103
103
  * @param {Object} stepTemplates - Compiled step templates
104
104
  * @returns {Promise<Object>} Template context
105
105
  */
106
- async function getTemplateContext(config, options = {}, stepTemplates = {}) {
107
- const githubSteps = options.githubSteps || [];
106
+ /**
107
+ * Builds base context for template rendering
108
+ * @function buildBaseContext
109
+ * @param {Object} config - Application configuration
110
+ * @param {Object} options - Additional options
111
+ * @returns {Object} Base context object
112
+ */
113
+ /**
114
+ * Determines file extension based on language
115
+ * @function getFileExtension
116
+ * @param {string} language - Programming language
117
+ * @returns {string} File extension
118
+ */
119
+ function getFileExtension(language) {
120
+ return language === 'python' ? 'py' : 'js';
121
+ }
108
122
 
109
- // Render step templates with the base context
110
- const renderedSteps = {};
111
- const baseContext = {
123
+ /**
124
+ * Determines source directory based on language
125
+ * @function getSourceDir
126
+ * @param {string} language - Programming language
127
+ * @returns {string} Source directory
128
+ */
129
+ function getSourceDir(language) {
130
+ return language === 'python' ? 'src' : 'lib';
131
+ }
132
+
133
+ /**
134
+ * Builds base configuration values
135
+ * @function buildBaseConfigValues
136
+ * @param {Object} config - Configuration object
137
+ * @param {Object} options - Options object
138
+ * @returns {Object} Base configuration values
139
+ */
140
+ function buildBaseConfigValues(config, options) {
141
+ const language = config.language || 'typescript';
142
+ return {
112
143
  appName: config.appName || 'myapp',
113
144
  mainBranch: options.mainBranch || 'main',
114
- language: config.language || 'typescript',
115
- fileExtension: config.language === 'python' ? 'py' : 'js',
116
- sourceDir: config.language === 'python' ? 'src' : 'lib',
145
+ language: language,
146
+ fileExtension: getFileExtension(language),
147
+ sourceDir: getSourceDir(language),
117
148
  buildCommand: options.buildCommand || 'npm run build',
118
149
  port: config.port || 3000,
119
150
  database: config.database || false,
@@ -121,11 +152,37 @@ async function getTemplateContext(config, options = {}, stepTemplates = {}) {
121
152
  storage: config.storage || false,
122
153
  authentication: config.authentication || false
123
154
  };
155
+ }
124
156
 
157
+ function buildBaseContext(config, options) {
158
+ return buildBaseConfigValues(config, options);
159
+ }
160
+
161
+ /**
162
+ * Renders step templates
163
+ * @function renderStepTemplates
164
+ * @param {Object} stepTemplates - Step templates object
165
+ * @param {Object} baseContext - Base context for rendering
166
+ * @returns {Object} Rendered steps
167
+ */
168
+ function renderStepTemplates(stepTemplates, baseContext) {
169
+ const renderedSteps = {};
125
170
  for (const [stepName, template] of Object.entries(stepTemplates)) {
126
171
  renderedSteps[stepName] = template(baseContext);
127
172
  }
173
+ return renderedSteps;
174
+ }
128
175
 
176
+ /**
177
+ * Builds final template context
178
+ * @function buildFinalTemplateContext
179
+ * @param {Object} baseContext - Base context
180
+ * @param {Object} options - Additional options
181
+ * @param {Object} renderedSteps - Rendered step templates
182
+ * @returns {Object} Final template context
183
+ */
184
+ function buildFinalTemplateContext(baseContext, options, renderedSteps) {
185
+ const githubSteps = options.githubSteps || [];
129
186
  return {
130
187
  ...baseContext,
131
188
  uploadCoverage: options.uploadCoverage !== false,
@@ -136,6 +193,12 @@ async function getTemplateContext(config, options = {}, stepTemplates = {}) {
136
193
  };
137
194
  }
138
195
 
196
+ async function getTemplateContext(config, options = {}, stepTemplates = {}) {
197
+ const baseContext = buildBaseContext(config, options);
198
+ const renderedSteps = renderStepTemplates(stepTemplates, baseContext);
199
+ return buildFinalTemplateContext(baseContext, options, renderedSteps);
200
+ }
201
+
139
202
  /**
140
203
  * Generate a specific workflow file
141
204
  * @param {string} appPath - Path to application directory
@@ -151,7 +214,7 @@ async function generateWorkflowFile(appPath, templateName, config, options = {})
151
214
  await fs.mkdir(workflowsDir, { recursive: true });
152
215
 
153
216
  // Load template
154
- const templatePath = path.join(__dirname, '..', 'templates', 'github', `${templateName}.hbs`);
217
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'github', `${templateName}.hbs`);
155
218
  const templateContent = await fs.readFile(templatePath, 'utf8');
156
219
 
157
220
  // Compile template
@@ -185,11 +248,15 @@ async function generateWorkflowFile(appPath, templateName, config, options = {})
185
248
  * @param {Object} options - Generation options
186
249
  * @returns {Object} Validation result
187
250
  */
188
- function validateWorkflowConfig(config, options = {}) {
251
+ /**
252
+ * Validates required fields
253
+ * @function validateRequiredFields
254
+ * @param {Object} config - Application configuration
255
+ * @returns {string[]} Array of error messages
256
+ */
257
+ function validateRequiredFields(config) {
189
258
  const errors = [];
190
- const warnings = [];
191
259
 
192
- // Validate required fields
193
260
  if (!config.appName) {
194
261
  errors.push('Application name is required');
195
262
  }
@@ -202,6 +269,19 @@ function validateWorkflowConfig(config, options = {}) {
202
269
  errors.push('Language must be either typescript or python');
203
270
  }
204
271
 
272
+ return errors;
273
+ }
274
+
275
+ /**
276
+ * Validates optional fields
277
+ * @function validateOptionalFields
278
+ * @param {Object} config - Application configuration
279
+ * @param {Object} options - Generation options
280
+ * @returns {string[]} Array of error messages
281
+ */
282
+ function validateOptionalFields(config, options) {
283
+ const errors = [];
284
+
205
285
  // Validate port
206
286
  if (config.port && (config.port < 1 || config.port > 65535)) {
207
287
  errors.push('Port must be between 1 and 65535');
@@ -212,7 +292,18 @@ function validateWorkflowConfig(config, options = {}) {
212
292
  errors.push('Main branch name contains invalid characters');
213
293
  }
214
294
 
215
- // Warnings
295
+ return errors;
296
+ }
297
+
298
+ /**
299
+ * Collects validation warnings
300
+ * @function collectValidationWarnings
301
+ * @param {Object} config - Application configuration
302
+ * @returns {string[]} Array of warning messages
303
+ */
304
+ function collectValidationWarnings(config) {
305
+ const warnings = [];
306
+
216
307
  if (config.language === 'python' && !config.database) {
217
308
  warnings.push('Python applications typically require a database');
218
309
  }
@@ -221,6 +312,16 @@ function validateWorkflowConfig(config, options = {}) {
221
312
  warnings.push('Authentication typically requires a database for user storage');
222
313
  }
223
314
 
315
+ return warnings;
316
+ }
317
+
318
+ function validateWorkflowConfig(config, options = {}) {
319
+ const errors = [
320
+ ...validateRequiredFields(config),
321
+ ...validateOptionalFields(config, options)
322
+ ];
323
+ const warnings = collectValidationWarnings(config);
324
+
224
325
  return {
225
326
  valid: errors.length === 0,
226
327
  errors,
@@ -157,11 +157,13 @@ function validatePortalInput(portalInput, variableName) {
157
157
  * @returns {Array<Object>} Configuration array with merged portalInput
158
158
  * @throws {Error} If portalInput structure is invalid
159
159
  */
160
- function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
161
- const configuration = [];
162
- const lines = envTemplate.split('\n');
163
-
164
- // Create a map of portalInput configurations by variable name
160
+ /**
161
+ * Creates a map of portalInput configurations from variables config
162
+ * @function createPortalInputMap
163
+ * @param {Object|null} variablesConfig - Configuration from variables.yaml
164
+ * @returns {Map} Map of variable names to portalInput configurations
165
+ */
166
+ function createPortalInputMap(variablesConfig) {
165
167
  const portalInputMap = new Map();
166
168
  if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
167
169
  for (const configItem of variablesConfig.configuration) {
@@ -172,55 +174,103 @@ function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
172
174
  }
173
175
  }
174
176
  }
177
+ return portalInputMap;
178
+ }
175
179
 
176
- for (const line of lines) {
177
- const trimmed = line.trim();
180
+ /**
181
+ * Parses a single environment variable line
182
+ * @function parseEnvironmentVariableLine
183
+ * @param {string} line - Line to parse
184
+ * @returns {Object|null} Parsed variable object or null if invalid
185
+ */
186
+ function parseEnvironmentVariableLine(line) {
187
+ const trimmed = line.trim();
178
188
 
179
- // Skip empty lines and comments
180
- if (!trimmed || trimmed.startsWith('#')) {
181
- continue;
182
- }
189
+ // Skip empty lines and comments
190
+ if (!trimmed || trimmed.startsWith('#')) {
191
+ return null;
192
+ }
183
193
 
184
- // Parse KEY=VALUE format
185
- const equalIndex = trimmed.indexOf('=');
186
- if (equalIndex === -1) {
187
- continue;
188
- }
194
+ // Parse KEY=VALUE format
195
+ const equalIndex = trimmed.indexOf('=');
196
+ if (equalIndex === -1) {
197
+ return null;
198
+ }
189
199
 
190
- const key = trimmed.substring(0, equalIndex).trim();
191
- const value = trimmed.substring(equalIndex + 1).trim();
200
+ const key = trimmed.substring(0, equalIndex).trim();
201
+ const value = trimmed.substring(equalIndex + 1).trim();
192
202
 
193
- if (!key || !value) {
194
- continue;
195
- }
203
+ if (!key || !value) {
204
+ return null;
205
+ }
196
206
 
197
- // Determine location and required status
198
- let location = 'variable';
199
- let required = false;
207
+ return { key, value };
208
+ }
200
209
 
201
- if (value.startsWith('kv://')) {
202
- location = 'keyvault';
203
- required = true;
204
- }
210
+ /**
211
+ * Determines location and required status for a variable
212
+ * @function determineVariableLocation
213
+ * @param {string} value - Variable value
214
+ * @param {string} key - Variable key
215
+ * @returns {Object} Object with location and required properties
216
+ */
217
+ function determineVariableLocation(value, key) {
218
+ let location = 'variable';
219
+ let required = false;
205
220
 
206
- // Check if it's a sensitive variable
207
- const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
208
- if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
209
- required = true;
210
- }
221
+ if (value.startsWith('kv://')) {
222
+ location = 'keyvault';
223
+ required = true;
224
+ }
211
225
 
212
- const configItem = {
213
- name: key,
214
- value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
215
- location,
216
- required
217
- };
226
+ // Check if it's a sensitive variable
227
+ const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
228
+ if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
229
+ required = true;
230
+ }
231
+
232
+ return { location, required };
233
+ }
218
234
 
219
- // Merge portalInput if it exists in variables.yaml
220
- if (portalInputMap.has(key)) {
221
- configItem.portalInput = portalInputMap.get(key);
235
+ /**
236
+ * Creates a configuration item from parsed variable
237
+ * @function createConfigItem
238
+ * @param {string} key - Variable key
239
+ * @param {string} value - Variable value
240
+ * @param {string} location - Variable location
241
+ * @param {boolean} required - Whether variable is required
242
+ * @param {Map} portalInputMap - Map of portalInput configurations
243
+ * @returns {Object} Configuration item
244
+ */
245
+ function createConfigItem(key, value, location, required, portalInputMap) {
246
+ const configItem = {
247
+ name: key,
248
+ value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
249
+ location,
250
+ required
251
+ };
252
+
253
+ // Merge portalInput if it exists in variables.yaml
254
+ if (portalInputMap.has(key)) {
255
+ configItem.portalInput = portalInputMap.get(key);
256
+ }
257
+
258
+ return configItem;
259
+ }
260
+
261
+ function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
262
+ const configuration = [];
263
+ const lines = envTemplate.split('\n');
264
+ const portalInputMap = createPortalInputMap(variablesConfig);
265
+
266
+ for (const line of lines) {
267
+ const parsed = parseEnvironmentVariableLine(line);
268
+ if (!parsed) {
269
+ continue;
222
270
  }
223
271
 
272
+ const { location, required } = determineVariableLocation(parsed.value, parsed.key);
273
+ const configItem = createConfigItem(parsed.key, parsed.value, location, required, portalInputMap);
224
274
  configuration.push(configItem);
225
275
  }
226
276