@aifabrix/builder 2.7.0 → 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/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 +48 -0
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- 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 +894 -865
- package/lib/schema/external-datasource.schema.json +49 -1
- package/lib/schema/external-system.schema.json +4 -4
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/templates.js +32 -1
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/token-encryption.js +68 -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-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
|
@@ -38,6 +38,8 @@ function setupCommands(program) {
|
|
|
38
38
|
.option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
|
|
39
39
|
.option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
|
|
40
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")')
|
|
41
43
|
.action(async(options) => {
|
|
42
44
|
try {
|
|
43
45
|
await handleLogin(options);
|
|
@@ -103,12 +105,18 @@ function setupCommands(program) {
|
|
|
103
105
|
.option('-a, --authentication', 'Requires authentication/RBAC')
|
|
104
106
|
.option('-l, --language <lang>', 'Runtime language (typescript/python)')
|
|
105
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')
|
|
106
109
|
.option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
|
|
107
110
|
.option('-g, --github', 'Generate GitHub Actions workflows')
|
|
108
111
|
.option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
|
|
109
112
|
.option('--main-branch <branch>', 'Main branch name for workflows', 'main')
|
|
110
113
|
.action(async(appName, options) => {
|
|
111
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
|
+
}
|
|
112
120
|
await app.createApp(appName, options);
|
|
113
121
|
} catch (error) {
|
|
114
122
|
handleCommandError(error, 'create');
|
|
@@ -158,6 +166,46 @@ function setupCommands(program) {
|
|
|
158
166
|
}
|
|
159
167
|
});
|
|
160
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
|
+
|
|
161
209
|
program.command('deploy <app>')
|
|
162
210
|
.description('Deploy to Azure via Miso Controller')
|
|
163
211
|
.option('-c, --controller <url>', 'Controller URL')
|
package/lib/commands/login.js
CHANGED
|
@@ -302,20 +302,51 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Build scope string from options
|
|
307
|
+
* @param {boolean} [offline] - Whether to request offline_access
|
|
308
|
+
* @param {string} [customScope] - Custom scope string
|
|
309
|
+
* @returns {string} Scope string
|
|
310
|
+
*/
|
|
311
|
+
function buildScope(offline, customScope) {
|
|
312
|
+
const defaultScope = 'openid profile email';
|
|
313
|
+
|
|
314
|
+
if (customScope) {
|
|
315
|
+
// If custom scope provided, use it and optionally add offline_access
|
|
316
|
+
if (offline && !customScope.includes('offline_access')) {
|
|
317
|
+
return `${customScope} offline_access`;
|
|
318
|
+
}
|
|
319
|
+
return customScope;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Default scope with optional offline_access
|
|
323
|
+
if (offline) {
|
|
324
|
+
return `${defaultScope} offline_access`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return defaultScope;
|
|
328
|
+
}
|
|
329
|
+
|
|
305
330
|
/**
|
|
306
331
|
* Handle device code flow login
|
|
307
332
|
* @async
|
|
308
333
|
* @param {string} controllerUrl - Controller URL
|
|
309
334
|
* @param {string} [environment] - Environment key from options
|
|
335
|
+
* @param {boolean} [offline] - Whether to request offline_access scope
|
|
336
|
+
* @param {string} [scope] - Custom scope string
|
|
310
337
|
* @returns {Promise<{token: string, environment: string}>} Token and environment
|
|
311
338
|
*/
|
|
312
|
-
async function handleDeviceCodeLogin(controllerUrl, environment) {
|
|
339
|
+
async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
|
|
313
340
|
const envKey = await getEnvironmentKey(environment);
|
|
341
|
+
const requestScope = buildScope(offline, scope);
|
|
314
342
|
|
|
315
343
|
logger.log(chalk.blue('\n📱 Initiating device code flow...\n'));
|
|
344
|
+
if (offline) {
|
|
345
|
+
logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
|
|
346
|
+
}
|
|
316
347
|
|
|
317
348
|
try {
|
|
318
|
-
const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey);
|
|
349
|
+
const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
|
|
319
350
|
|
|
320
351
|
displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
|
|
321
352
|
|
|
@@ -369,6 +400,12 @@ async function handleLogin(options) {
|
|
|
369
400
|
let token;
|
|
370
401
|
let expiresAt;
|
|
371
402
|
|
|
403
|
+
// Validate scope options - only applicable to device flow
|
|
404
|
+
if (method === 'credentials' && (options.offline || options.scope)) {
|
|
405
|
+
logger.log(chalk.yellow('⚠️ Warning: --offline and --scope options are only available for device flow'));
|
|
406
|
+
logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
|
|
407
|
+
}
|
|
408
|
+
|
|
372
409
|
if (method === 'credentials') {
|
|
373
410
|
if (!options.app) {
|
|
374
411
|
logger.error(chalk.red('❌ --app is required for credentials login method'));
|
|
@@ -379,7 +416,7 @@ async function handleLogin(options) {
|
|
|
379
416
|
expiresAt = loginResult.expiresAt;
|
|
380
417
|
await saveCredentialsLoginConfig(controllerUrl, token, expiresAt, environment, options.app);
|
|
381
418
|
} else if (method === 'device') {
|
|
382
|
-
const result = await handleDeviceCodeLogin(controllerUrl, options.environment);
|
|
419
|
+
const result = await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
|
|
383
420
|
token = result.token;
|
|
384
421
|
environment = result.environment;
|
|
385
422
|
return; // Early return for device flow (already saved config)
|