@aifabrix/builder 2.44.5 → 2.44.6

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.
Files changed (207) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +48 -2
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/validation-runner.js +46 -25
  17. package/lib/app/deploy-config.js +11 -1
  18. package/lib/app/deploy-status-display.js +3 -3
  19. package/lib/app/deploy.js +36 -14
  20. package/lib/app/display.js +15 -11
  21. package/lib/app/push.js +46 -23
  22. package/lib/app/register.js +1 -1
  23. package/lib/app/restart-display.js +95 -0
  24. package/lib/app/rotate-secret.js +1 -1
  25. package/lib/app/run-container-start.js +12 -6
  26. package/lib/app/run-env-compose.js +30 -1
  27. package/lib/app/run-helpers.js +44 -12
  28. package/lib/app/run-reload-sync.js +148 -0
  29. package/lib/app/run-resolve-image.js +51 -1
  30. package/lib/app/run.js +99 -73
  31. package/lib/build/index.js +75 -45
  32. package/lib/cli/doctor-check.js +117 -0
  33. package/lib/cli/index.js +8 -2
  34. package/lib/cli/infra-guided.js +445 -0
  35. package/lib/cli/setup-app.js +20 -2
  36. package/lib/cli/setup-auth.js +26 -0
  37. package/lib/cli/setup-dev-path-commands.js +50 -3
  38. package/lib/cli/setup-infra.js +134 -61
  39. package/lib/cli/setup-integration-client.js +182 -0
  40. package/lib/cli/setup-parameters.js +21 -2
  41. package/lib/cli/setup-platform.js +102 -0
  42. package/lib/cli/setup-secrets.js +18 -6
  43. package/lib/cli/setup-utility.js +78 -33
  44. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  45. package/lib/commands/datasource-capability-output.js +29 -0
  46. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  47. package/lib/commands/datasource-capability.js +411 -0
  48. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  49. package/lib/commands/datasource.js +53 -13
  50. package/lib/commands/dev-down.js +3 -3
  51. package/lib/commands/dev-infra-gate.js +32 -0
  52. package/lib/commands/dev-init.js +13 -7
  53. package/lib/commands/dimension-value.js +179 -0
  54. package/lib/commands/dimension.js +330 -0
  55. package/lib/commands/integration-client.js +430 -0
  56. package/lib/commands/login-device.js +65 -30
  57. package/lib/commands/login.js +21 -10
  58. package/lib/commands/parameters-validate.js +78 -13
  59. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  60. package/lib/commands/repair-datasource-keys.js +10 -5
  61. package/lib/commands/repair-datasource.js +19 -7
  62. package/lib/commands/repair-env-template.js +4 -1
  63. package/lib/commands/repair-openapi-sync.js +172 -0
  64. package/lib/commands/repair-persist.js +102 -0
  65. package/lib/commands/repair-rbac-extract.js +27 -0
  66. package/lib/commands/repair-rbac-migrate.js +186 -0
  67. package/lib/commands/repair-rbac.js +214 -31
  68. package/lib/commands/repair-system-alignment.js +246 -0
  69. package/lib/commands/repair-system-permissions.js +168 -0
  70. package/lib/commands/repair.js +120 -338
  71. package/lib/commands/secure.js +1 -1
  72. package/lib/commands/setup-modes.js +455 -0
  73. package/lib/commands/setup-prompts.js +388 -0
  74. package/lib/commands/setup.js +149 -0
  75. package/lib/commands/teardown.js +228 -0
  76. package/lib/commands/up-common.js +79 -19
  77. package/lib/commands/up-dataplane.js +33 -11
  78. package/lib/commands/up-miso.js +7 -11
  79. package/lib/commands/upload.js +109 -23
  80. package/lib/commands/wizard-core-helpers.js +14 -11
  81. package/lib/commands/wizard-core.js +6 -5
  82. package/lib/commands/wizard-dataplane.js +2 -2
  83. package/lib/commands/wizard-entity-selection.js +4 -3
  84. package/lib/commands/wizard-headless.js +2 -1
  85. package/lib/commands/wizard.js +2 -1
  86. package/lib/constants/infra-compose-service-names.js +40 -0
  87. package/lib/core/env-reader.js +16 -3
  88. package/lib/core/secrets-admin-env.js +101 -0
  89. package/lib/core/secrets-ensure-infra.js +34 -1
  90. package/lib/core/secrets-ensure.js +88 -66
  91. package/lib/core/secrets-env-content.js +432 -0
  92. package/lib/core/secrets-env-write.js +27 -1
  93. package/lib/core/secrets-load.js +248 -0
  94. package/lib/core/secrets-names.js +32 -0
  95. package/lib/core/secrets.js +17 -757
  96. package/lib/datasource/capability/basic-exposure.js +76 -0
  97. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  98. package/lib/datasource/capability/capability-key.js +34 -0
  99. package/lib/datasource/capability/capability-resolve.js +172 -0
  100. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  101. package/lib/datasource/capability/copy-operations.js +348 -0
  102. package/lib/datasource/capability/copy-test-payload.js +139 -0
  103. package/lib/datasource/capability/create-operations.js +235 -0
  104. package/lib/datasource/capability/dimension-operations.js +151 -0
  105. package/lib/datasource/capability/dimension-validate.js +219 -0
  106. package/lib/datasource/capability/json-pointer.js +31 -0
  107. package/lib/datasource/capability/reference-rewrite.js +51 -0
  108. package/lib/datasource/capability/relate-operations.js +325 -0
  109. package/lib/datasource/capability/relate-validate.js +219 -0
  110. package/lib/datasource/capability/remove-operations.js +275 -0
  111. package/lib/datasource/capability/run-capability-copy.js +152 -0
  112. package/lib/datasource/capability/run-capability-diff.js +135 -0
  113. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  114. package/lib/datasource/capability/run-capability-edit.js +377 -0
  115. package/lib/datasource/capability/run-capability-relate.js +193 -0
  116. package/lib/datasource/capability/run-capability-remove.js +105 -0
  117. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  118. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  119. package/lib/datasource/list.js +136 -23
  120. package/lib/datasource/log-viewer.js +2 -4
  121. package/lib/datasource/unified-validation-run.js +51 -16
  122. package/lib/datasource/validate.js +53 -1
  123. package/lib/deployment/deploy-poll-ui.js +60 -0
  124. package/lib/deployment/deployer-status.js +29 -3
  125. package/lib/deployment/deployer.js +48 -30
  126. package/lib/deployment/environment.js +7 -2
  127. package/lib/deployment/poll-interval.js +72 -0
  128. package/lib/deployment/push.js +11 -9
  129. package/lib/external-system/deploy.js +4 -1
  130. package/lib/external-system/download.js +61 -32
  131. package/lib/external-system/sync-deploy-manifest.js +33 -0
  132. package/lib/infrastructure/index.js +49 -19
  133. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  134. package/lib/parameters/infra-kv-discovery.js +29 -4
  135. package/lib/parameters/infra-parameter-catalog.js +6 -3
  136. package/lib/parameters/infra-parameter-validate.js +67 -19
  137. package/lib/resolvers/datasource-resolver.js +53 -0
  138. package/lib/resolvers/dimension-file.js +52 -0
  139. package/lib/resolvers/manifest-resolver.js +133 -0
  140. package/lib/schema/external-datasource.schema.json +183 -53
  141. package/lib/schema/external-system.schema.json +23 -10
  142. package/lib/schema/infra.parameter.yaml +26 -11
  143. package/lib/schema/wizard-config.schema.json +1 -1
  144. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  145. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  146. package/lib/utils/app-run-containers.js +2 -2
  147. package/lib/utils/bash-secret-env.js +59 -0
  148. package/lib/utils/cli-secrets-error-format.js +78 -0
  149. package/lib/utils/cli-test-layout-chalk.js +31 -9
  150. package/lib/utils/cli-utils.js +4 -36
  151. package/lib/utils/datasource-test-run-display.js +8 -0
  152. package/lib/utils/dev-hosts-helper.js +3 -2
  153. package/lib/utils/dev-init-ssh-merge.js +2 -1
  154. package/lib/utils/docker-build.js +17 -9
  155. package/lib/utils/docker-reload-mount.js +127 -0
  156. package/lib/utils/external-readme.js +71 -2
  157. package/lib/utils/external-system-local-test-tty.js +3 -2
  158. package/lib/utils/external-system-readiness-core.js +45 -12
  159. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  160. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  161. package/lib/utils/external-system-readiness-display.js +10 -1
  162. package/lib/utils/file-upload.js +40 -3
  163. package/lib/utils/health-check-db-init.js +107 -0
  164. package/lib/utils/health-check-public-warn.js +69 -0
  165. package/lib/utils/health-check-url.js +19 -4
  166. package/lib/utils/health-check.js +135 -105
  167. package/lib/utils/help-builder.js +5 -1
  168. package/lib/utils/image-name.js +34 -7
  169. package/lib/utils/integration-file-backup.js +74 -0
  170. package/lib/utils/mutagen-install.js +30 -3
  171. package/lib/utils/paths.js +108 -25
  172. package/lib/utils/postgres-wipe.js +212 -0
  173. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  174. package/lib/utils/remote-dev-auth.js +21 -5
  175. package/lib/utils/remote-docker-env.js +9 -1
  176. package/lib/utils/remote-secrets-loader.js +42 -3
  177. package/lib/utils/resolve-docker-image-ref.js +9 -3
  178. package/lib/utils/secrets-ancestor-paths.js +47 -0
  179. package/lib/utils/secrets-helpers.js +17 -10
  180. package/lib/utils/secrets-kv-refs.js +42 -0
  181. package/lib/utils/secrets-kv-scope.js +19 -2
  182. package/lib/utils/secrets-materialize-local.js +134 -0
  183. package/lib/utils/secrets-path.js +24 -10
  184. package/lib/utils/secrets-utils.js +2 -2
  185. package/lib/utils/system-builder-root.js +34 -0
  186. package/lib/utils/url-declarative-resolve-build.js +6 -1
  187. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  188. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  189. package/lib/utils/urls-local-registry.js +23 -12
  190. package/lib/utils/validation-poll-ui.js +81 -0
  191. package/lib/utils/validation-run-poll.js +29 -5
  192. package/lib/utils/with-muted-logger.js +53 -0
  193. package/package.json +1 -1
  194. package/templates/applications/dataplane/application.yaml +1 -1
  195. package/templates/applications/dataplane/rbac.yaml +10 -10
  196. package/templates/applications/keycloak/env.template +8 -6
  197. package/templates/applications/miso-controller/application.yaml +7 -0
  198. package/templates/applications/miso-controller/env.template +1 -1
  199. package/templates/applications/miso-controller/rbac.yaml +9 -9
  200. package/templates/external-system/README.md.hbs +83 -123
  201. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  202. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  203. package/.nyc_output/processinfo/index.json +0 -1
  204. package/lib/api/service-users.api.js +0 -150
  205. package/lib/api/types/service-users.types.js +0 -65
  206. package/lib/cli/setup-service-user.js +0 -187
  207. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,432 @@
