@aifabrix/builder 2.22.2 → 2.31.1

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 (63) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +210 -80
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secure.js +59 -24
  16. package/lib/config.js +79 -45
  17. package/lib/datasource-deploy.js +89 -29
  18. package/lib/deployer.js +164 -129
  19. package/lib/diff.js +63 -21
  20. package/lib/environment-deploy.js +36 -19
  21. package/lib/external-system-deploy.js +134 -66
  22. package/lib/external-system-download.js +244 -171
  23. package/lib/external-system-test.js +199 -164
  24. package/lib/generator-external.js +145 -72
  25. package/lib/generator-helpers.js +49 -17
  26. package/lib/generator-split.js +105 -58
  27. package/lib/infra.js +101 -131
  28. package/lib/schema/application-schema.json +895 -896
  29. package/lib/schema/env-config.yaml +11 -4
  30. package/lib/template-validator.js +13 -4
  31. package/lib/utils/api.js +8 -8
  32. package/lib/utils/app-register-auth.js +36 -18
  33. package/lib/utils/app-run-containers.js +140 -0
  34. package/lib/utils/auth-headers.js +6 -6
  35. package/lib/utils/build-copy.js +60 -2
  36. package/lib/utils/build-helpers.js +94 -0
  37. package/lib/utils/cli-utils.js +177 -76
  38. package/lib/utils/compose-generator.js +12 -2
  39. package/lib/utils/config-tokens.js +151 -9
  40. package/lib/utils/deployment-errors.js +137 -69
  41. package/lib/utils/deployment-validation-helpers.js +103 -0
  42. package/lib/utils/docker-build.js +57 -0
  43. package/lib/utils/dockerfile-utils.js +13 -3
  44. package/lib/utils/env-copy.js +163 -94
  45. package/lib/utils/env-map.js +226 -86
  46. package/lib/utils/error-formatters/network-errors.js +0 -1
  47. package/lib/utils/external-system-display.js +14 -19
  48. package/lib/utils/external-system-env-helpers.js +107 -0
  49. package/lib/utils/external-system-test-helpers.js +144 -0
  50. package/lib/utils/health-check.js +10 -8
  51. package/lib/utils/infra-status.js +123 -0
  52. package/lib/utils/paths.js +228 -49
  53. package/lib/utils/schema-loader.js +125 -57
  54. package/lib/utils/token-manager.js +3 -3
  55. package/lib/utils/yaml-preserve.js +55 -16
  56. package/lib/validate.js +87 -89
  57. package/package.json +7 -5
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/scripts/install-local.js +210 -0
  61. package/templates/applications/miso-controller/test.yaml +1 -0
  62. package/templates/python/Dockerfile.hbs +8 -45
  63. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -55,19 +55,13 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
55
55
  }
56
56
 
57
57
  /**
58
- * Deploys datasource to dataplane
59
- *
60
- * @async
61
- * @function deployDatasource
58
+ * Validate deployment inputs
62
59
  * @param {string} appKey - Application key
63
- * @param {string} filePath - Path to datasource JSON file
64
- * @param {Object} options - Deployment options
65
- * @param {string} options.controller - Controller URL
66
- * @param {string} options.environment - Environment key
67
- * @returns {Promise<Object>} Deployment result
68
- * @throws {Error} If deployment fails
60
+ * @param {string} filePath - File path
61
+ * @param {Object} options - Options
62
+ * @throws {Error} If validation fails
69
63
  */
