@aifabrix/builder 2.32.2 → 2.33.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 +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +8 -7
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +10 -5
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +207 -38
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +78 -37
- package/lib/app/prompts.js +9 -5
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +50 -32
- package/lib/cli.js +243 -65
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +261 -0
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +43 -29
- package/lib/commands/login.js +22 -13
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +129 -357
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +34 -23
- package/lib/datasource/list.js +8 -6
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +54 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +34 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +5 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +24 -5
- package/lib/schema/external-datasource.schema.json +303 -17
- package/lib/schema/external-system.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +37 -42
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/app-register-display.js +2 -1
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/cli-utils.js +3 -1
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +115 -0
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-map.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +149 -28
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +69 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +38 -4
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +19 -17
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/external-system/external-system.json.hbs +1 -1
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- package/templates/typescript/docker-compose.hbs +26 -0
package/lib/app/list.js
CHANGED
|
@@ -126,14 +126,35 @@ async function findDeviceTokenFromConfig(deviceConfig) {
|
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Format URL and port for display
|
|
131
|
+
* @param {Object} app - Application object
|
|
132
|
+
* @returns {string} Formatted URL and port string
|
|
133
|
+
*/
|
|
134
|
+
function formatUrlAndPort(app) {
|
|
135
|
+
const url = app.url || app.dataplaneUrl || app.dataplane?.url || app.configuration?.dataplaneUrl || null;
|
|
136
|
+
const port = app.port || app.configuration?.port || null;
|
|
137
|
+
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (url) {
|
|
140
|
+
parts.push(`URL: ${chalk.blue(url)}`);
|
|
141
|
+
}
|
|
142
|
+
if (port) {
|
|
143
|
+
parts.push(`Port: ${chalk.blue(port)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
147
|
+
}
|
|
148
|
+
|
|
129
149
|
/**
|
|
130
150
|
* Display applications list
|
|
131
151
|
* @param {Array} applications - Array of application objects
|
|
132
152
|
* @param {string} environment - Environment name or key
|
|
153
|
+
* @param {string} controllerUrl - Controller URL
|
|
133
154
|
*/
|
|
134
|
-
function displayApplications(applications, environment) {
|
|
155
|
+
function displayApplications(applications, environment, controllerUrl) {
|
|
135
156
|
const environmentName = environment || 'miso';
|
|
136
|
-
const header = `Applications in ${environmentName} environment`;
|
|
157
|
+
const header = `Applications in ${environmentName} environment (${controllerUrl})`;
|
|
137
158
|
|
|
138
159
|
if (applications.length === 0) {
|
|
139
160
|
logger.log(chalk.bold(`\n📱 ${header}:\n`));
|
|
@@ -144,7 +165,8 @@ function displayApplications(applications, environment) {
|
|
|
144
165
|
logger.log(chalk.bold(`\n📱 ${header}:\n`));
|
|
145
166
|
applications.forEach((app) => {
|
|
146
167
|
const hasPipeline = app.configuration?.pipeline?.isActive ? '✓' : '✗';
|
|
147
|
-
|
|
168
|
+
const urlAndPort = formatUrlAndPort(app);
|
|
169
|
+
logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
|
|
148
170
|
});
|
|
149
171
|
logger.log('');
|
|
150
172
|
}
|
|
@@ -152,23 +174,19 @@ function displayApplications(applications, environment) {
|
|
|
152
174
|
/**
|
|
153
175
|
* Try to get device token from controller URL
|
|
154
176
|
* @async
|
|
155
|
-
* @param {string} controllerUrl - Controller URL
|
|
156
|
-
* @returns {Promise<Object|null>} Object with token and controllerUrl, or null
|
|
177
|
+
* @param {string} controllerUrl - Controller URL (explicitly provided by user)
|
|
178
|
+
* @returns {Promise<Object|null>} Object with token and controllerUrl, or null if token not found
|
|
179
|
+
* @throws {Error} If authentication/refresh fails
|
|
157
180
|
*/
|
|
158
181
|
async function tryGetTokenFromController(controllerUrl) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
|
|
170
|
-
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
171
|
-
process.exit(1);
|
|
182
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
183
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
184
|
+
if (deviceToken && deviceToken.token) {
|
|
185
|
+
// Always use the provided controller URL (normalized) to ensure we use the exact URL the user specified
|
|
186
|
+
return {
|
|
187
|
+
token: deviceToken.token,
|
|
188
|
+
actualControllerUrl: normalizedUrl
|
|
189
|
+
};
|
|
172
190
|
}
|
|
173
191
|
return null;
|
|
174
192
|
}
|
|
@@ -215,25 +233,36 @@ function validateAuthToken(token, actualControllerUrl, controllerUrl) {
|
|
|
215
233
|
/**
|
|
216
234
|
* Get authentication token for listing applications
|
|
217
235
|
* @async
|
|
218
|
-
* @param {string} [controllerUrl] - Optional controller URL
|
|
236
|
+
* @param {string} [controllerUrl] - Optional controller URL (if provided, must use this specific URL)
|
|
219
237
|
* @param {Object} config - Configuration object
|
|
220
238
|
* @returns {Promise<Object>} Object with token and actualControllerUrl
|
|
221
239
|
* @throws {Error} If authentication fails
|
|
222
240
|
*/
|
|
223
241
|
async function getListAuthToken(controllerUrl, config) {
|
|
224
|
-
//
|
|
225
|
-
let authResult = null;
|
|
242
|
+
// If controller URL is explicitly provided, use only that URL (no fallback)
|
|
226
243
|
if (controllerUrl) {
|
|
227
|
-
authResult = await tryGetTokenFromController(controllerUrl);
|
|
244
|
+
const authResult = await tryGetTokenFromController(controllerUrl);
|
|
245
|
+
if (!authResult || !authResult.token) {
|
|
246
|
+
// No token found for explicitly provided controller URL
|
|
247
|
+
logger.error(chalk.red(`❌ No authentication token found for controller: ${controllerUrl}`));
|
|
248
|
+
logger.error(chalk.gray('Please login to this controller using: aifabrix login'));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
// Return to prevent further execution in tests where process.exit is mocked
|
|
251
|
+
return { token: null, actualControllerUrl: null };
|
|
252
|
+
}
|
|
253
|
+
return validateAuthToken(authResult.token, authResult.actualControllerUrl, controllerUrl);
|
|
228
254
|
}
|
|
229
255
|
|
|
230
|
-
// If no
|
|
231
|
-
if (
|
|
232
|
-
authResult = await tryGetTokenFromConfig(config.device);
|
|
256
|
+
// If no controller URL provided, try to find device token from config
|
|
257
|
+
if (config.device) {
|
|
258
|
+
const authResult = await tryGetTokenFromConfig(config.device);
|
|
259
|
+
if (authResult && authResult.token) {
|
|
260
|
+
return validateAuthToken(authResult.token, authResult.actualControllerUrl, null);
|
|
261
|
+
}
|
|
233
262
|
}
|
|
234
263
|
|
|
235
|
-
//
|
|
236
|
-
return validateAuthToken(
|
|
264
|
+
// No token found anywhere
|
|
265
|
+
return validateAuthToken(null, null, null);
|
|
237
266
|
}
|
|
238
267
|
|
|
239
268
|
/**
|
|
@@ -257,26 +286,38 @@ function handleListResponse(response, actualControllerUrl) {
|
|
|
257
286
|
}
|
|
258
287
|
|
|
259
288
|
/**
|
|
260
|
-
* List applications in an environment
|
|
289
|
+
* List applications in an environment.
|
|
290
|
+
* Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
|
|
261
291
|
* @async
|
|
262
|
-
* @param {Object}
|
|
263
|
-
* @param {string} options.environment - Environment ID or key
|
|
264
|
-
* @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
|
|
292
|
+
* @param {Object} [_options] - Command options (reserved)
|
|
265
293
|
* @throws {Error} If listing fails
|
|
266
294
|
*/
|
|
267
|
-
async function listApplications(options) {
|
|
268
|
-
const
|
|
295
|
+
async function listApplications(options = {}) {
|
|
296
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
297
|
+
const { resolveEnvironment } = require('../core/config');
|
|
269
298
|
|
|
270
|
-
|
|
271
|
-
|
|
299
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
300
|
+
if (!controllerUrl) {
|
|
301
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml'));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const environment = options.environment || (await resolveEnvironment());
|
|
307
|
+
const config = await getConfig();
|
|
272
308
|
const { token, actualControllerUrl } = await getListAuthToken(controllerUrl, config);
|
|
273
309
|
|
|
310
|
+
// Check if authentication succeeded (may be null after process.exit in tests)
|
|
311
|
+
if (!token || !actualControllerUrl) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
274
315
|
// Use centralized API client
|
|
275
316
|
const authConfig = { type: 'bearer', token: token };
|
|
276
317
|
try {
|
|
277
|
-
const response = await listEnvironmentApplications(actualControllerUrl,
|
|
318
|
+
const response = await listEnvironmentApplications(actualControllerUrl, environment, authConfig);
|
|
278
319
|
const applications = handleListResponse(response, actualControllerUrl);
|
|
279
|
-
displayApplications(applications,
|
|
320
|
+
displayApplications(applications, environment, actualControllerUrl);
|
|
280
321
|
} catch (error) {
|
|
281
322
|
logger.error(chalk.red(`❌ Failed to list applications from controller: ${actualControllerUrl}`));
|
|
282
323
|
logger.error(chalk.gray(`Error: ${error.message}`));
|
package/lib/app/prompts.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const inquirer = require('inquirer');
|
|
12
|
+
const { getDefaultControllerUrl } = require('../utils/controller-url');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Builds basic questions (port, language)
|
|
@@ -223,10 +224,11 @@ function buildExternalSystemQuestions(options, appName) {
|
|
|
223
224
|
|
|
224
225
|
/**
|
|
225
226
|
* Builds workflow questions (GitHub, Controller)
|
|
227
|
+
* @async
|
|
226
228
|
* @param {Object} options - Provided options
|
|
227
|
-
* @returns {Array} Array of question objects
|
|
229
|
+
* @returns {Promise<Array>} Array of question objects
|
|
228
230
|
*/
|
|
229
|
-
function buildWorkflowQuestions(options) {
|
|
231
|
+
async function buildWorkflowQuestions(options) {
|
|
230
232
|
const questions = [];
|
|
231
233
|
|
|
232
234
|
// GitHub workflows
|
|
@@ -255,11 +257,13 @@ function buildWorkflowQuestions(options) {
|
|
|
255
257
|
if (!options.controllerUrl && options.controller &&
|
|
256
258
|
!Object.prototype.hasOwnProperty.call(options, 'controllerUrl')) {
|
|
257
259
|
const misoHost = process.env.MISO_HOST || 'localhost';
|
|
260
|
+
const defaultControllerUrl = await getDefaultControllerUrl();
|
|
261
|
+
const defaultUrl = defaultControllerUrl.replace('http://localhost:', `http://${misoHost}:`);
|
|
258
262
|
questions.push({
|
|
259
263
|
type: 'input',
|
|
260
264
|
name: 'controllerUrl',
|
|
261
265
|
message: 'Enter Controller URL:',
|
|
262
|
-
default:
|
|
266
|
+
default: defaultUrl,
|
|
263
267
|
when: (answers) => answers.controller === true
|
|
264
268
|
});
|
|
265
269
|
}
|
|
@@ -424,14 +428,14 @@ async function promptForOptions(appName, options) {
|
|
|
424
428
|
// For external type, prompt for external system configuration
|
|
425
429
|
questions = [
|
|
426
430
|
...buildExternalSystemQuestions(options, appName),
|
|
427
|
-
...buildWorkflowQuestions(options)
|
|
431
|
+
...(await buildWorkflowQuestions(options))
|
|
428
432
|
];
|
|
429
433
|
} else {
|
|
430
434
|
// For regular apps, use standard prompts
|
|
431
435
|
questions = [
|
|
432
436
|
...buildBasicQuestions(options, appType),
|
|
433
437
|
...buildServiceQuestions(options, appType),
|
|
434
|
-
...buildWorkflowQuestions(options)
|
|
438
|
+
...(await buildWorkflowQuestions(options))
|
|
435
439
|
];
|
|
436
440
|
}
|
|
437
441
|
|
package/lib/app/readme.js
CHANGED
|
@@ -12,6 +12,7 @@ const fs = require('fs').promises;
|
|
|
12
12
|
const fsSync = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const handlebars = require('handlebars');
|
|
15
|
+
const { generateExternalReadmeContent } = require('../utils/external-readme');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Checks if a file exists
|
|
@@ -86,6 +87,27 @@ function extractServiceFlags(config) {
|
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Builds placeholder datasources for external README generation
|
|
92
|
+
* @function buildExternalDatasourcePlaceholders
|
|
93
|
+
* @param {number} datasourceCount - Datasource count
|
|
94
|
+
* @returns {Array<Object>} Datasource placeholders
|
|
95
|
+
*/
|
|
96
|
+
function buildExternalDatasourcePlaceholders(systemKey, datasourceCount) {
|
|
97
|
+
const normalizedCount = Number.isInteger(datasourceCount)
|
|
98
|
+
? datasourceCount
|
|
99
|
+
: parseInt(datasourceCount, 10);
|
|
100
|
+
const total = Number.isFinite(normalizedCount) && normalizedCount > 0 ? normalizedCount : 0;
|
|
101
|
+
return Array.from({ length: total }, (_value, index) => {
|
|
102
|
+
const entityType = `entity${index + 1}`;
|
|
103
|
+
return {
|
|
104
|
+
entityType,
|
|
105
|
+
displayName: `Datasource ${index + 1}`,
|
|
106
|
+
fileName: `${systemKey}-datasource-${entityType}.json`
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
/**
|
|
90
112
|
* Builds template context for README generation
|
|
91
113
|
* @function buildReadmeContext
|
|
@@ -95,8 +117,11 @@ function extractServiceFlags(config) {
|
|
|
95
117
|
*/
|
|
96
118
|
function buildReadmeContext(appName, config) {
|
|
97
119
|
const displayName = formatAppDisplayName(appName);
|
|
98
|
-
const
|
|
99
|
-
const
|
|
120
|
+
const port = config.port ?? 3000;
|
|
121
|
+
const localPort = (typeof config.build?.localPort === 'number' && config.build.localPort > 0)
|
|
122
|
+
? config.build.localPort
|
|
123
|
+
: port;
|
|
124
|
+
const imageName = config.image?.name || `aifabrix/${appName}`;
|
|
100
125
|
// Extract registry from nested structure (config.image.registry) or flattened (config.registry)
|
|
101
126
|
const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
|
|
102
127
|
|
|
@@ -108,6 +133,7 @@ function buildReadmeContext(appName, config) {
|
|
|
108
133
|
displayName,
|
|
109
134
|
imageName,
|
|
110
135
|
port,
|
|
136
|
+
localPort,
|
|
111
137
|
registry,
|
|
112
138
|
...serviceFlags,
|
|
113
139
|
hasAnyService
|
|
@@ -115,117 +141,20 @@ function buildReadmeContext(appName, config) {
|
|
|
115
141
|
}
|
|
116
142
|
|
|
117
143
|
function generateReadmeMd(appName, config) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
function generateComprehensiveReadme(context) {
|
|
130
|
-
const { appName, displayName, imageName, port, registry, hasDatabase, hasRedis, hasStorage, hasAuthentication, hasAnyService } = context;
|
|
131
|
-
|
|
132
|
-
let prerequisites = 'Before running this application, ensure the following prerequisites are met:\n';
|
|
133
|
-
prerequisites += '- `@aifabrix/builder` installed globally\n';
|
|
134
|
-
prerequisites += '- Docker Desktop running\n';
|
|
135
|
-
|
|
136
|
-
if (hasAnyService) {
|
|
137
|
-
if (hasDatabase) {
|
|
138
|
-
prerequisites += '- PostgreSQL database\n';
|
|
139
|
-
}
|
|
140
|
-
if (hasRedis) {
|
|
141
|
-
prerequisites += '- Redis\n';
|
|
142
|
-
}
|
|
143
|
-
if (hasStorage) {
|
|
144
|
-
prerequisites += '- File storage configured\n';
|
|
145
|
-
}
|
|
146
|
-
if (hasAuthentication) {
|
|
147
|
-
prerequisites += '- Authentication/RBAC configured\n';
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
prerequisites += '- Infrastructure running\n';
|
|
144
|
+
if (config.type === 'external') {
|
|
145
|
+
const systemKey = config.systemKey || appName;
|
|
146
|
+
const datasources = buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount);
|
|
147
|
+
return generateExternalReadmeContent({
|
|
148
|
+
appName,
|
|
149
|
+
systemKey,
|
|
150
|
+
systemType: config.systemType,
|
|
151
|
+
displayName: config.systemDisplayName,
|
|
152
|
+
description: config.systemDescription,
|
|
153
|
+
datasources
|
|
154
|
+
});
|
|
151
155
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (hasDatabase) {
|
|
155
|
-
troubleshooting = `### Database Connection Issues
|
|
156
|
-
|
|
157
|
-
If you encounter database connection errors, ensure:
|
|
158
|
-
- PostgreSQL is running and accessible
|
|
159
|
-
- Database credentials are correctly configured in your \`.env\` file
|
|
160
|
-
- The database name matches your configuration
|
|
161
|
-
- Verify infrastructure is running and PostgreSQL is accessible`;
|
|
162
|
-
} else {
|
|
163
|
-
troubleshooting = 'Verify infrastructure is running.';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return `# ${displayName} Builder
|
|
167
|
-
|
|
168
|
-
Build, run, and deploy ${displayName}.
|
|
169
|
-
|
|
170
|
-
## Prerequisites
|
|
171
|
-
|
|
172
|
-
${prerequisites}
|
|
173
|
-
|
|
174
|
-
## Quick Start
|
|
175
|
-
|
|
176
|
-
### 1. Install
|
|
177
|
-
|
|
178
|
-
Install the AI Fabrix Builder CLI if you haven't already.
|
|
179
|
-
|
|
180
|
-
### 2. Configure
|
|
181
|
-
|
|
182
|
-
Configure your application settings in \`variables.yaml\`.
|
|
183
|
-
|
|
184
|
-
### 3. Build & Run Locally
|
|
185
|
-
|
|
186
|
-
Build the application:
|
|
187
|
-
\`\`\`bash
|
|
188
|
-
aifabrix build ${appName}
|
|
189
|
-
\`\`\`
|
|
190
|
-
|
|
191
|
-
Run the application:
|
|
192
|
-
\`\`\`bash
|
|
193
|
-
aifabrix run ${appName}
|
|
194
|
-
\`\`\`
|
|
195
|
-
|
|
196
|
-
The application will be available at http://localhost:${port} (default: ${port}).
|
|
197
|
-
|
|
198
|
-
### 4. Deploy to Azure
|
|
199
|
-
|
|
200
|
-
Push to registry:
|
|
201
|
-
\`\`\`bash
|
|
202
|
-
aifabrix push ${appName} --registry ${registry} --tag "v1.0.0,latest"
|
|
203
|
-
\`\`\`
|
|
204
|
-
|
|
205
|
-
## Configuration
|
|
206
|
-
|
|
207
|
-
- **Port**: ${port} (default: 3000)
|
|
208
|
-
- **Image**: ${imageName}:latest
|
|
209
|
-
- **Registry**: ${registry}
|
|
210
|
-
|
|
211
|
-
## Docker Commands
|
|
212
|
-
|
|
213
|
-
View logs:
|
|
214
|
-
\`\`\`bash
|
|
215
|
-
docker logs aifabrix-${appName} -f
|
|
216
|
-
\`\`\`
|
|
217
|
-
|
|
218
|
-
Stop the application:
|
|
219
|
-
\`\`\`bash
|
|
220
|
-
aifabrix down ${appName}
|
|
221
|
-
\`\`\`
|
|
222
|
-
|
|
223
|
-
## Troubleshooting
|
|
224
|
-
|
|
225
|
-
${troubleshooting}
|
|
226
|
-
|
|
227
|
-
For more information, see the [AI Fabrix Builder documentation](https://docs.aifabrix.com).
|
|
228
|
-
`;
|
|
156
|
+
const context = buildReadmeContext(appName, config);
|
|
157
|
+
return _loadReadmeTemplate()(context);
|
|
229
158
|
}
|
|
230
159
|
|
|
231
160
|
/**
|
package/lib/app/register.js
CHANGED
|
@@ -43,7 +43,7 @@ function buildRegistrationData(appConfig, options) {
|
|
|
43
43
|
|
|
44
44
|
// Handle external type vs non-external types differently
|
|
45
45
|
if (appConfig.appType === 'external') {
|
|
46
|
-
// For external type: include externalIntegration, exclude registryMode/port/image
|
|
46
|
+
// For external type: include externalIntegration, exclude registryMode/port/image/url
|
|
47
47
|
if (appConfig.externalIntegration) {
|
|
48
48
|
registrationData.externalIntegration = appConfig.externalIntegration;
|
|
49
49
|
}
|
|
@@ -60,6 +60,12 @@ function buildRegistrationData(appConfig, options) {
|
|
|
60
60
|
if (appConfig.image) {
|
|
61
61
|
registrationData.image = appConfig.image;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
// URL: always set when we have port so controller DB has it. Precedence: --url, variables (app.url, deployment.dataplaneUrl, deployment.appUrl), else http://localhost:{localPort|port}
|
|
65
|
+
const portForUrl = appConfig.localPort ?? appConfig.port;
|
|
66
|
+
if (portForUrl) {
|
|
67
|
+
registrationData.url = options.url || appConfig.url || `http://localhost:${portForUrl}`;
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
return registrationData;
|
|
@@ -103,20 +109,48 @@ async function saveLocalCredentials(responseData, apiUrl) {
|
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/**
|
|
106
|
-
*
|
|
112
|
+
* For localhost controller: apply developer-id offset to port and URL fallback so the
|
|
113
|
+
* controller can reach the app on the correct Docker/exposed host port.
|
|
114
|
+
* @async
|
|
115
|
+
* @param {Object} appConfig - App config (mutated: port, url)
|
|
116
|
+
* @param {string} apiUrl - Controller API URL
|
|
117
|
+
* @param {Object} options - CLI options (url override)
|
|
118
|
+
*/
|
|
119
|
+
async function applyLocalhostPortAdjustment(appConfig, apiUrl, options) {
|
|
120
|
+
if (!isLocalhost(apiUrl) || appConfig.port === null || appConfig.port === undefined) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const { getDeveloperId } = require('../core/config');
|
|
124
|
+
const devId = await getDeveloperId();
|
|
125
|
+
const devIdNum = (devId !== null && devId !== undefined && devId !== '') ? parseInt(devId, 10) : 0;
|
|
126
|
+
if (Number.isNaN(devIdNum) || devIdNum <= 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const adjusted = appConfig.port + devIdNum * 100;
|
|
130
|
+
appConfig.port = adjusted;
|
|
131
|
+
if (!options.url && !appConfig.url) {
|
|
132
|
+
appConfig.url = `http://localhost:${adjusted}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register an application.
|
|
138
|
+
* Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
|
|
107
139
|
* @async
|
|
108
140
|
* @param {string} appKey - Application key
|
|
109
141
|
* @param {Object} options - Registration options
|
|
110
|
-
* @param {string} options.environment - Environment ID or key
|
|
111
|
-
* @param {string} [options.controller] - Controller URL (overrides variables.yaml)
|
|
112
142
|
* @param {number} [options.port] - Application port
|
|
143
|
+
* @param {string} [options.url] - Application URL (overrides variables; see app register --help for fallback when omitted)
|
|
113
144
|
* @param {string} [options.name] - Override display name
|
|
114
145
|
* @param {string} [options.description] - Override description
|
|
115
146
|
* @throws {Error} If registration fails
|
|
116
147
|
*/
|
|
117
|
-
async function registerApplication(appKey, options) {
|
|
148
|
+
async function registerApplication(appKey, options = {}) {
|
|
118
149
|
logger.log(chalk.blue('📋 Registering application...\n'));
|
|
119
150
|
|
|
151
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
152
|
+
const { resolveEnvironment } = require('../core/config');
|
|
153
|
+
|
|
120
154
|
// Load variables.yaml
|
|
121
155
|
const { variables, created } = await loadVariablesYaml(appKey);
|
|
122
156
|
const finalVariables = created
|
|
@@ -127,10 +161,11 @@ async function registerApplication(appKey, options) {
|
|
|
127
161
|
const appConfig = await extractAppConfiguration(finalVariables, appKey, options);
|
|
128
162
|
await validateAppRegistrationData(appConfig, appKey);
|
|
129
163
|
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
164
|
+
const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
|
|
165
|
+
const authConfig = await checkAuthentication(controllerUrl, environmentKey);
|
|
166
|
+
const environment = registerApplicationSchema.environmentId(environmentKey);
|
|
167
|
+
|
|
168
|
+
await applyLocalhostPortAdjustment(appConfig, authConfig.apiUrl, options);
|
|
134
169
|
|
|
135
170
|
// Register application
|
|
136
171
|
const registrationData = buildRegistrationData(appConfig, options);
|
package/lib/app/rotate-secret.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const chalk = require('chalk');
|
|
12
|
-
const { getConfig, normalizeControllerUrl } = require('../core/config');
|
|
12
|
+
const { getConfig, normalizeControllerUrl, resolveEnvironment } = require('../core/config');
|
|
13
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
14
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
15
|
const { rotateApplicationSecret } = require('../api/applications.api');
|
|
15
16
|
const { formatApiError } = require('../utils/api-error-handler');
|
|
@@ -61,6 +62,22 @@ function validateEnvironment(environment) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Resolve controller URL and environment from config; exit if controller is missing.
|
|
67
|
+
* @async
|
|
68
|
+
* @returns {Promise<{controllerUrl: string, environment: string}>}
|
|
69
|
+
*/
|
|
70
|
+
async function resolveControllerAndEnvironment() {
|
|
71
|
+
const controllerUrl = await resolveControllerUrl();
|
|
72
|
+
if (!controllerUrl) {
|
|
73
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const environment = await resolveEnvironment();
|
|
77
|
+
validateEnvironment(environment);
|
|
78
|
+
return { controllerUrl, environment };
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
/**
|
|
65
82
|
* Validate credentials object structure
|
|
66
83
|
* @param {Object} credentials - Credentials object to validate
|
|
@@ -152,7 +169,8 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
|
|
|
152
169
|
logger.log(chalk.green('✅ Secret rotated successfully!\n'));
|
|
153
170
|
logger.log(chalk.bold('📋 Application Details:'));
|
|
154
171
|
logger.log(` Key: ${appKey}`);
|
|
155
|
-
logger.log(` Environment: ${environment}
|
|
172
|
+
logger.log(` Environment: ${environment}`);
|
|
173
|
+
logger.log(` Controller: ${apiUrl}\n`);
|
|
156
174
|
|
|
157
175
|
logger.log(chalk.bold.yellow('🔑 NEW CREDENTIALS:'));
|
|
158
176
|
logger.log(chalk.yellow(` Client ID: ${credentials.clientId}`));
|
|
@@ -285,46 +303,46 @@ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl)
|
|
|
285
303
|
}
|
|
286
304
|
|
|
287
305
|
/**
|
|
288
|
-
*
|
|
306
|
+
* Call rotate API, validate response, save locally, and display results.
|
|
307
|
+
* @async
|
|
308
|
+
* @param {string} appKey - Application key
|
|
309
|
+
* @param {string} actualControllerUrl - Resolved controller URL
|
|
310
|
+
* @param {string} environment - Environment ID or key
|
|
311
|
+
* @param {string} token - Bearer token
|
|
312
|
+
*/
|
|
313
|
+
async function executeRotation(appKey, actualControllerUrl, environment, token) {
|
|
314
|
+
const authConfig = { type: 'bearer', token };
|
|
315
|
+
const response = await rotateApplicationSecret(actualControllerUrl, environment, appKey, authConfig);
|
|
316
|
+
|
|
317
|
+
if (!response.success) {
|
|
318
|
+
const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
|
|
319
|
+
logger.error(formattedError);
|
|
320
|
+
logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { credentials, message } = validateResponse(response);
|
|
325
|
+
await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
|
|
326
|
+
displayRotationResults(appKey, environment, credentials, actualControllerUrl, message);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Rotate secret for an application.
|
|
331
|
+
* Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
|
|
289
332
|
* @async
|
|
290
333
|
* @param {string} appKey - Application key
|
|
291
|
-
* @param {Object}
|
|
292
|
-
* @param {string} options.environment - Environment ID or key
|
|
293
|
-
* @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
|
|
334
|
+
* @param {Object} [_options] - Command options (reserved)
|
|
294
335
|
* @throws {Error} If rotation fails
|
|
295
336
|
*/
|
|
296
|
-
async function rotateSecret(appKey,
|
|
337
|
+
async function rotateSecret(appKey, _options = {}) {
|
|
297
338
|
logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
|
|
298
339
|
|
|
340
|
+
const { controllerUrl, environment } = await resolveControllerAndEnvironment();
|
|
299
341
|
const config = await getConfig();
|
|
300
|
-
|
|
301
|
-
// Get authentication token
|
|
302
|
-
const controllerUrl = options.controller || null;
|
|
303
342
|
const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
|
|
304
343
|
|
|
305
|
-
// Validate environment
|
|
306
|
-
validateEnvironment(options.environment);
|
|
307
|
-
|
|
308
|
-
// Use centralized API client
|
|
309
|
-
const authConfig = { type: 'bearer', token: token };
|
|
310
344
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (!response.success) {
|
|
314
|
-
const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
|
|
315
|
-
logger.error(formattedError);
|
|
316
|
-
logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
|
|
317
|
-
process.exit(1);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Validate response structure and extract credentials
|
|
321
|
-
const { credentials, message } = validateResponse(response);
|
|
322
|
-
|
|
323
|
-
// Save credentials locally
|
|
324
|
-
await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
|
|
325
|
-
|
|
326
|
-
// Display results
|
|
327
|
-
displayRotationResults(appKey, options.environment, credentials, actualControllerUrl, message);
|
|
345
|
+
await executeRotation(appKey, actualControllerUrl, environment, token);
|
|
328
346
|
} catch (error) {
|
|
329
347
|
logger.error(chalk.red(`❌ Failed to rotate secret via controller: ${actualControllerUrl}`));
|
|
330
348
|
logger.error(chalk.gray(`Error: ${error.message}`));
|