@aifabrix/builder 2.6.3 → 2.8.0

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 (43) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/bin/aifabrix.js +4 -0
  3. package/lib/app-config.js +10 -0
  4. package/lib/app-deploy.js +18 -0
  5. package/lib/app-dockerfile.js +15 -0
  6. package/lib/app-prompts.js +172 -9
  7. package/lib/app-push.js +15 -0
  8. package/lib/app-register.js +14 -0
  9. package/lib/app-run.js +25 -0
  10. package/lib/app.js +30 -13
  11. package/lib/audit-logger.js +9 -4
  12. package/lib/build.js +8 -0
  13. package/lib/cli.js +99 -2
  14. package/lib/commands/datasource.js +94 -0
  15. package/lib/commands/login.js +40 -3
  16. package/lib/config.js +121 -114
  17. package/lib/datasource-deploy.js +182 -0
  18. package/lib/datasource-diff.js +73 -0
  19. package/lib/datasource-list.js +138 -0
  20. package/lib/datasource-validate.js +63 -0
  21. package/lib/diff.js +266 -0
  22. package/lib/environment-deploy.js +305 -0
  23. package/lib/external-system-deploy.js +262 -0
  24. package/lib/external-system-generator.js +187 -0
  25. package/lib/schema/application-schema.json +869 -698
  26. package/lib/schema/external-datasource.schema.json +512 -0
  27. package/lib/schema/external-system.schema.json +262 -0
  28. package/lib/schema/infrastructure-schema.json +1 -1
  29. package/lib/secrets.js +20 -1
  30. package/lib/templates.js +32 -1
  31. package/lib/utils/device-code.js +10 -2
  32. package/lib/utils/env-copy.js +24 -0
  33. package/lib/utils/env-endpoints.js +50 -11
  34. package/lib/utils/schema-loader.js +220 -0
  35. package/lib/utils/schema-resolver.js +174 -0
  36. package/lib/utils/secrets-helpers.js +65 -17
  37. package/lib/utils/token-encryption.js +68 -0
  38. package/lib/validate.js +299 -0
  39. package/lib/validator.js +47 -3
  40. package/package.json +1 -1
  41. package/tatus +181 -0
  42. package/templates/external-system/external-datasource.json.hbs +55 -0
  43. package/templates/external-system/external-system.json.hbs +37 -0
