@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/lib/app.js CHANGED
@@ -11,206 +11,223 @@
11
11
 
12
12
  const fs = require('fs').promises;
13
13
  const path = require('path');
14
- const inquirer = require('inquirer');
15
14
  const chalk = require('chalk');
16
- const yaml = require('js-yaml');
17
- const { generateVariablesYaml, generateEnvTemplate, generateRbacYaml } = require('./templates');
18
- const { readExistingEnv, generateEnvTemplate: generateEnvTemplateFromReader } = require('./env-reader');
15
+ const { readExistingEnv } = require('./env-reader');
19
16
  const build = require('./build');
20
17
  const appRun = require('./app-run');
21
- const pushUtils = require('./push');
18
+ const { validateTemplate, copyTemplateFiles, copyAppFiles } = require('./template-validator');
19
+ const { promptForOptions } = require('./app-prompts');
20
+ const { generateConfigFiles } = require('./app-config');
21
+ const { validateAppName, pushApp } = require('./app-push');
22
+ const { generateDockerfileForApp } = require('./app-dockerfile');
23
+ const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables } = require('./utils/template-helpers');
24
+ const logger = require('./utils/logger');
22
25
 
23
26
  /**
24
- * Validate application name format
25
- * @param {string} appName - Application name to validate
26
- * @throws {Error} If app name is invalid
27
+ * Displays success message after app creation
28
+ * @param {string} appName - Application name
29
+ * @param {Object} config - Final configuration
30
+ * @param {string} envConversionMessage - Environment conversion message
27
31
  */
