@aifabrix/builder 2.37.9 → 2.39.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 (74) hide show
  1. package/.cursor/rules/project-rules.mdc +3 -0
  2. package/README.md +19 -0
  3. package/integration/hubspot/hubspot-deploy.json +1 -5
  4. package/integration/hubspot/hubspot-system.json +0 -3
  5. package/lib/api/applications.api.js +29 -1
  6. package/lib/api/auth.api.js +14 -0
  7. package/lib/api/credentials.api.js +34 -0
  8. package/lib/api/datasources-core.api.js +16 -1
  9. package/lib/api/datasources-extended.api.js +18 -1
  10. package/lib/api/deployments.api.js +32 -0
  11. package/lib/api/environments.api.js +11 -0
  12. package/lib/api/external-systems.api.js +16 -1
  13. package/lib/api/pipeline.api.js +12 -4
  14. package/lib/api/service-users.api.js +41 -0
  15. package/lib/api/types/applications.types.js +1 -1
  16. package/lib/api/types/deployments.types.js +1 -1
  17. package/lib/api/types/pipeline.types.js +1 -1
  18. package/lib/api/types/service-users.types.js +24 -0
  19. package/lib/api/wizard.api.js +40 -1
  20. package/lib/app/deploy.js +86 -21
  21. package/lib/app/rotate-secret.js +3 -1
  22. package/lib/app/run-helpers.js +35 -2
  23. package/lib/app/show-display.js +30 -11
  24. package/lib/app/show.js +34 -8
  25. package/lib/cli/index.js +4 -0
  26. package/lib/cli/setup-app.js +40 -0
  27. package/lib/cli/setup-credential-deployment.js +72 -0
  28. package/lib/cli/setup-infra.js +3 -3
  29. package/lib/cli/setup-service-user.js +52 -0
  30. package/lib/cli/setup-utility.js +1 -25
  31. package/lib/commands/app-down.js +80 -0
  32. package/lib/commands/app-logs.js +146 -0
  33. package/lib/commands/app.js +24 -1
  34. package/lib/commands/credential-list.js +104 -0
  35. package/lib/commands/deployment-list.js +184 -0
  36. package/lib/commands/service-user.js +193 -0
  37. package/lib/commands/up-common.js +74 -5
  38. package/lib/commands/up-dataplane.js +13 -7
  39. package/lib/commands/up-miso.js +17 -24
  40. package/lib/core/templates.js +2 -2
  41. package/lib/external-system/deploy.js +79 -15
  42. package/lib/generator/builders.js +8 -27
  43. package/lib/generator/external-controller-manifest.js +5 -4
  44. package/lib/generator/index.js +16 -14
  45. package/lib/generator/split.js +1 -0
  46. package/lib/generator/wizard.js +4 -1
  47. package/lib/schema/application-schema.json +6 -14
  48. package/lib/schema/deployment-rules.yaml +121 -0
  49. package/lib/schema/external-system.schema.json +0 -16
  50. package/lib/utils/app-register-config.js +10 -12
  51. package/lib/utils/app-run-containers.js +2 -1
  52. package/lib/utils/compose-generator.js +2 -1
  53. package/lib/utils/deployment-errors.js +10 -0
  54. package/lib/utils/environment-checker.js +25 -6
  55. package/lib/utils/help-builder.js +0 -1
  56. package/lib/utils/image-version.js +209 -0
  57. package/lib/utils/schema-loader.js +1 -1
  58. package/lib/utils/variable-transformer.js +7 -33
  59. package/lib/validation/external-manifest-validator.js +1 -1
  60. package/package.json +1 -1
  61. package/templates/applications/README.md.hbs +1 -3
  62. package/templates/applications/dataplane/Dockerfile +2 -2
  63. package/templates/applications/dataplane/README.md +20 -6
  64. package/templates/applications/dataplane/env.template +31 -2
  65. package/templates/applications/dataplane/rbac.yaml +1 -1
  66. package/templates/applications/dataplane/variables.yaml +7 -4
  67. package/templates/applications/keycloak/Dockerfile +3 -3
  68. package/templates/applications/keycloak/README.md +14 -4
  69. package/templates/applications/keycloak/env.template +17 -2
  70. package/templates/applications/keycloak/variables.yaml +2 -1
  71. package/templates/applications/miso-controller/README.md +1 -3
  72. package/templates/applications/miso-controller/env.template +85 -25
  73. package/templates/applications/miso-controller/rbac.yaml +15 -0
  74. package/templates/applications/miso-controller/variables.yaml +24 -23
