@aifabrix/builder 2.39.3 → 2.40.2

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 (117) hide show
  1. package/.cursor/rules/project-rules.mdc +6 -6
  2. package/README.md +3 -3
  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/jest.config.manual.js +29 -0
  10. package/lib/api/credentials.api.js +5 -5
  11. package/lib/api/deployments.api.js +2 -2
  12. package/lib/api/pipeline.api.js +17 -17
  13. package/lib/api/wizard.api.js +2 -2
  14. package/lib/app/config.js +11 -6
  15. package/lib/app/deploy-config.js +13 -16
  16. package/lib/app/deploy.js +29 -22
  17. package/lib/app/display.js +1 -1
  18. package/lib/app/dockerfile.js +11 -12
  19. package/lib/app/helpers.js +51 -13
  20. package/lib/app/index.js +14 -2
  21. package/lib/app/prompts.js +37 -45
  22. package/lib/app/push.js +8 -11
  23. package/lib/app/readme.js +16 -12
  24. package/lib/app/register.js +1 -1
  25. package/lib/app/run-helpers.js +31 -22
  26. package/lib/app/run.js +44 -5
  27. package/lib/app/show-display.js +104 -44
  28. package/lib/app/show.js +123 -43
  29. package/lib/build/index.js +11 -18
  30. package/lib/cli/setup-app.js +36 -29
  31. package/lib/cli/setup-auth.js +19 -15
  32. package/lib/cli/setup-credential-deployment.js +3 -1
  33. package/lib/cli/setup-external-system.js +35 -16
  34. package/lib/cli/setup-infra.js +45 -23
  35. package/lib/cli/setup-utility.js +85 -31
  36. package/lib/commands/app-logs.js +28 -20
  37. package/lib/commands/app.js +30 -26
  38. package/lib/commands/auth-status.js +36 -3
  39. package/lib/commands/convert.js +202 -0
  40. package/lib/commands/credential-list.js +78 -17
  41. package/lib/commands/datasource.js +24 -24
  42. package/lib/commands/deployment-list.js +13 -6
  43. package/lib/commands/up-common.js +80 -42
  44. package/lib/commands/up-dataplane.js +15 -14
  45. package/lib/commands/up-miso.js +15 -14
  46. package/lib/commands/upload.js +163 -0
  47. package/lib/commands/wizard-core.js +5 -4
  48. package/lib/core/diff.js +84 -9
  49. package/lib/core/key-generator.js +9 -12
  50. package/lib/core/secrets-docker-env.js +2 -2
  51. package/lib/core/secrets.js +3 -2
  52. package/lib/core/templates.js +2 -2
  53. package/lib/datasource/deploy.js +2 -1
  54. package/lib/deployment/deployer.js +76 -48
  55. package/lib/external-system/delete.js +0 -1
  56. package/lib/external-system/deploy-helpers.js +5 -6
  57. package/lib/external-system/deploy.js +7 -2
  58. package/lib/external-system/download-helpers.js +4 -4
  59. package/lib/external-system/download.js +11 -10
  60. package/lib/external-system/generator.js +19 -17
  61. package/lib/external-system/test.js +10 -15
  62. package/lib/generator/builders.js +1 -1
  63. package/lib/generator/external-controller-manifest.js +26 -29
  64. package/lib/generator/external-schema-utils.js +6 -18
  65. package/lib/generator/external.js +32 -27
  66. package/lib/generator/github.js +1 -1
  67. package/lib/generator/helpers.js +12 -19
  68. package/lib/generator/index.js +15 -15
  69. package/lib/generator/parse-image.js +35 -0
  70. package/lib/generator/split-readme.js +105 -0
  71. package/lib/generator/split-variables.js +149 -0
  72. package/lib/generator/split.js +86 -246
  73. package/lib/generator/wizard.js +51 -70
  74. package/lib/schema/application-schema.json +4 -4
  75. package/lib/schema/external-datasource.schema.json +5 -0
  76. package/lib/schema/external-system.schema.json +10 -0
  77. package/lib/utils/app-config-resolver.js +52 -0
  78. package/lib/utils/app-register-api.js +1 -1
  79. package/lib/utils/app-register-auth.js +1 -1
  80. package/lib/utils/app-register-config.js +16 -23
  81. package/lib/utils/app-register-validator.js +2 -2
  82. package/lib/utils/cli-utils.js +47 -3
  83. package/lib/utils/config-format.js +154 -0
  84. package/lib/utils/config-paths.js +19 -52
  85. package/lib/utils/config-tokens.js +1 -0
  86. package/lib/utils/docker-build.js +71 -94
  87. package/lib/utils/dockerfile-utils.js +1 -1
  88. package/lib/utils/env-copy.js +4 -4
  89. package/lib/utils/env-ports.js +2 -2
  90. package/lib/utils/error-formatter.js +1 -1
  91. package/lib/utils/error-formatters/validation-errors.js +1 -1
  92. package/lib/utils/external-readme.js +12 -5
  93. package/lib/utils/external-system-test-helpers.js +2 -0
  94. package/lib/utils/health-check.js +55 -66
  95. package/lib/utils/image-version.js +12 -21
  96. package/lib/utils/paths.js +45 -66
  97. package/lib/utils/port-resolver.js +8 -8
  98. package/lib/utils/schema-loader.js +22 -0
  99. package/lib/utils/schema-resolver.js +23 -33
  100. package/lib/utils/secrets-helpers.js +7 -7
  101. package/lib/utils/secrets-utils.js +10 -12
  102. package/lib/utils/template-helpers.js +13 -13
  103. package/lib/utils/token-manager.js +20 -2
  104. package/lib/utils/variable-transformer.js +2 -2
  105. package/lib/validation/validate-display.js +3 -4
  106. package/lib/validation/validate.js +34 -28
  107. package/lib/validation/validator.js +50 -30
  108. package/package.json +4 -1
  109. package/templates/README.md +1 -1
  110. package/templates/applications/README.md.hbs +3 -3
  111. package/templates/applications/miso-controller/env.template +3 -1
  112. package/templates/external-system/README.md.hbs +4 -4
  113. package/templates/external-system/external-system.json.hbs +1 -16
  114. package/integration/hubspot/variables.yaml +0 -17
  115. /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
  116. /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
  117. /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
