@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.
- package/.cursor/rules/project-rules.mdc +680 -0
- package/integration/hubspot/README.md +136 -0
- package/integration/hubspot/env.template +9 -0
- package/integration/hubspot/hubspot-deploy-company.json +200 -0
- package/integration/hubspot/hubspot-deploy-contact.json +228 -0
- package/integration/hubspot/hubspot-deploy-deal.json +248 -0
- package/integration/hubspot/hubspot-deploy.json +91 -0
- package/integration/hubspot/variables.yaml +17 -0
- package/lib/app-config.js +13 -2
- package/lib/app-deploy.js +9 -3
- package/lib/app-dockerfile.js +14 -1
- package/lib/app-prompts.js +177 -13
- package/lib/app-push.js +16 -1
- package/lib/app-register.js +37 -5
- package/lib/app-rotate-secret.js +10 -0
- package/lib/app-run.js +19 -0
- package/lib/app.js +70 -25
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +25 -13
- package/lib/cli.js +109 -2
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +14 -20
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +345 -0
- package/lib/external-system-download.js +431 -0
- package/lib/external-system-generator.js +190 -0
- package/lib/external-system-test.js +446 -0
- package/lib/generator-builders.js +323 -0
- package/lib/generator.js +200 -292
- package/lib/schema/application-schema.json +830 -800
- package/lib/schema/external-datasource.schema.json +868 -46
- package/lib/schema/external-system.schema.json +98 -80
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/templates.js +32 -1
- package/lib/utils/cli-utils.js +4 -4
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/external-system-display.js +159 -0
- package/lib/utils/external-system-validators.js +245 -0
- package/lib/utils/paths.js +151 -1
- package/lib/utils/schema-resolver.js +7 -2
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validator.js +52 -5
- package/package.json +1 -1
- package/tatus +181 -0
- package/templates/external-system/external-datasource.json.hbs +55 -0
- package/templates/external-system/external-system.json.hbs +37 -0
package/lib/app-prompts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
224
|
-
|
|
225
|
-
options.github = false;
|
|
226
|
-
}
|
|
367
|
+
// Get app type from options (default to webapp)
|
|
368
|
+
const appType = options.type || 'webapp';
|
|
227
369
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/lib/app-register.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/app-rotate-secret.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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,
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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(
|
|
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,
|
|
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, {
|
package/lib/audit-logger.js
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
await fs.
|
|
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
|
-
|
|
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 };
|