70
- async function deployDatasource(appKey, filePath, options) {
64
+ function validateDeploymentInputs(appKey, filePath, options) {
71
65
  if (!appKey || typeof appKey !== 'string') {
72
66
  throw new Error('Application key is required');
73
67
  }
@@ -80,10 +74,16 @@ async function deployDatasource(appKey, filePath, options) {
80
74
  if (!options.environment) {
81
75
  throw new Error('Environment is required (-e, --environment)');
82
76
  }
77
+ }
83
78
 
84
- logger.log(chalk.blue('📋 Deploying datasource...\n'));
85
-
86
- // Validate datasource file
79
+ /**
80
+ * Validate and load datasource file
81
+ * @async
82
+ * @param {string} filePath - Path to datasource file
83
+ * @returns {Promise<Object>} Datasource configuration
84
+ * @throws {Error} If validation or loading fails
85
+ */
86
+ async function validateAndLoadDatasourceFile(filePath) {
87
87
  logger.log(chalk.blue('🔍 Validating datasource file...'));
88
88
  const validation = await validateDatasourceFile(filePath);
89
89
  if (!validation.valid) {
@@ -95,32 +95,45 @@ async function deployDatasource(appKey, filePath, options) {
95
95
  }
96
96
  logger.log(chalk.green('✓ Datasource file is valid'));
97
97
 
98
- // Load datasource configuration
99
98
  const content = fs.readFileSync(filePath, 'utf8');
100
- let datasourceConfig;
101
99
  try {
102
- datasourceConfig = JSON.parse(content);
100
+ return JSON.parse(content);
103
101
  } catch (error) {
104
102
  throw new Error(`Failed to parse datasource file: ${error.message}`);
105
103
  }
104
+ }
106
105
 
107
- // Extract systemKey
108
- const systemKey = datasourceConfig.systemKey;
109
- if (!systemKey) {
110
- throw new Error('systemKey is required in datasource configuration');
111
- }
112
-
113
- // Get authentication
106
+ /**
107
+ * Setup authentication and get dataplane URL
108
+ * @async
109
+ * @param {string} controllerUrl - Controller URL
110
+ * @param {string} environment - Environment key
111
+ * @param {string} appKey - Application key
112
+ * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
113
+ */
114
+ async function setupDeploymentAuth(controllerUrl, environment, appKey) {
114
115
  logger.log(chalk.blue('🔐 Getting authentication...'));
115
- const authConfig = await getDeploymentAuth(options.controller, options.environment, appKey);
116
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, appKey);
116
117
  logger.log(chalk.green('✓ Authentication successful'));
117
118
 
118
- // Get dataplane URL from controller
119
119
  logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
120
- const dataplaneUrl = await getDataplaneUrl(options.controller, appKey, options.environment, authConfig);
120
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, appKey, environment, authConfig);
121
121
  logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
122
122
 
123
- // Publish to dataplane using pipeline workflow endpoint
123
+ return { authConfig, dataplaneUrl };
124
+ }
125
+
126
+ /**
127
+ * Publish datasource to dataplane
128
+ * @async
129
+ * @param {string} dataplaneUrl - Dataplane URL
130
+ * @param {string} systemKey - System key
131
+ * @param {Object} authConfig - Authentication configuration
132
+ * @param {Object} datasourceConfig - Datasource configuration
133
+ * @returns {Promise<Object>} Publish response
134
+ * @throws {Error} If publish fails
135
+ */
136
+ async function publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig) {
124
137
  logger.log(chalk.blue('\n🚀 Publishing datasource to dataplane...'));
125
138
 
126
139
  const publishResponse = await publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig);
@@ -133,9 +146,56 @@ async function deployDatasource(appKey, filePath, options) {
133
146
  }
134
147
 
135
148
  logger.log(chalk.green('\n✓ Datasource published successfully!'));
