@aifabrix/builder 2.0.0 → 2.0.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 (58) hide show
  1. package/README.md +6 -2
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # 🧱 @aifabrix/builder
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/%40aifabrix%2Fbuilder.svg)](https://www.npmjs.com/package/%40aifabrix%2Fbuilder)
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  Local development infrastructure + Azure deployment tool.
4
8
 
5
9
  ## Install
@@ -32,12 +36,12 @@ Want authentication or deployment controller?
32
36
 
33
37
  ```bash
34
38
  # Keycloak for authentication
35
- aifabrix create keycloak --port 8082 --database --template platform
39
+ aifabrix create keycloak --port 8082 --database --template keycloak
36
40
  aifabrix build keycloak
37
41
  aifabrix run keycloak
38
42
 
39
43
  # Miso Controller for Azure deployments
40
- aifabrix create miso-controller --port 3000 --database --redis --template platform
44
+ aifabrix create miso-controller --port 3000 --database --redis --template miso-controller
41
45
  aifabrix build miso-controller
42
46
  aifabrix run miso-controller
43
47
  ```
package/bin/aifabrix.js CHANGED
@@ -10,11 +10,14 @@
10
10
  *
11
11
  * @fileoverview CLI entry point for AI Fabrix Builder SDK
12
12
  * @author AI Fabrix Team
13
- * @version 2.0.0
13
+ * @version 2.0.2
14
14
  */
15
15
 
16
16
  const { Command } = require('commander');
17
17
  const cli = require('../lib/cli');
18
+ const { setupAppCommands } = require('../lib/commands/app');
19
+ const logger = require('../lib/utils/logger');
20
+ const packageJson = require('../package.json');
18
21
 
19
22
  /**
20
23
  * Initialize and configure the CLI
@@ -24,12 +27,15 @@ function initializeCLI() {
24
27
  const program = new Command();
25
28
 
26
29
  program.name('aifabrix')
27
- .version('2.0.0')
30
+ .version(packageJson.version)
28
31
  .description('AI Fabrix Local Fabric & Deployment SDK');
29
32
 
30
33
  // Delegate command setup to lib/cli.js
31
34
  cli.setupCommands(program);
32
35
 
36
+ // Add application management commands
37
+ setupAppCommands(program);
38
+
33
39
  // Parse command line arguments
34
40
  program.parse();
35
41
  }
@@ -43,7 +49,7 @@ if (require.main === module) {
43
49
  try {
44
50
  initializeCLI();
45
51
  } catch (error) {
46
- console.error('āŒ Failed to initialize CLI:', error.message);
52
+ logger.error('āŒ Failed to initialize CLI:', error.message);
47
53
  process.exit(1);
48
54
  }
49
55
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Jest Configuration for Integration Tests
3
+ * Ensures tests run in correct order with proper error handling
4
+ *
5
+ * @fileoverview Jest config specifically for integration tests
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const baseConfig = require('./jest.config');
11
+
12
+ module.exports = {
13
+ ...baseConfig,
14
+ // Override testPathIgnorePatterns for integration tests
15
+ testPathIgnorePatterns: [
16
+ '/node_modules/',
17
+ '\\\\node_modules\\\\'
18
+ ],
19
+ // Use custom test sequencer for correct order
20
+ testSequencer: '<rootDir>/tests/integration/test-sequencer.js',
21
+ // Longer timeout for integration tests
22
+ testTimeout: 300000, // 5 minutes
23
+ // Run tests sequentially
24
+ maxWorkers: 1,
25
+ // Don't ignore integration tests
26
+ testMatch: [
27
+ '**/tests/integration/**/*.test.js'
28
+ ]
29
+ };
30
+
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Application Configuration File Generation
3
+ *
4
+ * Generates configuration files for applications (variables.yaml, env.template, rbac.yaml, etc.)
5
+ *
6
+ * @fileoverview Configuration file generation for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const { generateVariablesYaml, generateEnvTemplate, generateRbacYaml } = require('./templates');
15
+ const { generateEnvTemplate: generateEnvTemplateFromReader } = require('./env-reader');
16
+ const logger = require('./utils/logger');
17
+
18
+ /**
19
+ * Checks if a file exists
20
+ * @async
21
+ * @param {string} filePath - Path to file
22
+ * @returns {Promise<boolean>} True if file exists
23
+ */
24
+ async function fileExists(filePath) {
25
+ try {
26
+ await fs.access(filePath);
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Generates variables.yaml file if it doesn't exist
35
+ * @async
36
+ * @param {string} appPath - Path to application directory
37
+ * @param {string} appName - Application name
38
+ * @param {Object} config - Application configuration
39
+ */
40
+ async function generateVariablesYamlFile(appPath, appName, config) {
41
+ const variablesPath = path.join(appPath, 'variables.yaml');
42
+ if (!(await fileExists(variablesPath))) {
43
+ const variablesYaml = generateVariablesYaml(appName, config);
44
+ await fs.writeFile(variablesPath, variablesYaml);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Generates env.template file if it doesn't exist
50
+ * @async
51
+ * @param {string} appPath - Path to application directory
52
+ * @param {Object} config - Application configuration
53
+ * @param {Object} existingEnv - Existing environment variables
54
+ */
55
+ async function generateEnvTemplateFile(appPath, config, existingEnv) {
56
+ const envTemplatePath = path.join(appPath, 'env.template');
57
+ if (!(await fileExists(envTemplatePath))) {
58
+ let envTemplate;
59
+ if (existingEnv) {
60
+ const envResult = await generateEnvTemplateFromReader(config, existingEnv);
61
+ envTemplate = envResult.template;
62
+
63
+ if (envResult.warnings.length > 0) {
64
+ logger.log(chalk.yellow('\nāš ļø Environment conversion warnings:'));
65
+ envResult.warnings.forEach(warning => logger.log(chalk.yellow(` - ${warning}`)));
66
+ }
67
+ } else {
68
+ envTemplate = generateEnvTemplate(config);
69
+ }
70
+ await fs.writeFile(envTemplatePath, envTemplate);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Generates rbac.yaml file if authentication is enabled and file doesn't exist
76
+ * @async
77
+ * @param {string} appPath - Path to application directory
78
+ * @param {string} appName - Application name
79
+ * @param {Object} config - Application configuration
80
+ */
81
+ async function generateRbacYamlFile(appPath, appName, config) {
82
+ if (!config.authentication) {
83
+ return;
84
+ }
85
+
86
+ const rbacPath = path.join(appPath, 'rbac.yaml');
87
+ if (!(await fileExists(rbacPath))) {
88
+ const rbacYaml = generateRbacYaml(appName, config);
89
+ if (rbacYaml) {
90
+ await fs.writeFile(rbacPath, rbacYaml);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Generates aifabrix-deploy.json file
97
+ * @async
98
+ * @param {string} appPath - Path to application directory
99
+ * @param {string} appName - Application name
100
+ * @param {Object} config - Application configuration
101
+ */
102
+ async function generateDeployJsonFile(appPath, appName, config) {
103
+ const deployJson = {
104
+ apiVersion: 'v1',
105
+ kind: 'ApplicationDeployment',
106
+ metadata: {
107
+ name: appName,
108
+ namespace: 'default'
109
+ },
110
+ spec: {
111
+ application: {
112
+ name: appName,
113
+ version: '1.0.0',
114
+ language: config.language,
115
+ port: config.port
116
+ },
117
+ services: {
118
+ database: config.database,
119
+ redis: config.redis,
120
+ storage: config.storage,
121
+ authentication: config.authentication
122
+ },
123
+ deployment: {
124
+ replicas: 1,
125
+ strategy: 'RollingUpdate'
126
+ }
127
+ }
128
+ };
129
+
130
+ await fs.writeFile(
131
+ path.join(appPath, 'aifabrix-deploy.json'),
132
+ JSON.stringify(deployJson, null, 2)
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Generate all configuration files for the application
138
+ * @param {string} appPath - Path to application directory
139
+ * @param {string} appName - Application name
140
+ * @param {Object} config - Application configuration
141
+ * @param {Object} existingEnv - Existing environment variables
142
+ */
143
+ async function generateConfigFiles(appPath, appName, config, existingEnv) {
144
+ try {
145
+ await generateVariablesYamlFile(appPath, appName, config);
146
+ await generateEnvTemplateFile(appPath, config, existingEnv);
147
+ await generateRbacYamlFile(appPath, appName, config);
148
+ await generateDeployJsonFile(appPath, appName, config);
149
+ } catch (error) {
150
+ throw new Error(`Failed to generate configuration files: ${error.message}`);
151
+ }
152
+ }
153
+
154
+ module.exports = {
155
+ generateConfigFiles
156
+ };
157
+
package/lib/app-deploy.js CHANGED
@@ -14,6 +14,7 @@ const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const chalk = require('chalk');
16
16
  const pushUtils = require('./push');
17
+ const logger = require('./utils/logger');
17
18
 
18
19
  /**
19
20
  * Validate application name format
@@ -42,6 +43,63 @@ function validateAppName(appName) {
42
43
  }
43
44
  }
44
45
 
46
+ /**
47
+ * Validates push prerequisites
48
+ * @async
49
+ * @function validatePushPrerequisites
50
+ * @param {string} appName - Application name
51
+ * @param {string} registry - Registry URL
52
+ * @throws {Error} If prerequisites are not met
53
+ */
54
+ async function validatePushPrerequisites(appName, registry) {
55
+ if (!pushUtils.validateRegistryURL(registry)) {
56
+ throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
57
+ }
58
+
59
+ if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
60
+ throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
61
+ }
62
+
63
+ if (!await pushUtils.checkAzureCLIInstalled()) {
64
+ throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Executes push operations
70
+ * @async
71
+ * @function executePush
72
+ * @param {string} appName - Application name
73
+ * @param {string} registry - Registry URL
74
+ * @param {string[]} tags - Tags to push
75
+ * @throws {Error} If push fails
76
+ */
77
+ async function executePush(appName, registry, tags) {
78
+ if (await pushUtils.checkACRAuthentication(registry)) {
79
+ logger.log(chalk.green(`āœ“ Already authenticated with ${registry}`));
80
+ } else {
81
+ await pushUtils.authenticateACR(registry);
82
+ }
83
+
84
+ await Promise.all(tags.map(async(tag) => {
85
+ await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
86
+ await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
87
+ }));
88
+ }
89
+
90
+ /**
91
+ * Verifies push result
92
+ * @function verifyPushResult
93
+ * @param {string[]} tags - Tags that were pushed
94
+ * @param {string} registry - Registry URL
95
+ * @param {string} appName - Application name
96
+ */
97
+ function verifyPushResult(tags, registry, appName) {
98
+ logger.log(chalk.green(`\nāœ“ Successfully pushed ${tags.length} tag(s) to ${registry}`));
99
+ logger.log(chalk.gray(`Image: ${registry}/${appName}:*`));
100
+ logger.log(chalk.gray(`Tags: ${tags.join(', ')}`));
101
+ }
102
+
45
103
  /**
46
104
  * Pushes application image to Azure Container Registry
47
105
  * @async
@@ -52,7 +110,6 @@ function validateAppName(appName) {
52
110
  */
53
111
  async function pushApp(appName, options = {}) {
54
112
  try {
55
- // Validate app name
56
113
  validateAppName(appName);
57
114
 
58
115
  const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
@@ -68,37 +125,181 @@ async function pushApp(appName, options = {}) {
68
125
  throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
69
126
  }
70
127
 
71
- if (!pushUtils.validateRegistryURL(registry)) {
72
- throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
128
+ if (!/^[^.]+\.azurecr\.io$/.test(registry)) {
129
+ throw new Error(`Invalid ACR URL format: ${registry}. Expected format: *.azurecr.io`);
73
130
  }
74
131
 
75
132
  const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
76
133
 
77
- if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
78
- throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
79
- }
134
+ await validatePushPrerequisites(appName, registry);
135
+ await executePush(appName, registry, tags);
136
+ verifyPushResult(tags, registry, appName);
80
137
 
81
- if (!await pushUtils.checkAzureCLIInstalled()) {
82
- throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
83
- }
138
+ } catch (error) {
139
+ throw new Error(`Failed to push application: ${error.message}`);
140
+ }
141
+ }
84
142
 
85
- if (await pushUtils.checkACRAuthentication(registry)) {
86
- console.log(chalk.green(`āœ“ Already authenticated with ${registry}`));
87
- } else {
88
- await pushUtils.authenticateACR(registry);
143
+ /**
144
+ * Validates that the application directory exists
145
+ * @async
146
+ * @param {string} builderPath - Path to builder directory
147
+ * @param {string} appName - Application name
148
+ * @throws {Error} If directory doesn't exist
149
+ */
150
+ async function validateAppDirectory(builderPath, appName) {
151
+ try {
152
+ await fs.access(builderPath);
153
+ } catch (error) {
154
+ if (error.code === 'ENOENT') {
155
+ throw new Error(`Application '${appName}' not found in builder/. Run 'aifabrix create ${appName}' first`);
89
156
  }
157
+ throw error;
158
+ }
159
+ }
90
160
 
91
- await Promise.all(tags.map(async(tag) => {
92
- await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
93
- await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
94
- }));
161
+ /**
162
+ * Loads variables.yaml file
163
+ * @async
164
+ * @param {string} variablesPath - Path to variables.yaml
165
+ * @returns {Promise<Object>} Variables object
166
+ * @throws {Error} If file cannot be loaded
167
+ */
168
+ async function loadVariablesFile(variablesPath) {
169
+ try {
170
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
171
+ return yaml.load(variablesContent);
172
+ } catch (error) {
173
+ throw new Error(`Failed to load configuration from variables.yaml: ${error.message}`);
174
+ }
175
+ }
95
176
 
96
- console.log(chalk.green(`\nāœ“ Successfully pushed ${tags.length} tag(s) to ${registry}`));
97
- console.log(chalk.gray(`Image: ${registry}/${appName}:*`));
98
- console.log(chalk.gray(`Tags: ${tags.join(', ')}`));
177
+ /**
178
+ * Extracts deployment configuration from options and variables
179
+ * @param {Object} options - CLI options
180
+ * @param {Object} variables - Variables from variables.yaml
181
+ * @returns {Object} Extracted configuration
182
+ */
183
+ function extractDeploymentConfig(options, variables) {
184
+ return {
185
+ controllerUrl: options.controller || variables.deployment?.controllerUrl,
186
+ envKey: options.environment || variables.deployment?.environment || 'dev',
187
+ clientId: options.clientId || variables.deployment?.clientId,
188
+ clientSecret: options.clientSecret || variables.deployment?.clientSecret,
189
+ poll: options.poll !== false,
190
+ pollInterval: options.pollInterval || 5000,
191
+ pollMaxAttempts: options.pollMaxAttempts || 60
192
+ };
193
+ }
99
194
 
100
- } catch (error) {
101
- throw new Error(`Failed to push application: ${error.message}`);
195
+ /**
196
+ * Validates required deployment configuration
197
+ * @param {Object} config - Deployment configuration
198
+ * @throws {Error} If configuration is invalid
199
+ */
200
+ function validateDeploymentConfig(config) {
201
+ if (!config.controllerUrl) {
202
+ throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
203
+ }
204
+ if (!config.clientId || !config.clientSecret) {
205
+ throw new Error('Client ID and Client Secret are required. Set them in variables.yaml or use --client-id and --client-secret flags');
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Loads deployment configuration from variables.yaml
211
+ * @async
212
+ * @param {string} appName - Application name
213
+ * @param {Object} options - CLI options
214
+ * @returns {Promise<Object>} Deployment configuration
215
+ * @throws {Error} If configuration is invalid
216
+ */
217
+ async function loadDeploymentConfig(appName, options) {
218
+ const builderPath = path.join(process.cwd(), 'builder', appName);
219
+ await validateAppDirectory(builderPath, appName);
220
+
221
+ const variablesPath = path.join(builderPath, 'variables.yaml');
222
+ const variables = await loadVariablesFile(variablesPath);
223
+
224
+ const config = extractDeploymentConfig(options, variables);
225
+ validateDeploymentConfig(config);
226
+
227
+ return config;
228
+ }
229
+
230
+ /**
231
+ * Generates and validates deployment manifest
232
+ * @async
233
+ * @param {string} appName - Application name
234
+ * @returns {Promise<Object>} Deployment manifest
235
+ * @throws {Error} If generation or validation fails
236
+ */
237
+ async function generateAndValidateManifest(appName) {
238
+ logger.log(chalk.blue(`\nšŸ“‹ Generating deployment manifest for ${appName}...`));
239
+ const generator = require('./generator');
240
+
241
+ // generateDeployJson already validates against schema and throws on error
242
+ const manifestPath = await generator.generateDeployJson(appName);
243
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
244
+
245
+ // Additional validation for warnings (schema validation already passed)
246
+ // Note: Schema validation happens in generateDeployJson, this is just for additional checks if needed
247
+ return { manifest, manifestPath };
248
+ }
249
+
250
+ /**
251
+ * Displays deployment information
252
+ * @param {Object} manifest - Deployment manifest
253
+ * @param {string} manifestPath - Path to manifest file
254
+ */
255
+ function displayDeploymentInfo(manifest, manifestPath) {
256
+ logger.log(chalk.green(`āœ“ Manifest generated: ${manifestPath}`));
257
+ logger.log(chalk.blue(` Key: ${manifest.key}`));
258
+ logger.log(chalk.blue(` Display Name: ${manifest.displayName}`));
259
+ logger.log(chalk.blue(` Image: ${manifest.image}`));
260
+ logger.log(chalk.blue(` Port: ${manifest.port}`));
261
+ }
262
+
263
+ /**
264
+ * Executes deployment to controller
265
+ * @async
266
+ * @param {Object} manifest - Deployment manifest
267
+ * @param {Object} config - Deployment configuration
268
+ * @returns {Promise<Object>} Deployment result
269
+ */
270
+ async function executeDeployment(manifest, config) {
271
+ logger.log(chalk.blue(`\nšŸš€ Deploying to ${config.controllerUrl} (environment: ${config.envKey})...`));
272
+ const deployer = require('./deployer');
273
+ return await deployer.deployToController(
274
+ manifest,
275
+ config.controllerUrl,
276
+ config.envKey,
277
+ config.clientId,
278
+ config.clientSecret,
279
+ {
280
+ poll: config.poll,
281
+ pollInterval: config.pollInterval,
282
+ pollMaxAttempts: config.pollMaxAttempts
283
+ }
284
+ );
285
+ }
286
+
287
+ /**
288
+ * Displays deployment results
289
+ * @param {Object} result - Deployment result
290
+ */
291
+ function displayDeploymentResults(result) {
292
+ logger.log(chalk.green('\nāœ… Deployment initiated successfully'));
293
+ if (result.deploymentUrl) {
294
+ logger.log(chalk.blue(` URL: ${result.deploymentUrl}`));
295
+ }
296
+ if (result.deploymentId) {
297
+ logger.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
298
+ }
299
+ if (result.status) {
300
+ const statusIcon = result.status.status === 'completed' ? 'āœ…' :
301
+ result.status.status === 'failed' ? 'āŒ' : 'ā³';
302
+ logger.log(chalk.blue(` Status: ${statusIcon} ${result.status.status}`));
102
303
  }
103
304
  }
104
305
 
@@ -129,70 +330,20 @@ async function deployApp(appName, options = {}) {
129
330
 
130
331
  validateAppName(appName);
131
332
 
132
- if (!options.controller) {
133
- throw new Error('Controller URL is required (--controller flag required)');
134
- }
333
+ // 2. Load deployment configuration
334
+ const config = await loadDeploymentConfig(appName, options);
135
335
 
136
- // 2. Load application configuration
137
- const builderPath = path.join(process.cwd(), 'builder', appName);
138
- try {
139
- await fs.access(builderPath);
140
- } catch (error) {
141
- if (error.code === 'ENOENT') {
142
- throw new Error(`Application '${appName}' not found in builder/. Run 'aifabrix create ${appName}' first`);
143
- }
144
- throw error;
145
- }
336
+ // 3. Generate and validate manifest
337
+ const { manifest, manifestPath } = await generateAndValidateManifest(appName);
146
338
 
147
- // 3. Generate deployment manifest
148
- console.log(chalk.blue(`\nšŸ“‹ Generating deployment manifest for ${appName}...`));
149
- const generator = require('./generator');
150
- const manifestPath = await generator.generateDeployJson(appName);
151
- const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
152
-
153
- // 4. Validate manifest
154
- const validation = generator.validateDeploymentJson(manifest);
155
- if (!validation.valid) {
156
- console.log(chalk.red('\nāŒ Validation failed:'));
157
- validation.errors.forEach(error => console.log(chalk.red(` • ${error}`)));
158
- throw new Error('Deployment manifest validation failed');
159
- }
339
+ // 4. Display deployment info
340
+ displayDeploymentInfo(manifest, manifestPath);
160
341
 
161
- if (validation.warnings.length > 0) {
162
- console.log(chalk.yellow('\nāš ļø Warnings:'));
163
- validation.warnings.forEach(warning => console.log(chalk.yellow(` • ${warning}`)));
164
- }
342
+ // 5. Execute deployment
343
+ const result = await executeDeployment(manifest, config);
165
344
 
166
- // 5. Display deployment info
167
- console.log(chalk.green(`āœ“ Manifest generated: ${manifestPath}`));
168
- console.log(chalk.blue(` Key: ${manifest.key}`));
169
- console.log(chalk.blue(` Display Name: ${manifest.displayName}`));
170
- console.log(chalk.blue(` Image: ${manifest.image}`));
171
- console.log(chalk.blue(` Port: ${manifest.port}`));
172
-
173
- // 6. Deploy to controller
174
- console.log(chalk.blue(`\nšŸš€ Deploying to ${options.controller}...`));
175
- const deployer = require('./deployer');
176
- const result = await deployer.deployToController(manifest, options.controller, {
177
- environment: options.environment,
178
- poll: options.poll !== false, // Poll by default
179
- pollInterval: options.pollInterval || 5000,
180
- pollMaxAttempts: options.pollMaxAttempts || 60
181
- });
182
-
183
- // 7. Display results
184
- console.log(chalk.green('\nāœ… Deployment initiated successfully'));
185
- if (result.deploymentUrl) {
186
- console.log(chalk.blue(` URL: ${result.deploymentUrl}`));
187
- }
188
- if (result.deploymentId) {
189
- console.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
190
- }
191
- if (result.status) {
192
- const statusIcon = result.status.status === 'completed' ? 'āœ…' :
193
- result.status.status === 'failed' ? 'āŒ' : 'ā³';
194
- console.log(chalk.blue(` Status: ${statusIcon} ${result.status.status}`));
195
- }
345
+ // 6. Display results
346
+ displayDeploymentResults(result);
196
347
 
197
348
  return result;
198
349