@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
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Variables YAML extraction for deployment JSON split.
3
+ * Extracts application.yaml (variables) structure from deployment JSON.
4
+ *
5
+ * @fileoverview Split variables extraction for deployment JSON reverse conversion
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const { parseImageReference } = require('./parse-image');
11
+
12
+ function extractAppSection(deployment) {
13
+ if (!deployment.key && !deployment.displayName && !deployment.description && !deployment.type) {
14
+ return undefined;
15
+ }
16
+ const app = {};
17
+ if (deployment.key) app.key = deployment.key;
18
+ if (deployment.displayName) app.displayName = deployment.displayName;
19
+ if (deployment.description) app.description = deployment.description;
20
+ if (deployment.type) app.type = deployment.type;
21
+ if (deployment.version) app.version = deployment.version;
22
+ return app;
23
+ }
24
+
25
+ function extractImageSection(deployment) {
26
+ if (!deployment.image) return undefined;
27
+ const imageParts = parseImageReference(deployment.image);
28
+ const image = {};
29
+ if (imageParts.name) image.name = imageParts.name;
30
+ if (imageParts.registry) image.registry = imageParts.registry;
31
+ if (imageParts.tag) image.tag = imageParts.tag;
32
+ if (deployment.registryMode) image.registryMode = deployment.registryMode;
33
+ return image;
34
+ }
35
+
36
+ function extractRequirementsSection(deployment) {
37
+ if (!deployment.requiresDatabase && !deployment.requiresRedis && !deployment.requiresStorage && !deployment.databases) {
38
+ return undefined;
39
+ }
40
+ const requires = {};
41
+ if (deployment.requiresDatabase !== undefined) requires.database = deployment.requiresDatabase;
42
+ if (deployment.requiresRedis !== undefined) requires.redis = deployment.requiresRedis;
43
+ if (deployment.requiresStorage !== undefined) requires.storage = deployment.requiresStorage;
44
+ if (deployment.databases) requires.databases = deployment.databases;
45
+ return requires;
46
+ }
47
+
48
+ function extractOptionalSection(deployment, sectionName, optional) {
49
+ if (!deployment[sectionName]) return;
50
+ optional[sectionName] = sectionName === 'authentication'
51
+ ? { ...deployment[sectionName] }
52
+ : deployment[sectionName];
53
+ }
54
+
55
+ function extractOptionalSections(deployment) {
56
+ const optional = {};
57
+ const names = [
58
+ 'healthCheck', 'authentication', 'build', 'repository', 'deployment',
59
+ 'startupCommand', 'runtimeVersion', 'scaling', 'frontDoorRouting'
60
+ ];
61
+ for (const sectionName of names) {
62
+ extractOptionalSection(deployment, sectionName, optional);
63
+ }
64
+ return optional;
65
+ }
66
+
67
+ /**
68
+ * Portal-only configuration for application.yaml (name + portalInput per entry).
69
+ * @param {Array} configuration - Configuration array from deployment JSON
70
+ * @returns {Array<{ name: string, portalInput: Object }>}
71
+ */
72
+ function extractPortalInputConfiguration(configuration) {
73
+ if (!Array.isArray(configuration) || configuration.length === 0) return [];
74
+ return configuration
75
+ .filter(item => item && item.portalInput && typeof item.name === 'string')
76
+ .map(item => ({ name: item.name, portalInput: item.portalInput }));
77
+ }
78
+
79
+ /**
80
+ * Datasource filename for externalIntegration.dataSources.
81
+ * @param {string} systemKey - System key
82
+ * @param {Object} datasource - Datasource from deployment.dataSources
83
+ * @param {number} index - Index
84
+ * @returns {string} Filename e.g. test-hubspot-datasource-companies-data.yaml
85
+ */
86
+ function getExternalDatasourceFileName(systemKey, datasource, index) {
87
+ const key = datasource.key || '';
88
+ let suffix;
89
+ if (key.startsWith(`${systemKey}-deploy-`)) suffix = key.slice(`${systemKey}-deploy-`.length);
90
+ else if (key.startsWith(`${systemKey}-`)) suffix = key.slice(systemKey.length + 1);
91
+ else if (key) suffix = key;
92
+ else suffix = datasource.entityType || datasource.entityKey || `entity${index + 1}`;
93
+ return `${systemKey}-datasource-${suffix}.yaml`;
94
+ }
95
+
96
+ function extractVariablesYamlForExternal(deployment) {
97
+ const system = deployment.system;
98
+ const systemKey = system.key || 'external-system';
99
+ const dataSourcesList = deployment.dataSources || deployment.datasources || [];
100
+ const variables = {
101
+ app: {
102
+ key: systemKey,
103
+ displayName: system.displayName || systemKey,
104
+ description: system.description || `External system integration for ${systemKey}`,
105
+ type: 'external'
106
+ },
107
+ externalIntegration: {
108
+ schemaBasePath: './',
109
+ systems: [`${systemKey}-system.yaml`],
110
+ dataSources: dataSourcesList.map((ds, i) => getExternalDatasourceFileName(systemKey, ds, i)),
111
+ autopublish: true,
112
+ version: '1.0.0'
113
+ }
114
+ };
115
+ const portalOnlyConfig = extractPortalInputConfiguration(deployment.configuration);
116
+ if (portalOnlyConfig.length > 0) variables.configuration = portalOnlyConfig;
117
+ return variables;
118
+ }
119
+
120
+ /**
121
+ * Extracts deployment JSON into application config (variables) structure.
122
+ * @param {Object} deployment - Deployment JSON object
123
+ * @returns {Object} Variables YAML structure
124
+ */
125
+ function extractVariablesYaml(deployment) {
126
+ if (!deployment || typeof deployment !== 'object') {
127
+ throw new Error('Deployment object is required');
128
+ }
129
+ if (deployment.system && typeof deployment.system === 'object') {
130
+ return extractVariablesYamlForExternal(deployment);
131
+ }
132
+ const variables = {};
133
+ const appSection = extractAppSection(deployment);
134
+ if (appSection) variables.app = appSection;
135
+ const imageSection = extractImageSection(deployment);
136
+ if (imageSection) variables.image = imageSection;
137
+ if (deployment.port !== undefined) variables.port = deployment.port;
138
+ const requirementsSection = extractRequirementsSection(deployment);
139
+ if (requirementsSection) variables.requires = requirementsSection;
140
+ Object.assign(variables, extractOptionalSections(deployment));
141
+ const portalOnlyConfig = extractPortalInputConfiguration(deployment.configuration);
142
+ if (portalOnlyConfig.length > 0) variables.configuration = portalOnlyConfig;
143
+ return variables;
144
+ }
145
+
146
+ module.exports = {
147
+ extractVariablesYaml,
148
+ getExternalDatasourceFileName
149
+ };
@@ -11,6 +11,9 @@
11
11
  const fs = require('fs').promises;
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
+ const { parseImageReference } = require('./parse-image');
15
+ const { generateReadmeFromDeployJson } = require('./split-readme');
16
+ const { extractVariablesYaml, getExternalDatasourceFileName } = require('./split-variables');
14
17
 