149
+ return publishResponse;
150
+ }
151
+
152
+ /**
153
+ * Display deployment results
154
+ * @param {Object} datasourceConfig - Datasource configuration
155
+ * @param {string} systemKey - System key
156
+ * @param {string} environment - Environment key
157
+ */
158
+ function displayDeploymentResults(datasourceConfig, systemKey, environment) {
136
159
  logger.log(chalk.blue(`\nDatasource: ${datasourceConfig.key || datasourceConfig.displayName}`));
137
160
  logger.log(chalk.blue(`System: ${systemKey}`));
138
- logger.log(chalk.blue(`Environment: ${options.environment}`));
161
+ logger.log(chalk.blue(`Environment: ${environment}`));
162
+ }
163
+
164
+ /**
165
+ * Deploys datasource to dataplane
166
+ *
167
+ * @async
168
+ * @function deployDatasource
169
+ * @param {string} appKey - Application key
170
+ * @param {string} filePath - Path to datasource JSON file
171
+ * @param {Object} options - Deployment options
172
+ * @param {string} options.controller - Controller URL
173
+ * @param {string} options.environment - Environment key
174
+ * @returns {Promise<Object>} Deployment result
175
+ * @throws {Error} If deployment fails
176
+ */
177
+ async function deployDatasource(appKey, filePath, options) {
178
+ validateDeploymentInputs(appKey, filePath, options);
179
+
180
+ logger.log(chalk.blue('📋 Deploying datasource...\n'));
181
+
182
+ // Validate and load datasource file
183
+ const datasourceConfig = await validateAndLoadDatasourceFile(filePath);
184
+
185
+ // Extract systemKey
186
+ const systemKey = datasourceConfig.systemKey;
187
+ if (!systemKey) {
188
+ throw new Error('systemKey is required in datasource configuration');
189
+ }
190
+
191
+ // Setup authentication and get dataplane URL
192
+ const { authConfig, dataplaneUrl } = await setupDeploymentAuth(options.controller, options.environment, appKey);
193
+
194
+ // Publish to dataplane
195
+ await publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig);
196
+
197
+ // Display results
198
+ displayDeploymentResults(datasourceConfig, systemKey, options.environment);
139
199
 
