@aifabrix/builder 2.7.0 → 2.9.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 (47) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/integration/hubspot/README.md +136 -0
  3. package/integration/hubspot/env.template +9 -0
  4. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  5. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  6. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  7. package/integration/hubspot/hubspot-deploy.json +91 -0
  8. package/integration/hubspot/variables.yaml +17 -0
  9. package/lib/app-config.js +13 -2
  10. package/lib/app-deploy.js +9 -3
  11. package/lib/app-dockerfile.js +14 -1
  12. package/lib/app-prompts.js +177 -13
  13. package/lib/app-push.js +16 -1
  14. package/lib/app-register.js +37 -5
  15. package/lib/app-rotate-secret.js +10 -0
  16. package/lib/app-run.js +19 -0
  17. package/lib/app.js +70 -25
  18. package/lib/audit-logger.js +9 -4
  19. package/lib/build.js +25 -13
  20. package/lib/cli.js +109 -2
  21. package/lib/commands/login.js +40 -3
  22. package/lib/config.js +121 -114
  23. package/lib/datasource-deploy.js +14 -20
  24. package/lib/environment-deploy.js +305 -0
  25. package/lib/external-system-deploy.js +345 -0
  26. package/lib/external-system-download.js +431 -0
  27. package/lib/external-system-generator.js +190 -0
  28. package/lib/external-system-test.js +446 -0
  29. package/lib/generator-builders.js +323 -0
  30. package/lib/generator.js +200 -292
  31. package/lib/schema/application-schema.json +830 -800
  32. package/lib/schema/external-datasource.schema.json +868 -46
  33. package/lib/schema/external-system.schema.json +98 -80
  34. package/lib/schema/infrastructure-schema.json +1 -1
  35. package/lib/templates.js +32 -1
  36. package/lib/utils/cli-utils.js +4 -4
  37. package/lib/utils/device-code.js +10 -2
  38. package/lib/utils/external-system-display.js +159 -0
  39. package/lib/utils/external-system-validators.js +245 -0
  40. package/lib/utils/paths.js +151 -1
  41. package/lib/utils/schema-resolver.js +7 -2
  42. package/lib/utils/token-encryption.js +68 -0
  43. package/lib/validator.js +52 -5
  44. package/package.json +1 -1
  45. package/tatus +181 -0
  46. package/templates/external-system/external-datasource.json.hbs +55 -0
  47. package/templates/external-system/external-system.json.hbs +37 -0
@@ -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
  /**
@@ -220,22 +364,42 @@ function mergePromptAnswers(appName, options, answers) {
220
364
  * @returns {Promise<Object>} Complete configuration
221
365
  */
222
366
  async function promptForOptions(appName, options) {
223
- // Default github to false if not provided (make it truly optional)
224
- if (!Object.prototype.hasOwnProperty.call(options, 'github')) {
225
- options.github = false;
226
- }
367
+ // Get app type from options (default to webapp)
368
+ const appType = options.type || 'webapp';
227
369
 
228
- const questions = [
229
- ...buildBasicQuestions(options),
230
- ...buildServiceQuestions(options),
231
- ...buildWorkflowQuestions(options)
232
- ];
370
+ // Build questions based on app type
371
+ let questions = [];
372
+ if (appType === 'external') {
373
+ // For external type, prompt for external system configuration
374
+ questions = [
375
+ ...buildExternalSystemQuestions(options, appName),
376
+ ...buildWorkflowQuestions(options)
377
+ ];
378
+ } else {
379
+ // For regular apps, use standard prompts
380
+ questions = [
381
+ ...buildBasicQuestions(options, appType),
382
+ ...buildServiceQuestions(options, appType),
383
+ ...buildWorkflowQuestions(options)
384
+ ];
385
+ }
233
386
 
234
387
  // Prompt for missing options
235
388
  const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
236
389
 
237
390
  // Merge provided options with answers
238
- return mergePromptAnswers(appName, options, answers);
391
+ const merged = mergePromptAnswers(appName, options, answers);
392
+
393
+ // Add type to merged config
394
+ merged.type = appType;
395
+
396
+ // For external type, remove port and language as they're not applicable
397
+ if (appType === 'external') {
398
+ delete merged.port;
399
+ delete merged.language;
400
+ }
401
+
402
+ return merged;
239
403
  }
240
404
 