15
18
  /**
16
19
  * Converts configuration array back to env.template format
@@ -43,191 +46,6 @@ function extractEnvTemplate(configuration) {
43
46
  return lines.join('\n');
44
47
  }
45
48
 
46
- /**
47
- * Parses image reference string into components
48
- * @function parseImageReference
49
- * @param {string} imageString - Full image string (e.g., "registry/name:tag")
50
- * @returns {Object} Object with registry, name, and tag
51
- */
52
- function parseImageReference(imageString) {
53
- if (!imageString || typeof imageString !== 'string') {
54
- return { registry: null, name: null, tag: 'latest' };
55
- }
56
-
57
- // Handle format: registry/name:tag or name:tag or registry/name
58
- const parts = imageString.split('/');
59
- let registry = null;
60
- let nameAndTag = imageString;
61
-
62
- if (parts.length > 1) {
63
- // Check if first part looks like a registry (contains .)
64
- if (parts[0].includes('.')) {
65
- registry = parts[0];
66
- nameAndTag = parts.slice(1).join('/');
67
- } else {
68
- // No registry, just name:tag
69
- nameAndTag = imageString;
70
- }
71
- }
72
-
73
- // Split name and tag
74
- const tagIndex = nameAndTag.lastIndexOf(':');
75
- let name = nameAndTag;
76
- let tag = 'latest';
77
-
78
- if (tagIndex !== -1) {
79
- name = nameAndTag.substring(0, tagIndex);
80
- tag = nameAndTag.substring(tagIndex + 1);
81
- }
82
-
83
- return { registry, name, tag };
84
- }
85
-
86
- /**
87
- * Extract app section from deployment
88
- * @param {Object} deployment - Deployment JSON object
89
- * @returns {Object|undefined} App section or undefined
90
- */
91
- function extractAppSection(deployment) {
92
- if (!deployment.key && !deployment.displayName && !deployment.description && !deployment.type) {
93
- return undefined;
94
- }
95
-
96
- const app = {};
97
- if (deployment.key) app.key = deployment.key;
98
- if (deployment.displayName) app.displayName = deployment.displayName;
99
- if (deployment.description) app.description = deployment.description;
100
- if (deployment.type) app.type = deployment.type;
101
- if (deployment.version) app.version = deployment.version;
102
- return app;
103
- }
104
-
105
- /**
106
- * Extract image section from deployment
107
- * @param {Object} deployment - Deployment JSON object
108
- * @returns {Object|undefined} Image section or undefined
109
- */
110
- function extractImageSection(deployment) {
111
- if (!deployment.image) {
112
- return undefined;
113
- }
114
-
115
- const imageParts = parseImageReference(deployment.image);
116
- const image = {};
117
- if (imageParts.name) image.name = imageParts.name;
118
- if (imageParts.registry) image.registry = imageParts.registry;
119
- if (imageParts.tag) image.tag = imageParts.tag;
120
- if (deployment.registryMode) image.registryMode = deployment.registryMode;
121
- return image;
122
- }
123
-
124
- /**
125
- * Extract requirements section from deployment
126
- * @param {Object} deployment - Deployment JSON object
127
- * @returns {Object|undefined} Requirements section or undefined
128
- */
129
- function extractRequirementsSection(deployment) {
130
- if (!deployment.requiresDatabase && !deployment.requiresRedis && !deployment.requiresStorage && !deployment.databases) {
131
- return undefined;
132
- }
133
-
134
- const requires = {};
135
- if (deployment.requiresDatabase !== undefined) requires.database = deployment.requiresDatabase;
136
- if (deployment.requiresRedis !== undefined) requires.redis = deployment.requiresRedis;
137
- if (deployment.requiresStorage !== undefined) requires.storage = deployment.requiresStorage;
138
- if (deployment.databases) requires.databases = deployment.databases;
139
- return requires;
140
- }
141
-
142
- /**
143
- * Extract optional sections from deployment
144
- * @param {Object} deployment - Deployment JSON object
145
- * @returns {Object} Object with optional sections
146
- */
147
- /**
148
- * Extracts a single optional section if present
149
- * @function extractOptionalSection
150
- * @param {Object} deployment - Deployment object
151
- * @param {string} sectionName - Section name to extract
152
- * @param {Object} optional - Optional sections object to update
153
- */
154
- function extractOptionalSection(deployment, sectionName, optional) {
155
- if (deployment[sectionName]) {
156
- if (sectionName === 'authentication') {
157
- optional[sectionName] = { ...deployment[sectionName] };
158
- } else {
159
- optional[sectionName] = deployment[sectionName];
160
- }
161
- }
162
- }
163
-
164
- function extractOptionalSections(deployment) {
165
- const optional = {};
166
-
167
- const optionalSectionNames = [
168
- 'healthCheck',
169
- 'authentication',
170
- 'build',
171
- 'repository',
172
- 'deployment',
173
- 'startupCommand',
174
- 'runtimeVersion',
175
- 'scaling',
176
- 'frontDoorRouting',
177
- 'roles',
178
- 'permissions'
179
- ];
180
-
181
- for (const sectionName of optionalSectionNames) {
182
- extractOptionalSection(deployment, sectionName, optional);
183
- }
184
-
185
- return optional;
186
- }
187
-
188
- /**
189
- * Extracts deployment JSON into variables.yaml structure
190
- * @function extractVariablesYaml
191
- * @param {Object} deployment - Deployment JSON object
192
- * @returns {Object} Variables YAML structure
193
- */
194
- function extractVariablesYaml(deployment) {
195
- if (!deployment || typeof deployment !== 'object') {
196
- throw new Error('Deployment object is required');
197
- }
198
-
199
- const variables = {};
200
-
201
- // Extract app section
202
- const appSection = extractAppSection(deployment);
203
- if (appSection) {
204
- variables.app = appSection;
205
- }
206
-
207
- // Extract image section
208
- const imageSection = extractImageSection(deployment);
209
- if (imageSection) {
210
- variables.image = imageSection;
211
- }
212
-
213
- // Extract port
214
- if (deployment.port !== undefined) {
215
- variables.port = deployment.port;
216
- }
217
-
218
- // Extract requirements section
219
- const requirementsSection = extractRequirementsSection(deployment);
220
- if (requirementsSection) {
221
- variables.requires = requirementsSection;
222
- }
223
-
224
- // Extract optional sections
225
- const optionalSections = extractOptionalSections(deployment);
226
- Object.assign(variables, optionalSections);
227
-
228
- return variables;
229
- }
230
-
231
49
  /**
232
50
  * Extracts roles and permissions from deployment JSON
233
51
  * @function extractRbacYaml
@@ -257,62 +75,6 @@ function extractRbacYaml(deployment) {
257
75
  return rbac;
258
76
  }
259
77
 
260
- /**
261
- * Generates README.md content from deployment JSON
262
- * @function generateReadmeFromDeployJson
263
- * @param {Object} deployment - Deployment JSON object
264
- * @returns {string} README.md content
265
- */
266
- function generateReadmeFromDeployJson(deployment) {
267
- if (!deployment || typeof deployment !== 'object') {
268
- throw new Error('Deployment object is required');
269
- }
270
-
271
- const appName = deployment.key || 'application';
272
- const displayName = deployment.displayName || appName;
273
- const description = deployment.description || 'Application deployment configuration';
274
- const port = deployment.port || 3000;
275
- const image = deployment.image || 'unknown';
276
-
277
- const lines = [
278
- `# ${displayName}`,
279
- '',
280
- description,
281
- '',
282
- '## Quick Start',
283
- '',
284
- 'This application is configured via deployment JSON and component files.',
285
- '',
286
- '## Configuration',
287
- '',
288
- `- **Application Key**: \`${appName}\``,
289
- `- **Port**: \`${port}\``,
290
- `- **Image**: \`${image}\``,
291
- '',
292
- '## Files',
293
- '',
294
- '- `variables.yaml` - Application configuration',
295
- '- `env.template` - Environment variables template',
296
- '- `rbac.yml` - Roles and permissions (if applicable)',
297
- '- `README.md` - This file',
298
- '',
299
- '## Documentation',
300
- '',
301
- 'For more information, see the [AI Fabrix Builder documentation](../../docs/README.md).'
302
- ];
303
-
304
- return lines.join('\n');
305
- }
306
-
307
- /**
308
- * Splits a deployment JSON file into component files
309
- * @async
310
- * @function splitDeployJson
311
- * @param {string} deployJsonPath - Path to deployment JSON file
312
- * @param {string} [outputDir] - Directory to write component files (defaults to same directory as JSON)
313
- * @returns {Promise<Object>} Object with paths to generated files
314
- * @throws {Error} If JSON file not found or invalid
315
- */
316
78
  /**
317
79
  * Validates deployment JSON path
318
80
  * @function validateDeployJsonPath
@@ -396,8 +158,8 @@ async function writeComponentFiles(outputDir, envTemplate, variables, rbac, read
396
158
  await writeComponentFile(envTemplatePath, envTemplate);
397
159
  results.envTemplate = envTemplatePath;
398
160
 
399
- // Write variables.yaml
400
- const variablesPath = path.join(outputDir, 'variables.yaml');
161
+ // Write application.yaml
162
+ const variablesPath = path.join(outputDir, 'application.yaml');
401
163
  const variablesYaml = yaml.dump(variables, { indent: 2, lineWidth: -1 });
402
164
  await writeComponentFile(variablesPath, variablesYaml);
403
165
  results.variables = variablesPath;
@@ -418,18 +180,96 @@ async function writeComponentFiles(outputDir, envTemplate, variables, rbac, read
418
180
  return results;
419
181
  }
420
182
 
183
+ /**
184
+ * Writes external system and datasource YAML files when deployment has system (external format).
185
+ * @async
186
+ * @param {string} outputDir - Output directory
187
+ * @param {Object} deployment - Deployment with system and dataSources
188
+ * @returns {Promise<{ systemFile?: string, datasourceFiles?: string[] }>} Paths to written files
189
+ */
190
+ async function writeExternalSystemAndDatasourceFiles(outputDir, deployment) {
191
+ if (!deployment || !deployment.system) {
192
+ return {};
193
+ }
194
+ const system = deployment.system;
195
+ const systemKey = system.key || 'external-system';
196
+ const dataSourcesList = deployment.dataSources || deployment.datasources || [];
197
+ const results = {};
198
+
199
+ const systemPath = path.join(outputDir, `${systemKey}-system.yaml`);
200
+ const systemYaml = yaml.dump(system, { indent: 2, lineWidth: -1 });
201
+ await writeComponentFile(systemPath, systemYaml);
202
+ results.systemFile = systemPath;
203
+
204
+ const datasourcePaths = [];
205
+ for (let i = 0; i < dataSourcesList.length; i++) {
206
+ const ds = dataSourcesList[i];
207
+ const fileName = getExternalDatasourceFileName(systemKey, ds, i);
208
+ const dsPath = path.join(outputDir, fileName);
209
+ const dsYaml = yaml.dump(ds, { indent: 2, lineWidth: -1 });
210
+ await writeComponentFile(dsPath, dsYaml);
211
+ datasourcePaths.push(dsPath);
212
+ }
213
+ results.datasourceFiles = datasourcePaths;
214
+
215
+ return results;
216
+ }
217
+
218
+ /**
219
+ * Normalizes deployment for split: for external format (deployment.system),
220
+ * lifts configuration and roles/permissions to top level so extractors work.
221
+ * @param {Object} deployment - Raw deployment object
222
+ * @returns {Object} Deployment (mutated) with configuration/roles/permissions at top level when from system
223
+ */
224
+ function normalizeDeploymentForSplit(deployment) {
225
+ if (!deployment || !deployment.system) {
226
+ return deployment;
227
+ }
228
+ const system = deployment.system;
229
+ if (system.configuration && !deployment.configuration) {
230
+ deployment.configuration = system.configuration;
231
+ }
232
+ if (system.roles && !deployment.roles) {
233
+ deployment.roles = system.roles;
234
+ }
235
+ if (system.permissions && !deployment.permissions) {
236
+ deployment.permissions = system.permissions;
237
+ }
238
+ return deployment;
239
+ }
240
+
241
+ /**
242
+ * Splits a deployment JSON file into component files.
243
+ * @async
244
+ * @function splitDeployJson
245
+ * @param {string} deployJsonPath - Path to deployment JSON file
246
+ * @param {string} [outputDir] - Directory to write component files (defaults to same directory as JSON)
247
+ * @returns {Promise<Object>} Object with paths to generated files
248
+ * @throws {Error} If JSON file not found or invalid
249
+ */
421
250
  async function splitDeployJson(deployJsonPath, outputDir = null) {
422
251
  validateDeployJsonPath(deployJsonPath);
423
252
  const finalOutputDir = await prepareOutputDirectory(deployJsonPath, outputDir);
424
253
  const deployment = await loadDeploymentJson(deployJsonPath);
254
+ normalizeDeploymentForSplit(deployment);
425
255
 
426
- // Extract components
427
- const envTemplate = extractEnvTemplate(deployment.configuration || []);
256
+ const configArray = deployment.configuration || [];
257
+ const envTemplate = extractEnvTemplate(configArray);
428
258
  const variables = extractVariablesYaml(deployment);
429
259
  const rbac = extractRbacYaml(deployment);
430
260
  const readme = generateReadmeFromDeployJson(deployment);
431
261
 
432
- return await writeComponentFiles(finalOutputDir, envTemplate, variables, rbac, readme);
262
+ const result = await writeComponentFiles(finalOutputDir, envTemplate, variables, rbac, readme);
263
+
264
+ if (deployment.system && typeof deployment.system === 'object') {
265
+ const externalFiles = await writeExternalSystemAndDatasourceFiles(finalOutputDir, deployment);
266
+ if (externalFiles.systemFile) result.systemFile = externalFiles.systemFile;
267
+ if (externalFiles.datasourceFiles && externalFiles.datasourceFiles.length > 0) {
268
+ result.datasourceFiles = externalFiles.datasourceFiles;
269
+ }
270
+ }
271
+
272
+ return result;
433
273
  }
434
274
 
435
275
  module.exports = {