140
200
  return {
141
201
  success: true,
package/lib/deployer.js CHANGED
@@ -15,25 +15,18 @@ const logger = require('./utils/logger');
15
15
  const { validateControllerUrl, validateEnvironmentKey } = require('./utils/deployment-validation');
16
16
  const { handleDeploymentError, handleDeploymentErrors } = require('./utils/deployment-errors');
17
17
  const { validatePipeline, deployPipeline, getPipelineDeployment } = require('./api/pipeline.api');
18
+ const { handleValidationResponse } = require('./utils/deployment-validation-helpers');
18
19
 
19
20
  /**
20
- * Validates deployment configuration via validate endpoint
21
- * This is the first step in the deployment process
22
- *
21
+ * Build validation data for deployment
23
22
  * @async
24
- * @param {string} url - Controller URL
25
- * @param {string} envKey - Environment key (miso, dev, tst, pro)
26
- * @param {Object} manifest - Deployment manifest (applicationConfig)
23
+ * @param {Object} manifest - Application manifest/config
24
+ * @param {string} validatedEnvKey - Validated environment key
27
25
  * @param {Object} authConfig - Authentication configuration
28
- * @param {Object} options - Validation options (repositoryUrl, timeout, retries, etc.)
29
- * @returns {Promise<Object>} Validation result with validateToken
30
- * @throws {Error} If validation fails
26
+ * @param {Object} options - Additional options
27
+ * @returns {Promise<Object>} Object with validationData and pipelineAuthConfig
31
28
  */
32
- async function validateDeployment(url, envKey, manifest, authConfig, options = {}) {
33
- const validatedEnvKey = validateEnvironmentKey(envKey);
34
- const maxRetries = options.maxRetries || 3;
35
-
36
- // Extract clientId and clientSecret
29
+ async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
37
30
  const tokenManager = require('./utils/token-manager');
38
31
  const { clientId, clientSecret } = await tokenManager.extractClientCredentials(
39
32
  authConfig,
@@ -42,7 +35,6 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
42
35
  options
43
36
  );
44
37
 
45
- // Build validation request
46
38
  const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
47
39
  const validationData = {
48
40
  clientId: clientId,
@@ -51,58 +43,40 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
51
43
  applicationConfig: manifest
52
44
  };
53
45
 
54
- // Use centralized API client with retry logic
55
46
  const pipelineAuthConfig = {
56
47
  type: 'client-credentials',
57
48
  clientId: clientId,
58
49
  clientSecret: clientSecret
59
50
  };
60
51
 
52
+ return { validationData, pipelineAuthConfig };
53
+ }
54
+
55
+ /**
56
+ * Validates deployment configuration via validate endpoint
57
+ * This is the first step in the deployment process
58
+ *
59
+ * @async
60
+ * @param {string} url - Controller URL
61
+ * @param {string} envKey - Environment key (miso, dev, tst, pro)
62
+ * @param {Object} manifest - Deployment manifest (applicationConfig)
63
+ * @param {Object} authConfig - Authentication configuration
64
+ * @param {Object} options - Validation options (repositoryUrl, timeout, retries, etc.)
65
+ * @returns {Promise<Object>} Validation result with validateToken
66
+ * @throws {Error} If validation fails
67
+ */
68
+ async function validateDeployment(url, envKey, manifest, authConfig, options = {}) {
69
+ const validatedEnvKey = validateEnvironmentKey(envKey);
70
+ const maxRetries = options.maxRetries || 3;
71
+
72
+ // Build validation data
73
+ const { validationData, pipelineAuthConfig } = await buildValidationData(manifest, validatedEnvKey, authConfig, options);
74
+
61
75
  let lastError;
62
76
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
63
77
  try {
64
78
  const response = await validatePipeline(url, validatedEnvKey, pipelineAuthConfig, validationData);
65
-
66
- // Handle successful validation (200 OK with valid: true)
67
- if (response.success && response.data) {
68
- const responseData = response.data.data || response.data;
69
- if (responseData.valid === true) {
70
- return {
71
- success: true,
72
- validateToken: responseData.validateToken,
73
- draftDeploymentId: responseData.draftDeploymentId,
74
- imageServer: responseData.imageServer,
75
- imageUsername: responseData.imageUsername,
76
- imagePassword: responseData.imagePassword,
77
- expiresAt: responseData.expiresAt
78
- };
79
- }
80
- // Handle validation failure (valid: false)
81
- if (responseData.valid === false) {
82
- const errorMessage = responseData.errors && responseData.errors.length > 0
83
- ? `Validation failed: ${responseData.errors.join(', ')}`
84
- : 'Validation failed: Invalid configuration';
85
- const error = new Error(errorMessage);
86
- error.status = 400;
87
- error.data = responseData;
88
- throw error;
89
- }
90
- }
91
-
92
- // Handle validation errors (non-success responses)
93
- if (!response.success) {
94
- const error = new Error(`Validation request failed: ${response.formattedError || response.error || 'Unknown error'}`);
95
- error.status = response.status || 400;
96
- error.data = response.data;
97
- throw error;
98
- }
99
-
100
- // If we get here, response.success is true but valid is not true and not false
101
- // This is an unexpected state, throw an error
102
- const error = new Error('Validation response is in an unexpected state');
103
- error.status = 400;
104
- error.data = response.data;
105
- throw error;
79
+ return handleValidationResponse(response);
106
80
  } catch (error) {
107
81
  lastError = error;
108
82
  const shouldRetry = attempt < maxRetries && error.status && error.status >= 500;
@@ -119,6 +93,83 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
119
93
  throw lastError;
120
94
  }
121
95
 
96
+ /**
97
+ * Validate client credentials for deployment
98
+ * @param {Object} authConfig - Authentication configuration
99
+ * @throws {Error} If credentials are missing
100
+ */
101
+ function validateDeploymentCredentials(authConfig) {
102
+ if (!authConfig.clientId || !authConfig.clientSecret) {
103
+ throw new Error('Client ID and Client Secret are required for deployment. These should have been loaded during validation.');
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Build deployment data and auth config
109
+ * @param {string} validateToken - Validation token
110
+ * @param {Object} authConfig - Authentication configuration
111
+ * @param {Object} options - Deployment options
112
+ * @returns {Object} Object with deployData and pipelineAuthConfig
113
+ */
114
+ function buildDeploymentData(validateToken, authConfig, options) {
115
+ const imageTag = options.imageTag || 'latest';
116
+ const deployData = {
117
+ validateToken: validateToken,
118
+ imageTag: imageTag
119
+ };
120
+
121
+ const pipelineAuthConfig = {
122
+ type: 'client-credentials',
123
+ clientId: authConfig.clientId,
124
+ clientSecret: authConfig.clientSecret
125
+ };
126
+
127
+ return { deployData, pipelineAuthConfig };
128
+ }
129
+
130
+ /**
131
+ * Handle deployment response
132
+ * @param {Object} response - API response
133
+ * @returns {Object} Deployment result
134
+ * @throws {Error} If deployment failed
135
+ */
136
+ function handleDeploymentResponse(response) {
137
+ if (response.success) {
138
+ return response.data.data || response.data;
139
+ }
140
+
141
+ // Handle deployment errors
142
+ if (response.status >= 400) {
143
+ const error = new Error(`Deployment request failed: ${response.formattedError || response.error || 'Unknown error'}`);
144
+ error.status = response.status;
145
+ error.data = response.data;
146
+ throw error;
147
+ }
148
+
149
+ throw new Error('Deployment request failed: Unknown error');
150
+ }
151
+
152
+ /**
153
+ * Preserve error information from last error
154
+ * @param {Error} lastError - Last error encountered
155
+ * @param {number} maxRetries - Maximum retry attempts
156
+ * @throws {Error} Formatted error with preserved information
157
+ */
158
+ function throwDeploymentError(lastError, maxRetries) {
159
+ const errorMessage = lastError.formatted || lastError.message;
160
+ const error = new Error(`Deployment failed after ${maxRetries} attempts: ${errorMessage}`);
161
+ if (lastError.formatted) {
162
+ error.formatted = lastError.formatted;
163
+ }
164
+ if (lastError.status) {
165
+ error.status = lastError.status;
166
+ }
167
+ if (lastError.data) {
168
+ error.data = lastError.data;
169
+ }
170
+ throw error;
171
+ }
172
+
122
173
  /**
123
174
  * Sends deployment request using validateToken from validation step
124
175
  * This is the second step in the deployment process
@@ -136,45 +187,18 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
136
187
  const validatedEnvKey = validateEnvironmentKey(envKey);
137
188
  const maxRetries = options.maxRetries || 3;
138
189
 
139
- // Extract clientId and clientSecret for deploy endpoint
140
- // These should have been loaded during validation and stored in authConfig
141
- if (!authConfig.clientId || !authConfig.clientSecret) {
142
- throw new Error('Client ID and Client Secret are required for deployment. These should have been loaded during validation.');
143
- }
144
- const clientId = authConfig.clientId;
145
- const clientSecret = authConfig.clientSecret;
190
+ // Validate credentials
191
+ validateDeploymentCredentials(authConfig);
146
192
 
147
- // Build deployment request
148
- const imageTag = options.imageTag || 'latest';
149
- const deployData = {
150
- validateToken: validateToken,
151
- imageTag: imageTag
152
- };
153
-
154
- // Use centralized API client with retry logic
155
- const pipelineAuthConfig = {
156
- type: 'client-credentials',
157
- clientId: clientId,
158
- clientSecret: clientSecret
159
- };
193
+ // Build deployment data
194
+ const { deployData, pipelineAuthConfig } = buildDeploymentData(validateToken, authConfig, options);
160
195
 
161
196
  // Wrap API call with retry logic
162
197
  let lastError;
163
198
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
164
199
  try {
165
200
  const response = await deployPipeline(url, validatedEnvKey, pipelineAuthConfig, deployData);
166
-
167
- if (response.success) {
168
- return response.data.data || response.data;
169
- }
170
-
171
- // Handle deployment errors
172
- if (response.status >= 400) {
173
- const error = new Error(`Deployment request failed: ${response.formattedError || response.error || 'Unknown error'}`);
174
- error.status = response.status;
175
- error.data = response.data;
176
- throw error;
177
- }
201
+ return handleDeploymentResponse(response);
178
202
  } catch (error) {
179
203
  lastError = error;
180
204
  if (attempt < maxRetries) {
@@ -185,19 +209,7 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
185
209
  }
186
210
  }
187
211
 
188
- // Preserve formatted error if available
189
- const errorMessage = lastError.formatted || lastError.message;
190
- const error = new Error(`Deployment failed after ${maxRetries} attempts: ${errorMessage}`);
191
- if (lastError.formatted) {
192
- error.formatted = lastError.formatted;
193
- }
194
- if (lastError.status) {
195
- error.status = lastError.status;
196
- }
197
- if (lastError.data) {
198
- error.data = lastError.data;
199
- }
200
- throw error;
212
+ throwDeploymentError(lastError, maxRetries);
201
213
  }
202
214
 
203
215
  /**
@@ -210,6 +222,52 @@ function isTerminalStatus(status) {
210
222
  return status === 'completed' || status === 'failed' || status === 'cancelled';
211
223
  }
212
224
 
225
+ /**
226
+ * Convert authConfig to pipeline auth config format
227
+ * @param {Object} authConfig - Authentication configuration
228
+ * @returns {Object} Pipeline auth config
229
+ */
230
+ function convertToPipelineAuthConfig(authConfig) {
231
+ return authConfig.type === 'bearer'
232
+ ? { type: 'bearer', token: authConfig.token }
233
+ : { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
234
+ }
235
+
236
+ /**
237
+ * Process deployment status response
238
+ * @param {Object} response - API response
239
+ * @param {number} attempt - Current attempt number
240
+ * @param {number} maxAttempts - Maximum attempts
241
+ * @param {number} interval - Polling interval
242
+ * @param {string} deploymentId - Deployment ID for error messages
243
+ * @returns {Object|null} Deployment data if terminal, null if needs to continue polling
244
+ */
245
+ async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
246
+ if (!response.success || !response.data) {
247
+ if (response.status === 404) {
248
+ throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
249
+ }
250
+ throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
251
+ }
252
+
253
+ const responseData = response.data;
254
+ const deploymentData = responseData.data || responseData;
255
+ const status = deploymentData.status;
256
+
257
+ if (isTerminalStatus(status)) {
258
+ return deploymentData;
259
+ }
260
+
261
+ const progress = deploymentData.progress || 0;
262
+ logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
263
+
264
+ if (attempt < maxAttempts - 1) {
265
+ await new Promise(resolve => setTimeout(resolve, interval));
266
+ }
267
+
268
+ return null;
269
+ }
270
+
213
271
  /**
214
272
  * Polls deployment status from controller
215
273
  * Uses pipeline endpoint for CI/CD monitoring with minimal deployment info
@@ -227,37 +285,14 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
227
285
  const maxAttempts = options.maxAttempts || 60;
228
286
 
229
287
  const validatedEnvKey = validateEnvironmentKey(envKey);
230
-
231
- // Convert authConfig to format expected by API client
232
- const pipelineAuthConfig = authConfig.type === 'bearer'
233
- ? { type: 'bearer', token: authConfig.token }
234
- : { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
288
+ const pipelineAuthConfig = convertToPipelineAuthConfig(authConfig);
235
289
 
236
290
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
237
291
  try {
238
292
  const response = await getPipelineDeployment(controllerUrl, validatedEnvKey, deploymentId, pipelineAuthConfig);
239
-
240
- if (response.success && response.data) {
241
- // OpenAPI spec: Response structure { success: boolean, data: { status, progress, ... }, timestamp: string }
242
- const responseData = response.data;
243
- const deploymentData = responseData.data || responseData;
244
- const status = deploymentData.status;
245
-
246
- if (isTerminalStatus(status)) {
247
- return deploymentData;
248
- }
249
-
250
- const progress = deploymentData.progress || 0;
251
- logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
252
-
253
- if (attempt < maxAttempts - 1) {
254
- await new Promise(resolve => setTimeout(resolve, interval));
255
- }
256
- } else {
257
- if (response.status === 404) {
258
- throw new Error(`Deployment ${deploymentId} not found`);
259
- }
260
- throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
293
+ const deploymentData = await processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId);
294
+ if (deploymentData) {
295
+ return deploymentData;
261
296
  }
262
297
  } catch (error) {
263
298
  if (error.message && error.message.includes('not found')) {
package/lib/diff.js CHANGED
@@ -14,6 +14,65 @@ const path = require('path');
14
14
  const chalk = require('chalk');
15
15
  const logger = require('./utils/logger');
16
16
 
17
+ /**
18
+ * Handle added field in comparison
19
+ * @param {string} key - Field key
20
+ * @param {*} value - Field value
21
+ * @param {string} path - Field path
22
+ * @param {Object} result - Result object to update
23
+ */
24
+ function handleAddedField(key, value, path, result) {
25
+ result.added.push({
26
+ path: path,
27
+ value: value,
28
+ type: typeof value
29
+ });
30
+ result.identical = false;
31
+ }
32
+
33
+ /**
34
+ * Handle removed field in comparison
35
+ * @param {string} key - Field key
36
+ * @param {*} value - Field value
37
+ * @param {string} path - Field path
38
+ * @param {Object} result - Result object to update
39
+ */
40
+ function handleRemovedField(key, value, path, result) {
41
+ result.removed.push({
42
+ path: path,
43
+ value: value,
44
+ type: typeof value
45
+ });
46
+ result.identical = false;
47
+ }
48
+
49
+ /**
50
+ * Handle changed field in comparison
51
+ * @param {*} oldValue - Old value
52
+ * @param {*} newValue - New value
53
+ * @param {string} path - Field path
54
+ * @param {Object} result - Result object to update
55
+ */
56
+ function handleChangedField(oldValue, newValue, path, result) {
57
+ result.changed.push({
58
+ path: path,
59
+ oldValue: oldValue,
60
+ newValue: newValue,
61
+ oldType: typeof oldValue,
62
+ newType: typeof newValue
63
+ });
64
+ result.identical = false;
65
+ }
66
+
67
+ /**
68
+ * Check if value is a nested object (not array, not null)
69
+ * @param {*} value - Value to check
70
+ * @returns {boolean} True if nested object
71
+ */
72
+ function isNestedObject(value) {
73
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
74
+ }
75
+
17
76
  /**
18
77
  * Performs deep comparison of two objects
19
78
  * Returns differences as structured result
@@ -40,20 +99,10 @@ function compareObjects(obj1, obj2, currentPath = '') {
40
99
  const val2 = obj2 && obj2[key];
41
100
 
42
101
  if (!(key in obj1)) {
43
- result.added.push({
44
- path: newPath,
45
- value: val2,
46
- type: typeof val2
47
- });
48
- result.identical = false;
102
+ handleAddedField(key, val2, newPath, result);
49
103
  } else if (!(key in obj2)) {
50
- result.removed.push({
51
- path: newPath,
52
- value: val1,
53
- type: typeof val1
54
- });
55
- result.identical = false;
56
- } else if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null && !Array.isArray(val1) && !Array.isArray(val2)) {
104
+ handleRemovedField(key, val1, newPath, result);
105
+ } else if (isNestedObject(val1) && isNestedObject(val2)) {
57
106
  // Recursively compare nested objects
58
107
  const nestedResult = compareObjects(val1, val2, newPath);
59
108
  result.added.push(...nestedResult.added);
@@ -63,14 +112,7 @@ function compareObjects(obj1, obj2, currentPath = '') {
63
112
  result.identical = false;
64
113
  }
65
114
  } else if (JSON.stringify(val1) !== JSON.stringify(val2)) {
66
- result.changed.push({
67
- path: newPath,
68
- oldValue: val1,
69
- newValue: val2,
70
- oldType: typeof val1,
71
- newType: typeof val2
72
- });
73
- result.identical = false;
115
+ handleChangedField(val1, val2, newPath, result);
74
116
  }
75
117
  }
76
118