@aifabrix/builder 2.44.6 → 2.45.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/cli-layout.mdc +7 -3
- package/jest.projects.js +56 -0
- package/lib/app/helpers.js +3 -3
- package/lib/app/index.js +3 -3
- package/lib/app/register.js +7 -6
- package/lib/app/restart-display.js +52 -21
- package/lib/app/rotate-secret.js +7 -6
- package/lib/app/run-helpers.js +15 -8
- package/lib/app/run.js +57 -9
- package/lib/app/show-display.js +7 -0
- package/lib/app/show.js +87 -5
- package/lib/build/index.js +9 -5
- package/lib/cli/infra-guided.js +42 -27
- package/lib/cli/installation-log-command.js +73 -0
- package/lib/cli/setup-app.js +11 -1
- package/lib/cli/setup-auth.js +94 -49
- package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
- package/lib/cli/setup-infra-up-platform-action.js +131 -0
- package/lib/cli/setup-infra.js +60 -119
- package/lib/cli/setup-platform.js +1 -1
- package/lib/cli/setup-utility-resolve.js +132 -0
- package/lib/cli/setup-utility.js +65 -51
- package/lib/commands/app-logs.js +81 -33
- package/lib/commands/auth-config.js +116 -18
- package/lib/commands/setup-modes.js +19 -6
- package/lib/commands/setup-prompts.js +41 -8
- package/lib/commands/setup.js +114 -9
- package/lib/commands/teardown.js +54 -5
- package/lib/commands/up-common.js +48 -14
- package/lib/commands/up-dataplane.js +21 -18
- package/lib/commands/up-miso.js +12 -8
- package/lib/commands/upload.js +5 -3
- package/lib/core/audit-logger.js +1 -34
- package/lib/core/config-admin-email.js +56 -0
- package/lib/core/config-normalize.js +60 -0
- package/lib/core/config-registered-controller-urls.js +54 -0
- package/lib/core/config.js +33 -50
- package/lib/core/secrets-ensure-infra.js +1 -1
- package/lib/core/secrets-env-content.js +86 -90
- package/lib/core/secrets-env-declarative-expand.js +170 -0
- package/lib/core/secrets-env-write.js +2 -0
- package/lib/core/secrets-load.js +106 -102
- package/lib/external-system/deploy.js +5 -1
- package/lib/internal/node-fs.js +2 -0
- package/lib/schema/application-schema.json +4 -0
- package/lib/schema/infra.parameter.yaml +10 -0
- package/lib/utils/app-config-resolver.js +24 -1
- package/lib/utils/applications-config-defaults.js +206 -0
- package/lib/utils/auth-config-validator.js +2 -12
- package/lib/utils/bash-secret-env.js +1 -1
- package/lib/utils/compose-generate-docker-compose.js +111 -6
- package/lib/utils/compose-generator.js +17 -8
- package/lib/utils/controller-url.js +50 -7
- package/lib/utils/env-copy.js +99 -14
- package/lib/utils/env-template.js +5 -1
- package/lib/utils/health-check-url.js +18 -15
- package/lib/utils/health-check.js +7 -5
- package/lib/utils/infra-optional-service-flags.js +69 -0
- package/lib/utils/installation-log-core.js +282 -0
- package/lib/utils/installation-log-record.js +237 -0
- package/lib/utils/installation-log.js +123 -0
- package/lib/utils/log-redaction.js +105 -0
- package/lib/utils/manifest-location.js +164 -0
- package/lib/utils/manifest-source-emit.js +162 -0
- package/lib/utils/paths.js +238 -89
- package/lib/utils/remote-secrets-loader.js +7 -1
- package/lib/utils/run-cli-flags.js +29 -0
- package/lib/utils/secrets-canonical.js +10 -3
- package/lib/utils/secrets-path.js +3 -4
- package/lib/utils/secrets-utils.js +20 -10
- package/lib/utils/system-builder-root.js +10 -2
- package/lib/utils/url-declarative-public-base.js +80 -12
- package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
- package/lib/utils/url-declarative-resolve-build.js +24 -393
- package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
- package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
- package/lib/utils/url-declarative-resolve.js +47 -7
- package/lib/utils/url-declarative-runtime-base-path.js +21 -1
- package/lib/utils/urls-local-registry-scan.js +103 -0
- package/lib/utils/urls-local-registry.js +161 -90
- package/package.json +3 -1
- package/templates/applications/dataplane/application.yaml +4 -0
- package/templates/applications/miso-controller/application.yaml +2 -0
- package/templates/applications/miso-controller/env.template +27 -29
- package/.npmrc.token +0 -1
|
@@ -15,6 +15,99 @@ const paths = require('./paths');
|
|
|
15
15
|
const { getInfraDirName } = require('../infrastructure/helpers');
|
|
16
16
|
const { resolveMisoEnvironment } = require('./compose-miso-env');
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* When running a pulled Python image (no bind-mounted source), the image CMD may invoke the
|
|
20
|
+
* `uvicorn` console script, which is not always on PATH. Derive a `python -m uvicorn` command from
|
|
21
|
+
* `build.reloadStart` so generated compose matches the same app entrypoint as hot-reload runs.
|
|
22
|
+
*
|
|
23
|
+
* @param {string|undefined} reloadStart - Value from application.yaml `build.reloadStart`
|
|
24
|
+
* @returns {string|null} Shell command fragment (no `cd`, no `exec`; image compose prepends `exec ` in JS)
|
|
25
|
+
*/
|
|
26
|
+
function derivePythonImageStartFromReload(reloadStart) {
|
|
27
|
+
if (typeof reloadStart !== 'string' || !reloadStart.trim()) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = reloadStart.trim().replace(/\s+--reload\b/g, '').trim();
|
|
31
|
+
if (/^python3?\s+-m\s+uvicorn\s+/i.test(trimmed)) {
|
|
32
|
+
return normalizeComposeShellPortRef(trimmed);
|
|
33
|
+
}
|
|
34
|
+
if (trimmed.startsWith('uvicorn ')) {
|
|
35
|
+
const rest = trimmed.slice('uvicorn '.length);
|
|
36
|
+
return normalizeComposeShellPortRef(`python -m uvicorn ${rest}`);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compose sets `PORT` in the service environment; replace `${PORT:-...}` with `$$PORT` in the
|
|
43
|
+
* fragment so the compose file avoids ambiguous `:` parsing in flow scalars and Compose expands
|
|
44
|
+
* `$$` to `$` for the container shell (which then expands `PORT` from the service environment).
|
|
45
|
+
*
|
|
46
|
+
* @param {string} cmd - Shell command fragment
|
|
47
|
+
* @returns {string} Same fragment with `${PORT:-...}` replaced by `$$PORT` (Compose escapes `$$` → `$` for the shell)
|
|
48
|
+
*/
|
|
49
|
+
function normalizeComposeShellPortRef(cmd) {
|
|
50
|
+
return cmd.replace(/\$\{PORT:-[^}]+\}/g, () => '$$PORT');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalizes `build.reloadStart` for Python when using a bind-mounted source in compose.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} raw - Trimmed reload command
|
|
57
|
+
* @returns {string} Command suitable for `cd /app && …` in compose
|
|
58
|
+
*/
|
|
59
|
+
function normalizePythonReloadForComposeMounted(raw) {
|
|
60
|
+
const s = normalizeComposeShellPortRef(raw);
|
|
61
|
+
if (/^python3?\s+-m\s+uvicorn\s+/i.test(s)) {
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
if (s.startsWith('uvicorn ')) {
|
|
65
|
+
return `python -m uvicorn ${s.slice('uvicorn '.length)}`;
|
|
66
|
+
}
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds the single `reloadStartCommand` passed to compose templates (`command: …` when set).
|
|
72
|
+
*
|
|
73
|
+
* `applications.<app>.reload` in config.yaml (and `aifabrix run --reload`) only controls **bind-mount +
|
|
74
|
+
* `--reload`** via `devMountPath`. For pulled images without a mount, use optional `build.imageRun` in
|
|
75
|
+
* application.yaml, or (Python only) derive from `build.reloadStart` when `imageRun` is unset.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} language - `application.yaml` build.language
|
|
78
|
+
* @param {string|null} devMountPath - Bind mount path when reload sync is active
|
|
79
|
+
* @param {string|undefined} reloadRaw - `build.reloadStart`
|
|
80
|
+
* @param {Object} [build] - `application.yaml` `build` object (`imageRun`, etc.)
|
|
81
|
+
* @returns {string|null}
|
|
82
|
+
*/
|
|
83
|
+
function buildReloadStartCommandForCompose(language, devMountPath, reloadRaw, build = {}) {
|
|
84
|
+
const buildObj = build && typeof build === 'object' ? build : {};
|
|
85
|
+
const imageRunRaw = typeof buildObj.imageRun === 'string' ? buildObj.imageRun.trim() : '';
|
|
86
|
+
const reloadTrimmed = typeof reloadRaw === 'string' ? reloadRaw.trim() : '';
|
|
87
|
+
|
|
88
|
+
if (devMountPath) {
|
|
89
|
+
if (!reloadTrimmed) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return language === 'python' ? normalizePythonReloadForComposeMounted(reloadTrimmed) : reloadTrimmed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (imageRunRaw) {
|
|
96
|
+
return normalizeComposeShellPortRef(imageRunRaw);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!reloadTrimmed) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (language === 'python') {
|
|
104
|
+
const imageCmd = derivePythonImageStartFromReload(reloadTrimmed);
|
|
105
|
+
return imageCmd ? `exec ${imageCmd}` : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
18
111
|
/**
|
|
19
112
|
* Resolve infra `pgpass` path: prefer system config dir; when home differs, allow legacy layout.
|
|
20
113
|
* @param {string|number} devId - Developer id (infra vs infra-dev{n})
|
|
@@ -119,14 +212,15 @@ async function loadComposeHead(deps, appName, appConfig, options) {
|
|
|
119
212
|
* @param {object} head - From loadComposeHead
|
|
120
213
|
* @returns {object}
|
|
121
214
|
*/
|
|
122
|
-
function buildComposeLayouts(deps, appName, appConfig, head) {
|
|
215
|
+
function buildComposeLayouts(deps, appName, appConfig, head, options) {
|
|
123
216
|
const { buildNetworkAndContainerNames, buildServiceConfig, buildVolumesConfig, buildNetworksConfig } = deps;
|
|
124
217
|
const { port, imageOverride, devId, idNum, scoped, remoteServer } = head;
|
|
125
218
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum, scoped);
|
|
126
219
|
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, {
|
|
127
220
|
imageOverride,
|
|
128
221
|
scopeOpts: scoped,
|
|
129
|
-
remoteServer
|
|
222
|
+
remoteServer,
|
|
223
|
+
omitAppTraefikLabels: options && options.omitAppTraefikLabels === true
|
|
130
224
|
});
|
|
131
225
|
const volumesConfig = buildVolumesConfig(appName);
|
|
132
226
|
const networksConfig = buildNetworksConfig(appConfig);
|
|
@@ -161,8 +255,13 @@ async function resolveComposePathsAndSecrets(ctx) {
|
|
|
161
255
|
);
|
|
162
256
|
const devMountPath = resolveDevMountPath(options);
|
|
163
257
|
const reloadRaw = appConfig.build?.reloadStart;
|
|
164
|
-
const
|
|
165
|
-
|
|
258
|
+
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
259
|
+
const reloadStartCommand = buildReloadStartCommandForCompose(
|
|
260
|
+
language,
|
|
261
|
+
devMountPath,
|
|
262
|
+
reloadRaw,
|
|
263
|
+
appConfig.build
|
|
264
|
+
);
|
|
166
265
|
const infraPgpassPath = resolveInfraPgpassPath(devId, paths, fsSync.existsSync);
|
|
167
266
|
const useInfraPgpass = serviceCf.requiresDatabase && fsSync.existsSync(infraPgpassPath);
|
|
168
267
|
return {
|
|
@@ -185,7 +284,7 @@ async function resolveComposePathsAndSecrets(ctx) {
|
|
|
185
284
|
*/
|
|
186
285
|
async function generateDockerComposeImpl(deps, appName, appConfig, options) {
|
|
187
286
|
const head = await loadComposeHead(deps, appName, appConfig, options);
|
|
188
|
-
const layouts = buildComposeLayouts(deps, appName, appConfig, head);
|
|
287
|
+
const layouts = buildComposeLayouts(deps, appName, appConfig, head, options);
|
|
189
288
|
const side = await resolveComposePathsAndSecrets({
|
|
190
289
|
options,
|
|
191
290
|
appName,
|
|
@@ -213,4 +312,10 @@ function createGenerateDockerCompose(deps) {
|
|
|
213
312
|
return (appName, appConfig, options) => generateDockerComposeImpl(deps, appName, appConfig, options);
|
|
214
313
|
}
|
|
215
314
|
|
|
216
|
-
module.exports = {
|
|
315
|
+
module.exports = {
|
|
316
|
+
createGenerateDockerCompose,
|
|
317
|
+
resolveInfraPgpassPath,
|
|
318
|
+
derivePythonImageStartFromReload,
|
|
319
|
+
buildReloadStartCommandForCompose,
|
|
320
|
+
normalizePythonReloadForComposeMounted
|
|
321
|
+
};
|
|
@@ -282,10 +282,16 @@ function buildRequiresConfig(config) {
|
|
|
282
282
|
* @param {string} [runExtras.imageOverride] - Full image reference for run (e.g. from --image)
|
|
283
283
|
* @param {Object|null} [runExtras.scopeOpts] - Traefik / scoped compose options
|
|
284
284
|
* @param {string|null|undefined} [runExtras.remoteServer] - For ${REMOTE_HOST} in frontDoorRouting.host
|
|
285
|
+
* @param {boolean} [runExtras.omitAppTraefikLabels] - When true (user config `traefik: false`), omit Traefik labels and BASE_PATH env from compose even if application.yaml enables frontDoorRouting
|
|
285
286
|
* @returns {Object} Service configuration
|
|
286
287
|
*/
|
|
287
288
|
function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
|
|
288
|
-
const {
|
|
289
|
+
const {
|
|
290
|
+
imageOverride = null,
|
|
291
|
+
scopeOpts = null,
|
|
292
|
+
remoteServer = null,
|
|
293
|
+
omitAppTraefikLabels = false
|
|
294
|
+
} = runExtras;
|
|
289
295
|
const containerPortValue = getContainerPort(config, 3000);
|
|
290
296
|
const hostPort = port;
|
|
291
297
|
const useTraefikScope =
|
|
@@ -305,13 +311,16 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
|
|
|
305
311
|
containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
|
|
306
312
|
hostPort: hostPort, // Host port (options.port if provided, else config.port)
|
|
307
313
|
healthCheck,
|
|
308
|
-
traefik:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
traefik:
|
|
315
|
+
omitAppTraefikLabels === true
|
|
316
|
+
? { enabled: false }
|
|
317
|
+
: buildTraefikConfig(
|
|
318
|
+
config,
|
|
319
|
+
devId,
|
|
320
|
+
scopeForHealthAndTraefik,
|
|
321
|
+
remoteServer,
|
|
322
|
+
healthCheck.path
|
|
323
|
+
),
|
|
315
324
|
...buildRequiresConfig(config)
|
|
316
325
|
};
|
|
317
326
|
}
|
|
@@ -42,8 +42,38 @@ function normalizeUrl(url) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
45
|
+
* True when `config.yaml` has a non-expired-looking device entry for this controller URL
|
|
46
|
+
* (normalized host/path comparison).
|
|
47
|
+
*
|
|
48
|
+
* @async
|
|
49
|
+
* @param {string} controllerUrl
|
|
50
|
+
* @returns {Promise<boolean>}
|
|
51
|
+
*/
|
|
52
|
+
async function hasStoredDeviceTokenForController(controllerUrl) {
|
|
53
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const want = normalizeUrl(controllerUrl);
|
|
57
|
+
if (!want) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const userConfig = await config.getConfig();
|
|
62
|
+
if (!userConfig.device || typeof userConfig.device !== 'object') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return Object.keys(userConfig.device).some((key) => normalizeUrl(key) === want);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get controller URL from logged-in user's device tokens.
|
|
73
|
+
* Prefers the entry under {@link config.controller} when it matches a `device` key; otherwise a
|
|
74
|
+
* single `device` entry; with multiple unrelated entries and no matching default, returns **null**
|
|
75
|
+
* (callers must not treat arbitrary key order as the active controller).
|
|
76
|
+
*
|
|
47
77
|
* @async
|
|
48
78
|
* @function getControllerUrlFromLoggedInUser
|
|
49
79
|
* @returns {Promise<string|null>} Controller URL from logged-in user, or null if not found
|
|
@@ -60,11 +90,23 @@ async function getControllerUrlFromLoggedInUser() {
|
|
|
60
90
|
return null;
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
const cfgController =
|
|
94
|
+
userConfig.controller && typeof userConfig.controller === 'string'
|
|
95
|
+
? normalizeUrl(userConfig.controller)
|
|
96
|
+
: '';
|
|
97
|
+
if (cfgController) {
|
|
98
|
+
const match = deviceUrls.find((u) => normalizeUrl(u) === cfgController);
|
|
99
|
+
if (match) {
|
|
100
|
+
return normalizeUrl(match);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (deviceUrls.length === 1) {
|
|
105
|
+
return normalizeUrl(deviceUrls[0]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
} catch {
|
|
68
110
|
return null;
|
|
69
111
|
}
|
|
70
112
|
}
|
|
@@ -109,6 +151,7 @@ async function resolveControllerUrl() {
|
|
|
109
151
|
|
|
110
152
|
module.exports = {
|
|
111
153
|
getDefaultControllerUrl,
|
|
154
|
+
hasStoredDeviceTokenForController,
|
|
112
155
|
getControllerUrlFromLoggedInUser,
|
|
113
156
|
getControllerFromConfig,
|
|
114
157
|
resolveControllerUrl
|
package/lib/utils/env-copy.js
CHANGED
|
@@ -11,6 +11,7 @@ const { formatSuccessLine } = require('./cli-test-layout-chalk');
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const fsp = require('fs').promises;
|
|
14
|
+
const fsRealSync = require('../internal/fs-real-sync');
|
|
14
15
|
const path = require('path');
|
|
15
16
|
const yaml = require('js-yaml');
|
|
16
17
|
const logger = require('./logger');
|
|
@@ -98,22 +99,37 @@ function resolveEnvOutputPath(rawOutputPath, variablesPath) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
|
-
* Writes .env to envOutputPath for reload
|
|
102
|
+
* Writes .env to envOutputPath for `--reload`: merge container `.env.run` into the host file.
|
|
103
|
+
* Preserves comments when a file already exists (e.g. after `aifabrix resolve`). Keys present only
|
|
104
|
+
* in `.env.run` (compose-only such as DB_0_NAME) are **not** appended unless they already exist as
|
|
105
|
+
* `KEY=` lines in the base file. If the output file is missing, seeds from `generateEnvContent`
|
|
106
|
+
* (`local`) so the host file keeps template comments and localhost-oriented URLs.
|
|
107
|
+
*
|
|
102
108
|
* @async
|
|
103
109
|
* @param {string} outputPath - Resolved output path
|
|
104
110
|
* @param {string} runEnvPath - Path to .env.run
|
|
111
|
+
* @param {string} appName - Application name (for template seed when output is absent)
|
|
105
112
|
*/
|
|
106
|
-
async function writeEnvOutputForReload(outputPath, runEnvPath) {
|
|
107
|
-
const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
113
|
+
async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
|
|
114
|
+
const { parseEnvContentToMap, mergeEnvMapIntoContent, generateEnvContent } = require('../core/secrets');
|
|
108
115
|
const runContent = await fsp.readFile(runEnvPath, 'utf8');
|
|
109
116
|
const runMap = parseEnvContentToMap(runContent);
|
|
110
|
-
let
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
let baseContent;
|
|
118
|
+
if (fsRealSync.existsSync(outputPath)) {
|
|
119
|
+
baseContent = await fsp.readFile(outputPath, 'utf8');
|
|
120
|
+
} else if (appName) {
|
|
121
|
+
baseContent = await generateEnvContent(appName, null, 'local', false);
|
|
122
|
+
baseContent = substituteMntDataForLocal(baseContent, outputPath);
|
|
123
|
+
} else {
|
|
124
|
+
baseContent = runContent;
|
|
114
125
|
}
|
|
126
|
+
const toWrite = mergeEnvMapIntoContent(baseContent, runMap, { appendMissingFromNewMap: false });
|
|
115
127
|
await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
|
|
116
|
-
logger.log(
|
|
128
|
+
logger.log(
|
|
129
|
+
formatSuccessLine(
|
|
130
|
+
`Wrote .env to envOutputPath (--reload: merged container env; no extra keys): ${outputPath}`
|
|
131
|
+
)
|
|
132
|
+
);
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
/**
|
|
@@ -245,6 +261,30 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
245
261
|
return envContent;
|
|
246
262
|
}
|
|
247
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Copy the just-written builder `<appPath>/.env` to `build.envOutputPath` so both files
|
|
266
|
+
* share the same resolution pass (no second `generateEnvContent` with a different environment).
|
|
267
|
+
* Applies {@link substituteMntDataForLocal} for host paths; merges into an existing output file
|
|
268
|
+
* when present (preserves comments).
|
|
269
|
+
*
|
|
270
|
+
* @async
|
|
271
|
+
* @param {string} envPath - Path to `<appPath>/.env` (already written)
|
|
272
|
+
* @param {string} outputPath - Resolved `build.envOutputPath`
|
|
273
|
+
* @param {string} envOutputPathLabel - Label for log (e.g. raw `build.envOutputPath` value)
|
|
274
|
+
*/
|
|
275
|
+
async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel) {
|
|
276
|
+
const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
277
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
278
|
+
const synced = substituteMntDataForLocal(raw, outputPath);
|
|
279
|
+
let toWrite = synced;
|
|
280
|
+
if (fs.existsSync(outputPath)) {
|
|
281
|
+
const existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
282
|
+
toWrite = mergeEnvMapIntoContent(existingContent, parseEnvContentToMap(synced));
|
|
283
|
+
}
|
|
284
|
+
fs.writeFileSync(outputPath, toWrite, { mode: 0o600 });
|
|
285
|
+
logger.log(formatSuccessLine(`Copied resolved .env to envOutputPath (${envOutputPathLabel})`));
|
|
286
|
+
}
|
|
287
|
+
|
|
248
288
|
/**
|
|
249
289
|
* Write regenerated local .env to output path (merge with existing if present).
|
|
250
290
|
* @async
|
|
@@ -252,10 +292,12 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
252
292
|
* @param {string} appName - Application name
|
|
253
293
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
254
294
|
* @param {string} envOutputPathLabel - Label for log message (e.g. variables.build.envOutputPath)
|
|
295
|
+
* @param {Object} [extraOpts] - Optional: `appPath` for {@link module:lib/core/secrets-env-content.generateEnvContent}
|
|
255
296
|
*/
|
|
256
|
-
async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel) {
|
|
297
|
+
async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel, extraOpts = {}) {
|
|
257
298
|
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
258
|
-
|
|
299
|
+
const genOpts = extraOpts.appPath ? { appPath: extraOpts.appPath } : {};
|
|
300
|
+
let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
|
|
259
301
|
localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
|
|
260
302
|
let toWrite = localEnvContent;
|
|
261
303
|
if (fs.existsSync(outputPath)) {
|
|
@@ -284,16 +326,45 @@ async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOu
|
|
|
284
326
|
}
|
|
285
327
|
|
|
286
328
|
/**
|
|
287
|
-
* Process and optionally copy env file to envOutputPath if configured
|
|
288
|
-
*
|
|
329
|
+
* Process and optionally copy env file to envOutputPath if configured.
|
|
330
|
+
* When `appName` is set and `preferLocalEnvOutputPath` is true, regenerates **local**-flavored env at
|
|
331
|
+
* `build.envOutputPath` (IDE/host). **False** only when `remote-server` and `applications.<app>.reload` are both set
|
|
332
|
+
* (docker flavor at env output). Otherwise, when `appName`
|
|
333
|
+
* is set and `envPath` exists, copies the same resolved `<appPath>/.env` content to the output path.
|
|
334
|
+
* Falls back to local regeneration when `envPath` is missing, or patched copy when `appName` is omitted.
|
|
335
|
+
*
|
|
289
336
|
* @async
|
|
290
337
|
* @function processEnvVariables
|
|
291
338
|
* @param {string} envPath - Path to generated .env file
|
|
292
339
|
* @param {string} variablesPath - Path to application config
|
|
293
340
|
* @param {string} appName - Application name (for regenerating with local env)
|
|
294
341
|
* @param {string} [secretsPath] - Path to secrets file (optional, for regenerating)
|
|
342
|
+
* @param {Object} [copyOptions] - Optional: `preferLocalEnvOutputPath` (IDE/host file gets **local** flavor
|
|
343
|
+
* while `<appPath>/.env` may be docker); `appPath` overrides app root for local regeneration
|
|
295
344
|
*/
|
|
296
|
-
async function
|
|
345
|
+
async function copyOrRegenerateEnvForNamedApp(opts) {
|
|
346
|
+
const {
|
|
347
|
+
envPath,
|
|
348
|
+
outputPath,
|
|
349
|
+
appName,
|
|
350
|
+
secretsPath,
|
|
351
|
+
label,
|
|
352
|
+
preferLocal,
|
|
353
|
+
appPathForLocal
|
|
354
|
+
} = opts;
|
|
355
|
+
const localOpts = { appPath: appPathForLocal || undefined };
|
|
356
|
+
if (preferLocal) {
|
|
357
|
+
await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (fs.existsSync(envPath)) {
|
|
361
|
+
await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function processEnvVariables(envPath, variablesPath, appName, secretsPath, copyOptions = {}) {
|
|
297
368
|
if (!variablesPath || !fs.existsSync(variablesPath)) {
|
|
298
369
|
return;
|
|
299
370
|
}
|
|
@@ -310,8 +381,21 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
|
|
|
310
381
|
}
|
|
311
382
|
|
|
312
383
|
const label = variables.build.envOutputPath;
|
|
384
|
+
const appPathForLocal =
|
|
385
|
+
(copyOptions && copyOptions.appPath) ||
|
|
386
|
+
(variablesPath ? path.dirname(variablesPath) : null);
|
|
387
|
+
const preferLocal =
|
|
388
|
+
copyOptions && typeof copyOptions === 'object' && copyOptions.preferLocalEnvOutputPath === true;
|
|
313
389
|
if (appName) {
|
|
314
|
-
await
|
|
390
|
+
await copyOrRegenerateEnvForNamedApp({
|
|
391
|
+
envPath,
|
|
392
|
+
outputPath,
|
|
393
|
+
appName,
|
|
394
|
+
secretsPath,
|
|
395
|
+
label,
|
|
396
|
+
preferLocal,
|
|
397
|
+
appPathForLocal
|
|
398
|
+
});
|
|
315
399
|
} else {
|
|
316
400
|
await writePatchedEnvToOutputPath(envPath, outputPath, variables, label);
|
|
317
401
|
}
|
|
@@ -321,6 +405,7 @@ module.exports = {
|
|
|
321
405
|
processEnvVariables,
|
|
322
406
|
resolveEnvOutputPath,
|
|
323
407
|
substituteMntDataForLocal,
|
|
408
|
+
syncWrittenBuilderEnvToOutputPath,
|
|
324
409
|
writeEnvOutputForReload,
|
|
325
410
|
writeEnvOutputForLocal
|
|
326
411
|
};
|
|
@@ -13,6 +13,7 @@ const fsSync = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const chalk = require('chalk');
|
|
15
15
|
const logger = require('../utils/logger');
|
|
16
|
+
const pathsUtil = require('./paths');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Updates env.template to add MISO_CLIENTID, MISO_CLIENTSECRET, and optionally MISO_CONTROLLER_URL.
|
|
@@ -105,7 +106,10 @@ ${missingEntries.join('\n')}
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, _controllerUrl) {
|
|
108
|
-
|
|
109
|
+
// Use paths.getBuilderPath so the system builder root (e.g. ~/.aifabrix/builder/<app>) and
|
|
110
|
+
// AIFABRIX_BUILDER_DIR are respected. Falling back to process.cwd() previously made the binary
|
|
111
|
+
// CLI skip env.template updates whenever cwd was a non-builder repo (e.g. aifabrix-training).
|
|
112
|
+
const envTemplatePath = path.join(pathsUtil.getBuilderPath(appKey), 'env.template');
|
|
109
113
|
|
|
110
114
|
if (!fsSync.existsSync(envTemplatePath)) {
|
|
111
115
|
logger.warn(chalk.yellow(`⚠ env.template not found for ${appKey}, skipping update`));
|
|
@@ -62,19 +62,9 @@ function frontDoorPattern(appConfig) {
|
|
|
62
62
|
return (typeof p === 'string' && p.trim()) ? p.trim() : null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
* Compute the Traefik front-door health check URL when applicable.
|
|
67
|
-
*
|
|
68
|
-
* Returns null when Traefik/frontDoorRouting isn't active or cannot be resolved.
|
|
69
|
-
*
|
|
70
|
-
* @async
|
|
71
|
-
* @param {string} appName
|
|
72
|
-
* @param {number} healthCheckPort
|
|
73
|
-
* @param {Object|null} appConfig
|
|
74
|
-
* @returns {Promise<string|null>}
|
|
75
|
-
*/
|
|
76
65
|
/**
|
|
77
66
|
* Public app URL (Traefik + frontDoor mount path), without the health path — for CLI summaries.
|
|
67
|
+
* Requires infra Traefik on, `frontDoorRouting.enabled`, host, and pattern.
|
|
78
68
|
*
|
|
79
69
|
* @async
|
|
80
70
|
* @param {string} appName
|
|
@@ -83,7 +73,7 @@ function frontDoorPattern(appConfig) {
|
|
|
83
73
|
* @returns {Promise<string|null>}
|
|
84
74
|
*/
|
|
85
75
|
async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig) {
|
|
86
|
-
if (!frontDoorEnabled(appConfig)) return null;
|
|
76
|
+
if (!appConfig || !frontDoorEnabled(appConfig)) return null;
|
|
87
77
|
const pattern = frontDoorPattern(appConfig);
|
|
88
78
|
if (!pattern) return null;
|
|
89
79
|
|
|
@@ -91,6 +81,13 @@ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig)
|
|
|
91
81
|
const userCfg = await coreConfig.getConfig();
|
|
92
82
|
if (!(userCfg && userCfg.traefik)) return null;
|
|
93
83
|
|
|
84
|
+
const fd = appConfig.frontDoorRouting;
|
|
85
|
+
if (!fd || typeof fd !== 'object') return null;
|
|
86
|
+
const hostTemplate = typeof fd.host === 'string' ? fd.host.trim() : '';
|
|
87
|
+
if (!hostTemplate) return null;
|
|
88
|
+
|
|
89
|
+
const mountPath = normalizeFrontDoorPatternForHealth(pattern);
|
|
90
|
+
|
|
94
91
|
const infraTlsEnabled = Boolean(userCfg && userCfg.tlsEnabled);
|
|
95
92
|
const remoteServer = await coreConfig.getRemoteServer();
|
|
96
93
|
const developerIdRaw = await coreConfig.getDeveloperId();
|
|
@@ -98,7 +95,6 @@ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig)
|
|
|
98
95
|
|
|
99
96
|
// URLs are resolved for the CLI on the host, not from inside a container.
|
|
100
97
|
const profile = 'local';
|
|
101
|
-
const fd = appConfig.frontDoorRouting;
|
|
102
98
|
const listenPort = Number(appConfig?.port || 3000);
|
|
103
99
|
|
|
104
100
|
const publicBase = computePublicUrlBaseString({
|
|
@@ -114,10 +110,18 @@ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig)
|
|
|
114
110
|
infraTlsEnabled
|
|
115
111
|
});
|
|
116
112
|
|
|
117
|
-
const mountPath = normalizeFrontDoorPatternForHealth(pattern);
|
|
118
113
|
return joinUrlPath(publicBase, mountPath);
|
|
119
114
|
}
|
|
120
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Traefik public URL including health path.
|
|
118
|
+
*
|
|
119
|
+
* @async
|
|
120
|
+
* @param {string} appName
|
|
121
|
+
* @param {number} healthCheckPort
|
|
122
|
+
* @param {Object|null} appConfig
|
|
123
|
+
* @returns {Promise<string|null>}
|
|
124
|
+
*/
|
|
121
125
|
async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig) {
|
|
122
126
|
const baseWithFrontDoor = await computeTraefikPublicAppUrl(appName, healthCheckPort, appConfig);
|
|
123
127
|
if (!baseWithFrontDoor) return null;
|
|
@@ -131,4 +135,3 @@ module.exports = {
|
|
|
131
135
|
computeTraefikPublicAppUrl,
|
|
132
136
|
computeTraefikHealthCheckUrl
|
|
133
137
|
};
|
|
134
|
-
|
|
@@ -16,7 +16,8 @@ const dns = require('dns');
|
|
|
16
16
|
const chalk = require('chalk');
|
|
17
17
|
const logger = require('./logger');
|
|
18
18
|
const { execWithDockerEnv } = require('./docker-exec');
|
|
19
|
-
|
|
19
|
+
/** Namespace require so tests can `jest.spyOn` on `./health-check-url` exports. */
|
|
20
|
+
const healthCheckUrl = require('./health-check-url');
|
|
20
21
|
const { computePathActive } = require('./url-declarative-url-flags');
|
|
21
22
|
const { isFrontDoorRoutingEnabledInDoc } = require('./url-declarative-vdir-inactive-env');
|
|
22
23
|
const { waitForDbInit } = require('./health-check-db-init');
|
|
@@ -69,10 +70,11 @@ async function computeHealthCheckUrl(appName, healthCheckPort, appConfig, _opts
|
|
|
69
70
|
const runOptions = (_opts && typeof _opts === 'object') ? _opts.runOptions : null;
|
|
70
71
|
const wantsTraefik =
|
|
71
72
|
Boolean(runOptions) &&
|
|
72
|
-
(runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true)
|
|
73
|
+
(runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true) &&
|
|
74
|
+
isFrontDoorRoutingEnabledInDoc(appConfig);
|
|
73
75
|
if (!wantsTraefik) return '';
|
|
74
76
|
try {
|
|
75
|
-
return await computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig);
|
|
77
|
+
return await healthCheckUrl.computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig);
|
|
76
78
|
} catch {
|
|
77
79
|
return '';
|
|
78
80
|
}
|
|
@@ -380,10 +382,10 @@ async function computePreferredHealthCheckUrls(appName, healthCheckPort, config,
|
|
|
380
382
|
let skippedPublicHealthUrl = '';
|
|
381
383
|
const wantsTraefikFirst = Boolean(
|
|
382
384
|
runOptions && (runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true)
|
|
383
|
-
);
|
|
385
|
+
) && isFrontDoorRoutingEnabledInDoc(config);
|
|
384
386
|
if (wantsTraefikFirst) {
|
|
385
387
|
try {
|
|
386
|
-
traefikUrl = await computeTraefikHealthCheckUrl(appName, healthCheckPort, config);
|
|
388
|
+
traefikUrl = await healthCheckUrl.computeTraefikHealthCheckUrl(appName, healthCheckPort, config);
|
|
387
389
|
} catch {
|
|
388
390
|
traefikUrl = '';
|
|
389
391
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional infra compose flags (Traefik, pgAdmin, Redis Commander) for config.yaml ↔ startInfra.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Effective flag resolution + backfill missing keys after up-infra / setup
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves effective boolean from CLI option vs config.
|
|
15
|
+
* @param {*} optValue - options.traefik | options.pgAdmin | options.redisAdmin
|
|
16
|
+
* @param {*} cfgValue - cfg.traefik | cfg.pgadmin | cfg.redisCommander
|
|
17
|
+
* @param {boolean} defaultWhenUndef - Default when config value is undefined
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function resolveInfraOptionalFlag(optValue, cfgValue, defaultWhenUndef = true) {
|
|
21
|
+
if (optValue === true) return true;
|
|
22
|
+
if (optValue === false) return false;
|
|
23
|
+
return cfgValue !== false && (cfgValue === true || defaultWhenUndef);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Effective optional infra service flags (same resolution as up-infra → startInfra).
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} cfg - Config from {@link config.getConfig}
|
|
30
|
+
* @param {Object} [options] - Commander options (omit for setup-modes)
|
|
31
|
+
* @returns {{ traefik: boolean, pgadmin: boolean, redisCommander: boolean }}
|
|
32
|
+
*/
|
|
33
|
+
function computeEffectiveInfraOptionalFlags(cfg, options = {}) {
|
|
34
|
+
return {
|
|
35
|
+
traefik: resolveInfraOptionalFlag(options.traefik, cfg.traefik, false),
|
|
36
|
+
pgadmin: resolveInfraOptionalFlag(options.pgAdmin, cfg.pgadmin, true),
|
|
37
|
+
redisCommander: resolveInfraOptionalFlag(options.redisAdmin, cfg.redisCommander, true)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Writes `traefik` / `pgadmin` / `redisCommander` when missing so config matches compose defaults.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} cfg - Mutable config (updated when save runs)
|
|
45
|
+
* @param {{ traefik: boolean, pgadmin: boolean, redisCommander: boolean }} effective
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
async function persistMissingInfraOptionalServiceFlags(cfg, effective) {
|
|
49
|
+
const keys = ['traefik', 'pgadmin', 'redisCommander'];
|
|
50
|
+
const merged = { ...cfg };
|
|
51
|
+
let dirty = false;
|
|
52
|
+
for (const k of keys) {
|
|
53
|
+
if (typeof merged[k] === 'undefined') {
|
|
54
|
+
merged[k] = effective[k];
|
|
55
|
+
dirty = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!dirty) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await config.saveConfig(merged);
|
|
62
|
+
Object.assign(cfg, merged);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
resolveInfraOptionalFlag,
|
|
67
|
+
computeEffectiveInfraOptionalFlags,
|
|
68
|
+
persistMissingInfraOptionalServiceFlags
|
|
69
|
+
};
|