@aifabrix/builder 2.39.3 → 2.40.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 (114) hide show
  1. package/.cursor/rules/project-rules.mdc +6 -6
  2. package/README.md +2 -2
  3. package/babel.config.js +6 -0
  4. package/integration/hubspot/README.md +53 -141
  5. package/integration/hubspot/application.yaml +37 -0
  6. package/integration/hubspot/env.template +2 -11
  7. package/integration/hubspot/hubspot-deploy.json +1 -0
  8. package/integration/hubspot/test.js +5 -5
  9. package/lib/api/credentials.api.js +5 -5
  10. package/lib/api/deployments.api.js +2 -2
  11. package/lib/api/pipeline.api.js +17 -17
  12. package/lib/api/wizard.api.js +2 -2
  13. package/lib/app/config.js +11 -6
  14. package/lib/app/deploy-config.js +13 -16
  15. package/lib/app/deploy.js +29 -22
  16. package/lib/app/display.js +1 -1
  17. package/lib/app/dockerfile.js +11 -12
  18. package/lib/app/helpers.js +51 -13
  19. package/lib/app/index.js +14 -2
  20. package/lib/app/prompts.js +37 -45
  21. package/lib/app/push.js +8 -11
  22. package/lib/app/readme.js +16 -12
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/run-helpers.js +31 -22
  25. package/lib/app/run.js +44 -5
  26. package/lib/app/show-display.js +104 -44
  27. package/lib/app/show.js +123 -43
  28. package/lib/build/index.js +11 -18
  29. package/lib/cli/setup-app.js +36 -29
  30. package/lib/cli/setup-auth.js +18 -15
  31. package/lib/cli/setup-credential-deployment.js +3 -1
  32. package/lib/cli/setup-external-system.js +35 -16
  33. package/lib/cli/setup-infra.js +45 -23
  34. package/lib/cli/setup-utility.js +79 -31
  35. package/lib/commands/app-logs.js +28 -20
  36. package/lib/commands/app.js +30 -26
  37. package/lib/commands/convert.js +202 -0
  38. package/lib/commands/credential-list.js +78 -17
  39. package/lib/commands/datasource.js +24 -24
  40. package/lib/commands/deployment-list.js +13 -6
  41. package/lib/commands/up-common.js +80 -42
  42. package/lib/commands/up-dataplane.js +15 -14
  43. package/lib/commands/up-miso.js +15 -14
  44. package/lib/commands/upload.js +163 -0
  45. package/lib/commands/wizard-core.js +5 -4
  46. package/lib/core/diff.js +84 -9
  47. package/lib/core/key-generator.js +9 -12
  48. package/lib/core/secrets-docker-env.js +2 -2
  49. package/lib/core/secrets.js +3 -2
  50. package/lib/core/templates.js +2 -2
  51. package/lib/datasource/deploy.js +2 -1
  52. package/lib/deployment/deployer.js +76 -48
  53. package/lib/external-system/delete.js +0 -1
  54. package/lib/external-system/deploy-helpers.js +5 -6
  55. package/lib/external-system/deploy.js +7 -2
  56. package/lib/external-system/download-helpers.js +4 -4
  57. package/lib/external-system/download.js +11 -10
  58. package/lib/external-system/generator.js +19 -17
  59. package/lib/external-system/test.js +10 -15
  60. package/lib/generator/builders.js +1 -1
  61. package/lib/generator/external-controller-manifest.js +26 -29
  62. package/lib/generator/external-schema-utils.js +6 -18
  63. package/lib/generator/external.js +32 -27
  64. package/lib/generator/github.js +1 -1
  65. package/lib/generator/helpers.js +12 -19
  66. package/lib/generator/index.js +15 -15
  67. package/lib/generator/parse-image.js +35 -0
  68. package/lib/generator/split-readme.js +105 -0
  69. package/lib/generator/split-variables.js +149 -0
  70. package/lib/generator/split.js +86 -246
  71. package/lib/generator/wizard.js +46 -69
  72. package/lib/schema/application-schema.json +4 -4
  73. package/lib/schema/external-datasource.schema.json +5 -0
  74. package/lib/schema/external-system.schema.json +10 -0
  75. package/lib/utils/app-config-resolver.js +52 -0
  76. package/lib/utils/app-register-api.js +1 -1
  77. package/lib/utils/app-register-auth.js +1 -1
  78. package/lib/utils/app-register-config.js +16 -23
  79. package/lib/utils/app-register-validator.js +2 -2
  80. package/lib/utils/cli-utils.js +47 -3
  81. package/lib/utils/config-format.js +154 -0
  82. package/lib/utils/config-paths.js +19 -52
  83. package/lib/utils/config-tokens.js +1 -0
  84. package/lib/utils/docker-build.js +71 -94
  85. package/lib/utils/dockerfile-utils.js +1 -1
  86. package/lib/utils/env-copy.js +4 -4
  87. package/lib/utils/env-ports.js +2 -2
  88. package/lib/utils/error-formatter.js +1 -1
  89. package/lib/utils/error-formatters/validation-errors.js +1 -1
  90. package/lib/utils/external-readme.js +12 -5
  91. package/lib/utils/external-system-test-helpers.js +2 -0
  92. package/lib/utils/health-check.js +55 -66
  93. package/lib/utils/image-version.js +12 -21
  94. package/lib/utils/paths.js +39 -66
  95. package/lib/utils/port-resolver.js +8 -8
  96. package/lib/utils/schema-loader.js +22 -0
  97. package/lib/utils/schema-resolver.js +23 -33
  98. package/lib/utils/secrets-helpers.js +7 -7
  99. package/lib/utils/secrets-utils.js +10 -12
  100. package/lib/utils/template-helpers.js +13 -13
  101. package/lib/utils/token-manager.js +20 -2
  102. package/lib/utils/variable-transformer.js +2 -2
  103. package/lib/validation/validate-display.js +3 -4
  104. package/lib/validation/validate.js +33 -27
  105. package/lib/validation/validator.js +50 -30
  106. package/package.json +2 -1
  107. package/templates/README.md +1 -1
  108. package/templates/applications/README.md.hbs +3 -3
  109. package/templates/applications/miso-controller/env.template +3 -1
  110. package/templates/external-system/README.md.hbs +4 -4
  111. package/integration/hubspot/variables.yaml +0 -17
  112. /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
  113. /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
  114. /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
