@aifabrix/builder 2.39.3 → 2.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.cursor/rules/project-rules.mdc +6 -6
  2. package/README.md +3 -3
  3. package/babel.config.js +6 -0
  4. package/integration/hubspot/README.md +53 -141
  5. package/integration/hubspot/application.yaml +37 -0
  6. package/integration/hubspot/env.template +2 -11
  7. package/integration/hubspot/hubspot-deploy.json +1 -0
  8. package/integration/hubspot/test.js +5 -5
  9. package/jest.config.manual.js +29 -0
  10. package/lib/api/credentials.api.js +5 -5
  11. package/lib/api/deployments.api.js +2 -2
  12. package/lib/api/pipeline.api.js +17 -17
  13. package/lib/api/wizard.api.js +2 -2
  14. package/lib/app/config.js +11 -6
  15. package/lib/app/deploy-config.js +13 -16
  16. package/lib/app/deploy.js +29 -22
  17. package/lib/app/display.js +1 -1
  18. package/lib/app/dockerfile.js +11 -12
  19. package/lib/app/helpers.js +51 -13
  20. package/lib/app/index.js +14 -2
  21. package/lib/app/prompts.js +37 -45
  22. package/lib/app/push.js +8 -11
  23. package/lib/app/readme.js +16 -12
  24. package/lib/app/register.js +1 -1
  25. package/lib/app/run-helpers.js +31 -22
  26. package/lib/app/run.js +44 -5
  27. package/lib/app/show-display.js +104 -44
  28. package/lib/app/show.js +123 -43
  29. package/lib/build/index.js +11 -18
  30. package/lib/cli/setup-app.js +36 -29
  31. package/lib/cli/setup-auth.js +19 -15
  32. package/lib/cli/setup-credential-deployment.js +3 -1
  33. package/lib/cli/setup-external-system.js +35 -16
  34. package/lib/cli/setup-infra.js +45 -23
  35. package/lib/cli/setup-utility.js +85 -31
  36. package/lib/commands/app-logs.js +28 -20
  37. package/lib/commands/app.js +30 -26
  38. package/lib/commands/auth-status.js +36 -3
  39. package/lib/commands/convert.js +202 -0
  40. package/lib/commands/credential-list.js +78 -17
  41. package/lib/commands/datasource.js +24 -24
  42. package/lib/commands/deployment-list.js +13 -6
  43. package/lib/commands/up-common.js +80 -42
  44. package/lib/commands/up-dataplane.js +15 -14
  45. package/lib/commands/up-miso.js +15 -14
  46. package/lib/commands/upload.js +163 -0
  47. package/lib/commands/wizard-core.js +5 -4
  48. package/lib/core/diff.js +84 -9
  49. package/lib/core/key-generator.js +9 -12
  50. package/lib/core/secrets-docker-env.js +2 -2
  51. package/lib/core/secrets.js +3 -2
  52. package/lib/core/templates.js +2 -2
  53. package/lib/datasource/deploy.js +2 -1
  54. package/lib/deployment/deployer.js +76 -48
  55. package/lib/external-system/delete.js +0 -1
  56. package/lib/external-system/deploy-helpers.js +5 -6
  57. package/lib/external-system/deploy.js +7 -2
  58. package/lib/external-system/download-helpers.js +4 -4
  59. package/lib/external-system/download.js +11 -10
  60. package/lib/external-system/generator.js +19 -17
  61. package/lib/external-system/test.js +10 -15
  62. package/lib/generator/builders.js +1 -1
  63. package/lib/generator/external-controller-manifest.js +26 -29
  64. package/lib/generator/external-schema-utils.js +6 -18
  65. package/lib/generator/external.js +32 -27
  66. package/lib/generator/github.js +1 -1
  67. package/lib/generator/helpers.js +12 -19
  68. package/lib/generator/index.js +15 -15
  69. package/lib/generator/parse-image.js +35 -0
  70. package/lib/generator/split-readme.js +105 -0
  71. package/lib/generator/split-variables.js +149 -0
  72. package/lib/generator/split.js +86 -246
  73. package/lib/generator/wizard.js +51 -70
  74. package/lib/schema/application-schema.json +4 -4
  75. package/lib/schema/external-datasource.schema.json +5 -0
  76. package/lib/schema/external-system.schema.json +10 -0
  77. package/lib/utils/app-config-resolver.js +52 -0
  78. package/lib/utils/app-register-api.js +1 -1
  79. package/lib/utils/app-register-auth.js +1 -1
  80. package/lib/utils/app-register-config.js +16 -23
  81. package/lib/utils/app-register-validator.js +2 -2
  82. package/lib/utils/cli-utils.js +47 -3
  83. package/lib/utils/config-format.js +154 -0
  84. package/lib/utils/config-paths.js +19 -52
  85. package/lib/utils/config-tokens.js +1 -0
  86. package/lib/utils/docker-build.js +71 -94
  87. package/lib/utils/dockerfile-utils.js +1 -1
  88. package/lib/utils/env-copy.js +4 -4
  89. package/lib/utils/env-ports.js +2 -2
  90. package/lib/utils/error-formatter.js +1 -1
  91. package/lib/utils/error-formatters/validation-errors.js +1 -1
  92. package/lib/utils/external-readme.js +12 -5
  93. package/lib/utils/external-system-test-helpers.js +2 -0
  94. package/lib/utils/health-check.js +55 -66
  95. package/lib/utils/image-version.js +12 -21
  96. package/lib/utils/paths.js +45 -66
  97. package/lib/utils/port-resolver.js +8 -8
  98. package/lib/utils/schema-loader.js +22 -0
  99. package/lib/utils/schema-resolver.js +23 -33
  100. package/lib/utils/secrets-helpers.js +7 -7
  101. package/lib/utils/secrets-utils.js +10 -12
  102. package/lib/utils/template-helpers.js +13 -13
  103. package/lib/utils/token-manager.js +20 -2
  104. package/lib/utils/variable-transformer.js +2 -2
  105. package/lib/validation/validate-display.js +3 -4
  106. package/lib/validation/validate.js +34 -28
  107. package/lib/validation/validator.js +50 -30
  108. package/package.json +4 -1
  109. package/templates/README.md +1 -1
  110. package/templates/applications/README.md.hbs +3 -3
  111. package/templates/applications/miso-controller/env.template +3 -1
  112. package/templates/external-system/README.md.hbs +4 -4
  113. package/templates/external-system/external-system.json.hbs +1 -16
  114. package/integration/hubspot/variables.yaml +0 -17
  115. /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
  116. /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
  117. /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
