@aifabrix/builder 2.22.2 → 2.31.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/jest.config.coverage.js +37 -0
- package/lib/api/pipeline.api.js +10 -9
- package/lib/app-deploy.js +36 -14
- package/lib/app-list.js +191 -71
- package/lib/app-prompts.js +77 -26
- package/lib/app-readme.js +123 -5
- package/lib/app-rotate-secret.js +101 -57
- package/lib/app-run-helpers.js +200 -172
- package/lib/app-run.js +137 -68
- package/lib/audit-logger.js +8 -7
- package/lib/build.js +161 -250
- package/lib/cli.js +73 -65
- package/lib/commands/login.js +45 -31
- package/lib/commands/logout.js +181 -0
- package/lib/commands/secure.js +59 -24
- package/lib/config.js +79 -45
- package/lib/datasource-deploy.js +89 -29
- package/lib/deployer.js +164 -129
- package/lib/diff.js +63 -21
- package/lib/environment-deploy.js +36 -19
- package/lib/external-system-deploy.js +134 -66
- package/lib/external-system-download.js +244 -171
- package/lib/external-system-test.js +199 -164
- package/lib/generator-external.js +145 -72
- package/lib/generator-helpers.js +49 -17
- package/lib/generator-split.js +105 -58
- package/lib/infra.js +101 -131
- package/lib/schema/application-schema.json +895 -896
- package/lib/schema/env-config.yaml +11 -4
- package/lib/template-validator.js +13 -4
- package/lib/utils/api.js +8 -8
- package/lib/utils/app-register-auth.js +36 -18
- package/lib/utils/app-run-containers.js +140 -0
- package/lib/utils/auth-headers.js +6 -6
- package/lib/utils/build-copy.js +60 -2
- package/lib/utils/build-helpers.js +94 -0
- package/lib/utils/cli-utils.js +177 -76
- package/lib/utils/compose-generator.js +12 -2
- package/lib/utils/config-tokens.js +151 -9
- package/lib/utils/deployment-errors.js +137 -69
- package/lib/utils/deployment-validation-helpers.js +103 -0
- package/lib/utils/docker-build.js +57 -0
- package/lib/utils/dockerfile-utils.js +13 -3
- package/lib/utils/env-copy.js +163 -94
- package/lib/utils/env-map.js +226 -86
- package/lib/utils/error-formatters/network-errors.js +0 -1
- package/lib/utils/external-system-display.js +14 -19
- package/lib/utils/external-system-env-helpers.js +107 -0
- package/lib/utils/external-system-test-helpers.js +144 -0
- package/lib/utils/health-check.js +10 -8
- package/lib/utils/infra-status.js +123 -0
- package/lib/utils/paths.js +228 -49
- package/lib/utils/schema-loader.js +125 -57
- package/lib/utils/token-manager.js +3 -3
- package/lib/utils/yaml-preserve.js +55 -16
- package/lib/validate.js +87 -89
- package/package.json +4 -4
- package/scripts/ci-fix.sh +19 -0
- package/scripts/ci-simulate.sh +19 -0
- package/templates/applications/miso-controller/test.yaml +1 -0
- package/templates/python/Dockerfile.hbs +8 -45
- package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/app-readme.js
CHANGED
|
@@ -44,11 +44,21 @@ function formatAppDisplayName(appName) {
|
|
|
44
44
|
* Loads and compiles README.md template
|
|
45
45
|
* @returns {Function} Compiled Handlebars template
|
|
46
46
|
* @throws {Error} If template not found
|
|
47
|
+
* @private
|
|
47
48
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
49
|
+
function _loadReadmeTemplate() {
|
|
50
|
+
// Use getProjectRoot to reliably find templates in all environments
|
|
51
|
+
const { getProjectRoot } = require('./utils/paths');
|
|
52
|
+
const projectRoot = getProjectRoot();
|
|
53
|
+
const templatePath = path.join(projectRoot, 'templates', 'applications', 'README.md.hbs');
|
|
54
|
+
|
|
50
55
|
if (!fsSync.existsSync(templatePath)) {
|
|
51
|
-
|
|
56
|
+
// Provide helpful error message with actual paths checked
|
|
57
|
+
const errorMessage = `README template not found at ${templatePath}\n` +
|
|
58
|
+
` Project root: ${projectRoot}\n` +
|
|
59
|
+
` Templates directory: ${path.join(projectRoot, 'templates', 'applications')}\n` +
|
|
60
|
+
` Global PROJECT_ROOT: ${typeof global !== 'undefined' && global.PROJECT_ROOT ? global.PROJECT_ROOT : 'not set'}`;
|
|
61
|
+
throw new Error(errorMessage);
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
@@ -62,7 +72,6 @@ function loadReadmeTemplate() {
|
|
|
62
72
|
* @returns {string} README.md content
|
|
63
73
|
*/
|
|
64
74
|
function generateReadmeMd(appName, config) {
|
|
65
|
-
const template = loadReadmeTemplate();
|
|
66
75
|
const displayName = formatAppDisplayName(appName);
|
|
67
76
|
const imageName = `aifabrix/${appName}`;
|
|
68
77
|
const port = config.port || 3000;
|
|
@@ -88,7 +97,116 @@ function generateReadmeMd(appName, config) {
|
|
|
88
97
|
hasAnyService
|
|
89
98
|
};
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
// Always generate comprehensive README programmatically to ensure consistency
|
|
101
|
+
// regardless of template file content
|
|
102
|
+
return generateComprehensiveReadme(context);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates comprehensive README.md content programmatically
|
|
107
|
+
* @param {Object} context - Template context
|
|
108
|
+
* @returns {string} Comprehensive README.md content
|
|
109
|
+
*/
|
|
110
|
+
function generateComprehensiveReadme(context) {
|
|
111
|
+
const { appName, displayName, imageName, port, registry, hasDatabase, hasRedis, hasStorage, hasAuthentication, hasAnyService } = context;
|
|
112
|
+
|
|
113
|
+
let prerequisites = 'Before running this application, ensure the following prerequisites are met:\n';
|
|
114
|
+
prerequisites += '- `@aifabrix/builder` installed globally\n';
|
|
115
|
+
prerequisites += '- Docker Desktop running\n';
|
|
116
|
+
|
|
117
|
+
if (hasAnyService) {
|
|
118
|
+
if (hasDatabase) {
|
|
119
|
+
prerequisites += '- PostgreSQL database\n';
|
|
120
|
+
}
|
|
121
|
+
if (hasRedis) {
|
|
122
|
+
prerequisites += '- Redis\n';
|
|
123
|
+
}
|
|
124
|
+
if (hasStorage) {
|
|
125
|
+
prerequisites += '- File storage configured\n';
|
|
126
|
+
}
|
|
127
|
+
if (hasAuthentication) {
|
|
128
|
+
prerequisites += '- Authentication/RBAC configured\n';
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
prerequisites += '- Infrastructure running\n';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let troubleshooting = '';
|
|
135
|
+
if (hasDatabase) {
|
|
136
|
+
troubleshooting = `### Database Connection Issues
|
|
137
|
+
|
|
138
|
+
If you encounter database connection errors, ensure:
|
|
139
|
+
- PostgreSQL is running and accessible
|
|
140
|
+
- Database credentials are correctly configured in your \`.env\` file
|
|
141
|
+
- The database name matches your configuration
|
|
142
|
+
- Verify infrastructure is running and PostgreSQL is accessible`;
|
|
143
|
+
} else {
|
|
144
|
+
troubleshooting = 'Verify infrastructure is running.';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return `# ${displayName} Builder
|
|
148
|
+
|
|
149
|
+
Build, run, and deploy ${displayName}.
|
|
150
|
+
|
|
151
|
+
## Prerequisites
|
|
152
|
+
|
|
153
|
+
${prerequisites}
|
|
154
|
+
|
|
155
|
+
## Quick Start
|
|
156
|
+
|
|
157
|
+
### 1. Install
|
|
158
|
+
|
|
159
|
+
Install the AI Fabrix Builder CLI if you haven't already.
|
|
160
|
+
|
|
161
|
+
### 2. Configure
|
|
162
|
+
|
|
163
|
+
Configure your application settings in \`variables.yaml\`.
|
|
164
|
+
|
|
165
|
+
### 3. Build & Run Locally
|
|
166
|
+
|
|
167
|
+
Build the application:
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
aifabrix build ${appName}
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
Run the application:
|
|
173
|
+
\`\`\`bash
|
|
174
|
+
aifabrix run ${appName}
|
|
175
|
+
\`\`\`
|
|
176
|
+
|
|
177
|
+
The application will be available at http://localhost:${port} (default: ${port}).
|
|
178
|
+
|
|
179
|
+
### 4. Deploy to Azure
|
|
180
|
+
|
|
181
|
+
Push to registry:
|
|
182
|
+
\`\`\`bash
|
|
183
|
+
aifabrix push ${appName} --registry ${registry} --tag "v1.0.0,latest"
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
- **Port**: ${port} (default: 3000)
|
|
189
|
+
- **Image**: ${imageName}:latest
|
|
190
|
+
- **Registry**: ${registry}
|
|
191
|
+
|
|
192
|
+
## Docker Commands
|
|
193
|
+
|
|
194
|
+
View logs:
|
|
195
|
+
\`\`\`bash
|
|
196
|
+
docker logs aifabrix-${appName} -f
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
Stop the application:
|
|
200
|
+
\`\`\`bash
|
|
201
|
+
aifabrix down ${appName}
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
## Troubleshooting
|
|
205
|
+
|
|
206
|
+
${troubleshooting}
|
|
207
|
+
|
|
208
|
+
For more information, see the [AI Fabrix Builder documentation](https://docs.aifabrix.com).
|
|
209
|
+
`;
|
|
92
210
|
}
|
|
93
211
|
|
|
94
212
|
/**
|
package/lib/app-rotate-secret.js
CHANGED
|
@@ -20,6 +20,36 @@ const { updateEnvTemplate } = require('./utils/env-template');
|
|
|
20
20
|
const { getEnvironmentPrefix } = require('./app-register');
|
|
21
21
|
const { generateEnvFile } = require('./secrets');
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Find device token from config by trying each stored URL
|
|
25
|
+
* @async
|
|
26
|
+
* @param {Object} deviceConfig - Device configuration object
|
|
27
|
+
* @returns {Promise<Object|null>} Token result with token and controllerUrl, or null if not found
|
|
28
|
+
*/
|
|
29
|
+
async function findDeviceTokenFromConfig(deviceConfig) {
|
|
30
|
+
const deviceUrls = Object.keys(deviceConfig);
|
|
31
|
+
if (deviceUrls.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const storedUrl of deviceUrls) {
|
|
36
|
+
try {
|
|
37
|
+
const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
|
|
38
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
|
|
39
|
+
if (deviceToken && deviceToken.token) {
|
|
40
|
+
return {
|
|
41
|
+
token: deviceToken.token,
|
|
42
|
+
controllerUrl: deviceToken.controller || normalizedStoredUrl
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Continue to next URL
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
/**
|
|
24
54
|
* Validate environment parameter
|
|
25
55
|
* @param {string} environment - Environment ID or key
|
|
@@ -79,21 +109,14 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
|
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
/**
|
|
82
|
-
*
|
|
112
|
+
* Get authentication token for rotation
|
|
83
113
|
* @async
|
|
84
|
-
* @param {string}
|
|
85
|
-
* @param {Object}
|
|
86
|
-
* @
|
|
87
|
-
* @
|
|
88
|
-
* @throws {Error} If rotation fails
|
|
114
|
+
* @param {string} [controllerUrl] - Optional controller URL
|
|
115
|
+
* @param {Object} config - Configuration object
|
|
116
|
+
* @returns {Promise<Object>} Object with token and actualControllerUrl
|
|
117
|
+
* @throws {Error} If authentication fails
|
|
89
118
|
*/
|
|
90
|
-
async function
|
|
91
|
-
logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
|
|
92
|
-
|
|
93
|
-
const config = await getConfig();
|
|
94
|
-
|
|
95
|
-
// Get controller URL with priority: options.controller > device tokens
|
|
96
|
-
const controllerUrl = options.controller || null;
|
|
119
|
+
async function getRotationAuthToken(controllerUrl, config) {
|
|
97
120
|
let token = null;
|
|
98
121
|
let actualControllerUrl = null;
|
|
99
122
|
|
|
@@ -107,7 +130,6 @@ async function rotateSecret(appKey, options) {
|
|
|
107
130
|
actualControllerUrl = deviceToken.controller || normalizedUrl;
|
|
108
131
|
}
|
|
109
132
|
} catch (error) {
|
|
110
|
-
// Show which controller URL failed
|
|
111
133
|
logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
|
|
112
134
|
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
113
135
|
process.exit(1);
|
|
@@ -116,21 +138,10 @@ async function rotateSecret(appKey, options) {
|
|
|
116
138
|
|
|
117
139
|
// If no token yet, try to find any device token in config
|
|
118
140
|
if (!token && config.device) {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
|
|
124
|
-
const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
|
|
125
|
-
if (deviceToken && deviceToken.token) {
|
|
126
|
-
token = deviceToken.token;
|
|
127
|
-
actualControllerUrl = deviceToken.controller || normalizedStoredUrl;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
} catch (error) {
|
|
131
|
-
// Continue to next URL
|
|
132
|
-
}
|
|
133
|
-
}
|
|
141
|
+
const tokenResult = await findDeviceTokenFromConfig(config.device);
|
|
142
|
+
if (tokenResult) {
|
|
143
|
+
token = tokenResult.token;
|
|
144
|
+
actualControllerUrl = tokenResult.controllerUrl;
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
|
|
@@ -143,6 +154,65 @@ async function rotateSecret(appKey, options) {
|
|
|
143
154
|
process.exit(1);
|
|
144
155
|
}
|
|
145
156
|
|
|
157
|
+
return { token, actualControllerUrl };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Save credentials locally and update env files
|
|
162
|
+
* @async
|
|
163
|
+
* @param {string} appKey - Application key
|
|
164
|
+
* @param {Object} credentials - Credentials object
|
|
165
|
+
* @param {string} actualControllerUrl - Controller URL
|
|
166
|
+
* @throws {Error} If saving fails
|
|
167
|
+
*/
|
|
168
|
+
async function saveCredentialsLocally(appKey, credentials, actualControllerUrl) {
|
|
169
|
+
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
170
|
+
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await saveLocalSecret(clientIdKey, credentials.clientId);
|
|
174
|
+
await saveLocalSecret(clientSecretKey, credentials.clientSecret);
|
|
175
|
+
|
|
176
|
+
// Update env.template if localhost
|
|
177
|
+
if (isLocalhost(actualControllerUrl)) {
|
|
178
|
+
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, actualControllerUrl);
|
|
179
|
+
|
|
180
|
+
// Regenerate .env file with updated credentials
|
|
181
|
+
try {
|
|
182
|
+
await generateEnvFile(appKey, null, 'local');
|
|
183
|
+
logger.log(chalk.green('✓ .env file updated with new credentials'));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
189
|
+
logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
|
|
190
|
+
} else {
|
|
191
|
+
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Rotate secret for an application
|
|
200
|
+
* @async
|
|
201
|
+
* @param {string} appKey - Application key
|
|
202
|
+
* @param {Object} options - Command options
|
|
203
|
+
* @param {string} options.environment - Environment ID or key
|
|
204
|
+
* @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
|
|
205
|
+
* @throws {Error} If rotation fails
|
|
206
|
+
*/
|
|
207
|
+
async function rotateSecret(appKey, options) {
|
|
208
|
+
logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
|
|
209
|
+
|
|
210
|
+
const config = await getConfig();
|
|
211
|
+
|
|
212
|
+
// Get authentication token
|
|
213
|
+
const controllerUrl = options.controller || null;
|
|
214
|
+
const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
|
|
215
|
+
|
|
146
216
|
// Validate environment
|
|
147
217
|
validateEnvironment(options.environment);
|
|
148
218
|
|
|
@@ -164,34 +234,8 @@ async function rotateSecret(appKey, options) {
|
|
|
164
234
|
const credentials = response.data.credentials;
|
|
165
235
|
const message = response.data.message;
|
|
166
236
|
|
|
167
|
-
// Save credentials
|
|
168
|
-
|
|
169
|
-
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
await saveLocalSecret(clientIdKey, credentials.clientId);
|
|
173
|
-
await saveLocalSecret(clientSecretKey, credentials.clientSecret);
|
|
174
|
-
|
|
175
|
-
// Update env.template if localhost
|
|
176
|
-
if (isLocalhost(actualControllerUrl)) {
|
|
177
|
-
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, actualControllerUrl);
|
|
178
|
-
|
|
179
|
-
// Regenerate .env file with updated credentials
|
|
180
|
-
try {
|
|
181
|
-
await generateEnvFile(appKey, null, 'local');
|
|
182
|
-
logger.log(chalk.green('✓ .env file updated with new credentials'));
|
|
183
|
-
} catch (error) {
|
|
184
|
-
logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
188
|
-
logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
|
|
189
|
-
} else {
|
|
190
|
-
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
|
|
191
|
-
}
|
|
192
|
-
} catch (error) {
|
|
193
|
-
logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
|
|
194
|
-
}
|
|
237
|
+
// Save credentials locally
|
|
238
|
+
await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
|
|
195
239
|
|
|
196
240
|
// Display results
|
|
197
241
|
displayRotationResults(appKey, options.environment, credentials, actualControllerUrl, message);
|