1
+ /**
2
+ * kv:// resolution, declarative URL expansion, and .env generation (content + file write).
3
+ *
4
+ * @fileoverview Split from secrets.js for module size limits
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
+ const config = require('./config');
15
+ const {
16
+ interpolateEnvVars,
17
+ collectMissingSecrets,
18
+ formatMissingSecretsFileInfo,
19
+ replaceKvInContent,
20
+ loadEnvTemplate,
21
+ adjustLocalEnvPortsInContent,
22
+ rewriteInfraEndpoints
23
+ } = require('../utils/secrets-helpers');
24
+ const { buildEnvVarMap } = require('../utils/env-map');
25
+ const { resolveServicePortsInEnvContent } = require('../utils/secrets-url');
26
+ const { materializeResolvedKvSecretsToUserLocal } = require('../utils/secrets-materialize-local');
27
+ const {
28
+ updatePortForDocker,
29
+ getBaseDockerEnv,
30
+ applyDockerEnvOverride,
31
+ getContainerPortFromDockerEnv
32
+ } = require('./secrets-docker-env');
33
+ const { getContainerPortFromPath, loadVariablesFromPath } = require('../utils/port-resolver');
34
+ const secretsEnsure = require('./secrets-ensure');
35
+ const { resolveSecretsPath, getActualSecretsPath } = require('../utils/secrets-path');
36
+ const pathsUtil = require('../utils/paths');
37
+ const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
38
+ const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv } = require('../utils/environment-scoped-resources');
39
+ const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
40
+ const { expandDeclarativeUrlsInEnvContent } = require('../utils/url-declarative-resolve');
41
+ const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
42
+ const {
43
+ mergeDockerManifestPublishedPort,
44
+ rewriteDockerManifestPublicPortEnvLine
45
+ } = require('../utils/docker-manifest-public-port');
46
+ const { loadSecrets } = require('./secrets-load');
47
+
48
+ /**
49
+ * Resolves kv:// references in environment template
50
+ * Replaces kv://keyName with actual values from secrets
51
+ *
52
+ * @async
53
+ * @param {string} envTemplate - Environment template content
54
+ * @param {Object} secrets - Secrets object from loadSecrets()
55
+ * @param {string} [environment='local'] - Environment context (docker/local)
56
+ * @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
57
+ * @param {string} [appName] - Application name (optional, for error messages)
58
+ * @returns {Promise<string>} Resolved environment content
59
+ * @throws {Error} If kv:// reference cannot be resolved
60
+ */
61
+ async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null, scopedKv = null) {
62
+ const os = require('os');
63
+
64
+ let developerId = null;
65
+ try {
66
+ developerId = await config.getDeveloperId();
67
+ } catch {
68
+ /* ignore */
69
+ }
70
+
71
+ const envKey = String(environment || 'local').toLowerCase();
72
+ const mapContext = envKey === 'docker' || envKey === 'local' ? envKey : 'local';
73
+
74
+ let envVars = await buildEnvVarMap(mapContext, os, developerId);
75
+ if (!envVars || Object.keys(envVars).length === 0) {
76
+ envVars = await buildEnvVarMap('local', os, developerId);
77
+ }
78
+ const resolved = interpolateEnvVars(envTemplate, envVars);
79
+ const missing = collectMissingSecrets(resolved, secrets, scopedKv);
80
+ if (missing.length > 0) {
81
+ const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
82
+ const resolveCommand = appName ? `aifabrix resolve ${appName}` : 'aifabrix resolve <app-name>';
83
+ throw new Error(`Missing secrets: ${missing.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
84
+ }
85
+ return replaceKvInContent(resolved, secrets, envVars, scopedKv);
86
+ }
87
+
88
+ /**
89
+ * Resolve run env key and effective env-scoped kv/redis behavior for generateEnvContent.
90
+ *
91
+ * @async
92
+ * @param {string} appPath - Builder application directory
93
+ * @param {Object} [options] - generateEnvContent options; may set runEnvKey
94
+ * @returns {Promise<{ runEnvKey: string, effective: boolean }>}
95
+ */
96
+ async function buildScopedKvContext(appPath, options = {}) {
97
+ let runEnvKey;
98
+ if (options.runEnvKey !== undefined && options.runEnvKey !== null) {
99
+ runEnvKey = String(options.runEnvKey).toLowerCase();
100
+ } else if (typeof config.getCurrentEnvironment === 'function') {
101
+ runEnvKey = String((await config.getCurrentEnvironment()) || 'dev').toLowerCase();
102
+ } else {
103
+ runEnvKey = 'dev';
104
+ }
105
+ const userCfg =
106
+ typeof config.getConfig === 'function'
107
+ ? await config.getConfig()
108
+ : { useEnvironmentScopedResources: false };
109
+ const useGate = Boolean(userCfg.useEnvironmentScopedResources);
110
+ const appFlag = readAppEnvironmentScopedFlagForAppPath(appPath);
111
+ const effective = computeEffectiveEnvironmentScopedResources(useGate, appFlag, runEnvKey);
112
+ return { runEnvKey, effective };
113
+ }
114
+
115
+ /**
116
+ * Redis/DB service endpoints for docker env interpolation.
117
+ * @returns {Promise<{ redisHost: string, redisPort: number, dbHost: string, dbPort: number }>}
118
+ */
119
+ async function getDockerRedisDbEndpoints() {
120
+ const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('../utils/env-endpoints');
121
+ const hosts = await getEnvHosts('docker');
122
+ const localhostOverride = getLocalhostOverride('docker');
123
+ const redisHost = getServiceHost(hosts.REDIS_HOST, 'docker', 'redis', localhostOverride);
124
+ const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
125
+ const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
126
+ const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
127
+ return { redisHost, redisPort, dbHost, dbPort };
128
+ }
129
+
130
+ /**
131
+ * Config inputs for declarative url:// expansion (keeps expandDeclarativeUrlsIfPresent small).
132
+ * @param {string} appPath
133
+ * @returns {Promise<Object>}
134
+ */
135
+ async function loadDeclarativeUrlExpandInputs(appPath) {
136
+ const userCfg = await config.getConfig();
137
+ let remoteServer = null;
138
+ try {
139
+ const rs = await config.getRemoteServer();
140
+ if (rs && String(rs).trim()) {
141
+ remoteServer = String(rs).trim();
142
+ }
143
+ } catch {
144
+ remoteServer = null;
145
+ }
146
+ let developerIdRaw = null;
147
+ try {
148
+ developerIdRaw = await config.getDeveloperId();
149
+ } catch {
150
+ developerIdRaw = null;
151
+ }
152
+ let infraTlsEnabled = false;
153
+ try {
154
+ infraTlsEnabled = await config.getTlsEnabled();
155
+ } catch {
156
+ infraTlsEnabled = false;
157
+ }
158
+ return {
159
+ userCfg,
160
+ remoteServer,
161
+ developerIdRaw,
162
+ infraTlsEnabled,
163
+ appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
164
+ };
165
+ }
166
+
167
+ /**
168
+ * After kv:// resolution, expand url:// references when application config exists.
169
+ * @param {string} resolved
170
+ * @param {string} appName
171
+ * @param {string} appPath
172
+ * @param {string|null} variablesPath
173
+ * @param {string} environment
174
+ * @param {boolean} envOnly
175
+ * @returns {Promise<string>}
176
+ */
177
+ async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variablesPath, environment, envOnly) {
178
+ if (envOnly || !variablesPath) {
179
+ return resolved;
180
+ }
181
+ const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
182
+ await loadDeclarativeUrlExpandInputs(appPath);
183
+ resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
184
+ if (!resolved.includes('url://')) {
185
+ return resolved;
186
+ }
187
+ return expandDeclarativeUrlsInEnvContent(resolved, {
188
+ profile: environment === 'docker' ? 'docker' : 'local',
189
+ currentAppKey: appName,
190
+ variablesPath,
191
+ useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
192
+ appEnvironmentScopedResources: appScopedFlag,
193
+ remoteServer,
194
+ developerIdRaw,
195
+ traefik: Boolean(userCfg.traefik),
196
+ infraTlsEnabled
197
+ });
198
+ }
199
+
200
+ /** Docker env transformations: ports, infra endpoints, PORT. */
201
+ async function applyDockerTransformations(resolved, variablesPath) {
202
+ resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
203
+ resolved = await rewriteInfraEndpoints(resolved, 'docker');
204
+ const { redisHost, redisPort, dbHost, dbPort } = await getDockerRedisDbEndpoints();
205
+ let dockerEnv = await getBaseDockerEnv();
206
+ dockerEnv = applyDockerEnvOverride(dockerEnv);
207
+ const containerPort = getContainerPortFromPath(variablesPath) ?? getContainerPortFromDockerEnv(dockerEnv) ?? 3000;
208
+ const envVars = await buildEnvVarMap('docker', null, null, { appPort: containerPort });
209
+ const appDoc = loadVariablesFromPath(variablesPath);
210
+ await mergeDockerManifestPublishedPort(envVars, appDoc);
211
+ envVars.REDIS_HOST = redisHost;
212
+ envVars.REDIS_PORT = String(redisPort);
213
+ envVars.DB_HOST = dbHost;
214
+ envVars.DB_PORT = String(dbPort);
215
+ envVars.PORT = String(containerPort);
216
+ resolved = interpolateEnvVars(resolved, envVars);
217
+ resolved = rewriteDockerManifestPublicPortEnvLine(resolved, envVars, appDoc);
218
+ return updatePortForDocker(resolved, variablesPath);
219
+ }
220
+
221
+ /** Environment-specific transformations to resolved content. */
222
+ async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
223
+ if (environment === 'docker') return applyDockerTransformations(resolved, variablesPath);
224
+ if (environment === 'local') return adjustLocalEnvPortsInContent(resolved, variablesPath);
225
+ return resolved;
226
+ }
227
+
228
+ /**
229
+ * Generate .env content from template and secrets (no disk write).
230
+ * When options.envOnly is true, variablesPath is null (no application config).
231
+ *
232
+ * @param {string} appName - Application name
233
+ * @param {string} [secretsPath] - Path to secrets file (optional)
234
+ * @param {string} [environment='local'] - Environment context
235
+ * @param {boolean} [force=false] - Generate missing secret keys
236
+ * @param {Object} [options] - Optional: appPath, envOnly (env-only mode uses only env.template); skipMaterializeKvToLocal skips persisting resolved kv to ~/.aifabrix/secrets.local.yaml. Materialization runs only when no explicit secretsPath is passed (default loadSecrets cascade uses ~/.aifabrix/secrets.local.yaml).
237
+ * @returns {Promise<string>} Resolved env content
238
+ */
239
+ async function generateEnvContent(appName, secretsPath, environment = 'local', force = false, options = {}) {
240
+ const appPath = (options && options.appPath) || pathsUtil.getBuilderPath(appName);
241
+ const templatePath = path.join(appPath, 'env.template');
242
+ const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
243
+ const template = loadEnvTemplate(templatePath);
244
+ const secretsPaths = await getActualSecretsPath(secretsPath, appName);
245
+ if (force) {
246
+ const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
247
+ await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
248
+ }
249
+ const secrets = await loadSecrets(secretsPath, appName);
250
+ const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
251
+ const scopedKv = { envKey: runEnvKey, effective };
252
+ let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, scopedKv);
253
+ if (!secretsPath) {
254
+ await materializeResolvedKvSecretsToUserLocal(template, secrets, scopedKv, options);
255
+ }
256
+ resolved = await expandDeclarativeUrlsIfPresent(
257
+ resolved,
258
+ appName,
259
+ appPath,
260
+ variablesPath,
261
+ environment,
262
+ Boolean(options.envOnly)
263
+ );
264
+ resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
265
+ if (effective) {
266
+ const idx = redisDbIndexForScopedRunEnv(runEnvKey);
267
+ resolved = applyRedisDbIndexToEnvContent(resolved, idx);
268
+ }
269
+
270
+ return resolved;
271
+ }
272
+
273
+ /**
274
+ * Parses .env file content into a key-to-value map.
275
+ * Only includes lines that look like KEY=value (first = separates key and value).
276
+ *
277
+ * @param {string} content - Raw .env file content
278
+ * @returns {Object.<string, string>} Map of variable name to value
279
+ */
280
+ function parseEnvContentToMap(content) {
281
+ if (!content || typeof content !== 'string') {
282
+ return {};
283
+ }
284
+ const map = {};
285
+ const lines = content.split(/\r?\n/);
286
+ for (const line of lines) {
287
+ const trimmed = line.trim();
288
+ if (!trimmed || trimmed.startsWith('#')) {
289
+ continue;
290
+ }
291
+ const eq = trimmed.indexOf('=');
292
+ if (eq > 0) {
293
+ const key = trimmed.substring(0, eq).trim();
294
+ const value = trimmed.substring(eq + 1);
295
+ map[key] = value;
296
+ }
297
+ }
298
+ return map;
299
+ }
300
+
301
+ /**
302
+ * Merges new .env content with existing .env: newly resolved content wins for keys it
303
+ * defines (so project secrets take effect when re-running). Keys only in existing .env
304
+ * are appended so manual additions are kept.
305
+ *
306
+ * @param {string} newContent - Newly generated .env content (from template + loadSecrets)
307
+ * @param {Object.<string, string>} existingMap - Existing key-to-value map from current .env
308
+ * @returns {string} Merged content: new values for keys in newContent, plus extra existing keys
309
+ */
310
+ function mergeEnvContentPreservingExisting(newContent, existingMap) {
311
+ const lines = newContent.split(/\r?\n/);
312
+ const newKeys = new Set();
313
+ const out = [];
314
+ for (const line of lines) {
315
+ const trimmed = line.trim();
316
+ if (!trimmed || trimmed.startsWith('#')) {
317
+ out.push(line);
318
+ continue;
319
+ }
320
+ const eq = trimmed.indexOf('=');
321
+ if (eq > 0) {
322
+ const key = trimmed.substring(0, eq).trim();
323
+ newKeys.add(key);
324
+ }
325
+ out.push(line);
326
+ }
327
+ if (existingMap && Object.keys(existingMap).length > 0) {
328
+ for (const key of Object.keys(existingMap)) {
329
+ if (!newKeys.has(key)) {
330
+ out.push(`${key}=${existingMap[key]}`);
331
+ }
332
+ }
333
+ }
334
+ return out.join('\n');
335
+ }
336
+
337
+ /**
338
+ * Merges a key-value map into existing .env file content, preserving comments and blank lines.
339
+ * For each KEY=value line in existing content, replaces value with newMap[KEY] when the key exists
340
+ * in newMap. Appends any keys from newMap that did not appear in the file.
341
+ *
342
+ * @param {string} existingContent - Full existing .env file content
343
+ * @param {Object.<string, string>} newMap - New key-to-value map (e.g. from resolved or run env)
344
+ * @returns {string} Merged content with comments preserved
345
+ */
346
+ function mergeEnvMapIntoContent(existingContent, newMap) {
347
+ if (!newMap || Object.keys(newMap).length === 0) {
348
+ return typeof existingContent === 'string' ? existingContent : '';
349
+ }
350
+ const lines = (existingContent || '').split(/\r?\n/);
351
+ const seen = new Set();
352
+ const out = [];
353
+ for (const line of lines) {
354
+ const trimmed = line.trim();
355
+ if (!trimmed || trimmed.startsWith('#')) {
356
+ out.push(line);
357
+ continue;
358
+ }
359
+ const eq = trimmed.indexOf('=');
360
+ if (eq > 0) {
361
+ const key = trimmed.substring(0, eq).trim();
362
+ seen.add(key);
363
+ out.push(Object.prototype.hasOwnProperty.call(newMap, key) ? `${key}=${newMap[key]}` : line);
364
+ continue;
365
+ }
366
+ out.push(line);
367
+ }
368
+ for (const key of Object.keys(newMap)) {
369
+ if (!seen.has(key)) out.push(`${key}=${newMap[key]}`);
370
+ }
371
+ return out.join('\n');
372
+ }
373
+
374
+ /**
375
+ * Resolves content to write for .env: merges with existing file when present.
376
+ * @param {string} resolved - Newly generated content
377
+ * @param {string} pathToPreserve - Path to existing .env to merge from (or null)
378
+ * @returns {string} Content to write
379
+ */
380
+ function resolveEnvContentToWrite(resolved, pathToPreserve) {
381
+ if (!pathToPreserve || !fs.existsSync(pathToPreserve)) return resolved;
382
+ const existingContent = fs.readFileSync(pathToPreserve, 'utf8');
383
+ const existingMap = parseEnvContentToMap(existingContent);
384
+ return mergeEnvContentPreservingExisting(resolved, existingMap);
385
+ }
386
+
387
+ /**
388
+ * Generates and writes .env file. Newly resolved values win over existing .env; extra vars in existing .env are kept.
389
+ * When options.envOnly is true, only env.template is used; .env is written to options.appPath.
390
+ * @async
391
+ * @param {string} appName - Name of the application
392
+ * @param {string} [secretsPath] - Path to secrets file (optional)
393
+ * @param {string} [environment='local'] - Environment context ('local' or 'docker')
394
+ * @param {boolean} [force=false] - Generate missing secret keys in secrets file
395
+ * @param {Object} [options] - Optional: appPath, envOnly, skipOutputPath, preserveFromPath
396
+ * @returns {Promise<string>} Path to generated .env file
397
+ */
398
+ async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, options = {}) {
399
+ const opts = options && typeof options === 'object' ? options : {};
400
+ const appPath = opts.appPath || pathsUtil.getBuilderPath(appName);
401
+ const envOnly = !!opts.envOnly;
402
+ const variablesPath = envOnly ? null : resolveApplicationConfigPath(appPath);
403
+ const envPath = path.join(appPath, '.env');
404
+
405
+ if (envOnly) {
406
+ const templatePath = path.join(appPath, 'env.template');
407
+ if (!fs.existsSync(templatePath)) {
408
+ throw new Error(`env.template not found at ${templatePath}. Resolve requires env.template in the app directory.`);
409
+ }
410
+ }
411
+
412
+ const resolved = await generateEnvContent(appName, secretsPath, environment, force, { appPath, envOnly });
413
+ const preservePath = opts.preserveFromPath !== undefined && opts.preserveFromPath !== null ? opts.preserveFromPath : null;
414
+ const pathToPreserve = preservePath !== null ? preservePath : envPath;
415
+ const toWrite = resolveEnvContentToWrite(resolved, pathToPreserve);
416
+ fs.writeFileSync(envPath, toWrite, { mode: 0o600 });
417
+
418
+ if (!opts.skipOutputPath) {
419
+ const { processEnvVariables } = require('../utils/env-copy');
420
+ await processEnvVariables(envPath, variablesPath, appName, secretsPath);
421
+ }
422
+
423
+ return envPath;
424
+ }
425
+
426
+ module.exports = {
427
+ resolveKvReferences,
428
+ generateEnvContent,
429
+ generateEnvFile,
430
+ parseEnvContentToMap,
431
+ mergeEnvMapIntoContent
432
+ };
@@ -100,6 +100,30 @@ async function injectRegistryTokens(content, secretsPath, appName) {
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * Append variables from `BASH_*` secret keys (merged secrets load) when not already set in content.
105
+ *
106
+ * @param {string} content - .env-style content
107
+ * @param {string|null} secretsPath - Optional secrets path
108
+ * @param {string|null} appName - Optional app name for loadSecrets
109
+ * @returns {Promise<string>}
110
+ */
111
+ async function injectBashPrefixedExportLines(content, secretsPath, appName) {
112
+ try {
113
+ const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
114
+ const bash = await getBashPrefixedProcessEnvOverlay(secretsPath, appName);
115
+ let out = content || '';
116
+ for (const [name, value] of Object.entries(bash)) {
117
+ if (!envContentHasKey(out, name)) {
118
+ out = appendEnvLine(out, name, value);
119
+ }
120
+ }
121
+ return out;
122
+ } catch {
123
+ return content || '';
124
+ }
125
+ }
126
+
103
127
  /**
104
128
  * Parse .env-style content into a key-value map (excludes comments and empty lines).
105
129
  * @param {string} content - .env-style content
@@ -123,7 +147,7 @@ function parseEnvContentToMap(content) {
123
147
 
124
148
  /**
125
149
  * Resolve .env in memory and write only to envOutputPath or temp (no builder/ or integration/).
126
- * Injects NPM_TOKEN and PYPI_TOKEN from secrets when missing, then from process.env, so shell/install/test/build can use exported tokens.
150
+ * Injects NPM_TOKEN and PYPI_TOKEN from secrets when missing, then from process.env, then names derived from `BASH_*` keys in the merged secrets store, so shell/install/build match private registry tooling.
127
151
  *
128
152
  * @async
129
153
  * @function resolveAndWriteEnvFile
@@ -145,6 +169,7 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
145
169
 
146
170
  let resolved = await secrets.generateEnvContent(appName, secretsPath, environment, force);
147
171
  resolved = await injectRegistryTokens(resolved, secretsPath, appName);
172
+ resolved = await injectBashPrefixedExportLines(resolved, secretsPath, appName);
148
173
 
149
174
  if (envOutputPath && typeof envOutputPath === 'string') {
150
175
  const dir = path.dirname(envOutputPath);
@@ -179,6 +204,7 @@ async function resolveAndGetEnvMap(appName, options = {}) {
179
204
  const force = options.force === true;
180
205
  let content = await secrets.generateEnvContent(appName, secretsPath, environment, force);
181
206
  content = await injectRegistryTokens(content, secretsPath, appName);
207
+ content = await injectBashPrefixedExportLines(content, secretsPath, appName);
182
208
  return parseEnvContentToMap(content);
183
209
  }
184
210