@aifabrix/builder 2.37.5 → 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 (51) 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/commands/up-miso.js +2 -2
  21. package/lib/commands/wizard-core.js +39 -27
  22. package/lib/core/config.js +16 -1
  23. package/lib/core/secrets.js +42 -50
  24. package/lib/core/templates.js +2 -1
  25. package/lib/deployment/environment.js +32 -21
  26. package/lib/generator/builders.js +8 -3
  27. package/lib/generator/external-controller-manifest.js +5 -4
  28. package/lib/generator/index.js +16 -14
  29. package/lib/generator/split.js +1 -0
  30. package/lib/generator/wizard.js +4 -1
  31. package/lib/schema/application-schema.json +6 -2
  32. package/lib/schema/deployment-rules.yaml +121 -0
  33. package/lib/utils/app-run-containers.js +2 -1
  34. package/lib/utils/compose-generator.js +2 -1
  35. package/lib/utils/help-builder.js +0 -1
  36. package/lib/utils/image-version.js +209 -0
  37. package/lib/utils/paths.js +6 -3
  38. package/lib/utils/schema-loader.js +1 -1
  39. package/lib/utils/variable-transformer.js +1 -19
  40. package/lib/validation/external-manifest-validator.js +1 -1
  41. package/package.json +1 -1
  42. package/templates/applications/README.md.hbs +1 -3
  43. package/templates/applications/dataplane/Dockerfile +2 -2
  44. package/templates/applications/dataplane/README.md +1 -3
  45. package/templates/applications/dataplane/variables.yaml +5 -3
  46. package/templates/applications/keycloak/Dockerfile +3 -3
  47. package/templates/applications/keycloak/README.md +14 -4
  48. package/templates/applications/keycloak/env.template +14 -2
  49. package/templates/applications/keycloak/variables.yaml +1 -1
  50. package/templates/applications/miso-controller/README.md +1 -3
  51. package/templates/applications/miso-controller/env.template +64 -11
