@aifabrix/builder 2.37.9 → 2.38.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 (45) hide show
  1. package/README.md +19 -0
  2. package/integration/hubspot/hubspot-deploy.json +1 -2
  3. package/lib/api/applications.api.js +23 -1
  4. package/lib/api/credentials.api.js +34 -0
  5. package/lib/api/deployments.api.js +27 -0
  6. package/lib/api/types/applications.types.js +1 -1
  7. package/lib/api/types/deployments.types.js +1 -1
  8. package/lib/api/types/pipeline.types.js +1 -1
  9. package/lib/api/wizard.api.js +21 -1
  10. package/lib/app/run-helpers.js +30 -2
  11. package/lib/cli/index.js +2 -0
  12. package/lib/cli/setup-app.js +32 -0
  13. package/lib/cli/setup-credential-deployment.js +72 -0
  14. package/lib/cli/setup-utility.js +1 -25
  15. package/lib/commands/app-down.js +80 -0
  16. package/lib/commands/app-logs.js +146 -0
  17. package/lib/commands/app.js +22 -0
  18. package/lib/commands/credential-list.js +104 -0
  19. package/lib/commands/deployment-list.js +184 -0
  20. package/lib/core/templates.js +2 -1
  21. package/lib/generator/builders.js +8 -3
  22. package/lib/generator/external-controller-manifest.js +5 -4
  23. package/lib/generator/index.js +16 -14
  24. package/lib/generator/split.js +1 -0
  25. package/lib/generator/wizard.js +4 -1
  26. package/lib/schema/application-schema.json +6 -2
  27. package/lib/schema/deployment-rules.yaml +121 -0
  28. package/lib/utils/app-run-containers.js +2 -1
  29. package/lib/utils/compose-generator.js +2 -1
  30. package/lib/utils/help-builder.js +0 -1
  31. package/lib/utils/image-version.js +209 -0
  32. package/lib/utils/schema-loader.js +1 -1
  33. package/lib/utils/variable-transformer.js +1 -19
  34. package/lib/validation/external-manifest-validator.js +1 -1
  35. package/package.json +1 -1
  36. package/templates/applications/README.md.hbs +1 -3
  37. package/templates/applications/dataplane/Dockerfile +2 -2
  38. package/templates/applications/dataplane/README.md +1 -3
  39. package/templates/applications/dataplane/variables.yaml +5 -3
  40. package/templates/applications/keycloak/Dockerfile +3 -3
  41. package/templates/applications/keycloak/README.md +14 -4
  42. package/templates/applications/keycloak/env.template +14 -2
  43. package/templates/applications/keycloak/variables.yaml +1 -1
  44. package/templates/applications/miso-controller/README.md +1 -3
  45. package/templates/applications/miso-controller/env.template +64 -11
@@ -15,6 +15,7 @@ const { listApplications } = require('../app/list');
15
15
  const { registerApplication } = require('../app/register');
16
16
  const { rotateSecret } = require('../app/rotate-secret');
17
17
  const { showApp } = require('../app/show');
18
+ const { runAppDeploymentList } = require('./deployment-list');
18
19
 