@@ -15,6 +15,7 @@ const logger = require('../utils/logger');
15
15
  const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
16
16
  const { handleDeploymentError, handleDeploymentErrors } = require('../utils/deployment-errors');
17
17
  const { validatePipeline, deployPipeline, getPipelineDeployment } = require('../api/pipeline.api');
18
+ const { getAuthUser } = require('../api/auth.api');
18
19
  const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
19
20
  const {
20
21
  convertToPipelineAuthConfig,
@@ -36,73 +37,72 @@ function transformExternalManifestForPipeline(manifest) {
36
37
 
37
38
  /**
38
39
  * Build validation data for deployment
40
+ * When authenticated with bearer token only, clientId/clientSecret are not sent (controller uses token).
39
41
  * @async
40
42
  * @param {Object} manifest - Application manifest/config
41
43
  * @param {string} validatedEnvKey - Validated environment key
42
44
  * @param {Object} authConfig - Authentication configuration
43
45
  * @param {Object} options - Additional options
44
- * @returns {Promise<Object>} Object with validationData and pipelineAuthConfig
46
+ * @returns {Promise<Object>} Object with validationData, pipelineAuthConfig, and useBearerOnly
45
47
  */
46
48
  async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
47
- const tokenManager = require('../utils/token-manager');
48
- let clientId;
49
- let clientSecret;
50
- let pipelineAuthConfig;
49
+ const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
51
50
 
52
- try {
53
- const credentials = await tokenManager.extractClientCredentials(
54
- authConfig,
55
- manifest.key,
56
- validatedEnvKey,
57
- options
58
- );
59
- clientId = credentials.clientId;
60
- clientSecret = credentials.clientSecret;
61
- pipelineAuthConfig = {
62
- type: 'client-credentials',
63
- clientId,
64
- clientSecret
51
+ if (authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId) {
52
+ const pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
53
+ const validationData = {
54
+ clientId: manifest.key,
55
+ repositoryUrl,
56
+ applicationConfig: manifest
65
57
  };
66
- } catch (credError) {
67
- if (authConfig.type === 'bearer' && authConfig.token) {
68
- pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
69
- clientId = manifest.key;
70
- clientSecret = '';
71
- } else {
72
- throw credError;
73
- }
58
+ return { validationData, pipelineAuthConfig, useBearerOnly: true };
74
59
  }
75
60
 
76
- const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
61
+ const tokenManager = require('../utils/token-manager');
62
+ const credentials = await tokenManager.extractClientCredentials(
63
+ authConfig,
64
+ manifest.key,
65
+ validatedEnvKey,
66
+ options
67
+ );
68
+ const pipelineAuthConfig = {
69
+ type: 'client-credentials',
70
+ clientId: credentials.clientId,
71
+ clientSecret: credentials.clientSecret
72
+ };
77
73
  const validationData = {
78
- clientId: clientId || '',
79
- clientSecret: clientSecret || '',
80
- repositoryUrl: repositoryUrl,
74
+ clientId: credentials.clientId || '',
75
+ clientSecret: credentials.clientSecret || '',
76
+ repositoryUrl,
81
77
  applicationConfig: manifest
82
78
  };
83
-
84
- return { validationData, pipelineAuthConfig };
79
+ return { validationData, pipelineAuthConfig, useBearerOnly: false };
85
80
  }
86
81
 
87
82
  /**
88
83
  * Handle authentication errors during validation
89
84
  * @param {Error} error - Error object
90
85
  * @param {string} appKey - Application key
86
+ * @param {boolean} [useBearerOnly] - True when auth was bearer token only (no client id/secret sent)
91
87
  * @throws {Error} Enhanced authentication error
92
88
  */
93
- function handleValidationAuthError(error, appKey) {
94
- if (error.status === 401 || (error.response && error.response.status === 401)) {
95
- const authError = new Error(
96
- `Authentication failed: Invalid or expired credentials for application '${appKey}'.\n` +
97
- 'The provided Client ID and Client Secret are incorrect or have been revoked.\n\n' +
98
- '💡 If the application already exists, rotate the secret:\n' +
99
- ` aifabrix app rotate-secret ${appKey}\n\n` +
100
- '💡 Otherwise, ensure credentials are correct in ~/.aifabrix/secrets.local.yaml or use --client-id and --client-secret flags.'
101
- );
102
- authError.status = 401;
103
- authError.originalError = error;
104
- throw authError;
89
+ function handleValidationAuthError(error, appKey, useBearerOnly) {
90
+ if (error.status !== 401 && (!error.response || error.response.status !== 401)) {
91
+ return;
105
92
  }
93
+ const authError = new Error(
94
+ useBearerOnly
95
+ ? 'Authentication failed: Your authentication token is invalid or expired.\n\n' +
96
+ 'To authenticate, run:\n aifabrix login --method device --controller <url>'
97
+ : `Authentication failed: Invalid or expired credentials for application '${appKey}'.\n` +
98
+ 'The provided Client ID and Client Secret are incorrect or have been revoked.\n\n' +
99
+ '💡 If the application already exists, rotate the secret:\n' +
100
+ ` aifabrix app rotate-secret ${appKey}\n\n` +
101
+ '💡 Otherwise, ensure credentials are correct in ~/.aifabrix/secrets.local.yaml or use --client-id and --client-secret flags.'
102
+ );
103
+ authError.status = 401;
104
+ authError.originalError = error;
105
+ throw authError;
106
106
  }
107
107
 
108
108
  /**
@@ -122,8 +122,8 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
122
122
  const validatedEnvKey = validateEnvironmentKey(envKey);
123
123
  const maxRetries = options.maxRetries || 3;
124
124
 
125
- // Build validation data
126
- const { validationData, pipelineAuthConfig } = await buildValidationData(manifest, validatedEnvKey, authConfig, options);
125
+ // Build validation data (bearer-only: no clientId/clientSecret sent)
126
+ const { validationData, pipelineAuthConfig, useBearerOnly } = await buildValidationData(manifest, validatedEnvKey, authConfig, options);
127
127
 
128
128
  let lastError;
129
129
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -133,8 +133,8 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
133
133
  } catch (error) {
134
134
  lastError = error;
135
135
 
136
- // Handle authentication errors (401) - credentials are invalid, not missing
137
- handleValidationAuthError(error, manifest.key);
136
+ // Handle authentication errors (401)
137
+ handleValidationAuthError(error, manifest.key, useBearerOnly);
138
138
 
139
139
  const shouldRetry = attempt < maxRetries && error.status && error.status >= 500;
140
140
  if (shouldRetry) {
@@ -310,6 +310,32 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
310
310
  throw new Error('Deployment timeout: Maximum polling attempts reached');
311
311
  }
312
312
 
313
+ /**
314
+ * When using bearer token only (no client credentials), verify token is valid before deploy.
315
+ * @async
316
+ * @param {string} controllerUrl - Controller URL
317
+ * @param {Object} authConfig - Authentication configuration
318
+ * @throws {Error} If token is invalid or expired
319
+ */
320
+ async function ensureBearerTokenValid(controllerUrl, authConfig) {
321
+ if (authConfig.type !== 'bearer' || !authConfig.token || authConfig.clientId) {
322
+ return;
323
+ }
324
+ try {
325
+ const response = await getAuthUser(controllerUrl, authConfig);
326
+ if (response && response.success && response.data && response.data.authenticated !== false) {
327
+ return;
328
+ }
329
+ } catch (_) {
330
+ // Fall through to throw below
331
+ }
332
+ throw new Error(
333
+ 'Your authentication token is invalid or expired.\n\n' +
334
+ 'Run: aifabrix login --method device --controller <url>\n\n' +
335
+ 'Then run deploy again.'
336
+ );
337
+ }
338
+
313
339
  /**
314
340
  * Validates and sends deployment request to controller
315
341
  * Implements two-step process: validate then deploy
@@ -322,6 +348,8 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
322
348
  * @returns {Promise<Object>} Deployment result
323
349
  */
324
350
  async function sendDeployment(url, validatedEnvKey, manifest, authConfig, options) {
351
+ await ensureBearerTokenValid(url, authConfig);
352
+
325
353
  // Step 1: Validate deployment
326
354
  logger.log(chalk.blue('🔍 Validating deployment configuration...'));
327
355
  const validateResult = await validateDeployment(url, validatedEnvKey, manifest, authConfig, {
@@ -50,7 +50,6 @@ async function getAuthAndDataplane(systemKey, _options) {
50
50
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
51
51
  logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
52
52
  const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
53
- logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
54
53
 
55
54
  return { authConfig, dataplaneUrl, environment, controllerUrl };
56
55
  }
@@ -11,11 +11,10 @@
11
11
  const fs = require('fs').promises;
12
12
  const fsSync = require('fs');
13
13
  const path = require('path');
14
- const yaml = require('js-yaml');
15
14
  const { detectAppType, getDeployJsonPath } = require('../utils/paths');
16
15
 
17
16
  /**
18
- * Loads variables.yaml for an application
17
+ * Loads application config for an application
19
18
  * @async
20
19
  * @function loadVariablesYaml
21
20
  * @param {string} appName - Application name
@@ -23,11 +22,11 @@ const { detectAppType, getDeployJsonPath } = require('../utils/paths');
23
22
  * @throws {Error} If file cannot be loaded
24
23
  */
25
24
  async function loadVariablesYaml(appName) {
26
- // Detect app type and get correct path (integration or builder)
27
25
  const { appPath } = await detectAppType(appName);
28
- const variablesPath = path.join(appPath, 'variables.yaml');
29
- const content = await fs.readFile(variablesPath, 'utf8');
30
- return yaml.load(content);
26
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
27
+ const { loadConfigFile } = require('../utils/config-format');
28
+ const configPath = resolveApplicationConfigPath(appPath);
29
+ return loadConfigFile(configPath);
31
30
  }
32
31
 
33
32
  /**
@@ -13,6 +13,8 @@ const chalk = require('chalk');
13
13
  const { getDeploymentAuth } = require('../utils/token-manager');
14
14
  const logger = require('../utils/logger');
15
15
  const { resolveControllerUrl } = require('../utils/controller-url');
16
+ const { detectAppType } = require('../utils/paths');
17
+ const { logOfflinePathWhenType } = require('../utils/cli-utils');
16
18
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
17
19
  const { getExternalSystem } = require('../api/external-systems.api');
18
20
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
@@ -132,11 +134,14 @@ async function prepareDeploymentConfig(appName, _options) {
132
134
  */
133
135
  async function deployExternalSystem(appName, options = {}) {
134
136
  try {
137
+ const { appPath } = await detectAppType(appName);
138
+ logOfflinePathWhenType(appPath);
139
+
135
140
  logger.log(chalk.blue(`\n🚀 Deploying external system: ${appName}`));
136
141
 
137
142
  // Step 0: Validate before deployment (same as validate command)
138
143
  logger.log(chalk.blue('🔍 Validating external system before deployment...'));
139
- const validationResult = await validateExternalSystemComplete(appName);
144
+ const validationResult = await validateExternalSystemComplete(appName, options);
140
145
 
141
146
  if (!validationResult.valid) {
142
147
  displayValidationResults(validationResult);
@@ -146,7 +151,7 @@ async function deployExternalSystem(appName, options = {}) {
146
151
  logger.log(chalk.green('✓ Validation passed, proceeding with deployment...'));
147
152
 
148
153
  // Step 1: Generate controller manifest (validated, ready for deployment)
149
- const manifest = await generateControllerManifest(appName);
154
+ const manifest = await generateControllerManifest(appName, options);
150
155
 
151
156
  // Step 2: Get deployment configuration (auth, controller URL, etc.)
152
157
  const { environment, controllerUrl, authConfig } = await prepareDeploymentConfig(appName, options);
@@ -11,14 +11,14 @@
11
11
  const { generateExternalReadmeContent } = require('../utils/external-readme');
12
12
 
13
13
  /**
14
- * Generates variables.yaml content for downloaded system
14
+ * Generates application.yaml content for downloaded system
15
15
  * @param {string} systemKey - System key
16
16
  * @param {Object} application - External system configuration
17
17
  * @param {Array} dataSources - Array of datasource configurations
18
18
  * @returns {Object} Variables YAML object
19
19
  */
20
20
  function generateVariablesYaml(systemKey, application, dataSources) {
21
- const systemFileName = `${systemKey}-system.json`;
21
+ const systemFileName = `${systemKey}-system.yaml`;
22
22
  const datasourceFiles = dataSources.map(ds => {
23
23
  // Extract datasource key (remove system key prefix if present)
24
24
  const datasourceKey = ds.key || '';
@@ -29,7 +29,7 @@ function generateVariablesYaml(systemKey, application, dataSources) {
29
29
  const entityType = ds.entityType || ds.entityKey || datasourceKey.split('-').pop();
30
30
  datasourceKeyOnly = entityType;
31
31
  }
32
- return `${systemKey}-datasource-${datasourceKeyOnly}.json`;
32
+ return `${systemKey}-datasource-${datasourceKeyOnly}.yaml`;
33
33
  });
34
34
 
35
35
  return {
@@ -73,7 +73,7 @@ function generateReadme(systemKey, application, dataSources) {
73
73
  return {
74
74
  entityType: datasourceKeyOnly,
75
75
  displayName: ds.displayName || ds.name || ds.key || `Datasource ${index + 1}`,
76
- fileName: `${systemKey}-datasource-${datasourceKeyOnly}.json`
76
+ fileName: `${systemKey}-datasource-${datasourceKeyOnly}.yaml`
77
77
  };
78
78
  });
79
79
 
@@ -19,6 +19,7 @@ const { getDeploymentAuth } = require('../utils/token-manager');
19
19
  const { getConfig } = require('../core/config');
20
20
  const { detectAppType } = require('../utils/paths');
21
21
  const logger = require('../utils/logger');
22
+ const { writeConfigFile } = require('../utils/config-format');
22
23
  const { generateEnvTemplate } = require('../utils/external-system-env-helpers');
23
24
  const { generateVariablesYaml, generateReadme } = require('./download-helpers');
24
25
  const { resolveControllerUrl } = require('../utils/controller-url');
@@ -201,9 +202,9 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
201
202
  * @returns {Promise<string>} System file path
202
203
  */
203
204
  async function generateSystemFile(tempDir, systemKey, application) {
204
- const systemFileName = `${systemKey}-system.json`;
205
+ const systemFileName = `${systemKey}-system.yaml`;
205
206
  const systemFilePath = path.join(tempDir, systemFileName);
206
- await fs.writeFile(systemFilePath, JSON.stringify(application, null, 2), 'utf8');
207
+ writeConfigFile(systemFilePath, application);
207
208
  return systemFilePath;
208
209
  }
209
210
 
@@ -230,9 +231,9 @@ async function generateDatasourceFiles(tempDir, systemKey, dataSources) {
230
231
  const entityType = datasource.entityType || datasource.entityKey || datasourceKey.split('-').pop();
231
232
  datasourceKeyOnly = entityType;
232
233
  }
233
- const datasourceFileName = `${systemKey}-datasource-${datasourceKeyOnly}.json`;
234
+ const datasourceFileName = `${systemKey}-datasource-${datasourceKeyOnly}.yaml`;
234
235
  const datasourceFilePath = path.join(tempDir, datasourceFileName);
235
- await fs.writeFile(datasourceFilePath, JSON.stringify(datasource, null, 2), 'utf8');
236
+ writeConfigFile(datasourceFilePath, datasource);
236
237
  datasourceFiles.push(datasourceFilePath);
237
238
  } catch (error) {
238
239
  datasourceErrors.push(new Error(`Failed to write datasource ${datasource.key}: ${error.message}`));
@@ -242,7 +243,7 @@ async function generateDatasourceFiles(tempDir, systemKey, dataSources) {
242
243
  }
243
244
 
244
245
  /**
245
- * Generates configuration files (variables.yaml, env.template, README.md)
246
+ * Generates configuration files (application.yaml, env.template, README.md)
246
247
  * @async
247
248
  * @function generateConfigFiles
248
249
  * @param {string} tempDir - Temporary directory
@@ -252,9 +253,9 @@ async function generateDatasourceFiles(tempDir, systemKey, dataSources) {
252
253
  * @returns {Promise<Object>} Object with file paths
253
254
  */
254
255
  async function generateConfigFiles(tempDir, systemKey, application, dataSources) {
255
- // Generate variables.yaml
256
+ // Generate application.yaml
256
257
  const variables = generateVariablesYaml(systemKey, application, dataSources);
257
- const variablesPath = path.join(tempDir, 'variables.yaml');
258
+ const variablesPath = path.join(tempDir, 'application.yaml');
258
259
  await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
259
260
 
260
261
  // Generate env.template
@@ -307,7 +308,7 @@ async function moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths
307
308
  const systemFileName = `${systemKey}-system.json`;
308
309
  const filesToMove = [
309
310
  { from: filePaths.systemFilePath, to: path.join(finalPath, systemFileName) },
310
- { from: filePaths.variablesPath, to: path.join(finalPath, 'variables.yaml') },
311
+ { from: filePaths.variablesPath, to: path.join(finalPath, 'application.yaml') },
311
312
  { from: filePaths.envTemplatePath, to: path.join(finalPath, 'env.template') },
312
313
  { from: filePaths.readmePath, to: path.join(finalPath, 'README.md') }
313
314
  ];
@@ -347,8 +348,8 @@ function handleDryRun(systemKey, dataplaneUrl) {
347
348
  logger.log(chalk.gray(` ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
348
349
  logger.log(chalk.yellow('\nWould create:'));
349
350
  logger.log(chalk.gray(` integration/${systemKey}/`));
350
- logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
351
- logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-system.json`));
351
+ logger.log(chalk.gray(` integration/${systemKey}/application.yaml`));
352
+ logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-system.yaml`));
352
353
  logger.log(chalk.gray(` integration/${systemKey}/env.template`));
353
354
  logger.log(chalk.gray(` integration/${systemKey}/README.md`));
354
355
  }
@@ -12,9 +12,10 @@
12
12
  const fs = require('fs').promises;
13
13
  const path = require('path');
14
14
  const handlebars = require('handlebars');
15
- const yaml = require('js-yaml');
16
15
  const chalk = require('chalk');
17
16
  const logger = require('../utils/logger');
17
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
18
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
19
 
19
20
  // Register Handlebars helper for equality check
20
21
  handlebars.registerHelper('eq', (a, b) => a === b);
@@ -52,11 +53,12 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
52
53
  };
53
54
 
54
55
  const rendered = template(context);
56
+ const parsed = JSON.parse(rendered);
55
57
 
56
- // Generate in same folder as variables.yaml (new structure)
57
- // Use naming: <app-name>-system.json
58
- const outputPath = path.join(appPath, `${systemKey}-system.json`);
59
- await fs.writeFile(outputPath, rendered, 'utf8');
58
+ // Generate in same folder as application.yaml (new structure)
59
+ // Use naming: <app-name>-system.yaml
60
+ const outputPath = path.join(appPath, `${systemKey}-system.yaml`);
61
+ writeConfigFile(outputPath, parsed);
60
62
 
61
63
  return outputPath;
62
64
  } catch (error) {
@@ -98,15 +100,16 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
98
100
  };
99
101
 
100
102
  const rendered = template(context);
103
+ const datasourceConfig = JSON.parse(rendered);
101
104
 
102
- // Generate in same folder as variables.yaml (new structure)
103
- // Use naming: <app-name>-datasource-<datasource-key>.json
105
+ // Generate in same folder as application.yaml (new structure)
106
+ // Use naming: <app-name>-datasource-<datasource-key>.yaml
104
107
  // Extract datasource key (remove system key prefix if present)
105
108
  const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${config.systemKey}-`)
106
109
  ? datasourceKey.substring(config.systemKey.length + 1)
107
110
  : datasourceKey;
108
- const outputPath = path.join(appPath, `${config.systemKey}-datasource-${datasourceKeyOnly}.json`);
109
- await fs.writeFile(outputPath, rendered, 'utf8');
111
+ const outputPath = path.join(appPath, `${config.systemKey}-datasource-${datasourceKeyOnly}.yaml`);
112
+ writeConfigFile(outputPath, datasourceConfig);
110
113
 
111
114
  return outputPath;
112
115
  } catch (error) {
@@ -160,7 +163,7 @@ async function generateExternalSystemFiles(appPath, appName, config) {
160
163
  logger.log(chalk.green(`✓ Generated datasource: ${path.basename(datasourcePath)}`));
161
164
  }
162
165
 
163
- // Update variables.yaml with externalIntegration block
166
+ // Update application.yaml with externalIntegration block
164
167
  await updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths);
165
168
 
166
169
  return {
@@ -173,7 +176,7 @@ async function generateExternalSystemFiles(appPath, appName, config) {
173
176
  }
174
177
 
175
178
  /**
176
- * Updates variables.yaml with externalIntegration block
179
+ * Updates application.yaml with externalIntegration block
177
180
  * @async
178
181
  * @function updateVariablesYamlWithExternalIntegration
179
182
  * @param {string} appPath - Application directory path
@@ -183,23 +186,22 @@ async function generateExternalSystemFiles(appPath, appName, config) {
183
186
  */
184
187
  async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths) {
185
188
  try {
186
- const variablesPath = path.join(appPath, 'variables.yaml');
187
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
188
- const variables = yaml.load(variablesContent);
189
+ const configPath = resolveApplicationConfigPath(appPath);
190
+ const variables = loadConfigFile(configPath);
189
191
 
190
192
  // Add externalIntegration block
191
193
  // Files are in same folder, so schemaBasePath is './'
192
194
  variables.externalIntegration = {
193
195
  schemaBasePath: './',
194
- systems: [`${systemKey}-system.json`],
196
+ systems: [`${systemKey}-system.yaml`],
195
197
  dataSources: datasourcePaths.map(p => path.basename(p)),
196
198
  autopublish: true,
197
199
  version: '1.0.0'
198
200
  };
199
201
 
200
- await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
202
+ writeConfigFile(configPath, variables);
201
203
  } catch (error) {
202
- throw new Error(`Failed to update variables.yaml: ${error.message}`);
204
+ throw new Error(`Failed to update application config: ${error.message}`);
203
205
  }
204
206
  }
205
207
 
@@ -12,7 +12,6 @@
12
12
  const fs = require('fs').promises;
13
13
  const fsSync = require('fs');
14
14
  const path = require('path');
15
- const yaml = require('js-yaml');
16
15
  const chalk = require('chalk');
17
16
  const testHelpers = require('../utils/external-system-test-helpers');
18
17
  const { retryApiCall } = require('../utils/external-system-test-helpers');
@@ -39,30 +38,26 @@ const {
39
38
  } = require('./test-execution');
40
39
 
41
40
  /**
42
- * Loads and parses variables.yaml file
41
+ * Loads and parses application config file
43
42
  * @async
44
43
  * @function loadVariablesYamlFile
45
- * @param {string} variablesPath - Path to variables.yaml
44
+ * @param {string} variablesPath - Path to application config
46
45
  * @returns {Promise<Object>} Parsed variables
47
- * @throws {Error} If file not found or invalid YAML
46
+ * @throws {Error} If file not found or invalid
48
47
  */
49
48
  async function loadVariablesYamlFile(variablesPath) {
50
- if (!fsSync.existsSync(variablesPath)) {
51
- throw new Error(`variables.yaml not found: ${variablesPath}`);
52
- }
53
-
54
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
49
+ const { loadConfigFile } = require('../utils/config-format');
55
50
  try {
56
- const variables = yaml.load(variablesContent);
51
+ const variables = loadConfigFile(variablesPath);
57
52
  if (!variables.externalIntegration) {
58
- throw new Error('externalIntegration block not found in variables.yaml');
53
+ throw new Error('externalIntegration block not found in application config');
59
54
  }
60
55
  return variables;
61
56
  } catch (error) {
62
57
  if (error.message.includes('externalIntegration')) {
63
58
  throw error;
64
59
  }
65
- throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
60
+ throw new Error(`Application config: ${error.message}`);
66
61
  }
67
62
  }
68
63
 
@@ -152,9 +147,9 @@ async function loadDatasourceFiles(datasourceFiles, schemaBasePath, appPath) {
152
147
  */
153
148
  async function loadExternalSystemFiles(appName) {
154
149
  const { appPath } = await detectAppType(appName);
155
- const variablesPath = path.join(appPath, 'variables.yaml');
156
-
157
- const variables = await loadVariablesYamlFile(variablesPath);
150
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
151
+ const configPath = resolveApplicationConfigPath(appPath);
152
+ const variables = await loadVariablesYamlFile(configPath);
158
153
 
159
154
  const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
160
155
  const systemFiles = variables.externalIntegration.systems || [];
@@ -290,7 +290,7 @@ function addHealthCheckToDeployment(deployment, variables) {
290
290
  * @param {Object|null} rbac - RBAC configuration
291
291
  */
292
292
  function addRolesAndPermissions(deployment, variables, rbac) {
293
- // Priority: variables.yaml > rbac.yaml
293
+ // Priority: application.yaml > rbac.yaml
294
294
  if (variables.roles) {
295
295
  deployment.roles = variables.roles;
296
296
  } else if (rbac && rbac.roles) {
@@ -11,6 +11,7 @@
11
11
 
12
12
  const path = require('path');
13
13
  const { detectAppType } = require('../utils/paths');
14
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
15
  const { loadSystemFile, loadDatasourceFiles } = require('./external');
15
16
  const { loadVariables, loadRbac } = require('./helpers');
16
17
 
@@ -42,18 +43,18 @@ function mergeRbacIntoSystemJson(systemJson, rbac) {
42
43
  * @param {Object} options - Options with optional appPath
43
44
  * @returns {Promise<string>} Application path
44
45
  */
45
- async function resolveAppPath(appName, options) {
46
- if (options.appPath) {
46
+ async function resolveAppPath(appName, options = {}) {
47
+ if (options && options.appPath) {
47
48
  return options.appPath;
48
49
  }
49
- const detected = await detectAppType(appName, { type: 'external' });
50
+ const detected = await detectAppType(appName);
50
51
  return detected.appPath;
51
52
  }
52
53
 
53
54
  /**
54
55
  * Extracts app metadata from variables
55
56
  * @function extractAppMetadata
56
- * @param {Object} variables - Parsed variables.yaml
57
+ * @param {Object} variables - Parsed application config
57
58
  * @param {string} appName - Application name
58
59
  * @returns {Object} App metadata { appKey, displayName, description }
59
60
  */
@@ -98,59 +99,55 @@ async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
98
99
  * const manifest = await generateControllerManifest('my-hubspot');
99
100
  * // Returns: { key, displayName, description, type: "external", system: {...}, dataSources: [...] }
100
101
  */
102
+ function normalizeSchemaBasePath(schemaBasePath, appPath, appName) {
103
+ const base = path.normalize(schemaBasePath || './').replace(/[/\\]+$/, '');
104
+ return base === path.join('integration', appName) ? './' : (schemaBasePath || './');
105
+ }
106
+
101
107
  async function generateControllerManifest(appName, options = {}) {
102
108
  if (!appName || typeof appName !== 'string') {
103
109
  throw new Error('App name is required and must be a string');
104
110
  }
105
-
106
111
  const appPath = await resolveAppPath(appName, options);
107
- const variablesPath = path.join(appPath, 'variables.yaml');
108
- const { parsed: variables } = loadVariables(variablesPath);
109
-
112
+ const { parsed: variables } = loadVariables(resolveApplicationConfigPath(appPath));
110
113
  if (!variables.externalIntegration) {
111
- throw new Error('externalIntegration block not found in variables.yaml');
114
+ throw new Error('externalIntegration block not found in application.yaml');
112
115
  }
113
-
114
116
  const metadata = extractAppMetadata(variables, appName);
115
- const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
117
+ const schemaBasePath = normalizeSchemaBasePath(
118
+ variables.externalIntegration.schemaBasePath,
119
+ appPath,
120
+ appName
121
+ );
116
122
  const systemFiles = variables.externalIntegration.systems || [];
117
-
118
123
  if (systemFiles.length === 0) {
119
124
  throw new Error('No system files specified in externalIntegration.systems');
120
125
  }
121
-
122
- const systemJson = await loadSystemWithRbac(appPath, schemaBasePath, systemFiles[0]);
123
- const datasourceFiles = variables.externalIntegration.dataSources || [];
124
- const datasourceJsons = await loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles);
125
-
126
+ const [systemJson, datasourceJsons] = await Promise.all([
127
+ loadSystemWithRbac(appPath, schemaBasePath, systemFiles[0]),
128
+ loadDatasourceFiles(appPath, schemaBasePath, variables.externalIntegration.dataSources || [])
129
+ ]);
126
130
  const appVersion = variables.app?.version || variables.externalIntegration?.version || '1.0.0';
127
-
128
- // Build externalIntegration block (required by application schema for type: "external")
129
131
  const externalIntegration = {
130
- schemaBasePath: schemaBasePath,
132
+ schemaBasePath,
131
133
  systems: systemFiles,
132
- dataSources: datasourceFiles,
133
- autopublish: variables.externalIntegration.autopublish !== false, // default true
134
+ dataSources: variables.externalIntegration.dataSources || [],
135
+ autopublish: variables.externalIntegration.autopublish !== false,
134
136
  version: appVersion
135
137
  };
136
-
137
- const manifest = {
138
+ return {
138
139
  key: metadata.appKey,
139
140
  displayName: metadata.displayName,
140
141
  description: metadata.description,
141
142
  type: 'external',
142
143
  version: appVersion,
143
- externalIntegration: externalIntegration,
144
- // Inline system and dataSources for atomic deployment (optional but recommended)
144
+ externalIntegration,
145
145
  system: systemJson,
146
146
  dataSources: datasourceJsons,
147
- // Explicitly set to false to satisfy conditional schema requirements
148
147
  requiresDatabase: false,
149
148
  requiresRedis: false,
150
149
  requiresStorage: false
151
150
  };
152
-
153
- return manifest;
154
151
  }
155
152
 
156
153
  module.exports = {