package/lib/app-config.js CHANGED
@@ -54,6 +54,11 @@ async function generateVariablesYamlFile(appPath, appName, config) {
54
54
  * @param {Object} existingEnv - Existing environment variables
55
55
  */
56
56
  async function generateEnvTemplateFile(appPath, config, existingEnv) {
57
+ // Skip env.template for external type
58
+ if (config.type === 'external') {
59
+ return;
60
+ }
61
+
57
62
  const envTemplatePath = path.join(appPath, 'env.template');
58
63
  if (!(await fileExists(envTemplatePath))) {
59
64
  let envTemplate;
@@ -101,6 +106,11 @@ async function generateRbacYamlFile(appPath, appName, config) {
101
106
  * @param {Object} config - Application configuration
102
107
  */
103
108
  async function generateDeployJsonFile(appPath, appName, config) {
109
+ // Skip aifabrix-deploy.json for external type (uses pipeline API instead)
110
+ if (config.type === 'external') {
111
+ return;
112
+ }
113
+
104
114
  const deployJson = {
105
115
  apiVersion: 'v1',
106
116
  kind: 'ApplicationDeployment',
package/lib/app-deploy.js CHANGED
@@ -366,6 +366,24 @@ async function deployApp(appName, options = {}) {
366
366
 
367
367
  validateAppName(appName);
368
368
 
369
+ // 2. Check if app type is external - route to external deployment
370
+ const yaml = require('js-yaml');
371
+ const fs = require('fs').promises;
372
+ const path = require('path');
373
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
374
+ try {
375
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
376
+ const variables = yaml.load(variablesContent);
377
+ if (variables.app && variables.app.type === 'external') {
378
+ const externalDeploy = require('./external-system-deploy');
379
+ await externalDeploy.deployExternalSystem(appName, options);
380
+ return { success: true, type: 'external' };
381
+ }
382
+ } catch (error) {
383
+ // If variables.yaml doesn't exist or can't be read, continue with normal deployment
384
+ // The error will be properly handled in loadDeploymentConfig
385
+ }
386
+
369
387
  // 2. Load deployment configuration
370
388
  config = await loadDeploymentConfig(appName, options);
371
389
  controllerUrl = config.controllerUrl || options.controller || 'unknown';
@@ -84,6 +84,21 @@ async function generateAndCopyDockerfile(appPath, dockerfilePath, config) {
84
84
  * @returns {Promise<string>} Path to generated Dockerfile
85
85
  */
86
86
  async function generateDockerfileForApp(appName, options = {}) {
87
+ // Check if app type is external - skip Dockerfile generation
88
+ const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
89
+ try {
90
+ const yamlContent = await fs.readFile(configPath, 'utf8');
91
+ const variables = yaml.load(yamlContent);
92
+ if (variables.app && variables.app.type === 'external') {
93
+ logger.log(chalk.yellow('⚠️ External systems don\'t require Dockerfiles. Skipping...'));
94
+ return null;
95
+ }
96
+ } catch (error) {
97
+ // If variables.yaml doesn't exist, continue with normal generation
98
+ if (error.code !== 'ENOENT') {
99
+ throw error;
100
+ }
101
+ }
87
102
  try {
88
103
  // Validate app name
89
104
  validateAppName(appName);
@@ -13,11 +13,17 @@ const inquirer = require('inquirer');
13
13
  /**
14
14
  * Builds basic questions (port, language)
15
15
  * @param {Object} options - Provided options
16
+ * @param {string} [appType] - Application type (webapp, api, service, functionapp, external)
16
17
  * @returns {Array} Array of question objects
17
18
  */
18
- function buildBasicQuestions(options) {
19
+ function buildBasicQuestions(options, appType) {
19
20
  const questions = [];
20
21
 
22
+ // Skip port and language for external type
23
+ if (appType === 'external') {
24
+ return questions;
25
+ }
26
+
21
27
  // Port validation
22
28
  if (!options.port) {
23
29
  questions.push({
@@ -55,11 +61,17 @@ function buildBasicQuestions(options) {
55
61
  /**
56
62
  * Builds service questions (database, redis, storage, authentication)
57
63
  * @param {Object} options - Provided options
64
+ * @param {string} [appType] - Application type (webapp, api, service, functionapp, external)
58
65
  * @returns {Array} Array of question objects
59
66
  */
60
- function buildServiceQuestions(options) {
67
+ function buildServiceQuestions(options, appType) {
61
68
  const questions = [];
62
69
 
70
+ // Skip service questions for external type
71
+ if (appType === 'external') {
72
+ return questions;
73
+ }
74
+
63
75
  if (!Object.prototype.hasOwnProperty.call(options, 'database')) {
64
76
  questions.push({
65
77
  type: 'confirm',
@@ -99,6 +111,116 @@ function buildServiceQuestions(options) {
99
111
  return questions;
100
112
  }
101
113
 
114
+ /**
115
+ * Builds external system configuration questions
116
+ * @param {Object} options - Provided options
117
+ * @param {string} appName - Application name
118
+ * @returns {Array} Array of question objects
119
+ */
120
+ function buildExternalSystemQuestions(options, appName) {
121
+ const questions = [];
122
+
123
+ // System key (defaults to app name)
124
+ if (!options.systemKey) {
125
+ questions.push({
126
+ type: 'input',
127
+ name: 'systemKey',
128
+ message: 'What is the system key?',
129
+ default: appName,
130
+ validate: (input) => {
131
+ if (!input || input.trim().length === 0) {
132
+ return 'System key is required';
133
+ }
134
+ if (!/^[a-z0-9-]+$/.test(input)) {
135
+ return 'System key must contain only lowercase letters, numbers, and hyphens';
136
+ }
137
+ return true;
138
+ }
139
+ });
140
+ }
141
+
142
+ // System display name
143
+ if (!options.systemDisplayName) {
144
+ questions.push({
145
+ type: 'input',
146
+ name: 'systemDisplayName',
147
+ message: 'What is the system display name?',
148
+ default: appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
149
+ validate: (input) => {
150
+ if (!input || input.trim().length === 0) {
151
+ return 'System display name is required';
152
+ }
153
+ return true;
154
+ }
155
+ });
156
+ }
157
+
158
+ // System description
159
+ if (!options.systemDescription) {
160
+ questions.push({
161
+ type: 'input',
162
+ name: 'systemDescription',
163
+ message: 'What is the system description?',
164
+ default: `External system integration for ${appName}`,
165
+ validate: (input) => {
166
+ if (!input || input.trim().length === 0) {
167
+ return 'System description is required';
168
+ }
169
+ return true;
170
+ }
171
+ });
172
+ }
173
+
174
+ // System type
175
+ if (!options.systemType) {
176
+ questions.push({
177
+ type: 'list',
178
+ name: 'systemType',
179
+ message: 'What is the system type?',
180
+ choices: [
181
+ { name: 'OpenAPI', value: 'openapi' },
182
+ { name: 'MCP (Model Context Protocol)', value: 'mcp' },
183
+ { name: 'Custom', value: 'custom' }
184
+ ],
185
+ default: 'openapi'
186
+ });
187
+ }
188
+
189
+ // Authentication type
190
+ if (!options.authType) {
191
+ questions.push({
192
+ type: 'list',
193
+ name: 'authType',
194
+ message: 'What authentication type does the system use?',
195
+ choices: [
196
+ { name: 'OAuth2', value: 'oauth2' },
197
+ { name: 'API Key', value: 'apikey' },
198
+ { name: 'Basic Auth', value: 'basic' }
199
+ ],
200
+ default: 'apikey'
201
+ });
202
+ }
203
+
204
+ // Number of datasources
205
+ if (!options.datasourceCount) {
206
+ questions.push({
207
+ type: 'input',
208
+ name: 'datasourceCount',
209
+ message: 'How many datasources do you want to create?',
210
+ default: '1',
211
+ validate: (input) => {
212
+ const count = parseInt(input, 10);
213
+ if (isNaN(count) || count < 1 || count > 10) {
214
+ return 'Datasource count must be a number between 1 and 10';
215
+ }
216
+ return true;
217
+ }
218
+ });
219
+ }
220
+
221
+ return questions;
222
+ }
223
+
102
224
  /**
103
225
  * Builds workflow questions (GitHub, Controller)
104
226
  * @param {Object} options - Provided options
@@ -186,7 +308,7 @@ function resolveOptionalBoolean(optionValue, answerValue, defaultValue) {
186
308
  * @returns {Object} Resolved configuration
187
309
  */
188
310
  function resolveConflicts(options, answers) {
189
- return {
311
+ const config = {
190
312
  port: parseInt(resolveField(options.port, answers.port, 3000), 10),
191
313
  language: resolveField(options.language, answers.language, 'typescript'),
192
314
  database: resolveField(options.database, answers.database, false),
@@ -197,6 +319,28 @@ function resolveConflicts(options, answers) {
197
319
  controller: resolveOptionalBoolean(options.controller, answers.controller, false),
198
320
  controllerUrl: resolveField(options.controllerUrl, answers.controllerUrl, undefined)
199
321
  };
322
+
323
+ // Add external system fields if present
324
+ if (answers.systemKey || options.systemKey) {
325
+ config.systemKey = resolveField(options.systemKey, answers.systemKey, undefined);
326
+ }
327
+ if (answers.systemDisplayName || options.systemDisplayName) {
328
+ config.systemDisplayName = resolveField(options.systemDisplayName, answers.systemDisplayName, undefined);
329
+ }
330
+ if (answers.systemDescription || options.systemDescription) {
331
+ config.systemDescription = resolveField(options.systemDescription, answers.systemDescription, undefined);
332
+ }
333
+ if (answers.systemType || options.systemType) {
334
+ config.systemType = resolveField(options.systemType, answers.systemType, 'openapi');
335
+ }
336
+ if (answers.authType || options.authType) {
337
+ config.authType = resolveField(options.authType, answers.authType, 'apikey');
338
+ }
339
+ if (answers.datasourceCount || options.datasourceCount) {
340
+ config.datasourceCount = parseInt(resolveField(options.datasourceCount, answers.datasourceCount, 1), 10);
341
+ }
342
+
343
+ return config;
200
344
  }
201
345
 
202
346
  /**
@@ -225,17 +369,36 @@ async function promptForOptions(appName, options) {
225
369
  options.github = false;
226
370
  }
227
371
 
228
- const questions = [
229
- ...buildBasicQuestions(options),
230
- ...buildServiceQuestions(options),
231
- ...buildWorkflowQuestions(options)
232
- ];
372
+ // Get app type from options (default to webapp)
373
+ const appType = options.type || 'webapp';
374
+
375
+ // Build questions based on app type
376
+ let questions = [];
377
+ if (appType === 'external') {
378
+ // For external type, prompt for external system configuration
379
+ questions = [
380
+ ...buildExternalSystemQuestions(options, appName),
381
+ ...buildWorkflowQuestions(options)
382
+ ];
383
+ } else {
384
+ // For regular apps, use standard prompts
385
+ questions = [
386
+ ...buildBasicQuestions(options, appType),
387
+ ...buildServiceQuestions(options, appType),
388
+ ...buildWorkflowQuestions(options)
389
+ ];
390
+ }
233
391
 
234
392
  // Prompt for missing options
235
393
  const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
236
394
 
237
395
  // Merge provided options with answers
238
- return mergePromptAnswers(appName, options, answers);
396
+ const merged = mergePromptAnswers(appName, options, answers);
397
+
398
+ // Add type to merged config
399
+ merged.type = appType;
400
+
401
+ return merged;
239
402
  }
240
403
 
241
404
  module.exports = {
package/lib/app-push.js CHANGED
@@ -179,6 +179,21 @@ function displayPushResults(registry, imageName, tags) {
179
179
  * @returns {Promise<void>} Resolves when push is complete
180
180
  */
181
181
  async function pushApp(appName, options = {}) {
182
+ // Check if app type is external - skip push
183
+ const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
184
+ try {
185
+ const yamlContent = await fs.readFile(configPath, 'utf8');
186
+ const config = yaml.load(yamlContent);
187
+ if (config.app && config.app.type === 'external') {
188
+ logger.log(chalk.yellow('⚠️ External systems don\'t require Docker images. Skipping push...'));
189
+ return;
190
+ }
191
+ } catch (error) {
192
+ // If variables.yaml doesn't exist, continue with normal push
193
+ if (error.code !== 'ENOENT') {
194
+ throw error;
195
+ }
196
+ }
182
197
  try {
183
198
  // Validate app name
184
199
  validateAppName(appName);
@@ -138,6 +138,20 @@ function extractAppConfiguration(variables, appKey, options) {
138
138
  const appKeyFromFile = variables.app?.key || appKey;
139
139
  const displayName = variables.app?.name || options.name || appKey;
140
140
  const description = variables.app?.description || '';
141
+
142
+ // Handle external type
143
+ if (variables.app?.type === 'external') {
144
+ return {
145
+ appKey: appKeyFromFile,
146
+ displayName,
147
+ description,
148
+ appType: 'external',
149
+ registryMode: 'external',
150
+ port: null, // External systems don't need ports
151
+ language: null // External systems don't need language
152
+ };
153
+ }
154
+
141
155
  const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
142
156
  const registryMode = 'external';
143
157
  const port = variables.build?.port || options.port || 3000;
package/lib/app-run.js CHANGED
@@ -43,6 +43,31 @@ async function runApp(appName, options = {}) {
43
43
  }
44
44
 
45
45
  try {
46
+ // Validate app name first
47
+ if (!appName || typeof appName !== 'string') {
48
+ throw new Error('Application name is required');
49
+ }
50
+
51
+ // Check if app type is external - skip Docker run
52
+ const yaml = require('js-yaml');
53
+ const fs = require('fs').promises;
54
+ const path = require('path');
55
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
56
+ try {
57
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
58
+ const variables = yaml.load(variablesContent);
59
+ if (variables.app && variables.app.type === 'external') {
60
+ logger.log(chalk.yellow('⚠️ External systems don\'t run as Docker containers.'));
61
+ logger.log(chalk.blue('Use "aifabrix build" to deploy to dataplane, then test via OpenAPI endpoints.'));
62
+ return;
63
+ }
64
+ } catch (error) {
65
+ // If variables.yaml doesn't exist, continue with normal run
66
+ if (error.code !== 'ENOENT') {
67
+ throw error;
68
+ }
69
+ }
70
+
46
71
  // Validate app name and load configuration
47
72
  const appConfig = await helpers.validateAppConfiguration(appName);
48
73
 
package/lib/app.js CHANGED
@@ -38,20 +38,31 @@ function displaySuccessMessage(appName, config, envConversionMessage, hasAppFile
38
38
  if (hasAppFiles) {
39
39
  logger.log(chalk.blue(`Application files: apps/${appName}/`));
40
40
  }
41
- logger.log(chalk.blue(`Language: ${config.language}`));
42
- logger.log(chalk.blue(`Port: ${config.port}`));
43
41
 
44
- if (config.database) logger.log(chalk.yellow(' - Database enabled'));
45
- if (config.redis) logger.log(chalk.yellow(' - Redis enabled'));
46
- if (config.storage) logger.log(chalk.yellow(' - Storage enabled'));
47
- if (config.authentication) logger.log(chalk.yellow(' - Authentication enabled'));
48
-
49
- logger.log(chalk.gray(envConversionMessage));
50
-
51
- logger.log(chalk.green('\nNext steps:'));
52
- logger.log(chalk.white('1. Copy env.template to .env and fill in your values'));
53
- logger.log(chalk.white('2. Run: aifabrix build ' + appName));
54
- logger.log(chalk.white('3. Run: aifabrix run ' + appName));
42
+ if (config.type === 'external') {
43
+ logger.log(chalk.blue('Type: External System'));
44
+ logger.log(chalk.blue(`System Key: ${config.systemKey || appName}`));
45
+ logger.log(chalk.green('\nNext steps:'));
46
+ logger.log(chalk.white('1. Edit external system JSON files in builder/' + appName + '/schemas/'));
47
+ logger.log(chalk.white('2. Run: aifabrix app register ' + appName + ' --environment dev'));
48
+ logger.log(chalk.white('3. Run: aifabrix build ' + appName + ' (deploys to dataplane)'));
49
+ logger.log(chalk.white('4. Run: aifabrix deploy ' + appName + ' (publishes to dataplane)'));
50
+ } else {
51
+ logger.log(chalk.blue(`Language: ${config.language}`));
52
+ logger.log(chalk.blue(`Port: ${config.port}`));
53
+
54
+ if (config.database) logger.log(chalk.yellow(' - Database enabled'));
55
+ if (config.redis) logger.log(chalk.yellow(' - Redis enabled'));
56
+ if (config.storage) logger.log(chalk.yellow(' - Storage enabled'));
57
+ if (config.authentication) logger.log(chalk.yellow(' - Authentication enabled'));
58
+
59
+ logger.log(chalk.gray(envConversionMessage));
60
+
61
+ logger.log(chalk.green('\nNext steps:'));
62
+ logger.log(chalk.white('1. Copy env.template to .env and fill in your values'));
63
+ logger.log(chalk.white('2. Run: aifabrix build ' + appName));
64
+ logger.log(chalk.white('3. Run: aifabrix run ' + appName));
65
+ }
55
66
  }
56
67
 
57
68
  /**
@@ -284,6 +295,12 @@ async function createApp(appName, options = {}) {
284
295
 
285
296
  await generateConfigFiles(appPath, appName, config, existingEnv);
286
297
 
298
+ // Generate external system files if type is external
299
+ if (config.type === 'external') {
300
+ const externalGenerator = require('./external-system-generator');
301
+ await externalGenerator.generateExternalSystemFiles(appPath, appName, config);
302
+ }
303
+
287
304
  if (options.app) {
288
305
  await setupAppFiles(appName, appPath, config, options);
289
306
  }
@@ -106,9 +106,14 @@ function createAuditEntry(level, message, metadata = {}) {
106
106
  for (const [key, value] of Object.entries(metadata)) {
107
107
  // Skip null and undefined values to keep logs clean
108
108
  if (value !== null && value !== undefined) {
109
- entry.metadata[key] = maskSensitiveData(
110
- typeof value === 'string' ? value : JSON.stringify(value)
111
- );
109
+ // Only mask strings; preserve other types (numbers, booleans, etc.)
110
+ if (typeof value === 'string') {
111
+ entry.metadata[key] = maskSensitiveData(value);
112
+ } else {
113
+ // For non-string values, stringify only if it's an object/array
114
+ // Preserve primitives (numbers, booleans) as-is
115
+ entry.metadata[key] = typeof value === 'object' ? JSON.stringify(value) : value;
116
+ }
112
117
  }
113
118
  }
114
119
 
@@ -280,7 +285,7 @@ async function logApiCall(url, options, statusCode, duration, success, errorInfo
280
285
  path,
281
286
  url: maskSensitiveData(url),
282
287
  controllerUrl: maskSensitiveData(controllerUrl),
283
- statusCode,
288
+ statusCode: Number(statusCode),
284
289
  duration,
285
290
  success,
286
291
  timestamp: Date.now()
package/lib/build.js CHANGED
@@ -335,6 +335,14 @@ async function postBuildTasks(appName, buildConfig) {
335
335
  * // Returns: 'myapp:latest'
336
336
  */
337
337
  async function buildApp(appName, options = {}) {
338
+ // Check if app type is external - deploy to dataplane instead of Docker build
339
+ const variables = await loadVariablesYaml(appName);
340
+ if (variables.app && variables.app.type === 'external') {
341
+ const externalDeploy = require('./external-system-deploy');
342
+ await externalDeploy.buildExternalSystem(appName, options);
343
+ return null;
344
+ }
345
+
338
346
  try {
339
347
  logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
340
348
 
package/lib/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * AI Fabrix Builder CLI Command Definitions
3
4
  *
@@ -37,6 +38,8 @@ function setupCommands(program) {
37
38
  .option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
38
39
  .option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
39
40
  .option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
41
+ .option('--offline', 'Request offline token (adds offline_access scope, device flow only)')
42
+ .option('--scope <scopes>', 'Custom OAuth2 scope string (device flow only, default: "openid profile email")')
40
43
  .action(async(options) => {
41
44
  try {
42
45
  await handleLogin(options);
@@ -102,12 +105,18 @@ function setupCommands(program) {
102
105
  .option('-a, --authentication', 'Requires authentication/RBAC')
103
106
  .option('-l, --language <lang>', 'Runtime language (typescript/python)')
104
107
  .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
108
+ .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
105
109
  .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
106
110
  .option('-g, --github', 'Generate GitHub Actions workflows')
107
111
  .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
108
112
  .option('--main-branch <branch>', 'Main branch name for workflows', 'main')
109
113
  .action(async(appName, options) => {
110
114
  try {
115
+ // Validate type if provided
116
+ const validTypes = ['webapp', 'api', 'service', 'functionapp', 'external'];
117
+ if (options.type && !validTypes.includes(options.type)) {
118
+ throw new Error(`Invalid type: ${options.type}. Must be one of: ${validTypes.join(', ')}`);
119
+ }
111
120
  await app.createApp(appName, options);
112
121
  } catch (error) {
113
122
  handleCommandError(error, 'create');
@@ -157,6 +166,46 @@ function setupCommands(program) {
157
166
  }
158
167
  });
159
168
 
169
+ // Environment deployment command
170
+ const environment = program
171
+ .command('environment')
172
+ .description('Manage environments');
173
+
174
+ const deployEnvHandler = async(envKey, options) => {
175
+ try {
176
+ const environmentDeploy = require('./environment-deploy');
177
+ await environmentDeploy.deployEnvironment(envKey, options);
178
+ } catch (error) {
179
+ handleCommandError(error, 'environment deploy');
180
+ process.exit(1);
181
+ }
182
+ };
183
+
184
+ environment
185
+ .command('deploy <env>')
186
+ .description('Deploy/setup environment in Miso Controller')
187
+ .option('-c, --controller <url>', 'Controller URL (required)')
188
+ .option('--config <file>', 'Environment configuration file')
189
+ .option('--skip-validation', 'Skip environment validation')
190
+ .option('--poll', 'Poll for deployment status', true)
191
+ .option('--no-poll', 'Do not poll for status')
192
+ .action(deployEnvHandler);
193
+
194
+ // Alias: env deploy (register as separate command since Commander.js doesn't support multi-word aliases)
195
+ const env = program
196
+ .command('env')
197
+ .description('Environment management (alias for environment)');
198
+
199
+ env
200
+ .command('deploy <env>')
201
+ .description('Deploy/setup environment in Miso Controller')
202
+ .option('-c, --controller <url>', 'Controller URL (required)')
203
+ .option('--config <file>', 'Environment configuration file')
204
+ .option('--skip-validation', 'Skip environment validation')
205
+ .option('--poll', 'Poll for deployment status', true)
206
+ .option('--no-poll', 'Do not poll for status')
207
+ .action(deployEnvHandler);
208
+
160
209
  program.command('deploy <app>')
161
210
  .description('Deploy to Azure via Miso Controller')
162
211
  .option('-c, --controller <url>', 'Controller URL')
@@ -265,12 +314,26 @@ function setupCommands(program) {
265
314
 
266
315
  // Utility commands
267
316
  program.command('resolve <app>')
268
- .description('Generate .env file from template')
317
+ .description('Generate .env file from template and validate application files')
269
318
  .option('-f, --force', 'Generate missing secret keys in secrets file')
319
+ .option('--skip-validation', 'Skip file validation after generating .env')
270
320
  .action(async(appName, options) => {
271
321
  try {
272
- const envPath = await secrets.generateEnvFile(appName, undefined, 'local', options.force);
322
+ // builder/.env should use docker context (postgres:5432)
323
+ // apps/.env (if envOutputPath is set) will be generated with local context by processEnvVariables
324
+ const envPath = await secrets.generateEnvFile(appName, undefined, 'docker', options.force);
273
325
  logger.log(`✓ Generated .env file: ${envPath}`);
326
+
327
+ // Validate application files after generating .env
328
+ if (!options.skipValidation) {
329
+ const validate = require('./validate');
330
+ const result = await validate.validateAppOrFile(appName);
331
+ validate.displayValidationResults(result);
332
+ if (!result.valid) {
333
+ logger.log(chalk.yellow('\n⚠️ Validation found errors. Fix them before deploying.'));
334
+ process.exit(1);
335
+ }
336
+ }
274
337
  } catch (error) {
275
338
  handleCommandError(error, 'resolve');
276
339
  process.exit(1);
@@ -330,6 +393,40 @@ function setupCommands(program) {
330
393
  }
331
394
  });
332
395
 
396
+ // Validation command
397
+ program.command('validate <appOrFile>')
398
+ .description('Validate application or external integration file')
399
+ .action(async(appOrFile) => {
400
+ try {
401
+ const validate = require('./validate');
402
+ const result = await validate.validateAppOrFile(appOrFile);
403
+ validate.displayValidationResults(result);
404
+ if (!result.valid) {
405
+ process.exit(1);
406
+ }
407
+ } catch (error) {
408
+ handleCommandError(error, 'validate');
409
+ process.exit(1);
410
+ }
411
+ });
412
+
413
+ // Diff command
414
+ program.command('diff <file1> <file2>')
415
+ .description('Compare two configuration files (for deployment pipeline)')
416
+ .action(async(file1, file2) => {
417
+ try {
418
+ const diff = require('./diff');
419
+ const result = await diff.compareFiles(file1, file2);
420
+ diff.formatDiffOutput(result);
421
+ if (!result.identical) {
422
+ process.exit(1);
423
+ }
424
+ } catch (error) {
425
+ handleCommandError(error, 'diff');
426
+ process.exit(1);
427
+ }
428
+ });
429
+
333
430
  program.command('dockerfile <app>')
334
431
  .description('Generate Dockerfile for an application')
335
432
  .option('-l, --language <lang>', 'Override language detection')