@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.
- package/.cursor/rules/project-rules.mdc +680 -0
- package/bin/aifabrix.js +4 -0
- package/lib/app-config.js +10 -0
- package/lib/app-deploy.js +18 -0
- package/lib/app-dockerfile.js +15 -0
- package/lib/app-prompts.js +172 -9
- package/lib/app-push.js +15 -0
- package/lib/app-register.js +14 -0
- package/lib/app-run.js +25 -0
- package/lib/app.js +30 -13
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +8 -0
- package/lib/cli.js +99 -2
- package/lib/commands/datasource.js +94 -0
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +182 -0
- package/lib/datasource-diff.js +73 -0
- package/lib/datasource-list.js +138 -0
- package/lib/datasource-validate.js +63 -0
- package/lib/diff.js +266 -0
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +262 -0
- package/lib/external-system-generator.js +187 -0
- package/lib/schema/application-schema.json +869 -698
- package/lib/schema/external-datasource.schema.json +512 -0
- package/lib/schema/external-system.schema.json +262 -0
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/secrets.js +20 -1
- package/lib/templates.js +32 -1
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/env-copy.js +24 -0
- package/lib/utils/env-endpoints.js +50 -11
- package/lib/utils/schema-loader.js +220 -0
- package/lib/utils/schema-resolver.js +174 -0
- package/lib/utils/secrets-helpers.js +65 -17
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validate.js +299 -0
- package/lib/validator.js +47 -3
- 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-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';
|
package/lib/app-dockerfile.js
CHANGED
|
@@ -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);
|
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
|
/**
|
|
@@ -225,17 +369,36 @@ async function promptForOptions(appName, options) {
|
|
|
225
369
|
options.github = false;
|
|
226
370
|
}
|
|
227
371
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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);
|
package/lib/app-register.js
CHANGED
|
@@ -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.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|
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
|
@@ -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
|
-
|
|
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')
|