@@ -10,24 +10,27 @@
10
10
 
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
- const yaml = require('js-yaml');
14
13
  const chalk = require('chalk');
15
14
  const logger = require('../utils/logger');
16
15
  const pathsUtil = require('../utils/paths');
16
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
17
+ const { isYamlPath } = require('../utils/config-format');
17
18
  const { copyTemplateFiles } = require('../validation/template');
18
19
  const { ensureReadmeForAppPath, ensureReadmeForApp } = require('../app/readme');
19
20
 
20
21
  /**
21
- * Copy template to a target path if variables.yaml is missing there.
22
+ * Copy template to a target path if application config is missing there.
22
23
  * After copy, generates README.md from templates/applications/README.md.hbs.
23
24
  * @param {string} appName - Application name
24
25
  * @param {string} targetAppPath - Target directory (e.g. builder/keycloak)
25
26
  * @returns {Promise<boolean>} True if template was copied, false if already present
26
27
  */
27
28
  async function ensureTemplateAtPath(appName, targetAppPath) {
28
- const variablesPath = path.join(targetAppPath, 'variables.yaml');
29
- if (fs.existsSync(variablesPath)) {
29
+ try {
30
+ pathsUtil.resolveApplicationConfigPath(targetAppPath);
30
31
  return false;
32
+ } catch {
33
+ // No application config; copy template
31
34
  }
32
35
  await copyTemplateFiles(appName, targetAppPath);
33
36
  await ensureReadmeForAppPath(targetAppPath, appName);
@@ -37,18 +40,55 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
37
40
  /**
38
41
  * Resolve the directory (folder) that would contain the .env file for envOutputPath.
39
42
  * @param {string} envOutputPath - Value from build.envOutputPath (e.g. ../../.env)
40
- * @param {string} variablesPath - Path to variables.yaml
43
+ * @param {string} configPath - Path to application config file
41
44
  * @returns {string} Absolute path to the folder that would contain the output .env file
42
45
  */
43
- function getEnvOutputPathFolder(envOutputPath, variablesPath) {
44
- const variablesDir = path.dirname(variablesPath);
45
- const resolvedFile = path.resolve(variablesDir, envOutputPath);
46
+ function getEnvOutputPathFolder(envOutputPath, configPath) {
47
+ const configDir = path.dirname(configPath);
48
+ const resolvedFile = path.resolve(configDir, envOutputPath);
46
49
  return path.dirname(resolvedFile);
47
50
  }
48
51
 
49
52
  /**
50
- * Validates envOutputPath: if the target folder does not exist, patches variables.yaml to set envOutputPath to null.
53
+ * Patches envOutputPath to null in raw YAML content so comments and formatting are preserved.
54
+ * Only touches the line that sets envOutputPath under build.
55
+ *
56
+ * @param {string} content - Raw file content (YAML)
57
+ * @returns {string|null} Patched content, or null if no change
58
+ */
59
+ function patchEnvOutputPathInYamlContent(content) {
60
+ const re = /^(\s*)envOutputPath\s*:\s*.+$/m;
61
+ const match = content.match(re);
62
+ if (!match) return null;
63
+ const indent = match[1];
64
+ return content.replace(re, `${indent}envOutputPath: null`);
65
+ }
66
+
67
+ /**
68
+ * Patches config file to set build.envOutputPath to null, preserving comments (YAML only).
69
+ * For JSON or when in-place patch fails, falls back to full write.
70
+ *
71
+ * @param {string} configPath - Path to application config file
72
+ * @returns {boolean} True if file was modified
73
+ */
74
+ function patchEnvOutputPathInFile(configPath) {
75
+ if (!isYamlPath(configPath)) {
76
+ const variables = loadConfigFile(configPath);
77
+ const updated = { ...variables, build: { ...(variables.build || {}), envOutputPath: null } };
78
+ writeConfigFile(configPath, updated);
79
+ return true;
80
+ }
81
+ const content = fs.readFileSync(configPath, 'utf8');
82
+ const patched = patchEnvOutputPathInYamlContent(content);
83
+ if (patched === null) return false;
84
+ fs.writeFileSync(configPath, patched, 'utf8');
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Validates envOutputPath: if the target folder does not exist, patches application config to set envOutputPath to null.
51
90
  * Used by up-platform, up-miso, up-dataplane so we do not keep a path that points outside an existing tree.
91
+ * Patches in place for YAML to preserve comments.
52
92
  *
53
93
  * @param {string} appName - Application name (e.g. keycloak, miso-controller, dataplane)
54
94
  */
@@ -59,48 +99,44 @@ function validateEnvOutputPathFolderOrNull(appName) {
59
99
  if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
60
100
  pathsToPatch.push(cwdBuilderPath);
61
101
  }
62
- const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
63
- const replacement = '$1 null # deploy only, no copy';
64
102
  for (const appPath of pathsToPatch) {
65
- const variablesPath = path.join(appPath, 'variables.yaml');
66
- if (!fs.existsSync(variablesPath)) continue;
103
+ let configPath;
104
+ try {
105
+ configPath = pathsUtil.resolveApplicationConfigPath(appPath);
106
+ } catch {
107
+ continue;
108
+ }
67
109
  try {
68
- const content = fs.readFileSync(variablesPath, 'utf8');
69
- const variables = yaml.load(content);
110
+ const variables = loadConfigFile(configPath);
70
111
  const value = variables?.build?.envOutputPath;
71
112
  if (value === null || value === undefined || value === '') continue;
72
- const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
113
+ const folder = getEnvOutputPathFolder(String(value).trim(), configPath);
73
114
  if (fs.existsSync(folder)) continue;
74
- const newContent = content.replace(envOutputPathLine, replacement);
75
- fs.writeFileSync(variablesPath, newContent, 'utf8');
115
+ patchEnvOutputPathInFile(configPath);
76
116
  } catch (err) {
77
- logger.warn(chalk.yellow(`Could not validate envOutputPath in ${variablesPath}: ${err.message}`));
117
+ logger.warn(chalk.yellow(`Could not validate envOutputPath in ${configPath}: ${err.message}`));
78
118
  }
79
119
  }
80
120
  }
81
121
 
82
122
  /**
83
- * Patches a single variables.yaml to set build.envOutputPath to null for deploy-only.
123
+ * Patches a single application config file to set build.envOutputPath to null for deploy-only.
124
+ * Only writes when a change is needed (value is set and target folder does not exist).
125
+ * Uses in-place YAML patch when possible to preserve comments.
84
126
  *
85
- * @param {string} variablesPath - Path to variables.yaml
86
- * @param {RegExp} envOutputPathLine - Regex for envOutputPath line
87
- * @param {string} replacement - Replacement string
127
+ * @param {string} configPath - Path to application config file
88
128
  */
89
- function patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement) {
90
- const content = fs.readFileSync(variablesPath, 'utf8');
91
- if (!envOutputPathLine.test(content)) return;
92
- const variables = yaml.load(content);
129
+ function patchOneVariablesFileForDeployOnly(configPath) {
130
+ const variables = loadConfigFile(configPath);
93
131
  const value = variables?.build?.envOutputPath;
94
- if (value !== null && value !== undefined && value !== '') {
95
- const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
96
- if (fs.existsSync(folder)) return;
97
- }
98
- const newContent = content.replace(envOutputPathLine, replacement);
99
- fs.writeFileSync(variablesPath, newContent, 'utf8');
132
+ if (value === null || value === undefined || value === '') return;
133
+ const folder = getEnvOutputPathFolder(String(value).trim(), configPath);
134
+ if (fs.existsSync(folder)) return;
135
+ patchEnvOutputPathInFile(configPath);
100
136
  }
101
137
 
102
138
  /**
103
- * Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
139
+ * Patches application config to set build.envOutputPath to null for deploy-only (no local code).
104
140
  * Only patches when the target folder does NOT exist; when folder exists, keeps the value.
105
141
  * Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
106
142
  * Patches both primary builder path and cwd/builder if different.
@@ -114,22 +150,24 @@ function patchEnvOutputPathForDeployOnly(appName) {
114
150
  if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
115
151
  pathsToPatch.push(cwdBuilderPath);
116
152
  }
117
- const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
118
- const replacement = '$1 null # deploy only, no copy';
119
153
  for (const appPath of pathsToPatch) {
120
- const variablesPath = path.join(appPath, 'variables.yaml');
121
- if (!fs.existsSync(variablesPath)) continue;
154
+ let configPath;
155
+ try {
156
+ configPath = pathsUtil.resolveApplicationConfigPath(appPath);
157
+ } catch {
158
+ continue;
159
+ }
122
160
  try {
123
- patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement);
161
+ patchOneVariablesFileForDeployOnly(configPath);
124
162
  } catch (err) {
125
- logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
163
+ logger.warn(chalk.yellow(`Could not patch envOutputPath in ${configPath}: ${err.message}`));
126
164
  }
127
165
  }
128
166
  }
129
167
 
130
168
  /**
131
- * Ensures builder app directory exists from template if variables.yaml is missing.
132
- * If builder/<appName>/variables.yaml does not exist, copies from templates/applications/<appName>.
169
+ * Ensures builder app directory exists from template if application config is missing.
170
+ * If builder/<appName>/application config does not exist, copies from templates/applications/<appName>.
133
171
  * Uses AIFABRIX_BUILDER_DIR when set (e.g. by up-miso/up-dataplane from config aifabrix-env-config).
134
172
  * When using a custom builder dir, also populates cwd/builder/<appName> so the repo's builder/ is not empty.
135
173
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Always local deployment: registers or rotates dataplane app in dev, sends
5
5
  * deployment manifest to Miso Controller, then runs the dataplane app locally
6
- * (same as aifabrix deploy dataplane --deployment=local). If app is already
6
+ * (same as aifabrix deploy dataplane --local). If app is already
7
7
  * registered, uses rotate-secret; otherwise registers.
8
8
  *
9
9
  * @fileoverview up-dataplane command implementation
@@ -11,10 +11,9 @@
11
11
  * @version 2.0.0
12
12
  */
13
13
 
14
- const path = require('path');
15
- const fs = require('fs');
16
- const yaml = require('js-yaml');
17
14
  const chalk = require('chalk');
15
+ const pathsUtil = require('../utils/paths');
16
+ const { loadConfigFile } = require('../utils/config-format');
18
17
  const logger = require('../utils/logger');
19
18
  const config = require('../core/config');
20
19
  const { checkAuthentication } = require('../utils/app-register-auth');
@@ -47,20 +46,22 @@ async function registerOrRotateDataplane(options, controllerUrl, environmentKey,
47
46
  }
48
47
 
49
48
  /**
50
- * Build full image ref from registry and dataplane variables (registry/name:tag)
49
+ * Build full image ref from registry and dataplane config (registry/name:tag)
51
50
  * @param {string} registry - Registry URL
52
51
  * @returns {string|undefined} Full image reference or undefined
53
52
  */
54
53
  function buildDataplaneImageRef(registry) {
55
- const pathsUtil = require('../utils/paths');
56
- const variablesPath = path.join(pathsUtil.getBuilderPath('dataplane'), 'variables.yaml');
57
- if (!fs.existsSync(variablesPath)) return undefined;
58
- const content = fs.readFileSync(variablesPath, 'utf8');
59
- const variables = yaml.load(content);
60
- const name = variables?.image?.name || variables?.app?.key || 'dataplane';
61
- const tag = variables?.image?.tag || 'latest';
62
- const base = (registry || '').replace(/\/+$/, '');
63
- return base ? `${base}/${name}:${tag}` : undefined;
54
+ try {
55
+ const builderPath = pathsUtil.getBuilderPath('dataplane');
56
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
57
+ const variables = loadConfigFile(configPath);
58
+ const name = variables?.image?.name || variables?.app?.key || 'dataplane';
59
+ const tag = variables?.image?.tag || 'latest';
60
+ const base = (registry || '').replace(/\/+$/, '');
61
+ return base ? `${base}/${name}:${tag}` : undefined;
62
+ } catch {
63
+ return undefined;
64
+ }
64
65
  }
65
66
 
66
67
  /**
@@ -9,10 +9,9 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const path = require('path');
13
- const fs = require('fs');
14
- const yaml = require('js-yaml');
15
12
  const chalk = require('chalk');
13
+ const pathsUtil = require('../utils/paths');
14
+ const { loadConfigFile } = require('../utils/config-format');
16
15
  const logger = require('../utils/logger');
17
16
  const config = require('../core/config');
18
17
  const secrets = require('../core/secrets');
@@ -21,7 +20,7 @@ const app = require('../app');
21
20
  const { saveLocalSecret } = require('../utils/local-secrets');
22
21
  const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly, validateEnvOutputPathFolderOrNull } = require('./up-common');
23
22
 
24
- /** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
23
+ /** Keycloak base port (from templates/applications/keycloak application config) */
25
24
  const KEYCLOAK_BASE_PORT = 8082;
26
25
  /** Miso controller base port (dev-config app base) */
27
26
  const MISO_BASE_PORT = 3000;
@@ -46,21 +45,23 @@ function parseImageOptions(imageOpts) {
46
45
  }
47
46
 
48
47
  /**
49
- * Build full image ref from registry and app variables (registry/name:tag)
48
+ * Build full image ref from registry and app config (registry/name:tag)
50
49
  * @param {string} appName - keycloak or miso-controller
51
50
  * @param {string} registry - Registry URL
52
51
  * @returns {string} Full image reference
53
52
  */
54
53
  function buildImageRefFromRegistry(appName, registry) {
55
- const pathsUtil = require('../utils/paths');
56
- const variablesPath = path.join(pathsUtil.getBuilderPath(appName), 'variables.yaml');
57
- if (!fs.existsSync(variablesPath)) return undefined;
58
- const content = fs.readFileSync(variablesPath, 'utf8');
59
- const variables = yaml.load(content);
60
- const name = variables?.image?.name || variables?.app?.key || appName;
61
- const tag = variables?.image?.tag || 'latest';
62
- const base = (registry || '').replace(/\/+$/, '');
63
- return base ? `${base}/${name}:${tag}` : undefined;
54
+ try {
55
+ const builderPath = pathsUtil.getBuilderPath(appName);
56
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
57
+ const variables = loadConfigFile(configPath);
58
+ const name = variables?.image?.name || variables?.app?.key || appName;
59
+ const tag = variables?.image?.tag || 'latest';
60
+ const base = (registry || '').replace(/\/+$/, '');
61
+ return base ? `${base}/${name}:${tag}` : undefined;
62
+ } catch {
63
+ return undefined;
64
+ }
64
65
  }
65
66
 
66
67
  /**
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Upload external system to dataplane (upload → validate → publish).
3
+ *
4
+ * @fileoverview Upload command handler for aifabrix upload <system-key>
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+ const { resolveControllerUrl } = require('../utils/controller-url');
12
+ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
13
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
14
+ const { validateExternalSystemComplete } = require('../validation/validate');
15
+ const { displayValidationResults } = require('../validation/validate-display');
16
+ const { generateControllerManifest } = require('../generator/external-controller-manifest');
17
+ const {
18
+ uploadApplicationViaPipeline,
19
+ validateUploadViaPipeline,
20
+ publishUploadViaPipeline
21
+ } = require('../api/pipeline.api');
22
+ const { formatApiError } = require('../utils/api-error-handler');
23
+
24
+ /**
25
+ * Validates system-key format (same as download).
26
+ * @param {string} systemKey - System key
27
+ * @throws {Error} If invalid
28
+ */
29
+ function validateSystemKeyFormat(systemKey) {
30
+ if (!systemKey || typeof systemKey !== 'string') {
31
+ throw new Error('System key is required and must be a string');
32
+ }
33
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
34
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Builds pipeline upload payload from controller manifest.
40
+ * Payload: { version, application, dataSources }; application = system with RBAC.
41
+ * @param {Object} manifest - Controller manifest from generateControllerManifest
42
+ * @returns {Object} { version, application, dataSources }
43
+ */
44
+ function buildUploadPayload(manifest) {
45
+ return {
46
+ version: manifest.version || '1.0.0',
47
+ application: manifest.system,
48
+ dataSources: manifest.dataSources || []
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Resolves dataplane URL and auth (same pattern as download).
54
+ * @param {string} systemKey - System key
55
+ * @param {Object} options - Options with optional dataplane override
56
+ * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }>}
57
+ */
58
+ async function resolveDataplaneAndAuth(systemKey, options) {
59
+ const { resolveEnvironment } = require('../core/config');
60
+ const environment = await resolveEnvironment();
61
+ const controllerUrl = await resolveControllerUrl();
62
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
63
+
64
+ if (!authConfig.token && !authConfig.clientId) {
65
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <system-key>" first.');
66
+ }
67
+
68
+ let dataplaneUrl;
69
+ if (options.dataplane) {
70
+ dataplaneUrl = options.dataplane.replace(/\/$/, '');
71
+ } else {
72
+ logger.log(chalk.blue('Resolving dataplane URL...'));
73
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
74
+ }
75
+
76
+ return { dataplaneUrl, authConfig, environment };
77
+ }
78
+
79
+ /**
80
+ * Runs upload → validate → publish on the dataplane.
81
+ * @param {string} dataplaneUrl - Dataplane base URL
82
+ * @param {Object} authConfig - Auth config
83
+ * @param {Object} payload - { version, application, dataSources }
84
+ * @returns {Promise<{ uploadId: string }>}
85
+ */
86
+ async function runUploadValidatePublish(dataplaneUrl, authConfig, payload) {
87
+ const uploadRes = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
88
+ const uploadId = uploadRes?.data?.uploadId ?? uploadRes?.data?.id ?? uploadRes?.uploadId;
89
+ if (!uploadId) {
90
+ const msg = uploadRes?.success === false
91
+ ? formatApiError(uploadRes, dataplaneUrl)
92
+ : 'Upload did not return an upload ID';
93
+ throw new Error(msg);
94
+ }
95
+
96
+ const validateRes = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
97
+ if (validateRes?.success === false) {
98
+ const msg = formatApiError(validateRes, dataplaneUrl);
99
+ throw new Error(`Upload validation failed: ${msg}`);
100
+ }
101
+
102
+ await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
103
+ return { uploadId };
104
+ }
105
+
106
+ /**
107
+ * Throws if validation result is invalid (displays results first).
108
+ * @param {Object} validationResult - Result from validateExternalSystemComplete
109
+ * @throws {Error} If validationResult.valid is false
110
+ */
111
+ function throwIfValidationFailed(validationResult) {
112
+ if (!validationResult.valid) {
113
+ displayValidationResults(validationResult);
114
+ throw new Error('Validation failed. Fix errors before uploading.');
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Uploads external system to dataplane (upload → validate → publish). No controller deploy.
120
+ * @param {string} systemKey - External system key (integration/<system-key>/)
121
+ * @param {Object} [options] - Options
122
+ * @param {boolean} [options.dryRun] - Validate and build payload only; no API calls
123
+ * @param {string} [options.dataplane] - Override dataplane URL
124
+ * @returns {Promise<void>}
125
+ * @throws {Error} If validation or API calls fail
126
+ */
127
+ async function uploadExternalSystem(systemKey, options = {}) {
128
+ validateSystemKeyFormat(systemKey);
129
+
130
+ logger.log(chalk.blue(`\nUploading external system to dataplane: ${systemKey}`));
131
+
132
+ const validationResult = await validateExternalSystemComplete(systemKey, { type: 'external' });
133
+ throwIfValidationFailed(validationResult);
134
+ logger.log(chalk.green('Validation passed.'));
135
+
136
+ const manifest = await generateControllerManifest(systemKey, { type: 'external' });
137
+ const payload = buildUploadPayload(manifest);
138
+
139
+ if (options.dryRun) {
140
+ logger.log(chalk.yellow('Dry run: would upload payload (no API calls).'));
141
+ logger.log(chalk.gray(` System: ${manifest.key}, version: ${payload.version}, datasources: ${payload.dataSources.length}`));
142
+ return;
143
+ }
144
+
145
+ const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey, options);
146
+ requireBearerForDataplanePipeline(authConfig);
147
+ logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
148
+
149
+ await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
150
+
151
+ logger.log(chalk.green('\nUpload validated and published to dataplane.'));
152
+ logger.log(chalk.blue(`Environment: ${environment}`));
153
+ logger.log(chalk.blue(`System: ${systemKey}`));
154
+ logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
155
+ }
156
+
157
+ module.exports = {
158
+ uploadExternalSystem,
159
+ buildUploadPayload,
160
+ resolveDataplaneAndAuth,
161
+ runUploadValidatePublish,
162
+ validateSystemKeyFormat
163
+ };
@@ -339,7 +339,7 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
339
339
  }
340
340
 
341
341
  /**
342
- * Fetches deployment docs and writes README.md when variables.yaml and deploy JSON are available.
342
+ * Fetches deployment docs and writes README.md when application config and deploy JSON are available.
343
343
  * @async
344
344
  * @param {string} appPath - Application path
345
345
  * @param {string} appName - Application name
@@ -348,12 +348,13 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
348
348
  * @param {string} systemKey - System key
349
349
  */
350
350
  async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
351
- const variablesPath = path.join(appPath, 'variables.yaml');
351
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
352
352
  const deployPath = path.join(appPath, `${appName}-deploy.json`);
353
353
  let variablesYaml = null;
354
354
  let deployJson = null;
355
355
  try {
356
- variablesYaml = await fs.readFile(variablesPath, 'utf8');
356
+ const configPath = resolveApplicationConfigPath(appPath);
357
+ variablesYaml = await fs.readFile(configPath, 'utf8');
357
358
  } catch {
358
359
  // optional
359
360
  }
@@ -372,7 +373,7 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
372
373
  if (content && typeof content === 'string') {
373
374
  const readmePath = path.join(appPath, 'README.md');
374
375
  await fs.writeFile(readmePath, content, 'utf8');
375
- logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
376
+ logger.log(chalk.gray(' Updated README.md from deployment-docs API (application config + deploy JSON).'));
376
377
  }
377
378
  }
378
379
 
package/lib/core/diff.js CHANGED
@@ -13,6 +13,10 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+ const { detectSchemaTypeFromParsed, loadExternalSystemSchema, loadExternalDataSourceSchema } = require('../utils/schema-loader');
18
+ const { validateObjectAgainstApplicationSchema } = require('../validation/validator');
19
+ const { formatValidationErrors } = require('../utils/error-formatter');
16
20
 
17
21
  /**
18
22
  * Handle added field in comparison
@@ -169,19 +173,22 @@ function identifyBreakingChanges(comparison) {
169
173
  }
170
174
 
171
175
  /**
172
- * Compares two configuration files
173
- * Loads files, parses JSON, and performs deep comparison
176
+ * Compares two configuration files.
177
+ * Both files must be the same config type (app, system, or datasource).
178
+ * By default validates both against their schema; pass { validate: false } to skip.
174
179
  *
175
180
  * @async
176
181
  * @function compareFiles
177
182
  * @param {string} file1 - Path to first file
178
183
  * @param {string} file2 - Path to second file
184
+ * @param {Object} [options] - Options
185
+ * @param {boolean} [options.validate=true] - If true, validate both files against their schema
179
186
  * @returns {Promise<Object>} Comparison result with differences
180
- * @throws {Error} If files cannot be read or parsed
187
+ * @throws {Error} If files cannot be read, parsed, types differ, or validation fails
181
188
  *
182
189
  * @example
183
190
  * const result = await compareFiles('./old.json', './new.json');
184
- * // Returns: { identical: false, added: [...], removed: [...], changed: [...] }
191
+ * const resultNoValidate = await compareFiles('./a.yaml', './b.yaml', { validate: false });
185
192
  */
186
193
  /**
187
194
  * Validates file paths
@@ -206,21 +213,70 @@ function validateFilePaths(file1, file2) {
206
213
  }
207
214
 
208
215
  /**
209
- * Reads and parses a JSON file
216
+ * Reads and parses a config file (JSON or YAML by extension: .json, .yaml, .yml).
210
217
  * @function readAndParseFile
211
218
  * @param {string} filePath - File path
212
- * @returns {Object} Parsed JSON object
219
+ * @returns {Object} Parsed object
213
220
  * @throws {Error} If file cannot be read or parsed
214
221
  */
215
222
  function readAndParseFile(filePath) {
216
223
  try {
217
- const content = fs.readFileSync(filePath, 'utf8');
218
- return JSON.parse(content);
224
+ return loadConfigFile(filePath);
219
225
  } catch (error) {
220
226
  throw new Error(`Failed to parse ${filePath}: ${error.message}`);
221
227
  }
222
228
  }
223
229
 
230
+ /**
231
+ * Maps schema type to user-facing label (app | system | datasource).
232
+ * @param {string} schemaType - 'application' | 'external-system' | 'external-datasource'
233
+ * @returns {string} 'app' | 'system' | 'datasource'
234
+ */
235
+ function toUserFacingType(schemaType) {
236
+ const map = {
237
+ application: 'app',
238
+ 'external-system': 'system',
239
+ 'external-datasource': 'datasource'
240
+ };
241
+ return map[schemaType] || 'app';
242
+ }
243
+
244
+ /**
245
+ * Runs external schema validator and returns error messages or null.
246
+ * @param {Function} validateFn - AJV validate function
247
+ * @param {Object} parsed - Parsed config object
248
+ * @returns {string[]|null} Error messages or null if valid
249
+ */
250
+ function getValidationErrors(validateFn, parsed) {
251
+ const valid = validateFn(parsed);
252
+ if (valid) return null;
253
+ return formatValidationErrors(validateFn.errors);
254
+ }
255
+
256
+ /**
257
+ * Validates parsed object against the schema for the given type.
258
+ * @param {Object} parsed - Parsed config object
259
+ * @param {string} schemaType - 'application' | 'external-system' | 'external-datasource'
260
+ * @param {string} filePath - File path (for error messages)
261
+ * @throws {Error} If validation fails
262
+ */
263
+ function validateParsedForType(parsed, schemaType, filePath) {
264
+ let messages = [];
265
+ if (schemaType === 'application') {
266
+ const result = validateObjectAgainstApplicationSchema(parsed);
267
+ if (!result.valid) messages = result.errors;
268
+ } else if (schemaType === 'external-system') {
269
+ messages = getValidationErrors(loadExternalSystemSchema(), parsed) || [];
270
+ } else if (schemaType === 'external-datasource') {
271
+ messages = getValidationErrors(loadExternalDataSourceSchema(), parsed) || [];
272
+ } else {
273
+ throw new Error(`Unknown schema type: ${schemaType}`);
274
+ }
275
+ if (messages.length > 0) {
276
+ throw new Error(`Validation failed for ${filePath}: ${messages.join('; ')}`);
277
+ }
278
+ }
279
+
224
280
  /**
225
281
  * Extracts version from parsed object
226
282
  * @function extractVersion
@@ -267,12 +323,31 @@ function buildComparisonResult(comparison, parsed1, parsed2, file1, file2) {
267
323
  };
268
324
  }
269
325
 
270
- async function compareFiles(file1, file2) {
326
+ async function compareFiles(file1, file2, options = {}) {
327
+ const shouldValidate = options.validate !== false;
328
+
271
329
  validateFilePaths(file1, file2);
272
330
 
273
331
  const parsed1 = readAndParseFile(file1);
274
332
  const parsed2 = readAndParseFile(file2);
275
333
 
334
+ const type1 = detectSchemaTypeFromParsed(parsed1, file1);
335
+ const type2 = detectSchemaTypeFromParsed(parsed2, file2);
336
+ const userType1 = toUserFacingType(type1);
337
+ const userType2 = toUserFacingType(type2);
338
+
339
+ if (userType1 !== userType2) {
340
+ throw new Error(
341
+ `Type mismatch: ${file1} is ${userType1} config and ${file2} is ${userType2} config. ` +
342
+ 'Both files must be the same type (app, system, or datasource).'
343
+ );
344
+ }
345
+
346
+ if (shouldValidate) {
347
+ validateParsedForType(parsed1, type1, file1);
348
+ validateParsedForType(parsed2, type2, file2);
349
+ }
350
+
276
351
  const comparison = compareObjects(parsed1, parsed2);
277
352
  return buildComparisonResult(comparison, parsed1, parsed2, file1, file2);
278
353
  }