@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
package/lib/app/readme.js CHANGED
@@ -12,7 +12,8 @@ const fs = require('fs').promises;
12
12
  const fsSync = require('fs');
13
13
  const path = require('path');
14
14
  const handlebars = require('handlebars');
15
- const yaml = require('js-yaml');
15
+ const { resolveApplicationConfigPath } = require('../utils/paths');
16
+ const { loadConfigFile } = require('../utils/config-format');
16
17
  const { generateExternalReadmeContent } = require('../utils/external-readme');
17
18
 
18
19
  /**
@@ -104,7 +105,7 @@ function buildExternalDatasourcePlaceholders(systemKey, datasourceCount) {
104
105
  return {
105
106
  entityType,
106
107
  displayName: `Datasource ${index + 1}`,
107
- fileName: `${systemKey}-datasource-${entityType}.json`
108
+ fileName: `${systemKey}-datasource-${entityType}.yaml`
108
109
  };
109
110
  });
110
111
  }
@@ -117,7 +118,7 @@ function buildExternalDatasourcePlaceholders(systemKey, datasourceCount) {
117
118
  * @returns {Object} Template context
118
119
  */
119
120
  function buildReadmeContext(appName, config) {
120
- const displayName = formatAppDisplayName(appName);
121
+ const displayName = config.displayName || formatAppDisplayName(appName);
121
122
  const port = config.port ?? 3000;
122
123
  const localPort = (typeof config.build?.localPort === 'number' && config.build.localPort > 0)
123
124
  ? config.build.localPort
@@ -144,7 +145,9 @@ function buildReadmeContext(appName, config) {
144
145
  function generateReadmeMd(appName, config) {
145
146
  if (config.type === 'external') {
146
147
  const systemKey = config.systemKey || appName;
147
- const datasources = buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount);
148
+ const datasources = Array.isArray(config.datasources) && config.datasources.length > 0
149
+ ? config.datasources
150
+ : buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount);
148
151
  return generateExternalReadmeContent({
149
152
  appName,
150
153
  systemKey,
@@ -164,7 +167,7 @@ function generateReadmeMd(appName, config) {
164
167
  * @function generateReadmeMdFile
165
168
  * @param {string} appPath - Path to application directory
166
169
  * @param {string} appName - Application name
167
- * @param {Object} config - Application configuration (e.g. from variables.yaml)
170
+ * @param {Object} config - Application configuration (e.g. from application.yaml/application.json)
168
171
  * @param {Object} [options] - Options
169
172
  * @param {boolean} [options.force] - If true, overwrite existing README.md (dynamic generation)
170
173
  * @returns {Promise<void>} Resolves when README.md is generated or skipped
@@ -181,21 +184,22 @@ async function generateReadmeMdFile(appPath, appName, config, options = {}) {
181
184
  }
182
185
 
183
186
  /**
184
- * Loads variables.yaml from app path and generates README.md (overwrites if present).
187
+ * Loads application config from app path and generates README.md (overwrites if present).
185
188
  * Used when copying template apps or running up-miso / up-platform / up-dataplane.
186
189
  * @async
187
190
  * @function ensureReadmeForAppPath
188
- * @param {string} appPath - Path to application directory (must contain variables.yaml)
191
+ * @param {string} appPath - Path to application directory (must contain application config)
189
192
  * @param {string} appName - Application name
190
- * @returns {Promise<void>} Resolves when README.md is written or skipped (no variables.yaml)
193
+ * @returns {Promise<void>} Resolves when README.md is written or skipped (no application config)
191
194
  */
192
195
  async function ensureReadmeForAppPath(appPath, appName) {
193
- const variablesPath = path.join(appPath, 'variables.yaml');
194
- if (!(await fileExists(variablesPath))) {
196
+ let configPath;
197
+ try {
198
+ configPath = resolveApplicationConfigPath(appPath);
199
+ } catch {
195
200
  return;
196
201
  }
197
- const content = await fs.readFile(variablesPath, 'utf8');
198
- const config = yaml.load(content) || {};
202
+ const config = loadConfigFile(configPath) || {};
199
203
  await generateReadmeMdFile(appPath, appName, config, { force: true });
200
204
  }
201
205
 
@@ -152,7 +152,7 @@ async function registerApplication(appKey, options = {}) {
152
152
  const { resolveControllerUrl } = require('../utils/controller-url');
153
153
  const { resolveEnvironment } = require('../core/config');
154
154
 
155
- // Load variables.yaml
155
+ // Load application config
156
156
  const { variables, created } = await loadVariablesYaml(appKey);
157
157
  const finalVariables = created
158
158
  ? await createMinimalAppIfNeeded(appKey, options)
@@ -13,8 +13,8 @@ const fs = require('fs').promises;
13
13
  const fsSync = require('fs');
14
14
  const path = require('path');
15
15
  const chalk = require('chalk');
16
- const yaml = require('js-yaml');
17
16
  const { exec } = require('child_process');
17
+ const { loadConfigFile } = require('../utils/config-format');
18
18
  const { promisify } = require('util');
19
19
  const validator = require('../validation/validator');
20
20
  const infra = require('../infrastructure');
@@ -31,7 +31,7 @@ const { resolveVersionForApp } = require('../utils/image-version');
31
31
 
32
32
  const execAsync = promisify(exec);
33
33
 
34
- /** Template apps (keycloak, miso-controller, dataplane) - never update variables.yaml when running */
34
+ /** Template apps (keycloak, miso-controller, dataplane) - never update application config when running */
35
35
  const TEMPLATE_APP_KEYS = ['keycloak', 'miso-controller', 'dataplane'];
36
36
 
37
37
  // Re-export container helper functions
@@ -72,19 +72,18 @@ function checkBuilderDirectory(appName) {
72
72
  function loadAppConfig(appName) {
73
73
  const currentDir = process.cwd();
74
74
  const builderPath = pathsUtil.getBuilderPath(appName);
75
- const configPath = path.join(builderPath, 'variables.yaml');
76
- if (!fsSync.existsSync(configPath)) {
75
+ let configPath;
76
+ try {
77
+ configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
78
+ } catch {
77
79
  throw new Error(
78
- `Application configuration not found: ${configPath}\n` +
80
+ `Application configuration not found in ${builderPath}\n` +
79
81
  `Current directory: ${currentDir}\n` +
80
- `Expected location: ${builderPath}\n` +
81
82
  'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
82
83
  `Run 'aifabrix create ${appName}' first if configuration doesn't exist`
83
84
  );
84
85
  }
85
-
86
- const configContent = fsSync.readFileSync(configPath, 'utf8');
87
- return yaml.load(configContent);
86
+ return loadConfigFile(configPath);
88
87
  }
89
88
 
90
89
  /**
@@ -96,7 +95,7 @@ function formatValidationErrors(validation) {
96
95
  const allErrors = [];
97
96
 
98
97
  if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
99
- allErrors.push('variables.yaml:');
98
+ allErrors.push('application config:');
100
99
  allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
101
100
  }
102
101
  if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
@@ -129,8 +128,8 @@ async function validateAppConfiguration(appName) {
129
128
  // Load config
130
129
  const appConfig = loadAppConfig(appName);
131
130
 
132
- // Validate configuration
133
- const validation = await validator.validateApplication(appName);
131
+ // Validate configuration (run only uses builder/ apps)
132
+ const validation = await validator.validateApplication(appName, { type: 'app' });
134
133
  if (!validation.valid) {
135
134
  const allErrors = formatValidationErrors(validation);
136
135
  if (allErrors.length === 0) {
@@ -143,8 +142,8 @@ async function validateAppConfiguration(appName) {
143
142
  }
144
143
 
145
144
  /**
146
- * Resolves version from image and updates builder/variables.yaml when running.
147
- * Template apps (keycloak, miso-controller, dataplane) are never updated - variables.yaml stays pristine.
145
+ * Resolves version from image and updates builder application config when running.
146
+ * Template apps (keycloak, miso-controller, dataplane) are never updated - config stays pristine.
148
147
  * @async
149
148
  * @param {string} appName - Application name
150
149
  * @param {Object} appConfig - Application configuration
@@ -263,18 +262,21 @@ async function copyEnvToDev(builderEnvPath, devEnvPath) {
263
262
  * Handle envOutputPath configuration
264
263
  * @async
265
264
  * @param {string} appName - Application name
266
- * @param {string} variablesPath - Path to variables.yaml
265
+ * @param {string} configPath - Path to application config file
267
266
  * @param {string} builderEnvPath - Path to builder .env file
268
267
  * @param {string} devEnvPath - Path to dev .env file
269
268
  * @param {boolean} [skipOutputPath=false] - When true, skip (e.g. up-miso/up-dataplane, no local code)
270
269
  */
271
- async function handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath, skipOutputPath = false) {
272
- if (skipOutputPath || !fsSync.existsSync(variablesPath)) {
270
+ async function handleEnvOutputPath(appName, configPath, builderEnvPath, devEnvPath, skipOutputPath = false) {
271
+ if (skipOutputPath) {
272
+ return;
273
+ }
274
+ let variables;
275
+ try {
276
+ variables = loadConfigFile(configPath);
277
+ } catch {
273
278
  return;
274
279
  }
275
-
276
- const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
277
- const variables = yaml.load(variablesContent);
278
280
 
279
281
  if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
280
282
  logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
@@ -338,8 +340,15 @@ async function prepareEnvironment(appName, appConfig, options) {
338
340
  await copyEnvToDev(builderEnvPath, devEnvPath);
339
341
 
340
342
  // Handle envOutputPath if configured (skipped when skipEnvOutputPath e.g. up-miso/up-dataplane)
341
- const variablesPath = path.join(devDir, 'variables.yaml');
342
- await handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath, skipEnvOutputPath);
343
+ let configPath;
344
+ try {
345
+ configPath = pathsUtil.resolveApplicationConfigPath(devDir);
346
+ } catch {
347
+ configPath = null;
348
+ }
349
+ if (configPath) {
350
+ await handleEnvOutputPath(appName, configPath, builderEnvPath, devEnvPath, skipEnvOutputPath);
351
+ }
343
352
 
344
353
  // Generate Docker Compose
345
354
  const composeOptions = { ...options };
package/lib/app/run.js CHANGED
@@ -10,13 +10,18 @@
10
10
  */
11
11
 
12
12
  const chalk = require('chalk');
13
+ const { exec } = require('child_process');
14
+ const { promisify } = require('util');
13
15
  const config = require('../core/config');
14
16
  const logger = require('../utils/logger');
15
17
  const { checkPortAvailable, waitForHealthCheck } = require('../utils/health-check');
16
18
  const composeGenerator = require('../utils/compose-generator');
19
+ const containerHelpers = require('../utils/app-run-containers');
17
20
  // Helper functions extracted to reduce file size and complexity
18
21
  const helpers = require('./run-helpers');
19
22
 
23
+ const execAsync = promisify(exec);
24
+
20
25
  /**
21
26
  * Validate app for run and check if it's an external system
22
27
  * @async
@@ -30,18 +35,20 @@ async function validateAppForRun(appName, _debug) {
30
35
  throw new Error('Application name is required');
31
36
  }
32
37
 
33
- // Check if app type is external - skip Docker run
38
+ // Run only supports regular apps in builder/ (path resolution: integration first, then builder)
34
39
  const { detectAppType } = require('../utils/paths');
35
40
  try {
36
- const { isExternal } = await detectAppType(appName);
37
- if (isExternal) {
41
+ const { isExternal, baseDir } = await detectAppType(appName);
42
+ if (baseDir !== 'builder' || isExternal) {
38
43
  logger.log(chalk.yellow('⚠️ External systems don\'t run as Docker containers.'));
39
44
  logger.log(chalk.blue('Use "aifabrix build" to deploy to dataplane, then test via OpenAPI endpoints.'));
40
45
  return false;
41
46
  }
42
47
  } catch (error) {
43
- // If detection fails, continue with normal run
44
- // (detectAppType throws if app doesn't exist, which is fine for run command)
48
+ throw new Error(
49
+ `Application "${appName}" not found in builder/. Only applications in builder/ can be run.\n` +
50
+ (error.message || '')
51
+ );
45
52
  }
46
53
 
47
54
  return true;
@@ -202,8 +209,40 @@ async function runApp(appName, options = {}) {
202
209
  throw new Error(`Failed to run application: ${error.message}`);
203
210
  }
204
211
  }
212
+
213
+ /**
214
+ * Restart a running application container (Docker restart).
215
+ * Only applies to apps in builder/ run via aifabrix run.
216
+ *
217
+ * @async
218
+ * @function restartApp
219
+ * @param {string} appName - Application name (must be running)
220
+ * @returns {Promise<void>} Resolves when container is restarted
221
+ * @throws {Error} If app name is invalid, container not found, or restart fails
222
+ *
223
+ * @example
224
+ * await restartApp('myapp');
225
+ */
226
+ async function restartApp(appName) {
227
+ if (!appName || typeof appName !== 'string') {
228
+ throw new Error('Application name is required and must be a string');
229
+ }
230
+ const developerId = await config.getDeveloperId();
231
+ const containerName = containerHelpers.getContainerName(appName, developerId);
232
+ try {
233
+ await execAsync(`docker restart ${containerName}`);
234
+ } catch (error) {
235
+ const msg = (error.stderr || error.stdout || error.message || '').toLowerCase();
236
+ if (msg.includes('no such container') || msg.includes('is not running')) {
237
+ throw new Error(`Application '${appName}' is not running. Start it with: aifabrix run ${appName}`);
238
+ }
239
+ throw new Error(`Failed to restart application: ${error.message}`);
240
+ }
241
+ }
242
+
205
243
  module.exports = {
206
244
  runApp,
245
+ restartApp,
207
246
  checkImageExists: helpers.checkImageExists,
208
247
  checkContainerRunning: helpers.checkContainerRunning,
209
248
  stopAndRemoveContainer: helpers.stopAndRemoveContainer,
@@ -15,14 +15,15 @@ const logger = require('../utils/logger');
15
15
  function logSourceAndHeader(summary) {
16
16
  const isOffline = summary.source === 'offline';
17
17
  const sourceLabel = isOffline
18
- ? `Source: offline (${summary.path || '—'})`
19
- : `Source: online (${summary.controllerUrl || '—'})`;
18
+ ? `🔴 Source: offline (${summary.path || '—'})`
19
+ : `🟢 Source: online (${summary.controllerUrl || '—'})`;
20
20
  logger.log(sourceLabel);
21
21
  logger.log('');
22
22
  }
23
23
 
24
24
  function logApplicationRequired(a) {
25
- logger.log('📱 Application');
25
+ const typeLabel = (a.type && String(a.type).toLowerCase()) || 'webapp';
26
+ logger.log(`📱 Application - ${typeLabel}`);
26
27
  logger.log(` Key: ${a.key ?? '—'}`);
27
28
  logger.log(` Display name: ${a.displayName ?? '—'}`);
28
29
  logger.log(` Description: ${a.description ?? '—'}`);
@@ -53,19 +54,49 @@ function logApplicationFields(a) {
53
54
  logApplicationOptional(a);
54
55
  }
55
56
 
56
- function logApplicationExternalIntegration(ei) {
57
+ /** Application section for online + external: Key, Display name, Description, Deployment, Status (no Type; Dataplane block follows). */
58
+ function logApplicationFieldsOnlineExternal(a) {
59
+ logger.log('🧷 Application - external');
60
+ logger.log(` Key: ${a.key ?? '—'}`);
61
+ logger.log(` Display name: ${a.displayName ?? '—'}`);
62
+ logger.log(` Description: ${a.description ?? '—'}`);
63
+ logger.log(' Deployment: —');
64
+ if (a.status !== undefined && a.status !== '—') logger.log(` Status: ${a.status}`);
65
+ }
66
+
67
+ /** Application section for offline + external: Key, Display name, Description, Deployment, Status, Version when present. */
68
+ function logApplicationFieldsOfflineExternal(a) {
69
+ logger.log('🧷 Application - external');
70
+ logger.log(` Key: ${a.key ?? '—'}`);
71
+ logger.log(` Display name: ${a.displayName ?? '—'}`);
72
+ logger.log(` Description: ${a.description ?? '—'}`);
73
+ logger.log(' Deployment: —');
74
+ if (a.status !== undefined && a.status !== '—') logger.log(` Status: ${a.status}`);
75
+ if (a.version !== undefined && a.version !== null) logger.log(` Version: ${a.version}`);
76
+ }
77
+
78
+ function logApplicationExternalIntegration(ei, options = {}) {
57
79
  logger.log(' External integration:');
58
80
  logger.log(` schemaBasePath: ${ei.schemaBasePath}`);
59
81
  logger.log(` systems: [${(ei.systems || []).join(', ')}]`);
60
82
  logger.log(` dataSources: [${(ei.dataSources || []).join(', ')}]`);
61
- logger.log(chalk.gray('\n For external system data as on dataplane, run: aifabrix show <appKey> --online or aifabrix app show <appKey>.'));
83
+ if (!options.skipHint) {
84
+ logger.log(chalk.gray('\n For external system data as on dataplane, run: aifabrix show <appKey> --online or aifabrix app show <appKey>.'));
85
+ }
62
86
  }
63
87
 
64
- function logApplicationSection(a, isExternal) {
65
- logApplicationFields(a);
66
- if (isExternal && a.externalIntegration) {
67
- logApplicationExternalIntegration(a.externalIntegration);
88
+ function logApplicationSection(a, summary) {
89
+ const isExternal = summary.isExternal;
90
+ const onlineExternal = summary.source === 'online' && isExternal;
91
+ const offlineExternal = summary.source === 'offline' && isExternal;
92
+ if (onlineExternal) {
93
+ logApplicationFieldsOnlineExternal(a);
94
+ } else if (offlineExternal) {
95
+ logApplicationFieldsOfflineExternal(a);
96
+ } else {
97
+ logApplicationFields(a);
68
98
  }
99
+ /* External integration is logged after Dataplane block in display() when external */
69
100
  }
70
101
 
71
102
  function logRolesSection(roles) {
@@ -125,13 +156,13 @@ function logDatabasesSection(dbNames) {
125
156
  }
126
157
 
127
158
  function logExternalSystemMain(ext) {
128
- logger.log('🔗 External system (dataplane)');
129
- logger.log(` Dataplane: ${ext.dataplaneUrl}`);
130
- logger.log(` System key: ${ext.systemKey}`);
131
- logger.log(` Display name: ${ext.displayName}`);
132
- logger.log(` Type: ${ext.type}`);
133
- logger.log(` Status: ${ext.status}`);
159
+ logger.log('🧩 Dataplane');
134
160
  logger.log(` Version: ${ext.version ?? '—'}`);
161
+ logger.log(` Credential: ${ext.credentialId ?? '—'}`);
162
+ logger.log(` Status: ${ext.status ?? '—'}`);
163
+ logger.log(` API docs: ${ext.openApiDocsPageUrl ?? ext.apiDocumentUrl ?? '—'}`);
164
+ logger.log(` MCP server: ${ext.mcpServerUrl ?? '—'}`);
165
+ logger.log(` OpenAPI spec: ${ext.apiDocumentUrl ?? '—'}`);
135
166
  }
136
167
 
137
168
  function logExternalSystemDataSources(dataSources) {
@@ -144,8 +175,8 @@ function logExternalSystemDataSources(dataSources) {
144
175
 
145
176
  /**
146
177
  * Log OpenAPI and MCP documentation links for external system (dataplane endpoints).
147
- * REST: /api/v1/rest/{systemKey}/docs; MCP: /api/v1/mcp/{systemKey}/{resourceType}/docs per dataSource.
148
- * @param {Object} ext - External system result (dataplaneUrl, systemKey, dataSources, openapiFiles, openapiEndpoints)
178
+ * Uses openApiDocsPageUrl from dataplane when available; otherwise REST/MCP constructed URLs.
179
+ * @param {Object} ext - External system result (dataplaneUrl, systemKey, dataSources, openapiFiles, openapiEndpoints, openApiDocsPageUrl)
149
180
  */
150
181
  function logExternalSystemServiceLinks(ext) {
151
182
  if (!ext || !ext.dataplaneUrl || !ext.systemKey) return;
@@ -155,9 +186,13 @@ function logExternalSystemServiceLinks(ext) {
155
186
  (ext.openapiEndpoints && ext.openapiEndpoints.length > 0);
156
187
  const dataSources = ext.dataSources || [];
157
188
  const hasMcp = dataSources.length > 0;
158
- if (!hasOpenApi && !hasMcp) return;
189
+ const openApiDocsPageUrl = ext.openApiDocsPageUrl;
190
+ if (!hasOpenApi && !hasMcp && !openApiDocsPageUrl) return;
159
191
 
160
192
  logger.log(' Service links:');
193
+ if (openApiDocsPageUrl) {
194
+ logger.log(` OpenAPI docs page: ${openApiDocsPageUrl}`);
195
+ }
161
196
  logger.log(` REST OpenAPI: ${base}/api/v1/rest/${sk}/docs`);
162
197
  if (hasMcp) {
163
198
  dataSources.forEach((ds) => {
@@ -175,24 +210,55 @@ function logExternalSystemApplication(ap) {
175
210
  if (ap.permissions) logger.log(` permissions: ${Array.isArray(ap.permissions) ? ap.permissions.join(', ') : ap.permissions}`);
176
211
  }
177
212
 
178
- function logExternalSystemSection(ext) {
213
+ function logExternalSystemSection(ext, options = {}) {
179
214
  if (!ext) return;
180
- logger.log('');
215
+ if (!options.afterApplication) logger.log('');
181
216
  if (ext.error) {
182
- logger.log('🔗 External system (dataplane): not available (dataplane unreachable or not found).');
217
+ logger.log('🧩 Dataplane: not available (dataplane unreachable or not found).');
183
218
  return;
184
219
  }
185
220
  logExternalSystemMain(ext);
186
- logExternalSystemDataSources(ext.dataSources);
187
- logExternalSystemApplication(ext.application);
188
- if (ext.openapiFiles && ext.openapiFiles.length > 0) {
189
- logger.log(` OpenAPI files: ${ext.openapiFiles.length}`);
221
+ if (!options.compact) {
222
+ logExternalSystemDataSources(ext.dataSources);
223
+ logExternalSystemApplication(ext.application);
224
+ if (ext.openapiFiles && ext.openapiFiles.length > 0) {
225
+ logger.log(` OpenAPI files: ${ext.openapiFiles.length}`);
226
+ }
227
+ if (ext.openapiEndpoints && ext.openapiEndpoints.length > 0) {
228
+ const sample = ext.openapiEndpoints.slice(0, 3).map((e) => `${e.method || 'GET'} ${e.path || e.pathPattern || e}`).join(', ');
229
+ logger.log(` OpenAPI endpoints: ${ext.openapiEndpoints.length} (e.g. ${sample}${ext.openapiEndpoints.length > 3 ? ' …' : ''})`);
230
+ }
231
+ logExternalSystemServiceLinks(ext);
190
232
  }
191
- if (ext.openapiEndpoints && ext.openapiEndpoints.length > 0) {
192
- const sample = ext.openapiEndpoints.slice(0, 3).map((e) => `${e.method || 'GET'} ${e.path || e.pathPattern || e}`).join(', ');
193
- logger.log(` OpenAPI endpoints: ${ext.openapiEndpoints.length} (e.g. ${sample}${ext.openapiEndpoints.length > 3 ? ' …' : ''})`);
233
+ }
234
+
235
+ function getDisplayContext(summary) {
236
+ const a = summary.application;
237
+ const databases = summary.databases ?? a.databases ?? [];
238
+ const dbNames = Array.isArray(databases) ? databases.map((d) => (d && d.name) || d).filter(Boolean) : [];
239
+ return {
240
+ a,
241
+ roles: summary.roles ?? a.roles ?? [],
242
+ permissions: summary.permissions ?? a.permissions ?? [],
243
+ authentication: summary.authentication ?? a.authentication,
244
+ portalInputConfigurations: summary.portalInputConfigurations ?? a.portalInputConfigurations ?? [],
245
+ dbNames
246
+ };
247
+ }
248
+
249
+ function displayExternalAppBlock(summary) {
250
+ logger.log('');
251
+ if (summary.externalSystem && !summary.externalSystem.error) {
252
+ logExternalSystemSection(summary.externalSystem, { afterApplication: true, compact: true });
253
+ } else {
254
+ const reason = summary.externalSystem && summary.externalSystem.error ? ` (${summary.externalSystem.error})` : '';
255
+ logger.log(`🧩 Dataplane: not available${reason}`);
256
+ }
257
+ if (summary.application.externalIntegration) {
258
+ logger.log('');
259
+ const skipHint = summary.source === 'online' && summary.externalSystem && !summary.externalSystem.error;
260
+ logApplicationExternalIntegration(summary.application.externalIntegration, { skipHint });
194
261
  }
195
- logExternalSystemServiceLinks(ext);
196
262
  }
197
263
 
198
264
  /**
@@ -202,27 +268,21 @@ function logExternalSystemSection(ext) {
202
268
  * @param {boolean} [options.permissionsOnly] - When true, output only source and Permissions section
203
269
  */
204
270
  function display(summary, options = {}) {
205
- const a = summary.application;
206
- const roles = summary.roles ?? a.roles ?? [];
207
- const permissions = summary.permissions ?? a.permissions ?? [];
208
- const authentication = summary.authentication ?? a.authentication;
209
- const portalInputConfigurations = summary.portalInputConfigurations ?? a.portalInputConfigurations ?? [];
210
- const databases = summary.databases ?? a.databases ?? [];
211
- const dbNames = Array.isArray(databases) ? databases.map((d) => (d && d.name) || d).filter(Boolean) : [];
271
+ const ctx = getDisplayContext(summary);
212
272
 
213
273
  logSourceAndHeader(summary);
214
274
  if (options.permissionsOnly) {
215
- logPermissionsSection(permissions, { showWhenEmpty: true });
275
+ logPermissionsSection(ctx.permissions, { showWhenEmpty: true });
216
276
  logger.log('');
217
277
  return;
218
278
  }
219
- logApplicationSection(a, summary.isExternal);
220
- logRolesSection(roles);
221
- /* Permissions section shown only when --permissions is set (permissionsOnly mode) */
222
- logAuthSection(authentication);
223
- logConfigurationsSection(portalInputConfigurations);
224
- logDatabasesSection(dbNames);
225
- logExternalSystemSection(summary.externalSystem);
279
+ logApplicationSection(ctx.a, summary);
280
+ if (summary.isExternal) displayExternalAppBlock(summary);
281
+ logRolesSection(ctx.roles);
282
+ logAuthSection(ctx.authentication);
283
+ logConfigurationsSection(ctx.portalInputConfigurations);
284
+ logDatabasesSection(ctx.dbNames);
285
+ if (!summary.isExternal) logExternalSystemSection(summary.externalSystem);
226
286
  logger.log('');
227
287
  }
228
288