@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
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Convert command: convert integration/external system and datasource config files between JSON and YAML.
3
+ *
4
+ * Process: validate first, then (unless --force) prompt for confirmation, then convert (write new files),
5
+ * update application config links, then delete old files.
6
+ *
7
+ * @fileoverview Convert config format (JSON/YAML) for external integration files
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
+ const readline = require('readline');
17
+ const { detectAppType } = require('../utils/paths');
18
+ const { logOfflinePathWhenType } = require('../utils/cli-utils');
19
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
20
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
21
+
22
+ const TARGET_EXT = { yaml: '.yaml', json: '.json' };
23
+ const APP_CONFIG_NAMES = { yaml: 'application.yaml', json: 'application.json' };
24
+
25
+ /**
26
+ * Prompts the user for confirmation (y/N).
27
+ *
28
+ * @param {string} message - Prompt message
29
+ * @returns {Promise<boolean>} True if user confirms (y/yes), false otherwise
30
+ */
31
+ function promptConfirm(message) {
32
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
33
+ return new Promise(resolve => {
34
+ rl.question(message, answer => {
35
+ rl.close();
36
+ const normalized = (answer || '').trim().toLowerCase();
37
+ resolve(normalized === 'y' || normalized === 'yes');
38
+ });
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Returns the target filename for a given file path and format (same base name, new extension).
44
+ *
45
+ * @param {string} filePath - Current file path
46
+ * @param {string} format - Target format: 'json' or 'yaml'
47
+ * @returns {string} New filename (basename only)
48
+ */
49
+ function targetFileName(filePath, format) {
50
+ const base = path.basename(filePath, path.extname(filePath));
51
+ const ext = TARGET_EXT[format] || (format === 'json' ? '.json' : '.yaml');
52
+ return base + ext;
53
+ }
54
+
55
+ /**
56
+ * Converts a single config file to the target format: writes to new path, returns old and new paths.
57
+ *
58
+ * @param {string} sourcePath - Absolute path to existing file
59
+ * @param {string} targetPath - Absolute path for new file
60
+ * @param {string} format - 'json' or 'yaml'
61
+ * @returns {{ oldPath: string, newPath: string }}
62
+ */
63
+ function convertOneFile(sourcePath, targetPath, format) {
64
+ const obj = loadConfigFile(sourcePath);
65
+ writeConfigFile(targetPath, obj, format);
66
+ return { oldPath: sourcePath, newPath: targetPath };
67
+ }
68
+
69
+ /**
70
+ * Converts a list of config files (system or datasource) in a directory; skips missing files.
71
+ *
72
+ * @param {string} schemaBasePath - Base directory for files
73
+ * @param {string[]} fileNames - List of filenames
74
+ * @param {string} format - 'json' or 'yaml'
75
+ * @returns {{ converted: string[], toDelete: string[], newNames: string[] }}
76
+ */
77
+ function convertFileList(schemaBasePath, fileNames, format) {
78
+ const converted = [];
79
+ const toDelete = [];
80
+ const newNames = [];
81
+ for (const fileName of fileNames) {
82
+ const sourcePath = path.join(schemaBasePath, fileName);
83
+ if (!fs.existsSync(sourcePath)) {
84
+ newNames.push(fileName);
85
+ continue;
86
+ }
87
+ const newFileName = targetFileName(sourcePath, format);
88
+ const targetPath = path.join(schemaBasePath, newFileName);
89
+ convertOneFile(sourcePath, targetPath, format);
90
+ converted.push(targetPath);
91
+ newNames.push(newFileName);
92
+ if (path.normalize(sourcePath) !== path.normalize(targetPath)) {
93
+ toDelete.push(sourcePath);
94
+ }
95
+ }
96
+ return { converted, toDelete, newNames };
97
+ }
98
+
99
+ /**
100
+ * Validates app and prompts for confirmation unless opts.force.
101
+ *
102
+ * @param {Object} opts - Options
103
+ * @param {string} opts.appName - Application name
104
+ * @param {Object} opts.cmdOptions - Command options (force, type)
105
+ * @param {string} opts.schemaBasePath - Base path for external files
106
+ * @param {string[]} opts.systemFiles - System file names
107
+ * @param {string[]} opts.datasourceFiles - Datasource file names
108
+ * @param {string} opts.configPath - Current application config path
109
+ * @param {string} opts.format - Target format
110
+ * @throws {Error} If validation fails or user cancels
111
+ */
112
+ async function validateAndPrompt(opts) {
113
+ const validate = require('../validation/validate');
114
+ const result = await validate.validateAppOrFile(opts.appName, opts.cmdOptions);
115
+ if (!result.valid) {
116
+ validate.displayValidationResults(result);
117
+ throw new Error('Validation failed. Fix errors before converting.');
118
+ }
119
+ const { configPath, format, schemaBasePath, systemFiles, datasourceFiles } = opts;
120
+ const appConfigName = APP_CONFIG_NAMES[format];
121
+ const targetConfigPath = path.join(path.dirname(configPath), appConfigName);
122
+ const willConvertAppConfig = path.normalize(configPath) !== path.normalize(targetConfigPath) ||
123
+ path.extname(configPath) !== (format === 'json' ? '.json' : '.yaml');
124
+ const summaryLines = [...systemFiles, ...datasourceFiles]
125
+ .filter(Boolean)
126
+ .map(f => ` • ${f} → ${targetFileName(path.join(schemaBasePath, f), format)}`);
127
+ if (willConvertAppConfig) summaryLines.push(` • application config → ${appConfigName}`);
128
+ summaryLines.push(' Old files will be removed after writing new ones.');
129
+ if (!opts.cmdOptions.force) {
130
+ const confirmed = await promptConfirm(`Convert the following to ${format}?\n${summaryLines.join('\n')}\nAre you sure? (y/N) `);
131
+ if (!confirmed) throw new Error('Convert cancelled.');
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Converts files, updates application config, and deletes old files.
137
+ *
138
+ * @param {Object} variables - Current application config
139
+ * @param {string} configPath - Current application config path
140
+ * @param {string} schemaBasePath - Base path for external files
141
+ * @param {string[]} systemFiles - System file names
142
+ * @param {string[]} datasourceFiles - Datasource file names
143
+ * @param {string} format - Target format
144
+ * @returns {{ converted: string[], deleted: string[] }}
145
+ */
146
+ function executeConversion(variables, configPath, schemaBasePath, systemFiles, datasourceFiles, format) {
147
+ const sys = convertFileList(schemaBasePath, systemFiles, format);
148
+ const ds = convertFileList(schemaBasePath, datasourceFiles, format);
149
+ const converted = [...sys.converted, ...ds.converted];
150
+ const toDelete = [...sys.toDelete, ...ds.toDelete];
151
+ const updatedVariables = { ...variables };
152
+ if (updatedVariables.externalIntegration) {
153
+ updatedVariables.externalIntegration = { ...updatedVariables.externalIntegration };
154
+ updatedVariables.externalIntegration.systems = sys.newNames;
155
+ updatedVariables.externalIntegration.dataSources = ds.newNames;
156
+ }
157
+ const appConfigName = APP_CONFIG_NAMES[format];
158
+ const targetConfigPath = path.join(path.dirname(configPath), appConfigName);
159
+ writeConfigFile(targetConfigPath, updatedVariables, format);
160
+ converted.push(targetConfigPath);
161
+ if (path.normalize(configPath) !== path.normalize(targetConfigPath)) toDelete.push(configPath);
162
+ toDelete.forEach(oldPath => {
163
+ if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
164
+ });
165
+ return { converted, deleted: toDelete };
166
+ }
167
+
168
+ /**
169
+ * Runs conversion: validate first, optional prompt, convert files, update app config links, delete old files.
170
+ *
171
+ * @param {string} appName - Application name
172
+ * @param {Object} options - Command options
173
+ * @param {string} options.format - Target format: 'json' or 'yaml'
174
+ * @param {boolean} [options.force] - Skip confirmation prompt
175
+ *
176
+ * @returns {Promise<{ converted: string[], deleted: string[] }>} Lists of converted and deleted file paths
177
+ * @throws {Error} If validation fails, user aborts, or conversion fails
178
+ */
179
+ async function runConvert(appName, options) {
180
+ const format = (options.format || '').toLowerCase();
181
+ if (format !== 'json' && format !== 'yaml') {
182
+ throw new Error('Option --format is required and must be \'json\' or \'yaml\'');
183
+ }
184
+ const { appPath } = await detectAppType(appName);
185
+ logOfflinePathWhenType(appPath);
186
+ const configPath = resolveApplicationConfigPath(appPath);
187
+ const variables = loadConfigFile(configPath);
188
+ const schemaBasePath = path.resolve(path.dirname(configPath), variables.externalIntegration?.schemaBasePath || './');
189
+ const systemFiles = variables.externalIntegration?.systems || [];
190
+ const datasourceFiles = variables.externalIntegration?.dataSources || [];
191
+ await validateAndPrompt({
192
+ appName, cmdOptions: options, schemaBasePath, systemFiles, datasourceFiles, configPath, format
193
+ });
194
+ return executeConversion(variables, configPath, schemaBasePath, systemFiles, datasourceFiles, format);
195
+ }
196
+
197
+ module.exports = {
198
+ runConvert,
199
+ promptConfirm,
200
+ targetFileName,
201
+ convertOneFile
202
+ };
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Credential list command – list credentials from controller/dataplane
2
+ * Credential list command – list credentials from Dataplane
3
3
  * GET /api/v1/credential. Used by `aifabrix credential list`.
4
+ * The Controller does not expose this endpoint; credentials are listed from the Dataplane.
4
5
  *
5
6
  * @fileoverview Credential list command implementation
6
7
  * @author AI Fabrix Team
@@ -11,7 +12,8 @@ const chalk = require('chalk');
11
12
  const logger = require('../utils/logger');
12
13
  const { resolveControllerUrl } = require('../utils/controller-url');
13
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
- const { normalizeControllerUrl } = require('../core/config');
15
+ const { normalizeControllerUrl, resolveEnvironment } = require('../core/config');
16
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
17
  const { listCredentials } = require('../api/credentials.api');
16
18
 
17
19
  const DEFAULT_PAGE_SIZE = 50;
@@ -48,10 +50,10 @@ function extractCredentials(response) {
48
50
  /**
49
51
  * Display credential list to user
50
52
  * @param {Array} list - Credentials array
51
- * @param {string} controllerUrl - Controller URL for header
53
+ * @param {string} baseUrl - Dataplane (or base) URL for header
52
54
  */
53
- function displayCredentialList(list, controllerUrl) {
54
- logger.log(chalk.bold(`\n🔐 Credentials (${controllerUrl}):\n`));
55
+ function displayCredentialList(list, baseUrl) {
56
+ logger.log(chalk.bold(`\n🔐 Credentials (${baseUrl}):\n`));
55
57
  if (list.length === 0) {
56
58
  logger.log(chalk.gray(' No credentials found.\n'));
57
59
  return;
@@ -65,36 +67,95 @@ function displayCredentialList(list, controllerUrl) {
65
67
  }
66
68
 
67
69
  /**
68
- * Run credential list command: call GET /api/v1/credential and display results
70
+ * Ensure controller URL and auth; exit on failure. Returns { controllerUrl, authConfig } when valid.
69
71
  * @async
70
- * @param {Object} options - CLI options
71
- * @param {string} [options.controller] - Controller URL override
72
- * @param {boolean} [options.activeOnly] - List only active credentials
73
- * @param {number} [options.pageSize] - Items per page
74
- * @returns {Promise<void>}
72
+ * @param {Object} options - CLI options with optional controller
73
+ * @returns {Promise<{controllerUrl: string, authConfig: Object}>}
75
74
  */
76
- async function runCredentialList(options = {}) {
75
+ async function ensureControllerAndAuth(options) {
77
76
  const controllerUrl = options.controller || (await resolveControllerUrl());
78
77
  if (!controllerUrl) {
79
78
  logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
80
79
  process.exit(1);
81
- return;
82
80
  }
83
81
  const authResult = await getCredentialListAuth(controllerUrl);
84
82
  if (!authResult || !authResult.token) {
85
83
  logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
86
84
  logger.error(chalk.gray('Run: aifabrix login'));
87
85
  process.exit(1);
88
- return;
89
86
  }
90
- const authConfig = { type: 'bearer', token: authResult.token };
87
+ return {
88
+ controllerUrl,
89
+ authConfig: { type: 'bearer', token: authResult.token }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Resolve Dataplane URL for credential list (override or discover from controller + environment)
95
+ * @async
96
+ * @param {string} controllerUrl - Controller base URL
97
+ * @param {Object} authConfig - Auth config with token
98
+ * @param {Object} options - CLI options
99
+ * @param {string} [options.dataplane] - Optional Dataplane URL override
100
+ * @returns {Promise<string>} Dataplane base URL
101
+ * @throws {Error} When resolution fails (caller should exit)
102
+ */
103
+ async function resolveCredentialListDataplaneUrl(controllerUrl, authConfig, options) {
104
+ if (options.dataplane) {
105
+ return options.dataplane.replace(/\/$/, '');
106
+ }
107
+ const environment = await resolveEnvironment();
108
+ return await resolveDataplaneUrl(controllerUrl, environment, authConfig);
109
+ }
110
+
111
+ /**
112
+ * Call Dataplane credential API and display results; exits on failure
113
+ * @param {string} dataplaneUrl - Dataplane base URL
114
+ * @param {Object} authConfig - Auth config
115
+ * @param {Object} listOptions - pageSize, activeOnly
116
+ */
117
+ async function fetchAndDisplayCredentials(dataplaneUrl, authConfig, listOptions) {
118
+ const response = await listCredentials(dataplaneUrl, authConfig, listOptions);
119
+ displayCredentialList(extractCredentials(response), dataplaneUrl);
120
+ }
121
+
122
+ /**
123
+ * Resolve Dataplane URL or log error and exit
124
+ * @async
125
+ * @param {string} controllerUrl - Controller URL
126
+ * @param {Object} authConfig - Auth config
127
+ * @param {Object} options - CLI options
128
+ * @returns {Promise<string>} Dataplane URL (never returns on failure; process.exit(1))
129
+ */
130
+ async function resolveDataplaneUrlOrExit(controllerUrl, authConfig, options) {
131
+ try {
132
+ return await resolveCredentialListDataplaneUrl(controllerUrl, authConfig, options);
133
+ } catch (err) {
134
+ logger.error(chalk.red(`❌ Could not resolve Dataplane URL: ${err.message}`));
135
+ logger.error(chalk.gray('Use --dataplane <url> to specify the Dataplane URL directly.'));
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Run credential list command: call GET /api/v1/credential on Dataplane and display results
142
+ * @async
143
+ * @param {Object} options - CLI options
144
+ * @param {string} [options.controller] - Controller URL override
145
+ * @param {string} [options.dataplane] - Dataplane URL override (default: resolved from controller + environment)
146
+ * @param {boolean} [options.activeOnly] - List only active credentials
147
+ * @param {number} [options.pageSize] - Items per page
148
+ * @returns {Promise<void>}
149
+ */
150
+ async function runCredentialList(options = {}) {
151
+ const { controllerUrl, authConfig } = await ensureControllerAndAuth(options);
152
+ const dataplaneUrl = await resolveDataplaneUrlOrExit(controllerUrl, authConfig, options);
91
153
  const listOptions = {
92
154
  pageSize: options.pageSize || DEFAULT_PAGE_SIZE,
93
155
  activeOnly: options.activeOnly
94
156
  };
95
157
  try {
96
- const response = await listCredentials(authResult.controllerUrl, authConfig, listOptions);
97
- displayCredentialList(extractCredentials(response), authResult.controllerUrl);
158
+ await fetchAndDisplayCredentials(dataplaneUrl, authConfig, listOptions);
98
159
  } catch (error) {
99
160
  logger.error(chalk.red(`❌ Failed to list credentials: ${error.message}`));
100
161
  process.exit(1);
@@ -16,18 +16,8 @@ const { listDatasources } = require('../datasource/list');
16
16
  const { compareDatasources } = require('../datasource/diff');
17
17
  const { deployDatasource } = require('../datasource/deploy');
18
18
 
19
- /**
20
- * Setup datasource management commands
21
- * @param {Command} program - Commander program instance
22
- */
23
- function setupDatasourceCommands(program) {
24
- const datasource = program
25
- .command('datasource')
26
- .description('Manage external data sources');
27
-
28
- // Validate command
29
- datasource
30
- .command('validate <file>')
19
+ function setupDatasourceValidateCommand(datasource) {
20
+ datasource.command('validate <file>')
31
21
  .description('Validate external datasource JSON file')
32
22
  .action(async(file) => {
33
23
  try {
@@ -36,9 +26,7 @@ function setupDatasourceCommands(program) {
36
26
  logger.log(chalk.green(`\n✓ Datasource file is valid: ${file}`));
37
27
  } else {
38
28
  logger.log(chalk.red(`\n✗ Datasource file has errors: ${file}`));
39
- result.errors.forEach(error => {
40
- logger.log(chalk.red(` • ${error}`));
41
- });
29
+ result.errors.forEach(error => logger.log(chalk.red(` • ${error}`)));
42
30
  process.exit(1);
43
31
  }
44
32
  } catch (error) {
@@ -46,10 +34,10 @@ function setupDatasourceCommands(program) {
46
34
  process.exit(1);
47
35
  }
48
36
  });
37
+ }
49
38
 
50
- // List command
51
- datasource
52
- .command('list')
39
+ function setupDatasourceListCommand(datasource) {
40
+ datasource.command('list')
53
41
  .description('List datasources from environment (uses environment from config.yaml)')
54
42
  .action(async() => {
55
43
  try {
@@ -59,10 +47,10 @@ function setupDatasourceCommands(program) {
59
47
  process.exit(1);
60
48
  }
61
49
  });
50
+ }
62
51
 
63
- // Diff command
64
- datasource
65
- .command('diff <file1> <file2>')
52
+ function setupDatasourceDiffCommand(datasource) {
53
+ datasource.command('diff <file1> <file2>')
66
54
  .description('Compare two datasource configuration files (for dataplane)')
67
55
  .action(async(file1, file2) => {
68
56
  try {
@@ -72,10 +60,10 @@ function setupDatasourceCommands(program) {
72
60
  process.exit(1);
73
61
  }
74
62
  });
63
+ }
75
64
 
76
- // Deploy command
77
- datasource
78
- .command('deploy <myapp> <file>')
65
+ function setupDatasourceDeployCommand(datasource) {
66
+ datasource.command('deploy <myapp> <file>')
79
67
  .description('Deploy datasource to dataplane')
80
68
  .action(async(myapp, file, options) => {
81
69
  try {
@@ -87,5 +75,17 @@ function setupDatasourceCommands(program) {
87
75
  });
88
76
  }
89
77
 
78
+ /**
79
+ * Setup datasource management commands
80
+ * @param {Command} program - Commander program instance
81
+ */
82
+ function setupDatasourceCommands(program) {
83
+ const datasource = program.command('datasource').description('Manage external data sources');
84
+ setupDatasourceValidateCommand(datasource);
85
+ setupDatasourceListCommand(datasource);
86
+ setupDatasourceDiffCommand(datasource);
87
+ setupDatasourceDeployCommand(datasource);
88
+ }
89
+
90
90
  module.exports = { setupDatasourceCommands };
91
91
 
@@ -36,13 +36,19 @@ async function getDeploymentListAuth(controllerUrl) {
36
36
  }
37
37
 
38
38
  /**
39
- * Extract deployments array from API response
40
- * @param {Object} response - API response
39
+ * Extract deployments array from API response.
40
+ * Supports OpenAPI/SDK paginated format: { meta, data: Deployment[], links }
41
+ * and legacy shapes: { data: { items } }, { data: { deployments } }, or { data: [] }.
42
+ * @param {Object} response - API response (from ApiClient: { success, data: body, status })
41
43
  * @returns {Array}
42
44
  */
43
45
  function extractDeployments(response) {
44
- const data = response?.data ?? response;
45
- const items = data?.items ?? data?.deployments ?? (Array.isArray(data) ? data : []);
46
+ const body = response?.data ?? response;
47
+ const items =
48
+ (Array.isArray(body?.data) ? body.data : undefined) ??
49
+ body?.items ??
50
+ body?.deployments ??
51
+ (Array.isArray(body) ? body : []);
46
52
  return Array.isArray(items) ? items : [];
47
53
  }
48
54
 
@@ -60,10 +66,11 @@ function displayDeploymentList(deployments, environment, controllerUrl) {
60
66
  }
61
67
  deployments.forEach((d) => {
62
68
  const id = d.id ?? d.deploymentId ?? '-';
63
- const appKey = d.applicationKey ?? d.appKey ?? d.application?.key ?? '-';
69
+ const target =
70
+ d.applicationKey ?? d.appKey ?? d.targetId ?? d.application?.key ?? '-';
64
71
  const status = d.status ?? '-';
65
72
  const createdAt = d.createdAt ?? d.created ?? '';
66
- logger.log(` ${chalk.cyan(id)} ${appKey} ${status} ${chalk.gray(createdAt)}`);
73
+ logger.log(` ${chalk.cyan(id)} ${target} ${status} ${chalk.gray(createdAt)}`);
67
74
  });
68
75
  logger.log('');
69
76
  }
@@ -10,24 +10,27 @@
10
10
 
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
- const yaml = require('js-yaml');
14
13
  const chalk = require('chalk');
15
14
  const logger = require('../utils/logger');
16
15
  const pathsUtil = require('../utils/paths');
16
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
17
+ const { isYamlPath } = require('../utils/config-format');
17
18
  const { copyTemplateFiles } = require('../validation/template');
18
19
  const { ensureReadmeForAppPath, ensureReadmeForApp } = require('../app/readme');
19
20
 
20
21
  /**
21
- * Copy template to a target path if variables.yaml is missing there.
22
+ * Copy template to a target path if application config is missing there.
22
23
  * After copy, generates README.md from templates/applications/README.md.hbs.
23
24
  * @param {string} appName - Application name
24
25
  * @param {string} targetAppPath - Target directory (e.g. builder/keycloak)
25
26
  * @returns {Promise<boolean>} True if template was copied, false if already present
26
27
  */
27
28
  async function ensureTemplateAtPath(appName, targetAppPath) {
28
- const variablesPath = path.join(targetAppPath, 'variables.yaml');
29
- if (fs.existsSync(variablesPath)) {
29
+ try {
30
+ pathsUtil.resolveApplicationConfigPath(targetAppPath);
30
31
  return false;
32
+ } catch {
33
+ // No application config; copy template
31
34
  }
32
35
  await copyTemplateFiles(appName, targetAppPath);
33
36
  await ensureReadmeForAppPath(targetAppPath, appName);
@@ -37,18 +40,55 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
37
40
  /**
38
41
  * Resolve the directory (folder) that would contain the .env file for envOutputPath.
39
42
  * @param {string} envOutputPath - Value from build.envOutputPath (e.g. ../../.env)
40
- * @param {string} variablesPath - Path to variables.yaml
43
+ * @param {string} configPath - Path to application config file
41
44
  * @returns {string} Absolute path to the folder that would contain the output .env file
42
45
  */
43
- function getEnvOutputPathFolder(envOutputPath, variablesPath) {
44
- const variablesDir = path.dirname(variablesPath);
45
- const resolvedFile = path.resolve(variablesDir, envOutputPath);
46
+ function getEnvOutputPathFolder(envOutputPath, configPath) {
47
+ const configDir = path.dirname(configPath);
48
+ const resolvedFile = path.resolve(configDir, envOutputPath);
46
49
  return path.dirname(resolvedFile);
47
50
  }
48
51
 
49
52
  /**
50
- * Validates envOutputPath: if the target folder does not exist, patches variables.yaml to set envOutputPath to null.
53
+ * Patches envOutputPath to null in raw YAML content so comments and formatting are preserved.
54
+ * Only touches the line that sets envOutputPath under build.
55
+ *
56
+ * @param {string} content - Raw file content (YAML)
57
+ * @returns {string|null} Patched content, or null if no change
58
+ */
59
+ function patchEnvOutputPathInYamlContent(content) {
60
+ const re = /^(\s*)envOutputPath\s*:\s*.+$/m;
61
+ const match = content.match(re);
62
+ if (!match) return null;
63
+ const indent = match[1];
64
+ return content.replace(re, `${indent}envOutputPath: null`);
65
+ }
66
+
67
+ /**
68
+ * Patches config file to set build.envOutputPath to null, preserving comments (YAML only).
69
+ * For JSON or when in-place patch fails, falls back to full write.
70
+ *
71
+ * @param {string} configPath - Path to application config file
72
+ * @returns {boolean} True if file was modified
73
+ */
74
+ function patchEnvOutputPathInFile(configPath) {
75
+ if (!isYamlPath(configPath)) {
76
+ const variables = loadConfigFile(configPath);
77
+ const updated = { ...variables, build: { ...(variables.build || {}), envOutputPath: null } };
78
+ writeConfigFile(configPath, updated);
79
+ return true;
80
+ }
81
+ const content = fs.readFileSync(configPath, 'utf8');
82
+ const patched = patchEnvOutputPathInYamlContent(content);
83
+ if (patched === null) return false;
84
+ fs.writeFileSync(configPath, patched, 'utf8');
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Validates envOutputPath: if the target folder does not exist, patches application config to set envOutputPath to null.
51
90
  * Used by up-platform, up-miso, up-dataplane so we do not keep a path that points outside an existing tree.
91
+ * Patches in place for YAML to preserve comments.
52
92
  *
53
93
  * @param {string} appName - Application name (e.g. keycloak, miso-controller, dataplane)
54
94
  */
@@ -59,48 +99,44 @@ function validateEnvOutputPathFolderOrNull(appName) {
59
99
  if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
60
100
  pathsToPatch.push(cwdBuilderPath);
61
101
  }
62
- const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
63
- const replacement = '$1 null # deploy only, no copy';
64
102
  for (const appPath of pathsToPatch) {
65
- const variablesPath = path.join(appPath, 'variables.yaml');
66
- if (!fs.existsSync(variablesPath)) continue;
103
+ let configPath;
104
+ try {
105
+ configPath = pathsUtil.resolveApplicationConfigPath(appPath);
106
+ } catch {
107
+ continue;
108
+ }
67
109
  try {
68
- const content = fs.readFileSync(variablesPath, 'utf8');
69
- const variables = yaml.load(content);
110
+ const variables = loadConfigFile(configPath);
70
111
  const value = variables?.build?.envOutputPath;
71
112
  if (value === null || value === undefined || value === '') continue;
72
- const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
113
+ const folder = getEnvOutputPathFolder(String(value).trim(), configPath);
73
114
  if (fs.existsSync(folder)) continue;
74
- const newContent = content.replace(envOutputPathLine, replacement);
75
- fs.writeFileSync(variablesPath, newContent, 'utf8');
115
+ patchEnvOutputPathInFile(configPath);
76
116
  } catch (err) {
77
- logger.warn(chalk.yellow(`Could not validate envOutputPath in ${variablesPath}: ${err.message}`));
117
+ logger.warn(chalk.yellow(`Could not validate envOutputPath in ${configPath}: ${err.message}`));
78
118
  }
79
119
  }
80
120
  }
81
121
 
82
122
  /**
83
- * Patches a single variables.yaml to set build.envOutputPath to null for deploy-only.
123
+ * Patches a single application config file to set build.envOutputPath to null for deploy-only.
124
+ * Only writes when a change is needed (value is set and target folder does not exist).
125
+ * Uses in-place YAML patch when possible to preserve comments.
84
126
  *
85
- * @param {string} variablesPath - Path to variables.yaml
86
- * @param {RegExp} envOutputPathLine - Regex for envOutputPath line
87
- * @param {string} replacement - Replacement string
127
+ * @param {string} configPath - Path to application config file
88
128
  */
89
- function patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement) {
90
- const content = fs.readFileSync(variablesPath, 'utf8');
91
- if (!envOutputPathLine.test(content)) return;
92
- const variables = yaml.load(content);
129
+ function patchOneVariablesFileForDeployOnly(configPath) {
130
+ const variables = loadConfigFile(configPath);
93
131
  const value = variables?.build?.envOutputPath;
94
- if (value !== null && value !== undefined && value !== '') {
95
- const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
96
- if (fs.existsSync(folder)) return;
97
- }
98
- const newContent = content.replace(envOutputPathLine, replacement);
99
- fs.writeFileSync(variablesPath, newContent, 'utf8');
132
+ if (value === null || value === undefined || value === '') return;
133
+ const folder = getEnvOutputPathFolder(String(value).trim(), configPath);
134
+ if (fs.existsSync(folder)) return;
135
+ patchEnvOutputPathInFile(configPath);
100
136
  }
101
137
 
102
138
  /**
103
- * Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
139
+ * Patches application config to set build.envOutputPath to null for deploy-only (no local code).
104
140
  * Only patches when the target folder does NOT exist; when folder exists, keeps the value.
105
141
  * Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
106
142
  * Patches both primary builder path and cwd/builder if different.
@@ -114,22 +150,24 @@ function patchEnvOutputPathForDeployOnly(appName) {
114
150
  if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
115
151
  pathsToPatch.push(cwdBuilderPath);
116
152
  }
117
- const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
118
- const replacement = '$1 null # deploy only, no copy';
119
153
  for (const appPath of pathsToPatch) {
120
- const variablesPath = path.join(appPath, 'variables.yaml');
121
- if (!fs.existsSync(variablesPath)) continue;
154
+ let configPath;
155
+ try {
156
+ configPath = pathsUtil.resolveApplicationConfigPath(appPath);
157
+ } catch {
158
+ continue;
159
+ }
122
160
  try {
123
- patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement);
161
+ patchOneVariablesFileForDeployOnly(configPath);
124
162
  } catch (err) {
125
- logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
163
+ logger.warn(chalk.yellow(`Could not patch envOutputPath in ${configPath}: ${err.message}`));
126
164
  }
127
165
  }
128
166
  }
129
167
 
130
168
  /**
131
- * Ensures builder app directory exists from template if variables.yaml is missing.
132
- * If builder/<appName>/variables.yaml does not exist, copies from templates/applications/<appName>.
169
+ * Ensures builder app directory exists from template if application config is missing.
170
+ * If builder/<appName>/application config does not exist, copies from templates/applications/<appName>.
133
171
  * Uses AIFABRIX_BUILDER_DIR when set (e.g. by up-miso/up-dataplane from config aifabrix-env-config).
134
172
  * When using a custom builder dir, also populates cwd/builder/<appName> so the repo's builder/ is not empty.
135
173
  *