@@ -10,6 +10,7 @@
10
10
 
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
+ const yaml = require('js-yaml');
13
14
  const chalk = require('chalk');
14
15
  const logger = require('../utils/logger');
15
16
  const pathsUtil = require('../utils/paths');
@@ -30,8 +31,74 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
30
31
  return true;
31
32
  }
32
33
 
34
+ /**
35
+ * Resolve the directory (folder) that would contain the .env file for envOutputPath.
36
+ * @param {string} envOutputPath - Value from build.envOutputPath (e.g. ../../.env)
37
+ * @param {string} variablesPath - Path to variables.yaml
38
+ * @returns {string} Absolute path to the folder that would contain the output .env file
39
+ */
40
+ function getEnvOutputPathFolder(envOutputPath, variablesPath) {
41
+ const variablesDir = path.dirname(variablesPath);
42
+ const resolvedFile = path.resolve(variablesDir, envOutputPath);
43
+ return path.dirname(resolvedFile);
44
+ }
45
+
46
+ /**
47
+ * Validates envOutputPath: if the target folder does not exist, patches variables.yaml to set envOutputPath to null.
48
+ * Used by up-platform, up-miso, up-dataplane so we do not keep a path that points outside an existing tree.
49
+ *
50
+ * @param {string} appName - Application name (e.g. keycloak, miso-controller, dataplane)
51
+ */
52
+ function validateEnvOutputPathFolderOrNull(appName) {
53
+ if (!appName || typeof appName !== 'string') return;
54
+ const pathsToPatch = [pathsUtil.getBuilderPath(appName)];
55
+ const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
56
+ if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
57
+ pathsToPatch.push(cwdBuilderPath);
58
+ }
59
+ const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
60
+ const replacement = '$1 null # deploy only, no copy';
61
+ for (const appPath of pathsToPatch) {
62
+ const variablesPath = path.join(appPath, 'variables.yaml');
63
+ if (!fs.existsSync(variablesPath)) continue;
64
+ try {
65
+ const content = fs.readFileSync(variablesPath, 'utf8');
66
+ const variables = yaml.load(content);
67
+ const value = variables?.build?.envOutputPath;
68
+ if (value === null || value === undefined || value === '') continue;
69
+ const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
70
+ if (fs.existsSync(folder)) continue;
71
+ const newContent = content.replace(envOutputPathLine, replacement);
72
+ fs.writeFileSync(variablesPath, newContent, 'utf8');
73
+ } catch (err) {
74
+ logger.warn(chalk.yellow(`Could not validate envOutputPath in ${variablesPath}: ${err.message}`));
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Patches a single variables.yaml to set build.envOutputPath to null for deploy-only.
81
+ *
82
+ * @param {string} variablesPath - Path to variables.yaml
83
+ * @param {RegExp} envOutputPathLine - Regex for envOutputPath line
84
+ * @param {string} replacement - Replacement string
85
+ */
86
+ function patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement) {
87
+ const content = fs.readFileSync(variablesPath, 'utf8');
88
+ if (!envOutputPathLine.test(content)) return;
89
+ const variables = yaml.load(content);
90
+ const value = variables?.build?.envOutputPath;
91
+ if (value !== null && value !== undefined && value !== '') {
92
+ const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
93
+ if (fs.existsSync(folder)) return;
94
+ }
95
+ const newContent = content.replace(envOutputPathLine, replacement);
96
+ fs.writeFileSync(variablesPath, newContent, 'utf8');
97
+ }
98
+
33
99
  /**
34
100
  * Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
101
+ * Only patches when the target folder does NOT exist; when folder exists, keeps the value.
35
102
  * Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
36
103
  * Patches both primary builder path and cwd/builder if different.
37
104
  *
@@ -50,10 +117,7 @@ function patchEnvOutputPathForDeployOnly(appName) {
50
117
  const variablesPath = path.join(appPath, 'variables.yaml');
51
118
  if (!fs.existsSync(variablesPath)) continue;
52
119
  try {
53
- let content = fs.readFileSync(variablesPath, 'utf8');
54
- if (!envOutputPathLine.test(content)) continue;
55
- content = content.replace(envOutputPathLine, replacement);
56
- fs.writeFileSync(variablesPath, content, 'utf8');
120
+ patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement);
57
121
  } catch (err) {
58
122
  logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
59
123
  }
@@ -99,4 +163,9 @@ async function ensureAppFromTemplate(appName) {
99
163
  return primaryCopied;
100
164
  }
101
165
 
102
- module.exports = { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly };
166
+ module.exports = {
167
+ ensureAppFromTemplate,
168
+ patchEnvOutputPathForDeployOnly,
169
+ validateEnvOutputPathFolderOrNull,
170
+ getEnvOutputPathFolder
171
+ };
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * AI Fabrix Builder - Up Dataplane Command
3
3
  *
4
- * Registers or rotates dataplane app in dev, then deploys. Miso-controller runs
5
- * the dataplane container; this command does not run the image locally.
6
- * If app is already registered, uses rotate-secret; otherwise registers.
4
+ * Always local deployment: registers or rotates dataplane app in dev, sends
5
+ * deployment manifest to Miso Controller, then runs the dataplane app locally
6
+ * (same as aifabrix deploy dataplane --deployment=local). If app is already
7
+ * registered, uses rotate-secret; otherwise registers.
7
8
  *
8
9
  * @fileoverview up-dataplane command implementation
9
10
  * @author AI Fabrix Team
@@ -23,7 +24,7 @@ const { registerApplication } = require('../app/register');
23
24
  const { rotateSecret } = require('../app/rotate-secret');
24
25
  const { checkApplicationExists } = require('../utils/app-existence');
25
26
  const app = require('../app');
26
- const { ensureAppFromTemplate } = require('./up-common');
27
+ const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
27
28
 
28
29
  /**
29
30
  * Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
@@ -64,7 +65,8 @@ function buildDataplaneImageRef(registry) {
64
65
 
65
66
  /**
66
67
  * Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
67
- * register or rotate (if already registered), then deploy (miso-controller runs the container).
68
+ * register or rotate (if already registered), deploy (send manifest to controller),
69
+ * then run dataplane app locally (always local deployment).
68
70
  *
69
71
  * @async
70
72
  * @function handleUpDataplane
@@ -80,7 +82,7 @@ async function handleUpDataplane(options = {}) {
80
82
  if (builderDir) {
81
83
  process.env.AIFABRIX_BUILDER_DIR = builderDir;
82
84
  }
83
- logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy dataplane in dev)...\n'));
85
+ logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
84
86
 
85
87
  const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
86
88
  const authConfig = await checkAuthentication(controllerUrl, environmentKey);
@@ -95,6 +97,8 @@ async function handleUpDataplane(options = {}) {
95
97
  logger.log(chalk.green('✓ Logged in and environment is dev'));
96
98
 
97
99
  await ensureAppFromTemplate('dataplane');
100
+ // If envOutputPath target folder does not exist, set envOutputPath to null
101
+ validateEnvOutputPathFolderOrNull('dataplane');
98
102
 
99
103
  await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
100
104
 
@@ -102,8 +106,10 @@ async function handleUpDataplane(options = {}) {
102
106
  const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
103
107
 
104
108
  await app.deployApp('dataplane', deployOpts);
109
+ logger.log('');
110
+ await app.runApp('dataplane', {});
105
111
 
106
- logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered and deployed in dev (miso-controller runs the container).'));
112
+ logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
107
113
  }
108
114
 
109
115
  module.exports = { handleUpDataplane, buildDataplaneImageRef };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AI Fabrix Builder - Up Miso Command
3
3
  *
4
- * Installs keycloak, miso-controller, and dataplane from images (no build).
4
+ * Installs keycloak and miso-controller from images (no build). For dataplane, use up-dataplane.
5
5
  * Assumes infra is up; sets dev secrets and resolves (no force; existing .env values preserved).
6
6
  *
7
7
  * @fileoverview up-miso command implementation
@@ -19,19 +19,16 @@ const secrets = require('../core/secrets');
19
19
  const infra = require('../infrastructure');
20
20
  const app = require('../app');
21
21
  const { saveLocalSecret } = require('../utils/local-secrets');
22
- const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly } = require('./up-common');
22
+ const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly, validateEnvOutputPathFolderOrNull } = require('./up-common');
23
23
 
24
24
  /** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
25
25
  const KEYCLOAK_BASE_PORT = 8082;
26
26
  /** Miso controller base port (dev-config app base) */
27
27
  const MISO_BASE_PORT = 3000;
28
- /** Dataplane base port (from templates/applications/dataplane/variables.yaml) */
29
- const _DATAPLANE_BASE_PORT = 3001;
30
-
31
28
  /**
32
- * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string, dataplane?: string }
33
- * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1', 'dataplane=reg/d:v1']
34
- * @returns {{ keycloak?: string, 'miso-controller'?: string, dataplane?: string }}
29
+ * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
30
+ * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1']
31
+ * @returns {{ keycloak?: string, 'miso-controller'?: string }}
35
32
  */
36
33
  function parseImageOptions(imageOpts) {
37
34
  const map = {};
@@ -50,7 +47,7 @@ function parseImageOptions(imageOpts) {
50
47
 
51
48
  /**
52
49
  * Build full image ref from registry and app variables (registry/name:tag)
53
- * @param {string} appName - keycloak, miso-controller, or dataplane
50
+ * @param {string} appName - keycloak or miso-controller
54
51
  * @param {string} registry - Registry URL
55
52
  * @returns {string} Full image reference
56
53
  */
@@ -67,7 +64,7 @@ function buildImageRefFromRegistry(appName, registry) {
67
64
  }
68
65
 
69
66
  /**
70
- * Set URL secrets and resolve keycloak + miso-controller + dataplane (no force; existing .env preserved)
67
+ * Set URL secrets and resolve keycloak + miso-controller (no force; existing .env preserved)
71
68
  * @async
72
69
  * @param {number} devIdNum - Developer ID number
73
70
  */
@@ -79,12 +76,11 @@ async function setMisoSecretsAndResolve(devIdNum) {
79
76
  logger.log(chalk.green('✓ Set keycloak and miso-controller URL secrets'));
80
77
  await secrets.generateEnvFile('keycloak', undefined, 'docker', false, true);
81
78
  await secrets.generateEnvFile('miso-controller', undefined, 'docker', false, true);
82
- await secrets.generateEnvFile('dataplane', undefined, 'docker', false, true);
83
- logger.log(chalk.green('✓ Resolved keycloak, miso-controller, and dataplane'));
79
+ logger.log(chalk.green(' Resolved keycloak and miso-controller'));
84
80
  }
85
81
 
86
82
  /**
87
- * Build run options and run keycloak, miso-controller, then dataplane
83
+ * Build run options and run keycloak, then miso-controller
88
84
  * @async
89
85
  * @param {Object} options - Commander options (image, registry, registryMode)
90
86
  */
@@ -92,27 +88,23 @@ async function runMisoApps(options) {
92
88
  const imageMap = parseImageOptions(options.image);
93
89
  const keycloakImage = imageMap.keycloak || (options.registry ? buildImageRefFromRegistry('keycloak', options.registry) : undefined);
94
90
  const misoImage = imageMap['miso-controller'] || (options.registry ? buildImageRefFromRegistry('miso-controller', options.registry) : undefined);
95
- const dataplaneImage = imageMap.dataplane || (options.registry ? buildImageRefFromRegistry('dataplane', options.registry) : undefined);
96
91
  const keycloakRunOpts = { image: keycloakImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
97
92
  const misoRunOpts = { image: misoImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
98
- const dataplaneRunOpts = { image: dataplaneImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
99
93
  logger.log(chalk.blue('Starting keycloak...'));
100
94
  await app.runApp('keycloak', keycloakRunOpts);
101
95
  logger.log(chalk.blue('Starting miso-controller...'));
102
96
  await app.runApp('miso-controller', misoRunOpts);
103
- logger.log(chalk.blue('Starting dataplane...'));
104
- await app.runApp('dataplane', dataplaneRunOpts);
105
97
  }
106
98
 
107
99
  /**
108
- * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak, miso-controller, then dataplane.
100
+ * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak and miso-controller.
109
101
  *
110
102
  * @async
111
103
  * @function handleUpMiso
112
104
  * @param {Object} options - Commander options
113
105
  * @param {string} [options.registry] - Override registry for all apps
114
106
  * @param {string} [options.registryMode] - Override registry mode (acr|external)
115
- * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1, miso-controller=reg/m:v1, dataplane=reg/d:v1
107
+ * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1, miso-controller=reg/m:v1
116
108
  * @returns {Promise<void>}
117
109
  * @throws {Error} If infra not up or any step fails
118
110
  */
@@ -121,7 +113,7 @@ async function handleUpMiso(options = {}) {
121
113
  if (builderDir) {
122
114
  process.env.AIFABRIX_BUILDER_DIR = builderDir;
123
115
  }
124
- logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller + dataplane from images)...\n'));
116
+ logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
125
117
  // Strict: only this developer's infra (same as status), so up-miso and status agree
126
118
  const health = await infra.checkInfraHealth(undefined, { strict: true });
127
119
  const allHealthy = Object.values(health).every(status => status === 'healthy');
@@ -131,17 +123,18 @@ async function handleUpMiso(options = {}) {
131
123
  logger.log(chalk.green('✓ Infrastructure is up'));
132
124
  await ensureAppFromTemplate('keycloak');
133
125
  await ensureAppFromTemplate('miso-controller');
134
- await ensureAppFromTemplate('dataplane');
126
+ // If envOutputPath target folder does not exist, set envOutputPath to null
127
+ validateEnvOutputPathFolderOrNull('keycloak');
128
+ validateEnvOutputPathFolderOrNull('miso-controller');
135
129
  // Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
136
130
  patchEnvOutputPathForDeployOnly('keycloak');
137
131
  patchEnvOutputPathForDeployOnly('miso-controller');
138
- patchEnvOutputPathForDeployOnly('dataplane');
139
132
  const developerId = await config.getDeveloperId();
140
133
  const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
141
134
  await setMisoSecretsAndResolve(devIdNum);
142
135
  await runMisoApps(options);
143
- logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.') +
144
- chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed.'));
136
+ logger.log(chalk.green('\n✓ up-miso complete. Keycloak and miso-controller are running.') +
137
+ chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed. Use \'aifabrix up-dataplane\' for dataplane.'));
145
138
  }
146
139
 
147
140
  module.exports = { handleUpMiso, parseImageOptions };
@@ -35,7 +35,6 @@ function generateExternalSystemVariables(appName, displayName, config) {
35
35
  type: 'external'
36
36
  },
37
37
  deployment: {
38
- controllerUrl: '',
39
38
  environment: 'dev'
40
39
  },
41
40
  externalIntegration: {
@@ -83,7 +82,8 @@ function buildWebappVariables(appName, displayName, config, imageName, imageTag)
83
82
  key: appName,
84
83
  displayName: displayName,
85
84
  description: `${appName.replace(/-/g, ' ')} application`,
86
- type: appType
85
+ type: appType,
86
+ version: config.version || '1.0.0'
87
87
  },
88
88
  image: {
89
89
  name: imageName,
@@ -13,10 +13,87 @@ const chalk = require('chalk');
13
13
  const { getDeploymentAuth } = require('../utils/token-manager');
14
14
  const logger = require('../utils/logger');
15
15
  const { resolveControllerUrl } = require('../utils/controller-url');
16
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
17
+ const { getExternalSystem } = require('../api/external-systems.api');
16
18
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
17
19
  const { validateExternalSystemComplete } = require('../validation/validate');
18
20
  const { displayValidationResults } = require('../validation/validate-display');
19
21
 
22
+ /**
23
+ * Displays API and MCP documentation URLs from dataplane when available
24
+ * @async
25
+ * @function displayDeploymentDocs
26
+ * @param {string} controllerUrl - Controller base URL
27
+ * @param {string} environment - Environment key
28
+ * @param {Object} authConfig - Authentication configuration
29
+ * @param {string} systemKey - External system key
30
+ */
31
+ async function displayDeploymentDocs(controllerUrl, environment, authConfig, systemKey) {
32
+ try {
33
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
34
+ const res = await getExternalSystem(dataplaneUrl, systemKey, authConfig);
35
+ const sys = res?.data || res;
36
+ if (!sys) return;
37
+
38
+ const apiDocumentUrl = sys.apiDocumentUrl;
39
+ const mcpServerUrl = sys.mcpServerUrl;
40
+ const openApiDocsPageUrl = sys.openApiDocsPageUrl;
41
+
42
+ const urls = [];
43
+ if (apiDocumentUrl && typeof apiDocumentUrl === 'string') {
44
+ urls.push({ label: 'API Docs', url: apiDocumentUrl });
45
+ }
46
+ if (mcpServerUrl && typeof mcpServerUrl === 'string') {
47
+ urls.push({ label: 'MCP Server', url: mcpServerUrl });
48
+ }
49
+ if (openApiDocsPageUrl && typeof openApiDocsPageUrl === 'string') {
50
+ urls.push({ label: 'OpenAPI Docs Page', url: openApiDocsPageUrl });
51
+ }
52
+
53
+ if (urls.length > 0) {
54
+ logger.log(chalk.blue('\nDocumentation:'));
55
+ urls.forEach(({ label, url }) => {
56
+ logger.log(chalk.blue(` ${label}: ${url}`));
57
+ });
58
+ }
59
+ } catch (_err) {
60
+ // Silently ignore: dataplane may be unreachable or docs not configured
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Deploys via controller and displays success summary with docs
66
+ * @async
67
+ * @function executeDeployAndDisplay
68
+ * @param {Object} manifest - Controller manifest
69
+ * @param {string} controllerUrl - Controller base URL
70
+ * @param {string} environment - Environment key
71
+ * @param {Object} authConfig - Authentication configuration
72
+ * @param {Object} options - Deployment options
73
+ * @returns {Promise<Object>} Deployment result
74
+ */
75
+ async function executeDeployAndDisplay(manifest, controllerUrl, environment, authConfig, options) {
76
+ const deployer = require('../deployment/deployer');
77
+ const pollOpts = {
78
+ poll: options.poll,
79
+ pollInterval: options.pollInterval !== undefined ? options.pollInterval : 500,
80
+ pollMaxAttempts: options.pollMaxAttempts,
81
+ ...options
82
+ };
83
+ const result = await deployer.deployToController(
84
+ manifest,
85
+ controllerUrl,
86
+ environment,
87
+ authConfig,
88
+ pollOpts
89
+ );
90
+ logger.log(chalk.green('\n✅ External system deployed successfully!'));
91
+ logger.log(chalk.blue(`System: ${manifest.key}`));
92
+ logger.log(chalk.blue(`Datasources: ${manifest.dataSources.length}`));
93
+ await displayDeploymentDocs(controllerUrl, environment, authConfig, manifest.key);
94
+ return result;
95
+ }
96
+
20
97
  /**
21
98
  * Prepares deployment configuration (auth, controller URL, environment)
22
99
  * @async
@@ -75,26 +152,13 @@ async function deployExternalSystem(appName, options = {}) {
75
152
  const { environment, controllerUrl, authConfig } = await prepareDeploymentConfig(appName, options);
76
153
 
77
154
  // Step 3: Deploy via controller pipeline (same as regular apps)
78
- // Use 500ms polling for external systems (faster than web apps which use 5000ms)
79
- const deployer = require('../deployment/deployer');
80
- const result = await deployer.deployToController(
155
+ const result = await executeDeployAndDisplay(
81
156
  manifest,
82
157
  controllerUrl,
83
158
  environment,
84
159
  authConfig,
85
- {
86
- poll: options.poll,
87
- pollInterval: options.pollInterval !== undefined ? options.pollInterval : 500,
88
- pollMaxAttempts: options.pollMaxAttempts,
89
- ...options
90
- }
160
+ options
91
161
  );
92
-
93
- // Display success summary
94
- logger.log(chalk.green('\n✅ External system deployed successfully!'));
95
- logger.log(chalk.blue(`System: ${manifest.key}`));
96
- logger.log(chalk.blue(`Datasources: ${manifest.dataSources.length}`));
97
-
98
162
  return result;
99
163
  } catch (error) {
100
164
  let message = `Failed to deploy external system: ${error.message}`;
@@ -126,11 +126,17 @@ function buildAuthentication(rbac) {
126
126
  * @returns {Object} App metadata
127
127
  */
128
128
  function buildAppMetadata(appName, variables) {
129
+ const rawVersion = variables.app?.version;
130
+ const version =
131
+ rawVersion !== undefined && rawVersion !== null && String(rawVersion).trim()
132
+ ? String(rawVersion).trim()
133
+ : '1.0.0';
129
134
  return {
130
135
  key: variables.app?.key || appName,
131
136
  displayName: variables.app?.displayName || appName,
132
137
  description: variables.app?.description || '',
133
- type: variables.app?.type || 'webapp'
138
+ type: variables.app?.type || 'webapp',
139
+ version
134
140
  };
135
141
  }
136
142
 
@@ -256,25 +262,6 @@ function validateBuildFields(build) {
256
262
  return Object.keys(buildConfig).length > 0 ? buildConfig : null;
257
263
  }
258
264
 
259
- /**
260
- * Validates and transforms deployment fields
261
- * @function validateDeploymentFields
262
- * @param {Object} deployment - Deployment configuration
263
- * @returns {Object|null} Validated deployment config or null
264
- */
265
- function validateDeploymentFields(deployment) {
266
- if (!deployment) {
267
- return null;
268
- }
269
-
270
- const deploymentConfig = {};
271
- if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
272
- deploymentConfig.controllerUrl = deployment.controllerUrl;
273
- }
274
-
275
- return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
276
- }
277
-
278
265
  /**
279
266
  * Adds optional fields to deployment manifest
280
267
  * @function buildOptionalFields
@@ -333,11 +320,6 @@ function addValidatedConfigSections(deployment, variables) {
333
320
  if (build) {
334
321
  deployment.build = build;
335
322
  }
336
-
337
- const deploymentConfig = validateDeploymentFields(variables.deployment);
338
- if (deploymentConfig) {
339
- deployment.deployment = deploymentConfig;
340
- }
341
323
  }
342
324
 
343
325
  /**
@@ -375,12 +357,11 @@ function buildOptionalFields(deployment, variables, rbac) {
375
357
  * Builds deployment manifest structure
376
358
  * @param {string} appName - Application name
377
359
  * @param {Object} variables - Variables configuration
378
- * @param {string} deploymentKey - Deployment key
379
360
  * @param {Array} configuration - Environment configuration
380
361
  * @param {Object|null} rbac - RBAC configuration
381
362
  * @returns {Object} Deployment manifest
382
363
  */
383
- function buildManifestStructure(appName, variables, deploymentKey, configuration, rbac) {
364
+ function buildManifestStructure(appName, variables, configuration, rbac) {
384
365
  const registryMode = variables.image?.registryMode || 'external';
385
366
  const filteredConfiguration = filterConfigurationByRegistryMode(configuration, registryMode);
386
367
  const deployment = buildBaseDeployment(appName, variables, filteredConfiguration);
@@ -11,7 +11,6 @@
11
11
 
12
12
  const path = require('path');
13
13
  const { detectAppType } = require('../utils/paths');
14
- const { generateDeploymentKeyFromJson } = require('../core/key-generator');
15
14
  const { loadSystemFile, loadDatasourceFiles } = require('./external');
16
15
  const { loadVariables, loadRbac } = require('./helpers');
17
16
 
@@ -97,7 +96,7 @@ async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
97
96
  *
98
97
  * @example
99
98
  * const manifest = await generateControllerManifest('my-hubspot');
100
- * // Returns: { key, displayName, description, type: "external", system: {...}, dataSources: [...], deploymentKey }
99
+ * // Returns: { key, displayName, description, type: "external", system: {...}, dataSources: [...] }
101
100
  */
102
101
  async function generateControllerManifest(appName, options = {}) {
103
102
  if (!appName || typeof appName !== 'string') {
@@ -124,13 +123,15 @@ async function generateControllerManifest(appName, options = {}) {
124
123
  const datasourceFiles = variables.externalIntegration.dataSources || [];
125
124
  const datasourceJsons = await loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles);
126
125
 
126
+ const appVersion = variables.app?.version || variables.externalIntegration?.version || '1.0.0';
127
+
127
128
  // Build externalIntegration block (required by application schema for type: "external")
128
129
  const externalIntegration = {
129
130
  schemaBasePath: schemaBasePath,
130
131
  systems: systemFiles,
131
132
  dataSources: datasourceFiles,
132
133
  autopublish: variables.externalIntegration.autopublish !== false, // default true
133
- version: variables.externalIntegration.version || '1.0.0'
134
+ version: appVersion
134
135
  };
135
136
 
136
137
  const manifest = {
@@ -138,6 +139,7 @@ async function generateControllerManifest(appName, options = {}) {
138
139
  displayName: metadata.displayName,
139
140
  description: metadata.description,
140
141
  type: 'external',
142
+ version: appVersion,
141
143
  externalIntegration: externalIntegration,
142
144
  // Inline system and dataSources for atomic deployment (optional but recommended)
143
145
  system: systemJson,
@@ -148,7 +150,6 @@ async function generateControllerManifest(appName, options = {}) {
148
150
  requiresStorage: false
149
151
  };
150
152
 
151
- manifest.deploymentKey = generateDeploymentKeyFromJson(manifest);
152
153
  return manifest;
153
154
  }
154
155
 
@@ -11,7 +11,6 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const _keyGenerator = require('../core/key-generator');
15
14
  const _validator = require('../validation/validator');
16
15
  const builders = require('./builders');
17
16
  const { detectAppType, getDeployJsonPath } = require('../utils/paths');
@@ -19,6 +18,7 @@ const splitFunctions = require('./split');
19
18
  const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } = require('./helpers');
20
19
  const { generateExternalSystemApplicationSchema, splitExternalApplicationSchema } = require('./external');
21
20
  const { generateControllerManifest } = require('./external-controller-manifest');
21
+ const { resolveVersionForApp } = require('../utils/image-version');
22
22
 
23
23
  /**
24
24
  * Generates deployment JSON from application configuration files
@@ -65,21 +65,15 @@ function loadDeploymentConfigFiles(appPath, appType, appName) {
65
65
  * @param {Object} variables - Variables configuration
66
66
  * @param {Object} envTemplate - Environment template
67
67
  * @param {Object} rbac - RBAC configuration
68
- * @returns {Object} Deployment manifest with deploymentKey
68
+ * @returns {Object} Deployment manifest
69
69
  * @throws {Error} If validation fails
70
70
  */
71
71
  function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
72
72
  // Parse environment variables from template and merge portalInput from variables.yaml
73
73
  const configuration = parseEnvironmentVariables(envTemplate, variables);
74
74
 
75
- // Build deployment manifest WITHOUT deploymentKey initially
76
- const deployment = builders.buildManifestStructure(appName, variables, null, configuration, rbac);
77
-
78
- // Generate deploymentKey from the manifest object (excluding deploymentKey field)
79
- const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
80
-
81
- // Add deploymentKey to manifest
82
- deployment.deploymentKey = deploymentKey;
75
+ // Build deployment manifest (Controller computes deploymentKey from schema)
76
+ const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
83
77
 
84
78
  // Validate deployment JSON against schema
85
79
  const validation = _validator.validateDeploymentJson(deployment);
@@ -114,10 +108,13 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
114
108
  }
115
109
 
116
110
  const { variables, envTemplate, rbac } = loadDeploymentConfigFiles(appPath, appType, appName);
111
+ const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
112
+ const variablesWithVersion = {
113
+ ...variables,
114
+ app: { ...variables.app, version: resolved.version }
115
+ };
117
116
  const configuration = parseEnvironmentVariables(envTemplate, variables);
118
- const deployment = builders.buildManifestStructure(appName, variables, null, configuration, rbac);
119
- const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
120
- deployment.deploymentKey = deploymentKey;
117
+ const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
121
118
 
122
119
  return { deployment, appPath };
123
120
  }
@@ -145,7 +142,12 @@ async function generateDeployJson(appName, options = {}) {
145
142
 
146
143
  // Regular app: generate deployment manifest
147
144
  const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
148
- const deployment = buildAndValidateDeployment(appName, variables, envTemplate, rbac);
145
+ const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
146
+ const variablesWithVersion = {
147
+ ...variables,
148
+ app: { ...variables.app, version: resolved.version }
149
+ };
150
+ const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac);
149
151
 
150
152
  // Write deployment JSON
151
153
  const jsonContent = JSON.stringify(deployment, null, 2);
@@ -98,6 +98,7 @@ function extractAppSection(deployment) {
98
98
  if (deployment.displayName) app.displayName = deployment.displayName;
99
99
  if (deployment.description) app.description = deployment.description;
100
100
  if (deployment.type) app.type = deployment.type;
101
+ if (deployment.version) app.version = deployment.version;
101
102
  return app;
102
103
  }
103
104
 
@@ -234,8 +234,11 @@ async function generateOrUpdateVariablesYaml(params) {
234
234
  key: appName,
235
235
  displayName: systemConfig.displayName || appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
236
236
  description: systemConfig.description || `External system integration for ${appName}`,
237
- type: 'external'
237
+ type: 'external',
238
+ version: '1.0.0'
238
239
  };
240
+ } else {
241
+ variables.app.version = variables.app.version || '1.0.0';
239
242
  }
240
243
 
241
244
  // Set deployment config if not present