241
405
  module.exports = {
package/lib/app-push.js CHANGED
@@ -69,7 +69,10 @@ function extractImageName(config, appName) {
69
69
  * @throws {Error} If configuration cannot be loaded
70
70
  */
71
71
  async function loadPushConfig(appName, options) {
72
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
72
+ // Detect app type and get correct path (integration or builder)
73
+ const { detectAppType } = require('./utils/paths');
74
+ const { appPath } = await detectAppType(appName);
75
+ const configPath = path.join(appPath, 'variables.yaml');
73
76
  try {
74
77
  const config = yaml.load(await fs.readFile(configPath, 'utf8'));
75
78
  const registry = options.registry || config.image?.registry;
@@ -179,6 +182,18 @@ function displayPushResults(registry, imageName, tags) {
179
182
  * @returns {Promise<void>} Resolves when push is complete
180
183
  */
181
184
  async function pushApp(appName, options = {}) {
185
+ // Check if app type is external - skip push
186
+ const { detectAppType } = require('./utils/paths');
187
+ try {
188
+ const { isExternal } = await detectAppType(appName);
189
+ if (isExternal) {
190
+ logger.log(chalk.yellow('⚠️ External systems don\'t require Docker images. Skipping push...'));
191
+ return;
192
+ }
193
+ } catch (error) {
194
+ // If detection fails, continue with normal push
195
+ // (detectAppType throws if app doesn't exist, which is fine for push command)
196
+ }
182
197
  try {
183
198
  // Validate app name
184
199
  validateAppName(appName);
@@ -19,6 +19,8 @@ const logger = require('./utils/logger');
19
19
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
20
20
  const { updateEnvTemplate } = require('./utils/env-template');
21
21
  const { getOrRefreshDeviceToken } = require('./utils/token-manager');
22
+ const { detectAppType } = require('./utils/paths');
23
+ const { generateEnvFile } = require('./secrets');
22
24
 
23
25
  // Import createApp to auto-generate config if missing
24
26
  let createApp;
@@ -86,7 +88,9 @@ const registerApplicationSchema = {
86
88
  * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
87
89
  */
88
90
  async function loadVariablesYaml(appKey) {
89
- const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
91
+ // Detect app type and get correct path (integration or builder)
92
+ const { appPath } = await detectAppType(appKey);
93
+ const variablesPath = path.join(appPath, 'variables.yaml');
90
94
 
91
95
  try {
92
96
  const variablesContent = await fs.readFile(variablesPath, 'utf-8');
@@ -122,7 +126,9 @@ async function createMinimalAppIfNeeded(appKey, options) {
122
126
  authentication: false
123
127
  });
124
128
 
125
- const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
129
+ // Detect app type and get correct path (integration or builder)
130
+ const { appPath } = await detectAppType(appKey);
131
+ const variablesPath = path.join(appPath, 'variables.yaml');
126
132
  const variablesContent = await fs.readFile(variablesPath, 'utf-8');
127
133
  return yaml.load(variablesContent);
128
134
  }
@@ -138,6 +144,20 @@ function extractAppConfiguration(variables, appKey, options) {
138
144
  const appKeyFromFile = variables.app?.key || appKey;
139
145
  const displayName = variables.app?.name || options.name || appKey;
140
146
  const description = variables.app?.description || '';
147
+
148
+ // Handle external type
149
+ if (variables.app?.type === 'external') {
150
+ return {
151
+ appKey: appKeyFromFile,
152
+ displayName,
153
+ description,
154
+ appType: 'external',
155
+ registryMode: 'external',
156
+ port: null, // External systems don't need ports
157
+ language: null // External systems don't need language
158
+ };
159
+ }
160
+
141
161
  const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
142
162
  const registryMode = 'external';
143
163
  const port = variables.build?.port || options.port || 3000;
@@ -156,11 +176,12 @@ function extractAppConfiguration(variables, appKey, options) {
156
176
 
157
177
  /**
158
178
  * Validate application registration data
179
+ * @async
159
180
  * @param {Object} config - Application configuration
160
181
  * @param {string} originalAppKey - Original app key for error messages
161
182
  * @throws {Error} If validation fails
162
183
  */
163
- function validateAppRegistrationData(config, originalAppKey) {
184
+ async function validateAppRegistrationData(config, originalAppKey) {
164
185
  const missingFields = [];
165
186
  if (!config.appKey) missingFields.push('app.key');
166
187
  if (!config.displayName) missingFields.push('app.name');
@@ -168,7 +189,10 @@ function validateAppRegistrationData(config, originalAppKey) {
168
189
  if (missingFields.length > 0) {
169
190
  logger.error(chalk.red('❌ Missing required fields in variables.yaml:'));
170
191
  missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
171
- logger.error(chalk.red(`\n Please update builder/${originalAppKey}/variables.yaml and try again.`));
192
+ // Detect app type to show correct path
193
+ const { appPath } = await detectAppType(originalAppKey);
194
+ const relativePath = path.relative(process.cwd(), appPath);
195
+ logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
172
196
  process.exit(1);
173
197
  }
174
198
 
@@ -368,7 +392,7 @@ async function registerApplication(appKey, options) {
368
392
  const appConfig = extractAppConfiguration(finalVariables, appKey, options);
369
393
 
370
394
  // Validate configuration (pass original appKey for error messages)
371
- validateAppRegistrationData(appConfig, appKey);
395
+ await validateAppRegistrationData(appConfig, appKey);
372
396
 
373
397
  // Get controller URL from variables.yaml if available
374
398
  const controllerUrl = finalVariables?.deployment?.controllerUrl;
@@ -420,6 +444,14 @@ async function registerApplication(appKey, options) {
420
444
  // Update env.template
421
445
  await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, authConfig.apiUrl);
422
446
 
447
+ // Regenerate .env file with updated credentials
448
+ try {
449
+ await generateEnvFile(registeredAppKey, null, 'local');
450
+ logger.log(chalk.green('✓ .env file updated with new credentials'));
451
+ } catch (error) {
452
+ logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
453
+ }
454
+
423
455
  logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
424
456
  logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
425
457
  } catch (error) {
@@ -17,6 +17,7 @@ const logger = require('./utils/logger');
17
17
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
18
18
  const { updateEnvTemplate } = require('./utils/env-template');
19
19
  const { getEnvironmentPrefix } = require('./app-register');
20
+ const { generateEnvFile } = require('./secrets');
20
21
 
21
22
  /**
22
23
  * Validate environment parameter
@@ -147,6 +148,15 @@ async function rotateSecret(appKey, options) {
147
148
  // Update env.template if localhost
148
149
  if (isLocalhost(controllerUrl)) {
149
150
  await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controllerUrl);
151
+
152
+ // Regenerate .env file with updated credentials
153
+ try {
154
+ await generateEnvFile(appKey, null, 'local');
155
+ logger.log(chalk.green('✓ .env file updated with new credentials'));
156
+ } catch (error) {
157
+ logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
158
+ }
159
+
150
160
  logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
151
161
  logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
152
162
  } else {
package/lib/app-run.js CHANGED
@@ -43,6 +43,25 @@ 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 { detectAppType } = require('./utils/paths');
53
+ try {
54
+ const { isExternal } = await detectAppType(appName);
55
+ if (isExternal) {
56
+ logger.log(chalk.yellow('⚠️ External systems don\'t run as Docker containers.'));
57
+ logger.log(chalk.blue('Use "aifabrix build" to deploy to dataplane, then test via OpenAPI endpoints.'));
58
+ return;
59
+ }
60
+ } catch (error) {
61
+ // If detection fails, continue with normal run
62
+ // (detectAppType throws if app doesn't exist, which is fine for run command)
63
+ }
64
+
46
65
  // Validate app name and load configuration
47
66
  const appConfig = await helpers.validateAppConfiguration(appName);
48
67
 
package/lib/app.js CHANGED
@@ -24,6 +24,7 @@ const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables }
24
24
  const logger = require('./utils/logger');
25
25
  const auditLogger = require('./audit-logger');
26
26
  const { downApp } = require('./app-down');
27
+ const { getAppPath } = require('./utils/paths');
27
28
 
28
29
  /**
29
30
  * Displays success message after app creation
@@ -31,27 +32,43 @@ const { downApp } = require('./app-down');
31
32
  * @param {Object} config - Final configuration
32
33
  * @param {string} envConversionMessage - Environment conversion message
33
34
  */
34
- function displaySuccessMessage(appName, config, envConversionMessage, hasAppFiles = false) {
35
+ function displaySuccessMessage(appName, config, envConversionMessage, hasAppFiles = false, appPath = null) {
35
36
  logger.log(chalk.green('\n✓ Application created successfully!'));
36
37
  logger.log(chalk.blue(`\nApplication: ${appName}`));
37
- logger.log(chalk.blue(`Location: builder/${appName}/`));
38
+
39
+ // Determine location based on app type
40
+ const baseDir = config.type === 'external' ? 'integration' : 'builder';
41
+ const location = appPath ? path.relative(process.cwd(), appPath) : `${baseDir}/${appName}/`;
42
+ logger.log(chalk.blue(`Location: ${location}`));
43
+
38
44
  if (hasAppFiles) {
39
45
  logger.log(chalk.blue(`Application files: apps/${appName}/`));
40
46
  }
41
- logger.log(chalk.blue(`Language: ${config.language}`));
42
- logger.log(chalk.blue(`Port: ${config.port}`));
43
-
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
47
 
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));
48
+ if (config.type === 'external') {
49
+ logger.log(chalk.blue('Type: External System'));
50
+ logger.log(chalk.blue(`System Key: ${config.systemKey || appName}`));
51
+ logger.log(chalk.green('\nNext steps:'));
52
+ logger.log(chalk.white('1. Edit external system JSON files in ' + location));
53
+ logger.log(chalk.white('2. Run: aifabrix app register ' + appName + ' --environment dev'));
54
+ logger.log(chalk.white('3. Run: aifabrix build ' + appName + ' (deploys to dataplane)'));
55
+ logger.log(chalk.white('4. Run: aifabrix deploy ' + appName + ' (publishes to dataplane)'));
56
+ } else {
57
+ logger.log(chalk.blue(`Language: ${config.language}`));
58
+ logger.log(chalk.blue(`Port: ${config.port}`));
59
+
60
+ if (config.database) logger.log(chalk.yellow(' - Database enabled'));
61
+ if (config.redis) logger.log(chalk.yellow(' - Redis enabled'));
62
+ if (config.storage) logger.log(chalk.yellow(' - Storage enabled'));
63
+ if (config.authentication) logger.log(chalk.yellow(' - Authentication enabled'));
64
+
65
+ logger.log(chalk.gray(envConversionMessage));
66
+
67
+ logger.log(chalk.green('\nNext steps:'));
68
+ logger.log(chalk.white('1. Copy env.template to .env and fill in your values'));
69
+ logger.log(chalk.white('2. Run: aifabrix build ' + appName));
70
+ logger.log(chalk.white('3. Run: aifabrix run ' + appName));
71
+ }
55
72
  }
56
73
 
57
74
  /**
@@ -72,6 +89,16 @@ async function validateAppDirectoryNotExists(appPath, appName, baseDir = 'builde
72
89
  }
73
90
  }
74
91
 
92
+ /**
93
+ * Gets the base directory path for an app based on its type
94
+ * @param {string} appName - Application name
95
+ * @param {string} appType - Application type ('external' or other)
96
+ * @returns {string} Base directory path ('integration' or 'builder')
97
+ */
98
+ function getBaseDirForAppType(appType) {
99
+ return appType === 'external' ? 'integration' : 'builder';
100
+ }
101
+
75
102
  /**
76
103
  * Handles GitHub workflow generation if requested
77
104
  * @async
@@ -113,9 +140,9 @@ async function handleGitHubWorkflows(options, config) {
113
140
  * @param {string} appPath - Application directory path
114
141
  * @throws {Error} If validation fails
115
142
  */
116
- async function validateAppCreation(appName, options, appPath) {
143
+ async function validateAppCreation(appName, options, appPath, baseDir = 'builder') {
117
144
  validateAppName(appName);
118
- await validateAppDirectoryNotExists(appPath, appName, 'builder');
145
+ await validateAppDirectoryNotExists(appPath, appName, baseDir);
119
146
 
120
147
  if (!options.app) {
121
148
  return;
@@ -261,10 +288,13 @@ async function createApp(appName, options = {}) {
261
288
  throw new Error('Application name is required');
262
289
  }
263
290
 
264
- const builderPath = path.join(process.cwd(), 'builder');
265
- const appPath = path.join(builderPath, appName);
291
+ // Determine app type from options (will be confirmed during prompts)
292
+ // For now, check if type is explicitly set in options
293
+ const initialType = options.type || 'webapp';
294
+ const baseDir = getBaseDirForAppType(initialType);
295
+ const appPath = getAppPath(appName, initialType);
266
296
 
267
- await validateAppCreation(appName, options, appPath);
297
+ await validateAppCreation(appName, options, appPath, baseDir);
268
298
 
269
299
  if (options.template) {
270
300
  await validateTemplate(options.template);
@@ -274,22 +304,37 @@ async function createApp(appName, options = {}) {
274
304
  const mergedOptions = mergeTemplateVariables(options, templateVariables);
275
305
  const config = await promptForOptions(appName, mergedOptions);
276
306
 
277
- await fs.mkdir(appPath, { recursive: true });
278
- await processTemplateFiles(options.template, appPath, appName, options, config);
307
+ // Update appPath based on final config type (may have changed during prompts)
308
+ const finalBaseDir = getBaseDirForAppType(config.type);
309
+ const finalAppPath = getAppPath(appName, config.type);
310
+
311
+ // If path changed, validate the new path
312
+ if (finalAppPath !== appPath) {
313
+ await validateAppDirectoryNotExists(finalAppPath, appName, finalBaseDir);
314
+ }
315
+
316
+ await fs.mkdir(finalAppPath, { recursive: true });
317
+ await processTemplateFiles(options.template, finalAppPath, appName, options, config);
279
318
 
280
319
  const existingEnv = await readExistingEnv(process.cwd());
281
320
  const envConversionMessage = existingEnv
282
321
  ? '\n✓ Found existing .env file - sensitive values will be converted to kv:// references'
283
322
  : '';
284
323
 
285
- await generateConfigFiles(appPath, appName, config, existingEnv);
324
+ await generateConfigFiles(finalAppPath, appName, config, existingEnv);
325
+
326
+ // Generate external system files if type is external
327
+ if (config.type === 'external') {
328
+ const externalGenerator = require('./external-system-generator');
329
+ await externalGenerator.generateExternalSystemFiles(finalAppPath, appName, config);
330
+ }
286
331
 
287
332
  if (options.app) {
288
- await setupAppFiles(appName, appPath, config, options);
333
+ await setupAppFiles(appName, finalAppPath, config, options);
289
334
  }
290
335
 
291
336
  await handleGitHubWorkflows(options, config);
292
- displaySuccessMessage(appName, config, envConversionMessage, options.app);
337
+ displaySuccessMessage(appName, config, envConversionMessage, options.app, finalAppPath);
293
338
 
294
339
  // Log application creation for audit trail
295
340
  await auditLogger.logApplicationCreation(appName, {
@@ -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
@@ -14,6 +14,7 @@ const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const path = require('path');
16
16
  const paths = require('./utils/paths');
17
+ const { detectAppType } = require('./utils/paths');
17
18
  const { exec } = require('child_process');
18
19
  const { promisify } = require('util');
19
20
  const chalk = require('chalk');
@@ -68,9 +69,17 @@ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
68
69
  const sourcePath = path.join(templatePath, entry);
69
70
  const targetPath = path.join(devDir, entry);
70
71
 
71
- const entryStats = await fs.stat(sourcePath);
72
- if (entryStats.isFile()) {
73
- await fs.copyFile(sourcePath, targetPath);
72
+ // Skip if source file doesn't exist (e.g., .gitignore might not be in template)
73
+ try {
74
+ const entryStats = await fs.stat(sourcePath);
75
+ if (entryStats.isFile()) {
76
+ await fs.copyFile(sourcePath, targetPath);
77
+ }
78
+ } catch (error) {
79
+ // Skip files that don't exist (e.g., .gitignore might not be in template)
80
+ if (error.code !== 'ENOENT') {
81
+ throw error;
82
+ }
74
83
  }
75
84
  }
76
85
  }
@@ -82,7 +91,9 @@ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
82
91
  * @throws {Error} If file cannot be loaded or parsed
83
92
  */
84
93
  async function loadVariablesYaml(appName) {
85
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
94
+ // Detect app type and get correct path (integration or builder)
95
+ const { appPath } = await detectAppType(appName);
96
+ const variablesPath = path.join(appPath, 'variables.yaml');
86
97
 
87
98
  if (!fsSync.existsSync(variablesPath)) {
88
99
  throw new Error(`Configuration not found. Run 'aifabrix create ${appName}' first.`);
@@ -335,6 +346,15 @@ async function postBuildTasks(appName, buildConfig) {
335
346
  * // Returns: 'myapp:latest'
336
347
  */
337
348
  async function buildApp(appName, options = {}) {
349
+ // Check if app type is external - generate JSON files only (not deploy)
350
+ const variables = await loadVariablesYaml(appName);
351
+ if (variables.app && variables.app.type === 'external') {
352
+ const generator = require('./generator');
353
+ const jsonPath = await generator.generateDeployJson(appName);
354
+ logger.log(chalk.green(`✓ Generated deployment JSON: ${jsonPath}`));
355
+ return null;
356
+ }
357
+
338
358
  try {
339
359
  logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
340
360
 
@@ -474,12 +494,4 @@ async function buildApp(appName, options = {}) {
474
494
  }
475
495
  }
476
496
 
477
- module.exports = {
478
- loadVariablesYaml,
479
- resolveContextPath,
480
- executeDockerBuild: dockerBuild.executeDockerBuild,
481
- detectLanguage,
482
- generateDockerfile,
483
- buildApp,
484
- postBuildTasks
485
- };
497
+ module.exports = { loadVariablesYaml, resolveContextPath, executeDockerBuild: dockerBuild.executeDockerBuild, detectLanguage, generateDockerfile, buildApp, postBuildTasks };