@@ -2,14 +2,19 @@
2
2
  * @fileoverview Wizard file generator - saves dataplane-generated configurations to files
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
+ *
6
+ * Standard credential-backed variable names (supplied at runtime from the selected credential;
7
+ * do not list in configuration array): CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME,
8
+ * PASSWORD, BASEURL. See docs/external-systems.md and docs/wizard.md.
5
9
  */
6
10
 
7
11
  const fs = require('fs').promises;
8
12
  const path = require('path');
9
- const yaml = require('js-yaml');
10
13
  const Handlebars = require('handlebars');
11
14
  const chalk = require('chalk');
12
15
  const logger = require('../utils/logger');
16
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
17
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
13
18
  const { generateExternalReadmeContent } = require('../utils/external-readme');
14
19
 
15
20
  /**
@@ -39,30 +44,30 @@ function toKeySegment(str) {
39
44
  /**
40
45
  * Writes system JSON file
41
46
  * @async
42
- * @function writeSystemJsonFile
47
+ * @function writeSystemYamlFile
43
48
  * @param {string} appPath - Application path
44
49
  * @param {string} finalSystemKey - Final system key
45
50
  * @param {Object} systemConfig - System configuration
46
51
  * @returns {Promise<string>} System file path
47
52
  */
48
- async function writeSystemJsonFile(appPath, finalSystemKey, systemConfig) {
49
- const systemFileName = `${finalSystemKey}-system.json`;
53
+ async function writeSystemYamlFile(appPath, finalSystemKey, systemConfig) {
54
+ const systemFileName = `${finalSystemKey}-system.yaml`;
50
55
  const systemFilePath = path.join(appPath, systemFileName);
51
- await fs.writeFile(systemFilePath, JSON.stringify(systemConfig, null, 2), 'utf8');
56
+ writeConfigFile(systemFilePath, systemConfig);
52
57
  logger.log(chalk.green(`✓ Generated system file: ${systemFileName}`));
53
58
  return systemFilePath;
54
59
  }
55
60
 
56
61
  /**
57
- * Writes datasource JSON files
62
+ * Writes datasource YAML files
58
63
  * @async
59
- * @function writeDatasourceJsonFiles
64
+ * @function writeDatasourceYamlFiles
60
65
  * @param {string} appPath - Application path
61
66
  * @param {string} finalSystemKey - Final system key
62
67
  * @param {Object[]} datasourceConfigs - Array of datasource configurations
63
68
  * @returns {Promise<string[]>} Array of datasource file names
64
69
  */
65
- async function writeDatasourceJsonFiles(appPath, finalSystemKey, datasourceConfigs) {
70
+ async function writeDatasourceYamlFiles(appPath, finalSystemKey, datasourceConfigs) {
66
71
  const datasourceFileNames = [];
67
72
  for (const datasourceConfig of datasourceConfigs) {
68
73
  const entityType = datasourceConfig.entityType || datasourceConfig.entityKey || datasourceConfig.key?.split('-').pop() || 'default';
@@ -72,9 +77,9 @@ async function writeDatasourceJsonFiles(appPath, finalSystemKey, datasourceConfi
72
77
  const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${finalSystemKey}-`)
73
78
  ? datasourceKey.substring(finalSystemKey.length + 1)
74
79
  : keySegment;
75
- const datasourceFileName = `${finalSystemKey}-datasource-${datasourceKeyOnly}.json`;
80
+ const datasourceFileName = `${finalSystemKey}-datasource-${datasourceKeyOnly}.yaml`;
76
81
  const datasourceFilePath = path.join(appPath, datasourceFileName);
77
- await fs.writeFile(datasourceFilePath, JSON.stringify(datasourceConfig, null, 2), 'utf8');
82
+ writeConfigFile(datasourceFilePath, datasourceConfig);
78
83
  datasourceFileNames.push(datasourceFileName);
79
84
  logger.log(chalk.green(`✓ Generated datasource file: ${datasourceFileName}`));
80
85
  }
@@ -99,8 +104,8 @@ async function writeDatasourceJsonFiles(appPath, finalSystemKey, datasourceConfi
99
104
  async function generateConfigFilesForWizard(params) {
100
105
  const { appPath, appName, finalSystemKey, systemFileName, datasourceFileNames, systemConfig, datasourceConfigs, aiGeneratedReadme } = params;
101
106
 
102
- // Generate or update variables.yaml with externalIntegration block
103
- await generateOrUpdateVariablesYaml({
107
+ // Generate or update application.yaml with externalIntegration block
108
+ const configPath = await generateOrUpdateVariablesYaml({
104
109
  appPath,
105
110
  appName,
106
111
  systemKey: finalSystemKey,
@@ -126,7 +131,7 @@ async function generateConfigFilesForWizard(params) {
126
131
  logger.log(chalk.green(`✓ Generated deployment manifest: ${finalSystemKey}-deploy.json`));
127
132
 
128
133
  return {
129
- variablesPath: path.join(appPath, 'variables.yaml'),
134
+ variablesPath: configPath,
130
135
  envTemplatePath: path.join(appPath, 'env.template'),
131
136
  readmePath: path.join(appPath, 'README.md'),
132
137
  applicationSchemaPath: deployManifestPath,
@@ -134,48 +139,28 @@ async function generateConfigFilesForWizard(params) {
134
139
  };
135
140
  }
136
141
 
142
+ async function prepareWizardContext(appName, systemConfig, datasourceConfigs) {
143
+ const appPath = path.join(process.cwd(), 'integration', appName);
144
+ await fs.mkdir(appPath, { recursive: true });
145
+ const finalSystemKey = appName;
146
+ const appDisplayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
147
+ const updatedSystemConfig = { ...systemConfig, key: finalSystemKey, displayName: appDisplayName };
148
+ const updatedDatasourceConfigs = datasourceConfigs.map(ds => {
149
+ const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || 'default';
150
+ const keySegment = toKeySegment(entityType);
151
+ const entityDisplayName = entityType.charAt(0).toUpperCase() + entityType.slice(1).replace(/-/g, ' ');
152
+ return { ...ds, key: `${finalSystemKey}-${keySegment}`, systemKey: finalSystemKey, displayName: `${appDisplayName} ${entityDisplayName}` };
153
+ });
154
+ return { appPath, finalSystemKey, updatedSystemConfig, updatedDatasourceConfigs, appDisplayName };
155
+ }
156
+
137
157
  async function generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, options = {}) {
138
158
  try {
139
159
  const { aiGeneratedReadme } = options || {};
140
- // Determine app path (integration directory for external systems)
141
- const appPath = path.join(process.cwd(), 'integration', appName);
142
-
143
- // Create directory if it doesn't exist
144
- await fs.mkdir(appPath, { recursive: true });
145
-
146
- // Use appName as the system key to ensure consistent naming
147
- // Priority: appName > systemKey parameter > systemConfig.key
148
- const finalSystemKey = appName;
149
-
150
- // Generate displayName from appName (e.g., "my-hubspot" -> "My Hubspot")
151
- const appDisplayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
152
-
153
- // Update system config to use the appName as key and displayName
154
- const updatedSystemConfig = {
155
- ...systemConfig,
156
- key: finalSystemKey,
157
- displayName: appDisplayName
158
- };
159
-
160
- // Update datasource configs to use appName-based keys and systemKey (key must match ^[a-z0-9-]+$)
161
- const updatedDatasourceConfigs = datasourceConfigs.map(ds => {
162
- const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || 'default';
163
- const keySegment = toKeySegment(entityType);
164
- const entityDisplayName = entityType.charAt(0).toUpperCase() + entityType.slice(1).replace(/-/g, ' ');
165
- return {
166
- ...ds,
167
- key: `${finalSystemKey}-${keySegment}`,
168
- systemKey: finalSystemKey,
169
- displayName: `${appDisplayName} ${entityDisplayName}`
170
- };
171
- });
172
-
173
- // Write system and datasource JSON files
174
- const systemFilePath = await writeSystemJsonFile(appPath, finalSystemKey, updatedSystemConfig);
175
- const datasourceFileNames = await writeDatasourceJsonFiles(appPath, finalSystemKey, updatedDatasourceConfigs);
176
-
177
- // Generate configuration files
178
- const systemFileName = `${finalSystemKey}-system.json`;
160
+ const { appPath, finalSystemKey, updatedSystemConfig, updatedDatasourceConfigs } = await prepareWizardContext(appName, systemConfig, datasourceConfigs);
161
+ const systemFilePath = await writeSystemYamlFile(appPath, finalSystemKey, updatedSystemConfig);
162
+ const datasourceFileNames = await writeDatasourceYamlFiles(appPath, finalSystemKey, updatedDatasourceConfigs);
163
+ const systemFileName = `${finalSystemKey}-system.yaml`;
179
164
  const configFiles = await generateConfigFilesForWizard({
180
165
  appPath,
181
166
  appName,
@@ -186,7 +171,6 @@ async function generateWizardFiles(appName, systemConfig, datasourceConfigs, sys
186
171
  datasourceConfigs: updatedDatasourceConfigs,
187
172
  aiGeneratedReadme
188
173
  });
189
-
190
174
  return {
191
175
  appPath,
192
176
  systemFilePath,
@@ -199,7 +183,7 @@ async function generateWizardFiles(appName, systemConfig, datasourceConfigs, sys
199
183
  }
200
184
 
201
185
  /**
202
- * Generate or update variables.yaml with externalIntegration block
186
+ * Generate or update application.yaml with externalIntegration block
203
187
  * @async
204
188
  * @function generateOrUpdateVariablesYaml
205
189
  * @param {Object} params - Parameters object
@@ -209,23 +193,19 @@ async function generateWizardFiles(appName, systemConfig, datasourceConfigs, sys
209
193
  * @param {string} params.systemFileName - System file name
210
194
  * @param {string[]} params.datasourceFileNames - Array of datasource file names
211
195
  * @param {Object} params.systemConfig - System configuration
196
+ * @returns {Promise<string>} Path to application config file
212
197
  * @throws {Error} If generation fails
213
198
  */
214
199
  async function generateOrUpdateVariablesYaml(params) {
215
200
  const { appPath, appName, systemFileName, datasourceFileNames, systemConfig } = params;
201
+ let configPath;
202
+ let variables = {};
216
203
  try {
217
- const variablesPath = path.join(appPath, 'variables.yaml');
218
- let variables = {};
219
-
220
- // Try to read existing variables.yaml
221
204
  try {
222
- const existingContent = await fs.readFile(variablesPath, 'utf8');
223
- variables = yaml.load(existingContent) || {};
224
- } catch (error) {
225
- // File doesn't exist, create new one
226
- if (error.code !== 'ENOENT') {
227
- throw error;
228
- }
205
+ configPath = resolveApplicationConfigPath(appPath);
206
+ variables = loadConfigFile(configPath) || {};
207
+ } catch {
208
+ configPath = path.join(appPath, 'application.yaml');
229
209
  }
230
210
 
231
211
  // Set basic app info if not present
@@ -258,10 +238,11 @@ async function generateOrUpdateVariablesYaml(params) {
258
238
  version: systemConfig.version || '1.0.0'
259
239
  };
260
240
 
261
- await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
262
- logger.log(chalk.green('✓ Generated/updated variables.yaml'));
241
+ writeConfigFile(configPath, variables);
242
+ logger.log(chalk.green('✓ Generated/updated application.yaml'));
243
+ return configPath;
263
244
  } catch (error) {
264
- throw new Error(`Failed to generate variables.yaml: ${error.message}`);
245
+ throw new Error(`Failed to generate application config: ${error.message}`);
265
246
  }
266
247
  }
267
248
 
@@ -345,7 +326,7 @@ function addAuthenticationLines(lines, auth, authType) {
345
326
  function addBaseUrlLines(lines, systemConfig) {
346
327
  if (systemConfig.baseUrl || systemConfig.baseURL) {
347
328
  lines.push('# API Base URL');
348
- lines.push(`BASE_URL=${systemConfig.baseUrl || systemConfig.baseURL}`);
329
+ lines.push(`BASEURL=${systemConfig.baseUrl || systemConfig.baseURL}`);
349
330
  lines.push('');
350
331
  }
351
332
  }
@@ -445,7 +426,7 @@ async function generateReadme(appPath, appName, systemKey, systemConfig, datasou
445
426
  return {
446
427
  entityType,
447
428
  displayName: ds.displayName || ds.name || ds.key || `Datasource ${index + 1}`,
448
- fileName: `${systemKey}-datasource-${datasourceKeyOnly}.json`
429
+ fileName: `${systemKey}-datasource-${datasourceKeyOnly}.yaml`
449
430
  };
450
431
  });
451
432
 
@@ -799,19 +799,19 @@
799
799
  },
800
800
  "systems": {
801
801
  "type": "array",
802
- "description": "List of external-system JSON files to deploy via pipeline.",
802
+ "description": "List of external-system files to deploy via pipeline (.json, .yaml, or .yml).",
803
803
  "items": {
804
804
  "type": "string",
805
- "pattern": "^[^ ].+\\.json$"
805
+ "pattern": "^[^ ].+\\.(json|yaml|yml)$"
806
806
  },
807
807
  "uniqueItems": true
808
808
  },
809
809
  "dataSources": {
810
810
  "type": "array",
811
- "description": "List of external-datasource JSON files belonging to this app.",
811
+ "description": "List of external-datasource files belonging to this app (.json, .yaml, or .yml).",
812
812
  "items": {
813
813
  "type": "string",
814
- "pattern": "^[^ ].+\\.json$"
814
+ "pattern": "^[^ ].+\\.(json|yaml|yml)$"
815
815
  },
816
816
  "uniqueItems": true
817
817
  },
@@ -1587,6 +1587,11 @@
1587
1587
  }
1588
1588
  },
1589
1589
  "additionalProperties":false
1590
+ },
1591
+ "triggerPathsHash":{
1592
+ "type":"string",
1593
+ "description":"SHA256 hash of triggerPaths payload (64-char hex). Used to detect structural changes. Optional; Dataplane computes when absent.",
1594
+ "pattern":"^[a-f0-9]{64}$"
1590
1595
  }
1591
1596
  },
1592
1597
  "additionalProperties":false
@@ -102,6 +102,11 @@
102
102
  "type": "boolean",
103
103
  "default": true
104
104
  },
105
+ "internal": {
106
+ "type": "boolean",
107
+ "description": "When true, this integration is deployed on dataplane startup (internal integration). When false or absent, deployed via pipeline only.",
108
+ "default": false
109
+ },
105
110
  "authentication": {
106
111
  "type": "object",
107
112
  "description": "Authentication configuration for external system",
@@ -414,6 +419,11 @@
414
419
  "type": "boolean",
415
420
  "description": "Reserved: whether to generate or expose OpenAPI contract on publish. Not yet implemented.",
416
421
  "default": true
422
+ },
423
+ "triggerPathsHash": {
424
+ "type": "string",
425
+ "description": "SHA256 hash of triggerPaths payload (64-char hex). Used to detect structural changes. Optional; Dataplane computes when absent.",
426
+ "pattern": "^[a-f0-9]{64}$"
417
427
  }
418
428
  },
419
429
  "additionalProperties": false
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Application config path resolution
3
+ *
4
+ * Single entry point for resolving path to application config file
5
+ * (application.yaml, application.json, or legacy variables.yaml).
6
+ *
7
+ * @fileoverview Resolve application config file path with legacy migration
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ /**
18
+ * Resolves path to application config file (application.yaml, application.json, or legacy variables.yaml).
19
+ * If only variables.yaml exists, renames it to application.yaml and returns the new path.
20
+ *
21
+ * @param {string} appPath - Absolute path to application directory
22
+ * @returns {string} Absolute path to application config file
23
+ * @throws {Error} If no config file found
24
+ */
25
+ function resolveApplicationConfigPath(appPath) {
26
+ if (!appPath || typeof appPath !== 'string') {
27
+ throw new Error('App path is required and must be a string');
28
+ }
29
+ const applicationYaml = path.join(appPath, 'application.yaml');
30
+ const applicationYml = path.join(appPath, 'application.yml');
31
+ const applicationJson = path.join(appPath, 'application.json');
32
+ const variablesYaml = path.join(appPath, 'variables.yaml');
33
+
34
+ if (fs.existsSync(applicationYaml)) {
35
+ return applicationYaml;
36
+ }
37
+ if (fs.existsSync(applicationYml)) {
38
+ return applicationYml;
39
+ }
40
+ if (fs.existsSync(applicationJson)) {
41
+ return applicationJson;
42
+ }
43
+ if (fs.existsSync(variablesYaml)) {
44
+ fs.renameSync(variablesYaml, applicationYaml);
45
+ return applicationYaml;
46
+ }
47
+ throw new Error(
48
+ `Application config not found in ${appPath}. Expected application.yaml, application.yml, application.json, or variables.yaml.`
49
+ );
50
+ }
51
+
52
+ module.exports = { resolveApplicationConfigPath };
@@ -39,7 +39,7 @@ function handleRegistrationError(response, apiUrl, registrationData) {
39
39
  logger.error(chalk.gray('\nRequest payload:'));
40
40
  logger.error(chalk.gray(JSON.stringify(registrationData, null, 2)));
41
41
  logger.error('');
42
- logger.error(chalk.gray('Check your variables.yaml file and ensure all required fields are correctly set.'));
42
+ logger.error(chalk.gray('Check your application.yaml file and ensure all required fields are correctly set.'));
43
43
  }
44
44
 
45
45
  process.exit(1);
@@ -82,7 +82,7 @@ async function findDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
82
82
  /**
83
83
  * Check if user is authenticated and get token
84
84
  * @async
85
- * @param {string} [controllerUrl] - Optional controller URL from variables.yaml or --controller flag
85
+ * @param {string} [controllerUrl] - Optional controller URL from application.yaml or --controller flag
86
86
  * @param {string} [environment] - Optional environment key
87
87
  * @returns {Promise<{apiUrl: string, token: string, controllerUrl: string}>} Configuration with API URL, token, and controller URL
88
88
  */
@@ -8,37 +8,35 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
- const fs = require('fs').promises;
12
11
  const path = require('path');
13
12
  const chalk = require('chalk');
14
- const yaml = require('js-yaml');
15
13
  const logger = require('./logger');
16
- const { detectAppType } = require('./paths');
14
+ const { detectAppType, resolveApplicationConfigPath } = require('./paths');
15
+ const { loadConfigFile } = require('./config-format');
17
16
  const { getContainerPort, getLocalPort } = require('./port-resolver');
18
17
 
19
18
  // createApp is imported dynamically in createMinimalAppIfNeeded to handle test mocking
20
19
 
21
20
  /**
22
- * Load variables.yaml file for an application
21
+ * Load application config for an application (application.yaml, application.json, or legacy).
23
22
  * @async
24
23
  * @param {string} appKey - Application key
25
24
  * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
26
25
  */
27
26
  async function loadVariablesYaml(appKey) {
28
- // Detect app type and get correct path (integration or builder)
29
27
  const { appPath } = await detectAppType(appKey);
30
- const variablesPath = path.join(appPath, 'variables.yaml');
31
-
32
28
  try {
33
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
34
- return { variables: yaml.load(variablesContent), created: false };
29
+ const configPath = resolveApplicationConfigPath(appPath);
30
+ const variables = loadConfigFile(configPath);
31
+ return { variables, created: false };
35
32
  } catch (error) {
36
- if (error.code === 'ENOENT') {
37
- logger.log(chalk.yellow(`⚠️ variables.yaml not found for ${appKey}`));
33
+ const isNotFound = error.code === 'ENOENT' || (error.message && error.message.includes('not found'));
34
+ if (isNotFound) {
35
+ logger.log(chalk.yellow(`⚠️ Application config not found for ${appKey}`));
38
36
  logger.log(chalk.yellow('📝 Creating minimal configuration...\n'));
39
37
  return { variables: null, created: true };
40
38
  }
41
- throw new Error(`Failed to read variables.yaml: ${error.message}`);
39
+ throw new Error(`Failed to read application config: ${error.message}`);
42
40
  }
43
41
  }
44
42
 
@@ -65,21 +63,19 @@ async function createMinimalAppIfNeeded(appKey, options) {
65
63
  authentication: false
66
64
  });
67
65
 
68
- // Detect app type and get correct path (integration or builder)
69
66
  const appTypeResult = await detectAppType(appKey);
70
67
  if (!appTypeResult || !appTypeResult.appPath) {
71
68
  throw new Error('Failed to detect app type after creation');
72
69
  }
73
70
  const { appPath } = appTypeResult;
74
- const variablesPath = path.join(appPath, 'variables.yaml');
75
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
76
- return yaml.load(variablesContent);
71
+ const configPath = resolveApplicationConfigPath(appPath);
72
+ return loadConfigFile(configPath);
77
73
  }
78
74
 
79
75
  /**
80
76
  * Builds image reference string from variables
81
77
  * Format: repository:tag (e.g., aifabrix/miso-controller:latest or myregistry.azurecr.io/miso-controller:v1.0.0)
82
- * @param {Object} variables - Variables from YAML file
78
+ * @param {Object} variables - Variables from application config
83
79
  * @param {string} appKey - Application key (fallback)
84
80
  * @returns {string} Image reference string
85
81
  */
@@ -94,7 +90,7 @@ function buildImageReference(variables, appKey) {
94
90
  * Extract URL from external system JSON file for registration
95
91
  * @async
96
92
  * @param {string} appKey - Application key
97
- * @param {Object} externalIntegration - External integration config from variables.yaml
93
+ * @param {Object} externalIntegration - External integration config from application config
98
94
  * @returns {Promise<{url: string, apiKey?: string}>} URL and optional API key
99
95
  */
100
96
  /**
@@ -171,12 +167,9 @@ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
171
167
  const systemFilePath = resolveSystemFilePath(appPath, schemaBasePath, systemFileName);
172
168
 
173
169
  try {
174
- const systemContent = await fs.readFile(systemFilePath, 'utf-8');
175
- const systemJson = JSON.parse(systemContent);
176
-
170
+ const systemJson = loadConfigFile(systemFilePath);
177
171
  const url = extractUrlFromSystemJson(systemJson, systemFileName);
178
172
  const apiKey = extractApiKeyFromSystemJson(systemJson);
179
-
180
173
  return { url, apiKey };
181
174
  } catch (error) {
182
175
  handleFileReadError(error, systemFilePath);
@@ -184,7 +177,7 @@ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
184
177
  }
185
178
 
186
179
  /**
187
- * Extract application configuration from variables.yaml
180
+ * Extract application configuration from application config
188
181
  * @async
189
182
  * @param {Object} variables - Variables from YAML file
190
183
  * @param {string} appKey - Application key
@@ -114,12 +114,12 @@ async function validateAppRegistrationData(config, originalAppKey) {
114
114
  if (!config.displayName) missingFields.push('app.name');
115
115
 
116
116
  if (missingFields.length > 0) {
117
- logger.error(chalk.red('❌ Missing required fields in variables.yaml:'));
117
+ logger.error(chalk.red('❌ Missing required fields in application.yaml:'));
118
118
  missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
119
119
  // Detect app type to show correct path
120
120
  const { appPath } = await detectAppType(originalAppKey);
121
121
  const relativePath = path.relative(process.cwd(), appPath);
122
- logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
122
+ logger.error(chalk.red(`\n Please update ${relativePath}/application.yaml and try again.`));
123
123
  process.exit(1);
124
124
  }
125
125
 
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const path = require('path');
12
+ const chalk = require('chalk');
12
13
  const fs = require('fs').promises;
13
14
  const logger = require('./logger');
14
15
 
@@ -85,6 +86,19 @@ function isPermissionDeniedError(errorMsg) {
85
86
  !errorMsg.includes('Field "permissions');
86
87
  }
87
88
 
89
+ /**
90
+ * Checks if permission denied error is Docker-related (daemon socket / CLI), not API auth.
91
+ * Used to avoid showing Docker hints when the error is from Controller/Dataplane "Permission denied".
92
+ * @function isDockerPermissionDeniedError
93
+ * @param {string} errorMsg - Error message
94
+ * @returns {boolean} True if Docker permission denied error
95
+ */
96
+ function isDockerPermissionDeniedError(errorMsg) {
97
+ if (!isPermissionDeniedError(errorMsg)) return false;
98
+ const lower = errorMsg.toLowerCase();
99
+ return lower.includes('docker') || lower.includes('socket') || errorMsg.includes('EACCES');
100
+ }
101
+
88
102
  /**
89
103
  * Format Docker-related errors
90
104
  * @param {string} errorMsg - Error message
@@ -109,7 +123,7 @@ function formatDockerError(errorMsg) {
109
123
  ' Run "aifabrix doctor" to check which ports are in use.'
110
124
  ];
111
125
  }
112
- if (isPermissionDeniedError(errorMsg)) {
126
+ if (isDockerPermissionDeniedError(errorMsg)) {
113
127
  return [
114
128
  ' Permission denied.',
115
129
  ' Make sure you have the necessary permissions to run Docker commands.'
@@ -149,7 +163,7 @@ function formatAzureError(errorMsg) {
149
163
  if (errorMsg.includes('Registry URL is required')) {
150
164
  return [
151
165
  ' Registry URL is required.',
152
- ' Provide via --registry flag or configure in variables.yaml under image.registry'
166
+ ' Provide via --registry flag or configure in application.yaml under image.registry'
153
167
  ];
154
168
  }
155
169
  return null;
@@ -222,6 +236,21 @@ function formatValidationError(errorMsg) {
222
236
  return null;
223
237
  }
224
238
 
239
+ /**
240
+ * Format API/Controller/Dataplane permission errors (403-style "Permission denied").
241
+ * Keeps the real message and adds a hint; avoids mis-classifying as Docker.
242
+ * @param {string} errorMsg - Error message
243
+ * @returns {string[]|null} Array of error message lines or null if not an API permission error
244
+ */
245
+ function formatApiPermissionError(errorMsg) {
246
+ if (!isPermissionDeniedError(errorMsg)) return null;
247
+ if (isDockerPermissionDeniedError(errorMsg)) return null;
248
+ return [
249
+ ` ${errorMsg}`,
250
+ ' Ensure your token has the required permission (e.g. external-system:delete for delete).'
251
+ ];
252
+ }
253
+
225
254
  /**
226
255
  * Formats error message based on error type
227
256
  * @function formatError
@@ -236,6 +265,7 @@ function formatValidationError(errorMsg) {
236
265
  */
237
266
  function tryFormatErrorWithFormatters(errorMsg) {
238
267
  const formatters = [
268
+ formatApiPermissionError,
239
269
  formatDockerError,
240
270
  formatAzureError,
241
271
  formatSecretsError,
@@ -283,6 +313,19 @@ function logError(command, errorMessages) {
283
313
  logger.error('\n💡 Run "aifabrix doctor" for environment diagnostics.\n');
284
314
  }
285
315
 
316
+ /**
317
+ * Logs the resolved app path so the user can see which directory (integration/<app> or builder/<app>) is used.
318
+ * Path resolution order is always integration first, then builder; no CLI flag overrides this.
319
+ *
320
+ * @param {string} appPath - Resolved application directory path
321
+ * @param {Object} [_options] - Reserved for backward compatibility; ignored
322
+ */
323
+ function logOfflinePathWhenType(appPath, options) {
324
+ if (!appPath || !options || (options.type !== 'app' && options.type !== 'external')) return;
325
+ const displayPath = path.relative(process.cwd(), appPath) || appPath;
326
+ logger.log(chalk.gray(`Using: ${displayPath}`));
327
+ }
328
+
286
329
  /**
287
330
  * Handles command errors with user-friendly messages
288
331
  * @param {Error} error - The error that occurred
@@ -332,6 +375,7 @@ async function appendWizardError(appKey, error) {
332
375
  module.exports = {
333
376
  validateCommand,
334
377
  handleCommandError,
335
- appendWizardError
378
+ appendWizardError,
379
+ logOfflinePathWhenType
336
380
  };
337
381