@aifabrix/builder 2.40.0 → 2.41.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/README.md +7 -5
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +29 -0
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/app/config.js +21 -0
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +9 -0
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +1 -3
- package/lib/app/run-env-compose.js +201 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +140 -14
- package/lib/cli/setup-auth.js +1 -0
- package/lib/cli/setup-dev.js +180 -17
- package/lib/cli/setup-environment.js +4 -2
- package/lib/cli/setup-external-system.js +71 -21
- package/lib/cli/setup-infra.js +29 -2
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +19 -4
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/auth-status.js +36 -3
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +309 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +26 -1
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +147 -81
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +7 -0
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test.js +5 -1
- package/lib/generator/index.js +174 -25
- package/lib/generator/wizard.js +13 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +88 -10
- package/lib/infrastructure/services.js +70 -15
- package/lib/schema/application-schema.json +24 -3
- package/lib/schema/external-system.schema.json +435 -413
- package/lib/utils/api.js +3 -3
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +76 -75
- package/lib/utils/compose-handlebars-helpers.js +43 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/credential-secrets-env.js +267 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +83 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -1
- package/lib/utils/help-builder.js +15 -2
- package/lib/utils/infra-status.js +30 -1
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +49 -33
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-generator.js +94 -6
- package/lib/utils/secrets-helpers.js +33 -25
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +5 -4
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/validate.js +1 -1
- package/lib/validation/validator.js +65 -0
- package/package.json +4 -2
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +5 -4
- package/templates/applications/dataplane/env.template +12 -7
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +11 -9
- package/templates/external-system/external-system.json.hbs +1 -16
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
package/lib/utils/api.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
const { parseErrorResponse } = require('./api-error-handler');
|
|
14
14
|
const auditLogger = require('../core/audit-logger');
|
|
15
15
|
|
|
16
|
-
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. */
|
|
17
|
-
const DEFAULT_REQUEST_TIMEOUT_MS =
|
|
16
|
+
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. 30s allows Azure Web App cold start to complete. */
|
|
17
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Logs API request performance metrics and errors to audit log
|
|
@@ -229,7 +229,7 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
231
|
* Make an API call with proper error handling
|
|
232
|
-
* Uses a
|
|
232
|
+
* Uses a 30s timeout to avoid hanging when the controller is unreachable (Azure cold start can exceed 5s).
|
|
233
233
|
* @param {string} url - API endpoint URL
|
|
234
234
|
* @param {Object} options - Fetch options (signal, method, headers, body, etc.)
|
|
235
235
|
* @returns {Promise<Object>} Response object with success flag
|
|
@@ -257,7 +257,22 @@ async function attemptGetAuthenticationToken(normalizedControllerUrl, config, at
|
|
|
257
257
|
return { token, finalControllerUrl, lastError };
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
/**
|
|
261
|
+
* When auth fails and throwOnFailure is true, throws an error with controllerUrl so caller can run login and retry.
|
|
262
|
+
* @param {string|null} controllerUrl - Controller URL to attach to the thrown error
|
|
263
|
+
* @param {Error|null} lastError - Last error from token attempt
|
|
264
|
+
* @param {string[]} attemptedUrls - URLs that were tried
|
|
265
|
+
* @throws {Error} Error with .controllerUrl and .authFailure set
|
|
266
|
+
*/
|
|
267
|
+
function throwAuthFailureError(controllerUrl, lastError, attemptedUrls) {
|
|
268
|
+
const message = lastError ? lastError.message : 'No valid authentication found';
|
|
269
|
+
const err = new Error(message);
|
|
270
|
+
err.controllerUrl = controllerUrl || (attemptedUrls && attemptedUrls[0]) || null;
|
|
271
|
+
err.authFailure = true;
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function checkAuthentication(controllerUrl, _environment, options = {}) {
|
|
261
276
|
try {
|
|
262
277
|
const config = await getConfig();
|
|
263
278
|
const normalizedControllerUrl = normalizeControllerUrlIfProvided(controllerUrl);
|
|
@@ -269,8 +284,12 @@ async function checkAuthentication(controllerUrl, _environment) {
|
|
|
269
284
|
attemptedUrls
|
|
270
285
|
);
|
|
271
286
|
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
if (!token || !finalControllerUrl) {
|
|
288
|
+
if (options.throwOnFailure) {
|
|
289
|
+
throwAuthFailureError(controllerUrl || finalControllerUrl, lastError, attemptedUrls);
|
|
290
|
+
}
|
|
291
|
+
validateAuthenticationResult(token, finalControllerUrl, lastError, controllerUrl, attemptedUrls);
|
|
292
|
+
}
|
|
274
293
|
|
|
275
294
|
return {
|
|
276
295
|
apiUrl: finalControllerUrl,
|
|
@@ -278,6 +297,9 @@ async function checkAuthentication(controllerUrl, _environment) {
|
|
|
278
297
|
controllerUrl: finalControllerUrl
|
|
279
298
|
};
|
|
280
299
|
} catch (error) {
|
|
300
|
+
if (error.authFailure) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
281
303
|
displayAuthenticationError(error, { controllerUrl: controllerUrl });
|
|
282
304
|
}
|
|
283
305
|
}
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -106,6 +106,10 @@ function isDockerPermissionDeniedError(errorMsg) {
|
|
|
106
106
|
*/
|
|
107
107
|
function formatDockerError(errorMsg) {
|
|
108
108
|
if (isDockerImageNotFoundError(errorMsg)) {
|
|
109
|
+
// Preserve custom hint for template apps (keycloak, miso-controller) from up-miso/up-dataplane
|
|
110
|
+
if (errorMsg.includes('use --image') || errorMsg.includes('Pull the image')) {
|
|
111
|
+
return errorMsg.split('\n').map(line => (line.trim() ? ` ${line.trim()}` : ''));
|
|
112
|
+
}
|
|
109
113
|
return [
|
|
110
114
|
' Docker image not found.',
|
|
111
115
|
' Run: aifabrix build <app> first'
|
|
@@ -326,6 +330,21 @@ function logOfflinePathWhenType(appPath, options) {
|
|
|
326
330
|
logger.log(chalk.gray(`Using: ${displayPath}`));
|
|
327
331
|
}
|
|
328
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Returns true if the error is likely due to authentication failure (e.g. 401, token expired, login required).
|
|
335
|
+
* Used to show "Run: aifabrix login" for commands that require Controller auth (e.g. up-dataplane).
|
|
336
|
+
* @param {Error} error - The error that occurred
|
|
337
|
+
* @returns {boolean} True if the error appears to be auth-related
|
|
338
|
+
*/
|
|
339
|
+
function isAuthenticationError(error) {
|
|
340
|
+
if (!error) return false;
|
|
341
|
+
if (error.authFailure === true) return true;
|
|
342
|
+
const msg = (error.message || '').toLowerCase();
|
|
343
|
+
const formatted = (typeof error.formatted === 'string' ? error.formatted : '').toLowerCase();
|
|
344
|
+
const combined = `${msg} ${formatted}`;
|
|
345
|
+
return /401|unauthorized|authentication|token expired|login required|aifabrix login|no authentication|device token|refresh token/.test(combined);
|
|
346
|
+
}
|
|
347
|
+
|
|
329
348
|
/**
|
|
330
349
|
* Handles command errors with user-friendly messages
|
|
331
350
|
* @param {Error} error - The error that occurred
|
|
@@ -375,6 +394,7 @@ async function appendWizardError(appKey, error) {
|
|
|
375
394
|
module.exports = {
|
|
376
395
|
validateCommand,
|
|
377
396
|
handleCommandError,
|
|
397
|
+
isAuthenticationError,
|
|
378
398
|
appendWizardError,
|
|
379
399
|
logOfflinePathWhenType
|
|
380
400
|
};
|
|
@@ -18,58 +18,12 @@ const buildCopy = require('./build-copy');
|
|
|
18
18
|
const { formatMissingDbPasswordError } = require('./error-formatter');
|
|
19
19
|
const { getContainerPort } = require('./port-resolver');
|
|
20
20
|
const { parseImageOverride } = require('./parse-image-ref');
|
|
21
|
+
const { registerComposeHelpers } = require('./compose-handlebars-helpers');
|
|
22
|
+
const { isVectorDatabaseName } = require('./compose-vector-helper');
|
|
23
|
+
const paths = require('./paths');
|
|
24
|
+
const { getInfraDirName } = require('../infrastructure/helpers');
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
24
|
-
|
|
25
|
-
// Register Handlebars helper for quoting PostgreSQL identifiers
|
|
26
|
-
// PostgreSQL requires identifiers with hyphens or special characters to be quoted
|
|
27
|
-
handlebars.registerHelper('pgQuote', (identifier) => {
|
|
28
|
-
if (!identifier) {
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
// Always quote identifiers to handle hyphens and special characters
|
|
32
|
-
// Return SafeString to prevent HTML escaping
|
|
33
|
-
return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Helper to generate quoted PostgreSQL user name from database name
|
|
37
|
-
// User names must use underscores (not hyphens) for PostgreSQL compatibility
|
|
38
|
-
handlebars.registerHelper('pgUser', (dbName) => {
|
|
39
|
-
if (!dbName) {
|
|
40
|
-
return '';
|
|
41
|
-
}
|
|
42
|
-
// Replace hyphens with underscores in user name (database names can have hyphens, but user names should not)
|
|
43
|
-
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
44
|
-
// Return SafeString to prevent HTML escaping
|
|
45
|
-
return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Helper to generate old user name format (for migration - drops old users with hyphens)
|
|
49
|
-
// This is used to drop legacy users that were created with hyphens before the fix
|
|
50
|
-
// Returns unquoted name (quotes should be added in template where needed)
|
|
51
|
-
handlebars.registerHelper('pgUserOld', (dbName) => {
|
|
52
|
-
if (!dbName) {
|
|
53
|
-
return '';
|
|
54
|
-
}
|
|
55
|
-
// Old format: database name + _user (preserving hyphens)
|
|
56
|
-
const userName = `${String(dbName)}_user`;
|
|
57
|
-
// Return unquoted name - template will add quotes where needed
|
|
58
|
-
return new handlebars.SafeString(userName);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Helper to generate unquoted PostgreSQL user name (for SQL WHERE clauses)
|
|
62
|
-
// Returns the user name without quotes for use in SQL queries
|
|
63
|
-
handlebars.registerHelper('pgUserName', (dbName) => {
|
|
64
|
-
if (!dbName) {
|
|
65
|
-
return '';
|
|
66
|
-
}
|
|
67
|
-
// Replace hyphens with underscores in user name
|
|
68
|
-
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
69
|
-
// Return unquoted name for SQL queries
|
|
70
|
-
return new handlebars.SafeString(userName);
|
|
71
|
-
});
|
|
72
|
-
|
|
26
|
+
registerComposeHelpers();
|
|
73
27
|
/**
|
|
74
28
|
* Loads and compiles Docker Compose template
|
|
75
29
|
* @param {string} language - Language type
|
|
@@ -95,7 +49,6 @@ function loadDockerComposeTemplate(language) {
|
|
|
95
49
|
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
96
50
|
return handlebars.compile(templateContent);
|
|
97
51
|
}
|
|
98
|
-
|
|
99
52
|
/**
|
|
100
53
|
* Extracts image name from configuration (same logic as build.js)
|
|
101
54
|
* @param {Object} config - Application configuration
|
|
@@ -112,7 +65,6 @@ function getImageName(config, appName) {
|
|
|
112
65
|
}
|
|
113
66
|
return appName;
|
|
114
67
|
}
|
|
115
|
-
|
|
116
68
|
/**
|
|
117
69
|
* Builds app configuration section
|
|
118
70
|
* @param {string} appName - Application name
|
|
@@ -125,7 +77,6 @@ function buildAppConfig(appName, config) {
|
|
|
125
77
|
name: config.displayName || appName
|
|
126
78
|
};
|
|
127
79
|
}
|
|
128
|
-
|
|
129
80
|
/**
|
|
130
81
|
* Builds image configuration section
|
|
131
82
|
* @param {Object} config - Application configuration
|
|
@@ -255,9 +206,6 @@ function buildServiceConfig(appName, config, port, devId, imageOverride) {
|
|
|
255
206
|
port: containerPortValue, // Container port (for health check and template)
|
|
256
207
|
containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
|
|
257
208
|
hostPort: hostPort, // Host port (options.port if provided, else config.port)
|
|
258
|
-
build: {
|
|
259
|
-
localPort: config.build?.localPort || null // Only used for .env file PORT variable, not for Docker Compose
|
|
260
|
-
},
|
|
261
209
|
healthCheck: buildHealthCheckConfig(config),
|
|
262
210
|
traefik: buildTraefikConfig(config, devId),
|
|
263
211
|
...buildRequiresConfig(config)
|
|
@@ -445,6 +393,52 @@ async function readDatabasePasswordsIfNeeded(requiresDatabase, databases, envFil
|
|
|
445
393
|
return { map: {}, array: [] };
|
|
446
394
|
}
|
|
447
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Resolves image override from options (--image, --tag, or null).
|
|
398
|
+
* @param {Object} options - Run options
|
|
399
|
+
* @param {Object} appConfig - Application configuration
|
|
400
|
+
* @param {string} appName - Application name
|
|
401
|
+
* @returns {string|null} Full image reference or null
|
|
402
|
+
*/
|
|
403
|
+
function resolveImageOverride(options, appConfig, appName) {
|
|
404
|
+
if (options.image) return options.image;
|
|
405
|
+
if (options.imageOverride) return options.imageOverride;
|
|
406
|
+
if (options.tag) return `${getImageName(appConfig, appName)}:${options.tag}`;
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Resolves Miso environment from options (tst, pro, or dev).
|
|
412
|
+
* @param {Object} options - Run options
|
|
413
|
+
* @returns {string} 'dev' | 'tst' | 'pro'
|
|
414
|
+
*/
|
|
415
|
+
function resolveMisoEnvironment(options) {
|
|
416
|
+
const env = (options.env && typeof options.env === 'string') ? options.env.toLowerCase() : 'dev';
|
|
417
|
+
return (env === 'tst' || env === 'pro') ? env : 'dev';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolves dev mount path from options.
|
|
422
|
+
* @param {Object} options - Run options
|
|
423
|
+
* @returns {string|null} Trimmed path or null
|
|
424
|
+
*/
|
|
425
|
+
function resolveDevMountPath(options) {
|
|
426
|
+
return (options.devMountPath && typeof options.devMountPath === 'string') ? options.devMountPath.trim() : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Resolves env file path from options or default dev dir (forward slashes).
|
|
431
|
+
* @param {Object} options - Run options
|
|
432
|
+
* @param {string} devDir - Default dev directory
|
|
433
|
+
* @returns {string} Absolute env file path
|
|
434
|
+
*/
|
|
435
|
+
function resolveEnvFilePath(options, devDir) {
|
|
436
|
+
const envFilePath = (options.envFilePath && typeof options.envFilePath === 'string')
|
|
437
|
+
? path.resolve(options.envFilePath)
|
|
438
|
+
: path.join(devDir, '.env');
|
|
439
|
+
return envFilePath.replace(/\\/g, '/');
|
|
440
|
+
}
|
|
441
|
+
|
|
448
442
|
/**
|
|
449
443
|
* Generates Docker Compose configuration from template
|
|
450
444
|
* @async
|
|
@@ -458,8 +452,7 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
458
452
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
459
453
|
const template = loadDockerComposeTemplate(language);
|
|
460
454
|
const port = options.port || appConfig.port || 3000;
|
|
461
|
-
const imageOverride = options
|
|
462
|
-
(options.tag ? `${getImageName(appConfig, appName)}:${options.tag}` : null);
|
|
455
|
+
const imageOverride = resolveImageOverride(options, appConfig, appName);
|
|
463
456
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
464
457
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
|
|
465
458
|
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
|
|
@@ -467,33 +460,41 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
467
460
|
const networksConfig = buildNetworksConfig(appConfig);
|
|
468
461
|
|
|
469
462
|
const devDir = buildCopy.getDevDirectory(appName, devId);
|
|
470
|
-
const
|
|
471
|
-
const
|
|
463
|
+
const envFileAbsolutePath = resolveEnvFilePath(options, devDir);
|
|
464
|
+
const dbInitEnvFileAbsolutePath = (options.dbInitEnvFilePath && typeof options.dbInitEnvFilePath === 'string')
|
|
465
|
+
? path.resolve(options.dbInitEnvFilePath).replace(/\\/g, '/')
|
|
466
|
+
: null;
|
|
472
467
|
|
|
473
468
|
const databasePasswords = await readDatabasePasswordsIfNeeded(
|
|
474
469
|
serviceConfig.requiresDatabase || false,
|
|
475
470
|
networksConfig.databases || [],
|
|
476
|
-
|
|
471
|
+
envFileAbsolutePath,
|
|
477
472
|
appName
|
|
478
473
|
);
|
|
479
|
-
|
|
480
|
-
const
|
|
474
|
+
const devMountPath = resolveDevMountPath(options);
|
|
475
|
+
const reloadStartRaw = appConfig.build?.reloadStart;
|
|
476
|
+
const reloadStartCommand =
|
|
477
|
+
devMountPath && typeof reloadStartRaw === 'string' && reloadStartRaw.trim().length > 0
|
|
478
|
+
? reloadStartRaw.trim()
|
|
479
|
+
: null;
|
|
480
|
+
|
|
481
|
+
const infraPgpassPath = path.join(paths.getAifabrixHome(), getInfraDirName(devId), 'pgpass');
|
|
482
|
+
const useInfraPgpass = serviceConfig.requiresDatabase && fsSync.existsSync(infraPgpassPath);
|
|
483
|
+
return template({
|
|
481
484
|
...serviceConfig,
|
|
482
485
|
...volumesConfig,
|
|
483
486
|
...networksConfig,
|
|
484
487
|
envFile: envFileAbsolutePath,
|
|
488
|
+
dbInitEnvFile: dbInitEnvFileAbsolutePath,
|
|
485
489
|
databasePasswords,
|
|
486
490
|
devId: idNum,
|
|
487
491
|
networkName,
|
|
488
|
-
containerName
|
|
489
|
-
|
|
490
|
-
|
|
492
|
+
containerName,
|
|
493
|
+
misoEnvironment: resolveMisoEnvironment(options),
|
|
494
|
+
devMountPath,
|
|
495
|
+
reloadStartCommand,
|
|
496
|
+
infraPgpassPath: useInfraPgpass ? infraPgpassPath : null,
|
|
497
|
+
useInfraPgpass: !!useInfraPgpass
|
|
498
|
+
});
|
|
491
499
|
}
|
|
492
|
-
|
|
493
|
-
module.exports = {
|
|
494
|
-
generateDockerCompose,
|
|
495
|
-
getImageName,
|
|
496
|
-
derivePathFromPattern,
|
|
497
|
-
buildTraefikConfig,
|
|
498
|
-
buildDevUsername
|
|
499
|
-
};
|
|
500
|
+
module.exports = { generateDockerCompose, getImageName, derivePathFromPattern, buildTraefikConfig, buildDevUsername, isVectorDatabaseName };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handlebars helpers for Docker Compose templates.
|
|
3
|
+
* @fileoverview Compose template helpers (pgQuote, pgUser, isVectorDatabase, etc.)
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const handlebars = require('handlebars');
|
|
9
|
+
const { isVectorDatabaseName } = require('./compose-vector-helper');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registers Handlebars helpers used by Docker Compose templates.
|
|
13
|
+
*/
|
|
14
|
+
function registerComposeHelpers() {
|
|
15
|
+
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
16
|
+
|
|
17
|
+
handlebars.registerHelper('pgQuote', (identifier) => {
|
|
18
|
+
if (!identifier) return '';
|
|
19
|
+
return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
handlebars.registerHelper('pgUser', (dbName) => {
|
|
23
|
+
if (!dbName) return '';
|
|
24
|
+
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
25
|
+
return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
handlebars.registerHelper('pgUserOld', (dbName) => {
|
|
29
|
+
if (!dbName) return '';
|
|
30
|
+
const userName = `${String(dbName)}_user`;
|
|
31
|
+
return new handlebars.SafeString(userName);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
handlebars.registerHelper('pgUserName', (dbName) => {
|
|
35
|
+
if (!dbName) return '';
|
|
36
|
+
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
37
|
+
return new handlebars.SafeString(userName);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
handlebars.registerHelper('isVectorDatabase', (name) => isVectorDatabaseName(name));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { registerComposeHelpers };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector database name predicate for Docker Compose generation.
|
|
3
|
+
* Used so db-init can run CREATE EXTENSION vector on vector-store databases.
|
|
4
|
+
* @fileoverview Vector database name helper for compose-generator
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when the database name ends with "vector" (case-insensitive).
|
|
11
|
+
* @param {string} name - Database name
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function isVectorDatabaseName(name) {
|
|
15
|
+
return name !== null && name !== undefined && String(name).toLowerCase().endsWith('vector');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { isVectorDatabaseName };
|
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* GET /api/dev/settings response parameter names (builder-cli.md §1).
|
|
15
|
+
* Single source of truth so CLI and config merge stay aligned with the contract.
|
|
16
|
+
*/
|
|
17
|
+
const SETTINGS_RESPONSE_KEYS = [
|
|
18
|
+
'user-mutagen-folder',
|
|
19
|
+
'secrets-encryption',
|
|
20
|
+
'aifabrix-secrets',
|
|
21
|
+
'aifabrix-env-config',
|
|
22
|
+
'remote-server',
|
|
23
|
+
'docker-endpoint',
|
|
24
|
+
'sync-ssh-user',
|
|
25
|
+
'sync-ssh-host'
|
|
26
|
+
];
|
|
27
|
+
|
|
13
28
|
/**
|
|
14
29
|
* Get path configuration value
|
|
15
30
|
* @async
|
|
@@ -73,6 +88,114 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
|
|
|
73
88
|
};
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
function createRemoteConfigGetters(getConfigFn) {
|
|
92
|
+
return {
|
|
93
|
+
async getRemoteServer() {
|
|
94
|
+
return getPathConfig(getConfigFn, 'remote-server');
|
|
95
|
+
},
|
|
96
|
+
async getDockerEndpoint() {
|
|
97
|
+
return getPathConfig(getConfigFn, 'docker-endpoint');
|
|
98
|
+
},
|
|
99
|
+
async getUserMutagenFolder() {
|
|
100
|
+
return getPathConfig(getConfigFn, 'user-mutagen-folder');
|
|
101
|
+
},
|
|
102
|
+
async getSyncSshUser() {
|
|
103
|
+
return getPathConfig(getConfigFn, 'sync-ssh-user');
|
|
104
|
+
},
|
|
105
|
+
async getSyncSshHost() {
|
|
106
|
+
return getPathConfig(getConfigFn, 'sync-ssh-host');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createRemoteConfigSetters(getConfigFn, saveConfigFn) {
|
|
112
|
+
return {
|
|
113
|
+
async setRemoteServer(value) {
|
|
114
|
+
if (value !== null && value !== undefined && typeof value !== 'string') {
|
|
115
|
+
throw new Error('remote-server must be a string');
|
|
116
|
+
}
|
|
117
|
+
const config = await getConfigFn();
|
|
118
|
+
config['remote-server'] = value ? value.trim().replace(/\/+$/, '') : undefined;
|
|
119
|
+
await saveConfigFn(config);
|
|
120
|
+
},
|
|
121
|
+
async setDockerEndpoint(value) {
|
|
122
|
+
if (value !== null && value !== undefined && typeof value !== 'string') {
|
|
123
|
+
throw new Error('docker-endpoint must be a string');
|
|
124
|
+
}
|
|
125
|
+
const config = await getConfigFn();
|
|
126
|
+
config['docker-endpoint'] = value || undefined;
|
|
127
|
+
await saveConfigFn(config);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isHttpUrl(value) {
|
|
133
|
+
return typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Derive hostname from a URL (e.g. https://builder.aifabrix.dev -> builder.aifabrix.dev).
|
|
138
|
+
* @param {string} url - URL string
|
|
139
|
+
* @returns {string|null} Hostname or null if invalid
|
|
140
|
+
*/
|
|
141
|
+
function hostnameFromUrl(url) {
|
|
142
|
+
if (!url || typeof url !== 'string') return null;
|
|
143
|
+
const s = url.trim().replace(/\/+$/, '');
|
|
144
|
+
if (!s) return null;
|
|
145
|
+
const withProtocol = s.match(/^https?:\/\//) ? s : `https://${s}`;
|
|
146
|
+
try {
|
|
147
|
+
return new URL(withProtocol).hostname || null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applySecretsUrlFromRemote(config) {
|
|
154
|
+
const remoteServer = config['remote-server'];
|
|
155
|
+
const secretsPath = config['aifabrix-secrets'];
|
|
156
|
+
if (!remoteServer || !secretsPath || isHttpUrl(secretsPath)) return;
|
|
157
|
+
const base = typeof remoteServer === 'string' ? remoteServer.trim().replace(/\/+$/, '') : '';
|
|
158
|
+
if (base) config['aifabrix-secrets'] = `${base}/api/dev/secrets`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function applySyncAndDockerFromHost(config) {
|
|
162
|
+
const host = hostnameFromUrl(config['remote-server']);
|
|
163
|
+
if (!host) return;
|
|
164
|
+
if (!config['sync-ssh-host']) config['sync-ssh-host'] = host;
|
|
165
|
+
if (!config['docker-endpoint']) config['docker-endpoint'] = `tcp://${host}:2376`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings) {
|
|
169
|
+
if (!settings || typeof settings !== 'object') return;
|
|
170
|
+
const config = await getConfigFn();
|
|
171
|
+
for (const key of SETTINGS_RESPONSE_KEYS) {
|
|
172
|
+
const raw = settings[key];
|
|
173
|
+
if (raw === undefined || raw === null) continue;
|
|
174
|
+
const value = typeof raw === 'string' ? raw.trim() : raw;
|
|
175
|
+
if (value === '') continue;
|
|
176
|
+
config[key] = value;
|
|
177
|
+
}
|
|
178
|
+
applySecretsUrlFromRemote(config);
|
|
179
|
+
applySyncAndDockerFromHost(config);
|
|
180
|
+
await saveConfigFn(config);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Remote Docker / Builder Server config. Used when remote-server is set.
|
|
185
|
+
* @param {Function} getConfigFn - Function to get config
|
|
186
|
+
* @param {Function} saveConfigFn - Function to save config
|
|
187
|
+
* @returns {Object} Remote config getters, setters, and mergeRemoteSettings
|
|
188
|
+
*/
|
|
189
|
+
function createRemoteConfigFunctions(getConfigFn, saveConfigFn) {
|
|
190
|
+
return {
|
|
191
|
+
...createRemoteConfigGetters(getConfigFn),
|
|
192
|
+
...createRemoteConfigSetters(getConfigFn, saveConfigFn),
|
|
193
|
+
async mergeRemoteSettings(settings) {
|
|
194
|
+
return mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
76
199
|
/**
|
|
77
200
|
* Create path configuration functions with config access
|
|
78
201
|
* @param {Function} getConfigFn - Function to get config
|
|
@@ -82,13 +205,15 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
|
|
|
82
205
|
function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
83
206
|
return {
|
|
84
207
|
...createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn),
|
|
85
|
-
...createEnvConfigPathFunctions(getConfigFn, saveConfigFn)
|
|
208
|
+
...createEnvConfigPathFunctions(getConfigFn, saveConfigFn),
|
|
209
|
+
...createRemoteConfigFunctions(getConfigFn, saveConfigFn)
|
|
86
210
|
};
|
|
87
211
|
}
|
|
88
212
|
|
|
89
213
|
module.exports = {
|
|
90
214
|
getPathConfig,
|
|
91
215
|
setPathConfig,
|
|
92
|
-
createPathConfigFunctions
|
|
216
|
+
createPathConfigFunctions,
|
|
217
|
+
SETTINGS_RESPONSE_KEYS
|
|
93
218
|
};
|
|
94
219
|
|