@@ -82,20 +82,8 @@ async function getEnvironmentAuth(controllerUrl) {
82
82
  * @returns {Promise<Object>} Deployment result
83
83
  * @throws {Error} If deployment fails
84
84
  */
85
- /**
86
- * Loads and validates environment deploy config from a JSON file
87
- * @param {string} configPath - Absolute or relative path to config JSON
88
- * @returns {Object} Valid deploy request { environmentConfig, dryRun? }
89
- * @throws {Error} If file missing, invalid JSON, or validation fails
90
- */
91
- function loadAndValidateEnvironmentDeployConfig(configPath) {
92
- const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
93
- if (!fs.existsSync(resolvedPath)) {
94
- throw new Error(
95
- `Environment config file not found: ${resolvedPath}\n` +
96
- 'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
97
- );
98
- }
85
+ /** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
86
+ function parseEnvironmentConfigFile(resolvedPath) {
99
87
  let raw;
100
88
  try {
101
89
  raw = fs.readFileSync(resolvedPath, 'utf8');
@@ -124,14 +112,21 @@ function loadAndValidateEnvironmentDeployConfig(configPath) {
124
112
  );
125
113
  }
126
114
  if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
127
- throw new Error(
128
- `"environmentConfig" must be an object. File: ${resolvedPath}`
129
- );
115
+ throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
130
116
  }
117
+ return parsed;
118
+ }
119
+
120
+ /**
121
+ * Validates parsed config against schema and returns deploy request.
122
+ * @param {Object} parsed - Parsed config object
123
+ * @param {string} resolvedPath - Path for error messages
124
+ * @returns {Object} { environmentConfig, dryRun? }
125
+ */
126
+ function validateEnvironmentDeployParsed(parsed, resolvedPath) {
131
127
  const ajv = new Ajv({ allErrors: true, strict: false });
132
128
  const validate = ajv.compile(environmentDeployRequestSchema);
133
- const valid = validate(parsed);
134
- if (!valid) {
129
+ if (!validate(parsed)) {
135
130
  const messages = formatValidationErrors(validate.errors);
136
131
  throw new Error(
137
132
  `Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
@@ -144,6 +139,24 @@ function loadAndValidateEnvironmentDeployConfig(configPath) {
144
139
  };
145
140
  }
146
141
 
142
+ /**
143
+ * Loads and validates environment deploy config from a JSON file
144
+ * @param {string} configPath - Absolute or relative path to config JSON
145
+ * @returns {Object} Valid deploy request { environmentConfig, dryRun? }
146
+ * @throws {Error} If file missing, invalid JSON, or validation fails
147
+ */
148
+ function loadAndValidateEnvironmentDeployConfig(configPath) {
149
+ const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
150
+ if (!fs.existsSync(resolvedPath)) {
151
+ throw new Error(
152
+ `Environment config file not found: ${resolvedPath}\n` +
153
+ 'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
154
+ );
155
+ }
156
+ const parsed = parseEnvironmentConfigFile(resolvedPath);
157
+ return validateEnvironmentDeployParsed(parsed, resolvedPath);
158
+ }
159
+
147
160
  /**
148
161
  * Builds environment deployment request from options (config file required)
149
162
  * @function buildEnvironmentDeploymentRequest
@@ -479,8 +492,6 @@ async function deployEnvironment(envKey, options = {}) {
479
492
  throw error;
480
493
  }
481
494
  }
482
-
483
495
  module.exports = {
484
496
  deployEnvironment
485
497
  };
486
-
@@ -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
  ]
@@ -0,0 +1,209 @@
1
+ /**
2
+ * AI Fabrix Builder - Image Version Resolution
3
+ *
4
+ * Resolves application version from Docker image (OCI label or semver tag).
5
+ * When template is empty or image version is greater, uses image version.
6
+ *
7
+ * @fileoverview Image version resolution utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const fs = require('fs').promises;
15
+ const fsSync = require('fs');
16
+ const path = require('path');
17
+ const yaml = require('js-yaml');
18
+ const { getBuilderPath } = require('./paths');
19
+ const composeGenerator = require('./compose-generator');
20
+ const containerHelpers = require('./app-run-containers');
21
+
22
+ const execAsync = promisify(exec);
23
+
24
+ const OCI_VERSION_LABEL = 'org.opencontainers.image.version';
25
+ const SEMVER_REGEX = /^v?(\d+\.\d+\.\d+)(?:-[-.\w]+)?(?:\+[-.\w]+)?$/i;
26
+
27
+ /**
28
+ * Gets version from Docker image via OCI label or semver tag
29
+ * @async
30
+ * @param {string} imageName - Image name (e.g. aifabrix/dataplane)
31
+ * @param {string} imageTag - Image tag (e.g. v1.0.0, latest)
32
+ * @returns {Promise<string|null>} Version string or null if not found
33
+ */
34
+ async function getVersionFromImage(imageName, imageTag) {
35
+ if (!imageName || typeof imageName !== 'string') {
36
+ return null;
37
+ }
38
+ const tag = imageTag || 'latest';
39
+ const fullImage = `${imageName}:${tag}`;
40
+
41
+ try {
42
+ const labelFormat = `{{index .Config.Labels "${OCI_VERSION_LABEL}"}}`;
43
+ const { stdout } = await execAsync(
44
+ `docker inspect --format '${labelFormat}' "${fullImage}" 2>/dev/null || true`,
45
+ { timeout: 10000 }
46
+ );
47
+ const labelValue = (stdout || '').trim();
48
+ if (labelValue && labelValue !== '<no value>') {
49
+ return labelValue;
50
+ }
51
+
52
+ const tagMatch = tag.match(SEMVER_REGEX);
53
+ if (tagMatch) {
54
+ return tagMatch[1];
55
+ }
56
+ return null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Compares two semantic versions
64
+ * @param {string} a - First version
65
+ * @param {string} b - Second version
66
+ * @returns {number} -1 if a < b, 0 if a === b, 1 if a > b
67
+ */
68
+ function compareSemver(a, b) {
69
+ if (!a || !b) {
70
+ return 0;
71
+ }
72
+ const parse = (v) => {
73
+ const m = String(v).match(SEMVER_REGEX);
74
+ if (!m) return null;
75
+ const parts = m[1].split('.').map(Number);
76
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
77
+ };
78
+ const pa = parse(a);
79
+ const pb = parse(b);
80
+ if (!pa || !pb) return 0;
81
+ if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1;
82
+ if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1;
83
+ if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1;
84
+ return 0;
85
+ }
86
+
87
+ /**
88
+ * Resolves version for external app (app.version or externalIntegration.version)
89
+ * @param {Object} variables - Parsed variables.yaml
90
+ * @returns {string}
91
+ */
92
+ function resolveExternalVersion(variables) {
93
+ const version =
94
+ variables.app?.version ||
95
+ variables.externalIntegration?.version ||
96
+ '1.0.0';
97
+ return String(version).trim() || '1.0.0';
98
+ }
99
+
100
+ /**
101
+ * Resolves version for regular app when image exists
102
+ * @async
103
+ * @param {string} imageName - Image name
104
+ * @param {string} imageTag - Image tag
105
+ * @param {string} templateVersion - Template version (may be empty)
106
+ * @returns {Promise<{ version: string, fromImage: boolean }>}
107
+ */
108
+ async function resolveRegularVersion(imageName, imageTag, templateVersion) {
109
+ const templateEmpty =
110
+ templateVersion === undefined ||
111
+ templateVersion === null ||
112
+ String(templateVersion).trim() === '';
113
+ const templateStr = String(templateVersion || '').trim();
114
+
115
+ const imageVersion = await getVersionFromImage(imageName, imageTag);
116
+ const useImage =
117
+ imageVersion &&
118
+ (templateEmpty || compareSemver(imageVersion, templateStr) >= 0);
119
+
120
+ const version = useImage ? imageVersion : (templateEmpty ? '1.0.0' : templateStr);
121
+ return { version, fromImage: Boolean(useImage) };
122
+ }
123
+
124
+ /**
125
+ * Resolves version for an app: from image when image exists and template empty or smaller
126
+ * @async
127
+ * @param {string} appName - Application name
128
+ * @param {Object} variables - Parsed variables.yaml
129
+ * @param {Object} [options] - Options
130
+ * @param {boolean} [options.updateBuilder] - When true, update builder/variables.yaml if fromImage
131
+ * @param {string} [options.builderPath] - Builder path (defaults to getBuilderPath(appName))
132
+ * @returns {Promise<{ version: string, fromImage: boolean, updated: boolean }>}
133
+ */
134
+ async function resolveVersionForApp(appName, variables, options = {}) {
135
+ if (!appName || typeof appName !== 'string') {
136
+ return { version: '1.0.0', fromImage: false, updated: false };
137
+ }
138
+
139
+ if (variables?.externalIntegration) {
140
+ const version = resolveExternalVersion(variables);
141
+ return { version, fromImage: false, updated: false };
142
+ }
143
+
144
+ const imageName = composeGenerator.getImageName(variables, appName);
145
+ const imageTag = variables?.image?.tag || 'latest';
146
+ const imageExists = await containerHelpers.checkImageExists(imageName, imageTag);
147
+
148
+ if (!imageExists) {
149
+ const templateVersion = variables?.app?.version;
150
+ const templateEmpty =
151
+ templateVersion === undefined ||
152
+ templateVersion === null ||
153
+ String(templateVersion).trim() === '';
154
+ const fallback = templateEmpty ? '1.0.0' : String(templateVersion).trim();
155
+ return { version: fallback, fromImage: false, updated: false };
156
+ }
157
+
158
+ const { version, fromImage } = await resolveRegularVersion(
159
+ imageName,
160
+ imageTag,
161
+ variables?.app?.version
162
+ );
163
+
164
+ let updated = false;
165
+ if (fromImage && options.updateBuilder) {
166
+ const builderPath = options.builderPath || getBuilderPath(appName);
167
+ updated = await updateAppVersionInVariablesYaml(builderPath, version);
168
+ }
169
+
170
+ return { version, fromImage, updated };
171
+ }
172
+
173
+ /**
174
+ * Updates app.version in builder variables.yaml
175
+ * @async
176
+ * @param {string} builderPath - Path to builder app directory
177
+ * @param {string} version - Version to set
178
+ * @returns {Promise<boolean>} True if file was updated
179
+ */
180
+ async function updateAppVersionInVariablesYaml(builderPath, version) {
181
+ if (!builderPath || !version || typeof version !== 'string') {
182
+ return false;
183
+ }
184
+ const variablesPath = path.join(builderPath, 'variables.yaml');
185
+ if (!fsSync.existsSync(variablesPath)) {
186
+ return false;
187
+ }
188
+
189
+ try {
190
+ const content = await fs.readFile(variablesPath, 'utf8');
191
+ const parsed = yaml.load(content) || {};
192
+ if (!parsed.app) {
193
+ parsed.app = {};
194
+ }
195
+ parsed.app.version = version;
196
+ const dumped = yaml.dump(parsed, { lineWidth: -1 });
197
+ await fs.writeFile(variablesPath, dumped, { mode: 0o644, encoding: 'utf8' });
198
+ return true;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ getVersionFromImage,
206
+ compareSemver,
207
+ resolveVersionForApp,
208
+ updateAppVersionInVariablesYaml
209
+ };
@@ -31,11 +31,15 @@ function safeHomedir() {
31
31
  }
32
32
 
33
33
  /**
34
- * Returns the path to the config file (AIFABRIX_HOME env or ~/.aifabrix).
35
- * Used so getAifabrixHome can read from the same location as config.js.
34
+ * Returns the path to the config directory (same precedence as config.js so both read the same config).
35
+ * Priority: AIFABRIX_CONFIG (dirname) AIFABRIX_HOME ~/.aifabrix.
36
36
  * @returns {string} Absolute path to config directory
37
37
  */
38
38
  function getConfigDirForPaths() {
39
+ const configFile = process.env.AIFABRIX_CONFIG && typeof process.env.AIFABRIX_CONFIG === 'string';
40
+ if (configFile) {
41
+ return path.dirname(path.resolve(process.env.AIFABRIX_CONFIG.trim()));
42
+ }
39
43
  if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
40
44
  return path.resolve(process.env.AIFABRIX_HOME.trim());
41
45
  }
@@ -480,7 +484,6 @@ async function detectAppType(appName, options = {}) {
480
484
  // Check builder folder (backward compatibility)
481
485
  return checkBuilderFolder(appName);
482
486
  }
483
-
484
487
  module.exports = {
485
488
  getAifabrixHome,
486
489
  getConfigDirForPaths,
@@ -199,7 +199,7 @@ function detectFromDatasourceFields(parsed) {
199
199
  * @returns {string|null} Schema type or null if not detected
200
200
  */
201
201
  function detectFromApplicationFields(parsed) {
202
- if (parsed.deploymentKey || (parsed.image && parsed.registryMode && parsed.port)) {
202
+ if (parsed.image && parsed.registryMode && parsed.port) {
203
203
  return 'application';
204
204
  }
205
205
  return null;