28
- function validateAppName(appName) {
29
- if (!appName || typeof appName !== 'string') {
30
- throw new Error('Application name is required');
32
+ function displaySuccessMessage(appName, config, envConversionMessage, hasAppFiles = false) {
33
+ logger.log(chalk.green('\n✓ Application created successfully!'));
34
+ logger.log(chalk.blue(`\nApplication: ${appName}`));
35
+ logger.log(chalk.blue(`Location: builder/${appName}/`));
36
+ if (hasAppFiles) {
37
+ logger.log(chalk.blue(`Application files: apps/${appName}/`));
31
38
  }
39
+ logger.log(chalk.blue(`Language: ${config.language}`));
40
+ logger.log(chalk.blue(`Port: ${config.port}`));
32
41
 
33
- // App name should be lowercase, alphanumeric with dashes, 3-40 characters
34
- const nameRegex = /^[a-z0-9-]{3,40}$/;
35
- if (!nameRegex.test(appName)) {
36
- throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
37
- }
42
+ if (config.database) logger.log(chalk.yellow(' - Database enabled'));
43
+ if (config.redis) logger.log(chalk.yellow(' - Redis enabled'));
44
+ if (config.storage) logger.log(chalk.yellow(' - Storage enabled'));
45
+ if (config.authentication) logger.log(chalk.yellow(' - Authentication enabled'));
38
46
 
39
- // Cannot start or end with dash
40
- if (appName.startsWith('-') || appName.endsWith('-')) {
41
- throw new Error('Application name cannot start or end with a dash');
42
- }
47
+ logger.log(chalk.gray(envConversionMessage));
43
48
 
44
- // Cannot have consecutive dashes
45
- if (appName.includes('--')) {
46
- throw new Error('Application name cannot have consecutive dashes');
47
- }
49
+ logger.log(chalk.green('\nNext steps:'));
50
+ logger.log(chalk.white('1. Copy env.template to .env and fill in your values'));
51
+ logger.log(chalk.white('2. Run: aifabrix build ' + appName));
52
+ logger.log(chalk.white('3. Run: aifabrix run ' + appName));
48
53
  }
49
54
 
50
55
  /**
51
- * Prompt for missing configuration options
56
+ * Validates that app directory doesn't already exist
57
+ * @async
58
+ * @param {string} appPath - Application directory path
52
59
  * @param {string} appName - Application name
53
- * @param {Object} options - Provided options
54
- * @returns {Promise<Object>} Complete configuration
60
+ * @throws {Error} If directory already exists
55
61
  */
56
- async function promptForOptions(appName, options) {
57
- const questions = [];
58
-
59
- // Port validation
60
- if (!options.port) {
61
- questions.push({
62
- type: 'input',
63
- name: 'port',
64
- message: 'What port should the application run on?',
65
- default: '3000',
66
- validate: (input) => {
67
- const port = parseInt(input, 10);
68
- if (isNaN(port) || port < 1 || port > 65535) {
69
- return 'Port must be a number between 1 and 65535';
70
- }
71
- return true;
72
- }
73
- });
62
+ async function validateAppDirectoryNotExists(appPath, appName, baseDir = 'builder') {
63
+ try {
64
+ await fs.access(appPath);
65
+ throw new Error(`Application '${appName}' already exists in ${baseDir}/${appName}/`);
66
+ } catch (error) {
67
+ if (error.code !== 'ENOENT') {
68
+ throw error;
69
+ }
74
70
  }
71
+ }
75
72
 
76
- // Language selection
77
- if (!options.language) {
78
- questions.push({
79
- type: 'list',
80
- name: 'language',
81
- message: 'What language is your application written in?',
82
- choices: [
83
- { name: 'TypeScript/Node.js', value: 'typescript' },
84
- { name: 'Python', value: 'python' }
85
- ],
86
- default: 'typescript'
87
- });
73
+ /**
74
+ * Handles GitHub workflow generation if requested
75
+ * @async
76
+ * @param {Object} options - Creation options
77
+ * @param {Object} config - Final configuration
78
+ */
79
+ async function handleGitHubWorkflows(options, config) {
80
+ if (!options.github) {
81
+ return;
88
82
  }
89
83
 
90
- // Service options
91
- if (!Object.prototype.hasOwnProperty.call(options, 'database')) {
92
- questions.push({
93
- type: 'confirm',
94
- name: 'database',
95
- message: 'Does your application need a database?',
96
- default: false
97
- });
98
- }
84
+ const githubGen = require('./github-generator');
85
+
86
+ // Parse github-steps if provided
87
+ const githubSteps = options.githubSteps
88
+ ? options.githubSteps.split(',').map(s => s.trim()).filter(s => s.length > 0)
89
+ : [];
90
+
91
+ const workflowFiles = await githubGen.generateGithubWorkflows(
92
+ process.cwd(),
93
+ config,
94
+ {
95
+ mainBranch: options.mainBranch || 'main',
96
+ uploadCoverage: true,
97
+ githubSteps: githubSteps
98
+ }
99
+ );
100
+
101
+ logger.log(chalk.green('✓ Generated GitHub Actions workflows:'));
102
+ workflowFiles.forEach(file => logger.log(chalk.gray(` - ${file}`)));
103
+ }
104
+
105
+ /**
106
+ * Validates app creation prerequisites
107
+ * @async
108
+ * @function validateAppCreation
109
+ * @param {string} appName - Application name
110
+ * @param {Object} options - Creation options
111
+ * @param {string} appPath - Application directory path
112
+ * @throws {Error} If validation fails
113
+ */
114
+ async function validateAppCreation(appName, options, appPath) {
115
+ validateAppName(appName);
116
+ await validateAppDirectoryNotExists(appPath, appName, 'builder');
99
117
 
100
- if (!Object.prototype.hasOwnProperty.call(options, 'redis')) {
101
- questions.push({
102
- type: 'confirm',
103
- name: 'redis',
104
- message: 'Does your application need Redis?',
105
- default: false
106
- });
118
+ if (!options.app) {
119
+ return;
107
120
  }
108
121
 
109
- if (!Object.prototype.hasOwnProperty.call(options, 'storage')) {
110
- questions.push({
111
- type: 'confirm',
112
- name: 'storage',
113
- message: 'Does your application need file storage?',
114
- default: false
115
- });
122
+ const appsPath = path.join(process.cwd(), 'apps', appName);
123
+ try {
124
+ await fs.access(appsPath);
125
+ throw new Error(`Application '${appName}' already exists in apps/${appName}/`);
126
+ } catch (error) {
127
+ if (error.code !== 'ENOENT') {
128
+ throw error;
129
+ }
116
130
  }
131
+ }
117
132
 
118
- if (!Object.prototype.hasOwnProperty.call(options, 'authentication')) {
119
- questions.push({
120
- type: 'confirm',
121
- name: 'authentication',
122
- message: 'Does your application need authentication/RBAC?',
123
- default: false
124
- });
133
+ /**
134
+ * Processes template files if template is specified
135
+ * @async
136
+ * @function processTemplateFiles
137
+ * @param {string} template - Template name
138
+ * @param {string} appPath - Application directory path
139
+ * @param {string} appName - Application name
140
+ * @param {Object} options - Creation options
141
+ * @param {Object} config - Final configuration
142
+ * @throws {Error} If template processing fails
143
+ */
144
+ async function processTemplateFiles(template, appPath, appName, options, config) {
145
+ if (!template) {
146
+ return;
125
147
  }
126
148
 
127
- // Prompt for missing options
128
- const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
129
-
130
- // Merge provided options with answers
131
- return {
132
- appName,
133
- port: parseInt(options.port || answers.port || 3000, 10),
134
- language: options.language || answers.language || 'typescript',
135
- database: options.database || answers.database || false,
136
- redis: options.redis || answers.redis || false,
137
- storage: options.storage || answers.storage || false,
138
- authentication: options.authentication || answers.authentication || false
139
- };
149
+ await validateTemplate(template);
150
+ const copiedFiles = await copyTemplateFiles(template, appPath);
151
+ logger.log(chalk.green(`✓ Copied ${copiedFiles.length} file(s) from template '${template}'`));
152
+ await updateTemplateVariables(appPath, appName, options, config);
140
153
  }
141
154
 
142
155
  /**
143
- * Generate all configuration files for the application
144
- * @param {string} appPath - Path to application directory
156
+ * Updates variables.yaml for --app flag
157
+ * @async
158
+ * @function updateVariablesForAppFlag
159
+ * @param {string} appPath - Application directory path
145
160
  * @param {string} appName - Application name
146
- * @param {Object} config - Application configuration
147
- * @param {Object} existingEnv - Existing environment variables
161
+ * @throws {Error} If update fails
148
162
  */
149
- async function generateConfigFiles(appPath, appName, config, existingEnv) {
163
+ async function updateVariablesForAppFlag(appPath, appName) {
150
164
  try {
151
- // Generate variables.yaml
152
- const variablesYaml = generateVariablesYaml(appName, config);
153
- await fs.writeFile(path.join(appPath, 'variables.yaml'), variablesYaml);
154
-
155
- // Generate env.template
156
- let envTemplate;
157
- if (existingEnv) {
158
- const envResult = await generateEnvTemplateFromReader(config, existingEnv);
159
- envTemplate = envResult.template;
160
-
161
- if (envResult.warnings.length > 0) {
162
- console.log(chalk.yellow('\n⚠️ Environment conversion warnings:'));
163
- envResult.warnings.forEach(warning => console.log(chalk.yellow(` - ${warning}`)));
164
- }
165
+ const yaml = require('js-yaml');
166
+ const variablesPath = path.join(appPath, 'variables.yaml');
167
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
168
+ const variables = yaml.load(variablesContent);
169
+
170
+ if (variables.build) {
171
+ variables.build.context = '../..';
172
+ variables.build.envOutputPath = `apps/${appName}/.env`;
165
173
  } else {
166
- envTemplate = generateEnvTemplate(config);
167
- }
168
- await fs.writeFile(path.join(appPath, 'env.template'), envTemplate);
169
-
170
- // Generate rbac.yaml if authentication is enabled
171
- if (config.authentication) {
172
- const rbacYaml = generateRbacYaml(appName, config);
173
- if (rbacYaml) {
174
- await fs.writeFile(path.join(appPath, 'rbac.yaml'), rbacYaml);
175
- }
174
+ variables.build = {
175
+ context: '../..',
176
+ envOutputPath: `apps/${appName}/.env`
177
+ };
176
178
  }
177
179
 
178
- // Generate aifabrix-deploy.json template
179
- const deployJson = {
180
- apiVersion: 'v1',
181
- kind: 'ApplicationDeployment',
182
- metadata: {
183
- name: appName,
184
- namespace: 'default'
185
- },
186
- spec: {
187
- application: {
188
- name: appName,
189
- version: '1.0.0',
190
- language: config.language,
191
- port: config.port
192
- },
193
- services: {
194
- database: config.database,
195
- redis: config.redis,
196
- storage: config.storage,
197
- authentication: config.authentication
198
- },
199
- deployment: {
200
- replicas: 1,
201
- strategy: 'RollingUpdate'
202
- }
203
- }
204
- };
205
-
206
- await fs.writeFile(
207
- path.join(appPath, 'aifabrix-deploy.json'),
208
- JSON.stringify(deployJson, null, 2)
209
- );
210
-
180
+ await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }));
211
181
  } catch (error) {
212
- throw new Error(`Failed to generate configuration files: ${error.message}`);
182
+ logger.warn(chalk.yellow(`⚠️ Warning: Could not update variables.yaml: ${error.message}`));
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Gets language from config or variables.yaml
188
+ * @async
189
+ * @function getLanguageForAppFiles
190
+ * @param {string} language - Language from config
191
+ * @param {string} appPath - Application directory path
192
+ * @returns {Promise<string>} Language to use
193
+ * @throws {Error} If language cannot be determined
194
+ */
195
+ async function getLanguageForAppFiles(language, appPath) {
196
+ if (language) {
197
+ return language;
198
+ }
199
+
200
+ const yaml = require('js-yaml');
201
+ const variablesPath = path.join(appPath, 'variables.yaml');
202
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
203
+ const variables = yaml.load(variablesContent);
204
+ const languageFromYaml = variables?.build?.language;
205
+
206
+ if (!languageFromYaml) {
207
+ throw new Error('Language not specified and could not be determined from variables.yaml. Use --language flag or ensure variables.yaml contains build.language');
213
208
  }
209
+
210
+ return languageFromYaml;
211
+ }
212
+
213
+ /**
214
+ * Sets up apps directory and copies application files
215
+ * @async
216
+ * @function setupAppFiles
217
+ * @param {string} appName - Application name
218
+ * @param {string} appPath - Application directory path
219
+ * @param {Object} config - Final configuration
220
+ * @param {Object} options - Creation options
221
+ * @throws {Error} If setup fails
222
+ */
223
+ async function setupAppFiles(appName, appPath, config, options) {
224
+ const appsPath = path.join(process.cwd(), 'apps', appName);
225
+ await fs.mkdir(appsPath, { recursive: true });
226
+ await updateVariablesForAppFlag(appPath, appName);
227
+
228
+ const language = await getLanguageForAppFiles(config.language || options.language, appPath);
229
+ const copiedFiles = await copyAppFiles(language, appsPath);
230
+ logger.log(chalk.green(`✓ Copied ${copiedFiles.length} application file(s) to apps/${appName}/`));
214
231
  }
215
232
 
216
233
  /**
@@ -227,7 +244,7 @@ async function generateConfigFiles(appPath, appName, config, existingEnv) {
227
244
  * @param {boolean} [options.storage] - Requires file storage
228
245
  * @param {boolean} [options.authentication] - Requires authentication/RBAC
229
246
  * @param {string} [options.language] - Runtime language (typescript/python)
230
- * @param {string} [options.template] - Template to use (platform for Keycloak/Miso)
247
+ * @param {string} [options.template] - Template to use (e.g., controller, keycloak)
231
248
  * @returns {Promise<void>} Resolves when app is created
232
249
  * @throws {Error} If creation fails
233
250
  *
@@ -237,75 +254,40 @@ async function generateConfigFiles(appPath, appName, config, existingEnv) {
237
254
  */
238
255
  async function createApp(appName, options = {}) {
239
256
  try {
240
- // Validate app name format
241
- validateAppName(appName);
257
+ // Validate appName early
258
+ if (!appName || typeof appName !== 'string') {
259
+ throw new Error('Application name is required');
260
+ }
242
261
 
243
- // Check if directory already exists
244
262
  const builderPath = path.join(process.cwd(), 'builder');
245
263
  const appPath = path.join(builderPath, appName);
246
264
 
247
- try {
248
- await fs.access(appPath);
249
- throw new Error(`Application '${appName}' already exists in builder/${appName}/`);
250
- } catch (error) {
251
- if (error.code !== 'ENOENT') {
252
- throw error;
253
- }
265
+ await validateAppCreation(appName, options, appPath);
266
+
267
+ if (options.template) {
268
+ await validateTemplate(options.template);
254
269
  }
255
270
 
256
- // Prompt for missing options
257
- const config = await promptForOptions(appName, options);
271
+ const templateVariables = await loadTemplateVariables(options.template);
272
+ const mergedOptions = mergeTemplateVariables(options, templateVariables);
273
+ const config = await promptForOptions(appName, mergedOptions);
258
274
 
259
- // Create directory structure
260
275
  await fs.mkdir(appPath, { recursive: true });
276
+ await processTemplateFiles(options.template, appPath, appName, options, config);
261
277
 
262
- // Check for existing .env file
263
278
  const existingEnv = await readExistingEnv(process.cwd());
264
- let envConversionMessage = '';
265
-
266
- if (existingEnv) {
267
- envConversionMessage = '\n✓ Found existing .env file - sensitive values will be converted to kv:// references';
268
- }
279
+ const envConversionMessage = existingEnv
280
+ ? '\n✓ Found existing .env file - sensitive values will be converted to kv:// references'
281
+ : '';
269
282
 
270
- // Generate configuration files
271
283
  await generateConfigFiles(appPath, appName, config, existingEnv);
272
284
 
273
- // Generate GitHub workflows if requested
274
- if (options.github) {
275
- const githubGen = require('./github-generator');
276
- const workflowFiles = await githubGen.generateGithubWorkflows(
277
- process.cwd(),
278
- config,
279
- {
280
- mainBranch: options.mainBranch || 'main',
281
- uploadCoverage: true,
282
- publishToNpm: options.template === 'platform'
283
- }
284
- );
285
-
286
- console.log(chalk.green('✓ Generated GitHub Actions workflows:'));
287
- workflowFiles.forEach(file => console.log(chalk.gray(` - ${file}`)));
285
+ if (options.app) {
286
+ await setupAppFiles(appName, appPath, config, options);
288
287
  }
289
288
 
290
- // Display success message
291
- console.log(chalk.green('\n✓ Application created successfully!'));
292
- console.log(chalk.blue(`\nApplication: ${appName}`));
293
- console.log(chalk.blue(`Location: builder/${appName}/`));
294
- console.log(chalk.blue(`Language: ${config.language}`));
295
- console.log(chalk.blue(`Port: ${config.port}`));
296
-
297
- if (config.database) console.log(chalk.yellow(' - Database enabled'));
298
- if (config.redis) console.log(chalk.yellow(' - Redis enabled'));
299
- if (config.storage) console.log(chalk.yellow(' - Storage enabled'));
300
- if (config.authentication) console.log(chalk.yellow(' - Authentication enabled'));
301
-
302
- console.log(chalk.gray(envConversionMessage));
303
-
304
- console.log(chalk.green('\nNext steps:'));
305
- console.log(chalk.white('1. Copy env.template to .env and fill in your values'));
306
- console.log(chalk.white('2. Run: aifabrix build ' + appName));
307
- console.log(chalk.white('3. Run: aifabrix run ' + appName));
308
-
289
+ await handleGitHubWorkflows(options, config);
290
+ displaySuccessMessage(appName, config, envConversionMessage, options.app);
309
291
  } catch (error) {
310
292
  throw new Error(`Failed to create application: ${error.message}`);
311
293
  }
@@ -391,65 +373,13 @@ async function runApp(appName, options = {}) {
391
373
  }
392
374
 
393
375
  /**
394
- * Pushes application image to Azure Container Registry
376
+ * Deploys application to controller
395
377
  * @async
396
- * @function pushApp
378
+ * @function deployApp
397
379
  * @param {string} appName - Name of the application
398
- * @param {Object} options - Push options (registry, tag)
399
- * @returns {Promise<void>} Resolves when push is complete
380
+ * @param {Object} options - Deployment options
381
+ * @returns {Promise<void>} Resolves when deployment is complete
400
382
  */
401
- async function pushApp(appName, options = {}) {
402
- try {
403
- // Validate app name
404
- validateAppName(appName);
405
-
406
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
407
- let config;
408
- try {
409
- config = yaml.load(await fs.readFile(configPath, 'utf8'));
410
- } catch (error) {
411
- throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
412
- }
413
-
414
- const registry = options.registry || config.image?.registry;
415
- if (!registry) {
416
- throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
417
- }
418
-
419
- if (!pushUtils.validateRegistryURL(registry)) {
420
- throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
421
- }
422
-
423
- const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
424
-
425
- if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
426
- throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
427
- }
428
-
429
- if (!await pushUtils.checkAzureCLIInstalled()) {
430
- throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
431
- }
432
-
433
- if (await pushUtils.checkACRAuthentication(registry)) {
434
- console.log(chalk.green(`✓ Already authenticated with ${registry}`));
435
- } else {
436
- await pushUtils.authenticateACR(registry);
437
- }
438
-
439
- await Promise.all(tags.map(async(tag) => {
440
- await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
441
- await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
442
- }));
443
-
444
- console.log(chalk.green(`\n✓ Successfully pushed ${tags.length} tag(s) to ${registry}`));
445
- console.log(chalk.gray(`Image: ${registry}/${appName}:*`));
446
- console.log(chalk.gray(`Tags: ${tags.join(', ')}`));
447
-
448
- } catch (error) {
449
- throw new Error(`Failed to push application: ${error.message}`);
450
- }
451
- }
452
-
453
383
  async function deployApp(appName, options = {}) {
454
384
  const appDeploy = require('./app-deploy');
455
385
  return appDeploy.deployApp(appName, options);
@@ -461,8 +391,12 @@ module.exports = {
461
391
  runApp,
462
392
  detectLanguage,
463
393
  generateDockerfile,
394
+ generateDockerfileForApp,
464
395
  pushApp,
465
396
  deployApp,
397
+ loadTemplateVariables,
398
+ updateTemplateVariables,
399
+ mergeTemplateVariables,
466
400
  checkImageExists: appRun.checkImageExists,
467
401
  checkContainerRunning: appRun.checkContainerRunning,
468
402
  stopAndRemoveContainer: appRun.stopAndRemoveContainer,
@@ -9,6 +9,8 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
+ /* eslint-disable no-console */
13
+
12
14
  /**
13
15
  * Masks sensitive data in strings
14
16
  * Prevents secrets, keys, and passwords from appearing in logs