19
20
  /**
20
21
  * Setup application management commands
@@ -81,6 +82,27 @@ function setupAppCommands(program) {
81
82
  process.exit(1);
82
83
  }
83
84
  });
85
+
86
+ // Deployment list for an application
87
+ app
88
+ .command('deployment <appKey>')
89
+ .description('List last N deployments for an application in current environment (default pageSize=50)')
90
+ .option('--controller <url>', 'Controller URL (default: from config)')
91
+ .option('--environment <env>', 'Environment key (default: from config)')
92
+ .option('--page-size <n>', 'Items per page', '50')
93
+ .action(async(appKey, options) => {
94
+ try {
95
+ const opts = {
96
+ controller: options.controller,
97
+ environment: options.environment,
98
+ pageSize: parseInt(options.pageSize, 10) || 50
99
+ };
100
+ await runAppDeploymentList(appKey, opts);
101
+ } catch (error) {
102
+ logger.error(chalk.red(`Error: ${error.message}`));
103
+ process.exit(1);
104
+ }
105
+ });
84
106
  }
85
107
 
86
108
  module.exports = { setupAppCommands };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Credential list command – list credentials from controller/dataplane
3
+ * GET /api/v1/credential. Used by `aifabrix credential list`.
4
+ *
5
+ * @fileoverview Credential list command implementation
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { resolveControllerUrl } = require('../utils/controller-url');
13
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
+ const { normalizeControllerUrl } = require('../core/config');
15
+ const { listCredentials } = require('../api/credentials.api');
16
+
17
+ const DEFAULT_PAGE_SIZE = 50;
18
+
19
+ /**
20
+ * Get auth token for credential list (device token from config)
21
+ * @async
22
+ * @param {string} controllerUrl - Controller base URL
23
+ * @returns {Promise<{token: string, controllerUrl: string}|null>}
24
+ */
25
+ async function getCredentialListAuth(controllerUrl) {
26
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
27
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
28
+ if (deviceToken && deviceToken.token) {
29
+ return {
30
+ token: deviceToken.token,
31
+ controllerUrl: deviceToken.controller || normalizedUrl
32
+ };
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Extract credentials array from API response
39
+ * @param {Object} response - API response
40
+ * @returns {Array}
41
+ */
42
+ function extractCredentials(response) {
43
+ const data = response?.data ?? response;
44
+ const items = data?.credentials ?? data?.items ?? (Array.isArray(data) ? data : []);
45
+ return Array.isArray(items) ? items : [];
46
+ }
47
+
48
+ /**
49
+ * Display credential list to user
50
+ * @param {Array} list - Credentials array
51
+ * @param {string} controllerUrl - Controller URL for header
52
+ */
53
+ function displayCredentialList(list, controllerUrl) {
54
+ logger.log(chalk.bold(`\n🔐 Credentials (${controllerUrl}):\n`));
55
+ if (list.length === 0) {
56
+ logger.log(chalk.gray(' No credentials found.\n'));
57
+ return;
58
+ }
59
+ list.forEach((c) => {
60
+ const key = c.key ?? c.id ?? c.credentialKey ?? '-';
61
+ const name = c.displayName ?? c.name ?? key;
62
+ logger.log(` ${chalk.cyan(key)} - ${name}`);
63
+ });
64
+ logger.log('');
65
+ }
66
+
67
+ /**
68
+ * Run credential list command: call GET /api/v1/credential and display results
69
+ * @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>}
75
+ */
76
+ async function runCredentialList(options = {}) {
77
+ const controllerUrl = options.controller || (await resolveControllerUrl());
78
+ if (!controllerUrl) {
79
+ logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
80
+ process.exit(1);
81
+ return;
82
+ }
83
+ const authResult = await getCredentialListAuth(controllerUrl);
84
+ if (!authResult || !authResult.token) {
85
+ logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
86
+ logger.error(chalk.gray('Run: aifabrix login'));
87
+ process.exit(1);
88
+ return;
89
+ }
90
+ const authConfig = { type: 'bearer', token: authResult.token };
91
+ const listOptions = {
92
+ pageSize: options.pageSize || DEFAULT_PAGE_SIZE,
93
+ activeOnly: options.activeOnly
94
+ };
95
+ try {
96
+ const response = await listCredentials(authResult.controllerUrl, authConfig, listOptions);
97
+ displayCredentialList(extractCredentials(response), authResult.controllerUrl);
98
+ } catch (error) {
99
+ logger.error(chalk.red(`❌ Failed to list credentials: ${error.message}`));
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ module.exports = { runCredentialList };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Deployment list commands – list deployments for environment or for an app
3
+ * Uses GET .../deployments and GET .../applications/{appKey}/deployments.
4
+ *
5
+ * @fileoverview Deployment list command implementation
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { resolveControllerUrl } = require('../utils/controller-url');
13
+ const { resolveEnvironment } = require('../core/config');
14
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
15
+ const { listDeployments, listApplicationDeployments } = require('../api/deployments.api');
16
+ const { normalizeControllerUrl } = require('../core/config');
17
+
18
+ const DEFAULT_PAGE_SIZE = 50;
19
+
20
+ /**
21
+ * Get auth token for deployment list (device token from config)
22
+ * @async
23
+ * @param {string} controllerUrl - Controller base URL
24
+ * @returns {Promise<{token: string, controllerUrl: string}|null>}
25
+ */
26
+ async function getDeploymentListAuth(controllerUrl) {
27
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
28
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
29
+ if (deviceToken && deviceToken.token) {
30
+ return {
31
+ token: deviceToken.token,
32
+ controllerUrl: deviceToken.controller || normalizedUrl
33
+ };
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Extract deployments array from API response
40
+ * @param {Object} response - API response
41
+ * @returns {Array}
42
+ */
43
+ function extractDeployments(response) {
44
+ const data = response?.data ?? response;
45
+ const items = data?.items ?? data?.deployments ?? (Array.isArray(data) ? data : []);
46
+ return Array.isArray(items) ? items : [];
47
+ }
48
+
49
+ /**
50
+ * Display environment deployment list to user
51
+ * @param {Array} deployments - Deployments array
52
+ * @param {string} environment - Environment key
53
+ * @param {string} controllerUrl - Controller URL
54
+ */
55
+ function displayDeploymentList(deployments, environment, controllerUrl) {
56
+ logger.log(chalk.bold(`\n📋 Deployments (${environment}) at ${controllerUrl}:\n`));
57
+ if (deployments.length === 0) {
58
+ logger.log(chalk.gray(' No deployments found.\n'));
59
+ return;
60
+ }
61
+ deployments.forEach((d) => {
62
+ const id = d.id ?? d.deploymentId ?? '-';
63
+ const appKey = d.applicationKey ?? d.appKey ?? d.application?.key ?? '-';
64
+ const status = d.status ?? '-';
65
+ const createdAt = d.createdAt ?? d.created ?? '';
66
+ logger.log(` ${chalk.cyan(id)} ${appKey} ${status} ${chalk.gray(createdAt)}`);
67
+ });
68
+ logger.log('');
69
+ }
70
+
71
+ /**
72
+ * Run deployment list (environment): list last N deployments for current environment
73
+ * @async
74
+ * @param {Object} options - CLI options
75
+ * @param {string} [options.controller] - Controller URL override
76
+ * @param {string} [options.environment] - Environment key override
77
+ * @param {number} [options.pageSize] - Items per page (default 50)
78
+ * @returns {Promise<void>}
79
+ */
80
+ async function runDeploymentList(options = {}) {
81
+ const { environment, authResult } = await resolveDeploymentListContext(options);
82
+ const authConfig = { type: 'bearer', token: authResult.token };
83
+ const listOptions = { pageSize: options.pageSize || DEFAULT_PAGE_SIZE };
84
+ try {
85
+ const response = await listDeployments(
86
+ authResult.controllerUrl,
87
+ environment,
88
+ authConfig,
89
+ listOptions
90
+ );
91
+ displayDeploymentList(extractDeployments(response), environment, authResult.controllerUrl);
92
+ } catch (error) {
93
+ logger.error(chalk.red(`❌ Failed to list deployments: ${error.message}`));
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Display app deployment list to user
100
+ * @param {Array} deployments - Deployments array
101
+ * @param {string} appKey - Application key
102
+ * @param {string} environment - Environment key
103
+ * @param {string} controllerUrl - Controller URL
104
+ */
105
+ function displayAppDeploymentList(deployments, appKey, environment, controllerUrl) {
106
+ logger.log(chalk.bold(`\n📋 Deployments for ${appKey} (${environment}) at ${controllerUrl}:\n`));
107
+ if (deployments.length === 0) {
108
+ logger.log(chalk.gray(' No deployments found for this application.\n'));
109
+ return;
110
+ }
111
+ deployments.forEach((d) => {
112
+ const id = d.id ?? d.deploymentId ?? '-';
113
+ const status = d.status ?? '-';
114
+ const createdAt = d.createdAt ?? d.created ?? '';
115
+ logger.log(` ${chalk.cyan(id)} ${status} ${chalk.gray(createdAt)}`);
116
+ });
117
+ logger.log('');
118
+ }
119
+
120
+ /**
121
+ * Resolve controller URL, environment, and auth for deployment list commands
122
+ * @async
123
+ * @param {Object} options - Options with optional controller, environment
124
+ * @returns {Promise<{controllerUrl: string, environment: string, authResult: Object}>}
125
+ */
126
+ async function resolveDeploymentListContext(options) {
127
+ const controllerUrl = options.controller || (await resolveControllerUrl());
128
+ if (!controllerUrl) {
129
+ logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
130
+ process.exit(1);
131
+ }
132
+ const environment = options.environment || (await resolveEnvironment());
133
+ const authResult = await getDeploymentListAuth(controllerUrl);
134
+ if (!authResult || !authResult.token) {
135
+ logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
136
+ logger.error(chalk.gray('Run: aifabrix login'));
137
+ process.exit(1);
138
+ }
139
+ return { controllerUrl, environment, authResult };
140
+ }
141
+
142
+ /**
143
+ * Run app deployment list: list last N deployments for an application
144
+ * @async
145
+ * @param {string} appKey - Application key
146
+ * @param {Object} options - CLI options
147
+ * @param {string} [options.controller] - Controller URL override
148
+ * @param {string} [options.environment] - Environment key override
149
+ * @param {number} [options.pageSize] - Items per page (default 50)
150
+ * @returns {Promise<void>}
151
+ */
152
+ async function runAppDeploymentList(appKey, options = {}) {
153
+ if (!appKey || typeof appKey !== 'string') {
154
+ logger.error(chalk.red('❌ Application key is required.'));
155
+ process.exit(1);
156
+ return;
157
+ }
158
+ const { environment, authResult } = await resolveDeploymentListContext(options);
159
+ const authConfig = { type: 'bearer', token: authResult.token };
160
+ const listOptions = { pageSize: options.pageSize || DEFAULT_PAGE_SIZE };
161
+ try {
162
+ const response = await listApplicationDeployments(
163
+ authResult.controllerUrl,
164
+ environment,
165
+ appKey,
166
+ authConfig,
167
+ listOptions
168
+ );
169
+ displayAppDeploymentList(
170
+ extractDeployments(response),
171
+ appKey,
172
+ environment,
173
+ authResult.controllerUrl
174
+ );
175
+ } catch (error) {
176
+ logger.error(chalk.red(`❌ Failed to list deployments for ${appKey}: ${error.message}`));
177
+ process.exit(1);
178
+ }
179
+ }
180
+
181
+ module.exports = {
182
+ runDeploymentList,
183
+ runAppDeploymentList
184
+ };
@@ -83,7 +83,8 @@ function buildWebappVariables(appName, displayName, config, imageName, imageTag)
83
83
  key: appName,
84
84
  displayName: displayName,
85
85
  description: `${appName.replace(/-/g, ' ')} application`,
86
- type: appType
86
+ type: appType,
87
+ version: config.version || '1.0.0'
87
88
  },
88
89
  image: {
89
90
  name: imageName,
@@ -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
 
@@ -375,12 +381,11 @@ function buildOptionalFields(deployment, variables, rbac) {
375
381
  * Builds deployment manifest structure
376
382
  * @param {string} appName - Application name
377
383
  * @param {Object} variables - Variables configuration
378
- * @param {string} deploymentKey - Deployment key
379
384
  * @param {Array} configuration - Environment configuration
380
385
  * @param {Object|null} rbac - RBAC configuration
381
386
  * @returns {Object} Deployment manifest
382
387
  */
383
- function buildManifestStructure(appName, variables, deploymentKey, configuration, rbac) {
388
+ function buildManifestStructure(appName, variables, configuration, rbac) {
384
389
  const registryMode = variables.image?.registryMode || 'external';
385
390
  const filteredConfiguration = filterConfigurationByRegistryMode(configuration, registryMode);
386
391
  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
@@ -57,8 +57,7 @@
57
57
  "key",
58
58
  "displayName",
59
59
  "description",
60
- "type",
61
- "deploymentKey"
60
+ "type"
62
61
  ],
63
62
  "properties": {
64
63
  "key": {
@@ -91,6 +90,11 @@
91
90
  "external"
92
91
  ]
93
92
  },
93
+ "version": {
94
+ "type": "string",
95
+ "description": "Application version (semantic); default 1.0.0 when empty",
96
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.-]+)?$"
97
+ },
94
98
  "image": {
95
99
  "type": "string",
96
100
  "description": "Container image reference",
@@ -0,0 +1,121 @@
1
+ # Deployment rules – Central mapping for Controller
2
+ #
3
+ # Defines which manifest paths trigger deployment and which can differ per environment.
4
+ # Controller uses this file (or equivalent) for deployment key computation and value merge.
5
+ # Schemas (application, external-system, external-datasource) remain clean; no x-* annotations.
6
+ #
7
+ # Semantics:
8
+ # triggerPaths: Change affects deployment key / requires deploy
9
+ # overridablePaths: Value can differ per environment (preserve on promote)
10
+ # A path may appear in both (e.g. authentication.endpoints triggers deploy and is overridable).
11
+ #
12
+ # Path format: Dot notation. Child paths override parent when both match.
13
+ # Schema keys: application | externalSystem | externalDataSource
14
+
15
+ application:
16
+ triggerPaths:
17
+ - key
18
+ - displayName
19
+ - description
20
+ - type
21
+ - version
22
+ - image
23
+ - registryMode
24
+ - port
25
+ - requiresDatabase
26
+ - databases
27
+ - requiresRedis
28
+ - requiresStorage
29
+ - configuration
30
+ - configuration.items
31
+ - configuration.items.required
32
+ - configuration.items.portalInput
33
+ - healthCheck
34
+ - healthCheck.path
35
+ - healthCheck.probePath
36
+ - healthCheck.probeRequestType
37
+ - healthCheck.probeProtocol
38
+ - frontDoorRouting
39
+ - authentication
40
+ - roles
41
+ - permissions
42
+ - repository
43
+ - startupCommand
44
+ - runtimeVersion
45
+ - scaling
46
+ - build
47
+ - deployment
48
+ - externalIntegration
49
+ overridablePaths:
50
+ - configuration.items.value
51
+ - authentication.endpoints
52
+ - deployment.controllerUrl
53
+ - healthCheck.interval
54
+ - healthCheck.probeIntervalInSeconds
55
+
56
+ externalSystem:
57
+ triggerPaths:
58
+ - key
59
+ - displayName
60
+ - description
61
+ - type
62
+ - enabled
63
+ - environment
64
+ - authentication
65
+ - openapi
66
+ - mcp
67
+ - dataSources
68
+ - configuration
69
+ - configuration.items
70
+ - tags
71
+ - roles
72
+ - permissions
73
+ - endpoints
74
+ - endpointsActive
75
+ - generateMcpContract
76
+ - generateOpenApiContract
77
+ overridablePaths:
78
+ - environment.baseUrl
79
+ - environment.region
80
+ - authentication.oauth2
81
+ - authentication.apikey
82
+ - authentication.basic
83
+ - authentication.aad
84
+ - openapi.specUrl
85
+ - openapi.documentKey
86
+ - mcp.serverUrl
87
+ - mcp.toolPrefix
88
+ - configuration.items.value
89
+ - credentialIdOrKey
90
+
91
+ externalDataSource:
92
+ triggerPaths:
93
+ - key
94
+ - displayName
95
+ - description
96
+ - enabled
97
+ - systemKey
98
+ - entityType
99
+ - resourceType
100
+ - version
101
+ - metadataSchema
102
+ - fieldMappings
103
+ - exposed
104
+ - validation
105
+ - quality
106
+ - indexing
107
+ - context
108
+ - documentStorage
109
+ - portalInput
110
+ - capabilities
111
+ - execution
112
+ - config
113
+ - openapi
114
+ overridablePaths:
115
+ - sync
116
+ - sync.mode
117
+ - sync.schedule
118
+ - sync.batchSize
119
+ - sync.maxParallelRequests
120
+ - openapi.baseUrl
121
+ - openapi.resourcePath
@@ -156,6 +156,7 @@ module.exports = {
156
156
  checkImageExists,
157
157
  checkContainerRunning,
158
158
  stopAndRemoveContainer,
159
- logContainerStatus
159
+ logContainerStatus,
160
+ getContainerName
160
161
  };
161
162
 
@@ -458,7 +458,8 @@ async function generateDockerCompose(appName, appConfig, options) {
458
458
  const language = appConfig.build?.language || appConfig.language || 'typescript';
459
459
  const template = loadDockerComposeTemplate(language);
460
460
  const port = options.port || appConfig.port || 3000;
461
- const imageOverride = options.image || options.imageOverride;
461
+ const imageOverride = options.image || options.imageOverride ||
462
+ (options.tag ? `${getImageName(appConfig, appName)}:${options.tag}` : null);
462
463
  const { devId, idNum } = await getDeveloperIdAndNumeric();
463
464
  const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
464
465
  const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
@@ -75,7 +75,6 @@ const CATEGORIES = [
75
75
  { name: 'resolve', term: 'resolve <app>' },
76
76
  { name: 'json', term: 'json <app>' },
77
77
  { name: 'split-json', term: 'split-json <app>' },
78
- { name: 'genkey', term: 'genkey <app>' },
79
78
  { name: 'validate', term: 'validate <appOrFile>' },
80
79
  { name: 'diff', term: 'diff <file1> <file2>' }
81
80
  ]