@aifabrix/builder 2.40.2 → 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.
Files changed (103) hide show
  1. package/README.md +6 -4
  2. package/integration/hubspot/test.js +1 -1
  3. package/lib/api/credential.api.js +40 -0
  4. package/lib/api/dev.api.js +423 -0
  5. package/lib/api/types/credential.types.js +23 -0
  6. package/lib/api/types/dev.types.js +140 -0
  7. package/lib/app/config.js +21 -0
  8. package/lib/app/down.js +2 -1
  9. package/lib/app/index.js +9 -0
  10. package/lib/app/push.js +36 -12
  11. package/lib/app/readme.js +1 -3
  12. package/lib/app/run-env-compose.js +201 -0
  13. package/lib/app/run-helpers.js +121 -118
  14. package/lib/app/run.js +148 -28
  15. package/lib/app/show.js +5 -2
  16. package/lib/build/index.js +11 -3
  17. package/lib/cli/setup-app.js +140 -14
  18. package/lib/cli/setup-dev.js +180 -17
  19. package/lib/cli/setup-environment.js +4 -2
  20. package/lib/cli/setup-external-system.js +71 -21
  21. package/lib/cli/setup-infra.js +29 -2
  22. package/lib/cli/setup-secrets.js +52 -5
  23. package/lib/cli/setup-utility.js +12 -3
  24. package/lib/commands/app-install.js +172 -0
  25. package/lib/commands/app-shell.js +75 -0
  26. package/lib/commands/app-test.js +282 -0
  27. package/lib/commands/app.js +1 -1
  28. package/lib/commands/dev-cli-handlers.js +141 -0
  29. package/lib/commands/dev-down.js +114 -0
  30. package/lib/commands/dev-init.js +309 -0
  31. package/lib/commands/secrets-list.js +118 -0
  32. package/lib/commands/secrets-remove.js +97 -0
  33. package/lib/commands/secrets-set.js +30 -17
  34. package/lib/commands/secrets-validate.js +50 -0
  35. package/lib/commands/up-dataplane.js +2 -2
  36. package/lib/commands/up-miso.js +0 -25
  37. package/lib/commands/upload.js +26 -1
  38. package/lib/core/admin-secrets.js +96 -0
  39. package/lib/core/secrets-ensure.js +378 -0
  40. package/lib/core/secrets-env-write.js +157 -0
  41. package/lib/core/secrets.js +147 -81
  42. package/lib/datasource/field-reference-validator.js +91 -0
  43. package/lib/datasource/validate.js +21 -3
  44. package/lib/deployment/environment-config.js +137 -0
  45. package/lib/deployment/environment.js +21 -98
  46. package/lib/deployment/push.js +32 -2
  47. package/lib/external-system/download.js +7 -0
  48. package/lib/external-system/test-auth.js +7 -3
  49. package/lib/external-system/test.js +5 -1
  50. package/lib/generator/index.js +174 -25
  51. package/lib/generator/wizard.js +8 -0
  52. package/lib/infrastructure/helpers.js +103 -20
  53. package/lib/infrastructure/index.js +88 -10
  54. package/lib/infrastructure/services.js +70 -15
  55. package/lib/schema/application-schema.json +24 -3
  56. package/lib/schema/external-system.schema.json +435 -413
  57. package/lib/utils/api.js +3 -3
  58. package/lib/utils/app-register-auth.js +25 -3
  59. package/lib/utils/cli-utils.js +20 -0
  60. package/lib/utils/compose-generator.js +76 -75
  61. package/lib/utils/compose-handlebars-helpers.js +43 -0
  62. package/lib/utils/compose-vector-helper.js +18 -0
  63. package/lib/utils/config-paths.js +127 -2
  64. package/lib/utils/credential-secrets-env.js +267 -0
  65. package/lib/utils/dev-cert-helper.js +122 -0
  66. package/lib/utils/device-code-helpers.js +224 -0
  67. package/lib/utils/device-code.js +37 -336
  68. package/lib/utils/docker-build.js +40 -8
  69. package/lib/utils/env-copy.js +83 -13
  70. package/lib/utils/env-map.js +35 -5
  71. package/lib/utils/env-template.js +6 -5
  72. package/lib/utils/error-formatters/http-status-errors.js +20 -1
  73. package/lib/utils/help-builder.js +15 -2
  74. package/lib/utils/infra-status.js +30 -1
  75. package/lib/utils/local-secrets.js +7 -52
  76. package/lib/utils/mutagen-install.js +195 -0
  77. package/lib/utils/mutagen.js +146 -0
  78. package/lib/utils/paths.js +43 -33
  79. package/lib/utils/port-resolver.js +28 -16
  80. package/lib/utils/remote-dev-auth.js +38 -0
  81. package/lib/utils/remote-docker-env.js +43 -0
  82. package/lib/utils/remote-secrets-loader.js +60 -0
  83. package/lib/utils/secrets-generator.js +94 -6
  84. package/lib/utils/secrets-helpers.js +33 -25
  85. package/lib/utils/secrets-path.js +2 -2
  86. package/lib/utils/secrets-utils.js +52 -1
  87. package/lib/utils/secrets-validation.js +84 -0
  88. package/lib/utils/ssh-key-helper.js +116 -0
  89. package/lib/utils/token-manager-messages.js +90 -0
  90. package/lib/utils/token-manager.js +5 -4
  91. package/lib/utils/variable-transformer.js +3 -3
  92. package/lib/validation/validator.js +65 -0
  93. package/package.json +2 -2
  94. package/scripts/install-local.js +34 -15
  95. package/templates/README.md +0 -1
  96. package/templates/applications/README.md.hbs +4 -4
  97. package/templates/applications/dataplane/application.yaml +5 -4
  98. package/templates/applications/dataplane/env.template +12 -7
  99. package/templates/applications/keycloak/env.template +2 -0
  100. package/templates/applications/miso-controller/application.yaml +1 -0
  101. package/templates/applications/miso-controller/env.template +11 -9
  102. package/templates/python/docker-compose.hbs +49 -23
  103. package/templates/typescript/docker-compose.hbs +48 -22
@@ -11,11 +11,13 @@
11
11
 
12
12
  const path = require('path');
13
13
  const fs = require('fs');
14
+ const chalk = require('chalk');
14
15
  const handlebars = require('handlebars');
15
16
  const secrets = require('../core/secrets');
16
17
  const logger = require('../utils/logger');
17
18
  const dockerUtils = require('../utils/docker');
18
19
  const paths = require('../utils/paths');
20
+ const secretsEnsure = require('../core/secrets-ensure');
19
21
 
20
22
  /**
21
23
  * Gets infrastructure directory name based on developer ID
@@ -53,24 +55,92 @@ async function checkDockerAvailability() {
53
55
  }
54
56
  }
55
57
 
58
+ /** Default admin password for new local installations when admin-secrets.env is empty */
59
+ const DEFAULT_ADMIN_PASSWORD = 'admin123';
60
+
61
+ /**
62
+ * Log hint to reset Postgres volume when admin password was changed after first init.
63
+ * @param {string} infraDir - Path to infra directory
64
+ */
65
+ function logVolumeResetHint(infraDir) {
66
+ logger.log(chalk.yellow(
67
+ 'If Postgres was already started with a different password, login will fail until you reset the volume. ' +
68
+ `Run: cd ${infraDir} && docker compose -f compose.yaml -p aifabrix down -v , then run 'aifabrix up-infra --adminPwd <password>' again.`
69
+ ));
70
+ }
71
+
72
+ /**
73
+ * Apply password to admin-secrets file content (all three password keys).
74
+ * @param {string} content - Current file content
75
+ * @param {string} password - Password to set
76
+ * @returns {string} Updated content
77
+ */
78
+ function applyPasswordToAdminSecretsContent(content, password) {
79
+ return content
80
+ .replace(/^POSTGRES_PASSWORD=.*$/m, `POSTGRES_PASSWORD=${password}`)
81
+ .replace(/^PGADMIN_DEFAULT_PASSWORD=.*$/m, `PGADMIN_DEFAULT_PASSWORD=${password}`)
82
+ .replace(/^REDIS_COMMANDER_PASSWORD=.*$/m, `REDIS_COMMANDER_PASSWORD=${password}`);
83
+ }
84
+
56
85
  /**
57
- * Ensure admin secrets file exists
86
+ * Sync postgres-passwordKeyVault to the main secrets store (file or remote).
87
+ * @param {string} password - Password to store
88
+ */
89
+ async function syncPostgresPasswordToStore(password) {
90
+ try {
91
+ await secretsEnsure.setSecretInStore('postgres-passwordKeyVault', password);
92
+ } catch (err) {
93
+ logger.warn(`Could not sync postgres-passwordKeyVault to secrets store: ${err.message}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Ensure admin secrets file exists and set admin password.
99
+ * When adminPwd is provided, update POSTGRES_PASSWORD, PGADMIN_DEFAULT_PASSWORD, REDIS_COMMANDER_PASSWORD
100
+ * in admin-secrets.env (overwrites existing values). Otherwise only backfill empty fields.
101
+ *
58
102
  * @async
103
+ * @param {Object} [options] - Options
104
+ * @param {string} [options.adminPwd] - Override admin password for Postgres, pgAdmin, Redis Commander (updates file when provided)
59
105
  * @returns {Promise<string>} Path to admin secrets file
60
106
  */
61
- async function ensureAdminSecrets() {
107
+ async function ensureAdminSecrets(options = {}) {
108
+ const adminPwdOverride = options.adminPwd && typeof options.adminPwd === 'string' && options.adminPwd.trim() !== ''
109
+ ? options.adminPwd.trim()
110
+ : null;
111
+ const passwordToUse = adminPwdOverride || DEFAULT_ADMIN_PASSWORD;
112
+
62
113
  const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
63
114
  if (!fs.existsSync(adminSecretsPath)) {
64
115
  logger.log('Generating admin-secrets.env...');
65
- await secrets.generateAdminSecretsEnv();
116
+ await secrets.generateAdminSecretsEnv(undefined);
117
+ }
118
+ let content = fs.readFileSync(adminSecretsPath, 'utf8');
119
+ const needsBackfill = /^POSTGRES_PASSWORD=\s*$/m.test(content) ||
120
+ /^PGADMIN_DEFAULT_PASSWORD=\s*$/m.test(content) ||
121
+ /^REDIS_COMMANDER_PASSWORD=\s*$/m.test(content);
122
+ const shouldOverwriteWithAdminPwd = adminPwdOverride !== null;
123
+
124
+ if (shouldOverwriteWithAdminPwd) {
125
+ content = applyPasswordToAdminSecretsContent(content, passwordToUse);
126
+ fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
127
+ logger.log('Updated admin password in admin-secrets.env.');
128
+ await syncPostgresPasswordToStore(passwordToUse);
129
+ logVolumeResetHint(path.join(paths.getAifabrixHome(), getInfraDirName(0)));
130
+ } else if (needsBackfill) {
131
+ content = applyPasswordToAdminSecretsContent(content, passwordToUse);
132
+ fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
133
+ logger.log('Set default admin password in admin-secrets.env for local use.');
66
134
  }
67
135
  return adminSecretsPath;
68
136
  }
69
137
 
70
138
  /**
71
- * Generates pgAdmin4 configuration files (servers.json and pgpass)
139
+ * Generates pgAdmin4 servers.json only. pgpass is not written to disk (ISO 27K);
140
+ * it is created temporarily in startDockerServicesAndConfigure and deleted after copy to container.
141
+ *
72
142
  * @param {string} infraDir - Infrastructure directory path
73
- * @param {string} postgresPassword - PostgreSQL password
143
+ * @param {string} postgresPassword - PostgreSQL password (for servers.json PassFile reference only; password not stored in file)
74
144
  */
75
145
  function generatePgAdminConfig(infraDir, postgresPassword) {
76
146
  const serversJsonTemplatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'servers.json.hbs');
@@ -83,10 +153,6 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
83
153
  const serversJsonContent = serversJsonTemplate({ postgresPassword });
84
154
  const serversJsonPath = path.join(infraDir, 'servers.json');
85
155
  fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
86
-
87
- const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
88
- const pgpassPath = path.join(infraDir, 'pgpass');
89
- fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
90
156
  }
91
157
 
92
158
  /**
@@ -117,10 +183,8 @@ function extractPasswordFromUrlOrValue(urlOrPassword) {
117
183
 
118
184
  /**
119
185
  * Ensures Postgres init script exists for miso-controller app (database miso, user miso_user).
120
- * Uses password from secrets (databases-miso-controller-0-passwordKeyVault) or default miso_pass123.
121
- * Init scripts run only when the Postgres data volume is first created. If infra was already
122
- * started before this script existed, run `aifabrix down-infra -v` then `aifabrix up-infra` to re-init, or
123
- * create the database and user manually (e.g. via pgAdmin or psql).
186
+ * Reads password from configured store (file or remote). Fails with clear message if secret is missing.
187
+ * Run ensureInfraSecrets before startInfra so databases-miso-controller-0-passwordKeyVault exists.
124
188
  *
125
189
  * @async
126
190
  * @param {string} infraDir - Infrastructure directory path
@@ -132,15 +196,24 @@ async function ensureMisoInitScript(infraDir) {
132
196
  fs.mkdirSync(initScriptsDir, { recursive: true });
133
197
  }
134
198
 
135
- let password = 'miso_pass123';
199
+ const secretKey = 'databases-miso-controller-0-passwordKeyVault';
200
+ let password;
136
201
  try {
137
202
  const loaded = await secrets.loadSecrets(undefined);
138
- const urlOrPassword = loaded['databases-miso-controller-0-passwordKeyVault'] ||
139
- loaded['databases-miso-controller-0-urlKeyVault'];
203
+ const urlOrPassword = loaded[secretKey] || loaded['databases-miso-controller-0-urlKeyVault'];
140
204
  const extracted = extractPasswordFromUrlOrValue(urlOrPassword);
141
- if (extracted !== null) password = extracted;
142
- } catch {
143
- // No secrets or load failed; use default
205
+ if (extracted !== null && extracted.trim() !== '') {
206
+ password = extracted;
207
+ }
208
+ } catch (err) {
209
+ throw new Error(
210
+ `Secret ${secretKey} not found or could not load secrets. Run "aifabrix up-infra" to ensure infra secrets, or add it to your secrets file. ${err.message}`
211
+ );
212
+ }
213
+ if (!password || password.trim() === '') {
214
+ throw new Error(
215
+ `Secret ${secretKey} is missing or empty. Run "aifabrix up-infra" to ensure infra secrets, or add it to your secrets file.`
216
+ );
144
217
  }
145
218
 
146
219
  const passwordEscaped = escapePgString(password);
@@ -183,9 +256,19 @@ function prepareInfraDirectory(devId, adminSecretsPath) {
183
256
  fs.mkdirSync(infraDir, { recursive: true });
184
257
  }
185
258
 
259
+ const oldPgpassPath = path.join(infraDir, 'pgpass');
260
+ if (fs.existsSync(oldPgpassPath)) {
261
+ try {
262
+ fs.unlinkSync(oldPgpassPath);
263
+ } catch {
264
+ // Ignore
265
+ }
266
+ }
267
+
186
268
  const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
187
269
  const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
188
- const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
270
+ const raw = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
271
+ const postgresPassword = (raw && raw.trim()) || DEFAULT_ADMIN_PASSWORD;
189
272
  generatePgAdminConfig(infraDir, postgresPassword);
190
273
 
191
274
  return { infraDir, postgresPassword };
@@ -26,6 +26,7 @@ const {
26
26
  ensureMisoInitScript,
27
27
  registerHandlebarsHelper
28
28
  } = require('./helpers');
29
+ const secretsEnsure = require('../core/secrets-ensure');
29
30
  const {
30
31
  buildTraefikConfig,
31
32
  validateTraefikConfig,
@@ -36,17 +37,22 @@ const {
36
37
  startDockerServicesAndConfigure,
37
38
  checkInfraHealth
38
39
  } = require('./services');
40
+ // Lazy require to avoid circular dependency: infra -> app/down -> run-helpers -> infra
39
41
 
40
42
  /**
41
43
  * Prepares infrastructure environment
44
+ * Ensures infra secrets exist, then admin-secrets.env, then miso init script.
45
+ *
42
46
  * @async
43
47
  * @function prepareInfrastructureEnvironment
44
48
  * @param {string|number|null} developerId - Developer ID
49
+ * @param {Object} [options] - Options (traefik, adminPwd)
45
50
  * @returns {Promise<Object>} Prepared environment configuration
46
51
  */
47
- async function prepareInfrastructureEnvironment(developerId) {
52
+ async function prepareInfrastructureEnvironment(developerId, options = {}) {
48
53
  await checkDockerAvailability();
49
- const adminSecretsPath = await ensureAdminSecrets();
54
+ await secretsEnsure.ensureInfraSecrets({ adminPwd: options.adminPwd });
55
+ const adminSecretsPath = await ensureAdminSecrets({ adminPwd: options.adminPwd });
50
56
 
51
57
  const devId = developerId || await config.getDeveloperId();
52
58
  const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
@@ -82,7 +88,7 @@ async function prepareInfrastructureEnvironment(developerId) {
82
88
  * // Infrastructure services are now running
83
89
  */
84
90
  async function startInfra(developerId = null, options = {}) {
85
- const { devId, idNum, ports, templatePath, infraDir, adminSecretsPath } = await prepareInfrastructureEnvironment(developerId);
91
+ const { devId, idNum, ports, templatePath, infraDir } = await prepareInfrastructureEnvironment(developerId, options);
86
92
  const { traefik = false } = options;
87
93
  const traefikConfig = buildTraefikConfig(traefik);
88
94
  const validation = validateTraefikConfig(traefikConfig);
@@ -97,15 +103,56 @@ async function startInfra(developerId = null, options = {}) {
97
103
  const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, { traefik: traefikConfig });
98
104
 
99
105
  try {
100
- await startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir);
106
+ await startDockerServicesAndConfigure(composePath, devId, idNum, infraDir);
101
107
  } finally {
102
108
  // Keep the compose file for stop commands
103
109
  }
104
110
  }
105
111
 
106
112
  /**
107
- * Stops and removes local infrastructure services
108
- * Cleanly shuts down all infrastructure containers
113
+ * Stops and removes all app containers for the current developer (same network).
114
+ * @param {string} devId - Developer ID
115
+ * @returns {Promise<void>}
116
+ */
117
+ async function stopAllAppContainers(devId) {
118
+ const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
119
+ for (const name of containerNames) {
120
+ try {
121
+ await execAsyncWithCwd(`docker rm -f ${name}`);
122
+ logger.log(`Stopped and removed container: ${name}`);
123
+ } catch (err) {
124
+ logger.log(`Container ${name} not running or already removed`);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Removes Docker volumes for the given app names (current developer).
131
+ * @param {string[]} appNames - Application names
132
+ * @param {string} devId - Developer ID
133
+ * @returns {Promise<void>}
134
+ */
135
+ async function removeAppVolumes(appNames, devId) {
136
+ const { getAppVolumeName } = require('../app/down');
137
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
138
+ for (const appName of appNames) {
139
+ const primaryName = getAppVolumeName(appName, devId);
140
+ const legacyDev0Name = idNum === 0 ? `aifabrix_dev0_${appName}_data` : null;
141
+ const candidates = Array.from(new Set([primaryName, legacyDev0Name].filter(Boolean)));
142
+ for (const volumeName of candidates) {
143
+ try {
144
+ await execAsyncWithCwd(`docker volume rm -f ${volumeName}`);
145
+ logger.log(`Removed volume: ${volumeName}`);
146
+ } catch {
147
+ logger.log(`Volume ${volumeName} not found or already removed`);
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Stops and removes local infrastructure services and all application containers
155
+ * on the same network. Cleanly shuts down infra and app containers.
109
156
  *
110
157
  * @async
111
158
  * @function stopInfra
@@ -114,7 +161,7 @@ async function startInfra(developerId = null, options = {}) {
114
161
  *
115
162
  * @example
116
163
  * await stopInfra();
117
- * // All infrastructure containers are stopped and removed
164
+ * // All infrastructure and app containers on the same network are stopped and removed
118
165
  */
119
166
  async function stopInfra() {
120
167
  const devId = await config.getDeveloperId();
@@ -130,6 +177,8 @@ async function stopInfra() {
130
177
  }
131
178
 
132
179
  try {
180
+ logger.log('Stopping application containers on the same network...');
181
+ await stopAllAppContainers(devId);
133
182
  logger.log('Stopping infrastructure services...');
134
183
  const projectName = getInfraProjectName(devId);
135
184
  const composeCmd = await dockerUtils.getComposeCommand();
@@ -141,8 +190,35 @@ async function stopInfra() {
141
190
  }
142
191
 
143
192
  /**
144
- * Stops and removes local infrastructure services with volumes
145
- * Cleanly shuts down all infrastructure containers and removes all data
193
+ * Stops all app containers on the network and removes their volumes.
194
+ * @param {string} devId - Developer ID
195
+ * @returns {Promise<void>}
196
+ */
197
+ async function stopAllAppContainersAndVolumes(devId) {
198
+ const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
199
+ const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
200
+ for (const name of containerNames) {
201
+ try {
202
+ await execAsyncWithCwd(`docker rm -f ${name}`);
203
+ logger.log(`Stopped and removed container: ${name}`);
204
+ } catch (err) {
205
+ logger.log(`Container ${name} not running or already removed`);
206
+ }
207
+ }
208
+ const appNames = [...new Set(
209
+ containerNames
210
+ .map(n => statusHelpers.extractAppName(n, devIdNum, devId))
211
+ .filter(Boolean)
212
+ )];
213
+ if (appNames.length > 0) {
214
+ logger.log('Removing application volumes...');
215
+ await removeAppVolumes(appNames, devId);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Stops and removes local infrastructure services and all application containers
221
+ * on the same network, and removes all volumes (infra and app data).
146
222
  *
147
223
  * @async
148
224
  * @function stopInfraWithVolumes
@@ -151,7 +227,7 @@ async function stopInfra() {
151
227
  *
152
228
  * @example
153
229
  * await stopInfraWithVolumes();
154
- * // All infrastructure containers and data are removed
230
+ * // All infrastructure and app containers and volumes are removed
155
231
  */
156
232
  async function stopInfraWithVolumes() {
157
233
  const devId = await config.getDeveloperId();
@@ -167,6 +243,8 @@ async function stopInfraWithVolumes() {
167
243
  }
168
244
 
169
245
  try {
246
+ logger.log('Stopping application containers on the same network...');
247
+ await stopAllAppContainersAndVolumes(devId);
170
248
  logger.log('Stopping infrastructure services and removing all data...');
171
249
  const projectName = getInfraProjectName(devId);
172
250
  const composeCmd = await dockerUtils.getComposeCommand();
@@ -17,6 +17,7 @@ const containerUtils = require('../utils/infra-containers');
17
17
  const dockerUtils = require('../utils/docker');
18
18
  const config = require('../core/config');
19
19
  const { getInfraProjectName } = require('./helpers');
20
+ const adminSecrets = require('../core/admin-secrets');
20
21
 
21
22
  const execAsync = promisify(exec);
22
23
 
@@ -74,29 +75,83 @@ async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPa
74
75
  }
75
76
 
76
77
  /**
77
- * Starts Docker services and configures pgAdmin
78
+ * Prepare run env file from decrypted admin secrets.
79
+ * @async
80
+ * @param {string} infraDir - Infrastructure directory
81
+ * @returns {Promise<{ adminObj: Object, runEnvPath: string }>}
82
+ */
83
+ async function prepareRunEnv(infraDir) {
84
+ const runEnvPath = path.join(infraDir, '.env.run');
85
+ const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
86
+ const content = adminSecrets.envObjectToContent(adminObj);
87
+ fs.writeFileSync(runEnvPath, content, { mode: 0o600 });
88
+ return { adminObj, runEnvPath };
89
+ }
90
+
91
+ /**
92
+ * Write pgpass file and copy pgAdmin config into container.
93
+ * @async
94
+ * @param {string} infraDir - Infrastructure directory
95
+ * @param {Object} adminObj - Decrypted admin secrets object
96
+ * @param {string} devId - Developer ID
97
+ * @param {number} idNum - Developer ID number
98
+ * @returns {Promise<string>} Path to pgpass run file
99
+ */
100
+ async function writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum) {
101
+ const pgpassRunPath = path.join(infraDir, '.pgpass.run');
102
+ const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
103
+ const serversJsonPath = path.join(infraDir, 'servers.json');
104
+ const postgresPassword = adminObj.POSTGRES_PASSWORD || '';
105
+ const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
106
+ fs.writeFileSync(pgpassRunPath, pgpassContent, { mode: 0o600 });
107
+ await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassRunPath);
108
+ return pgpassRunPath;
109
+ }
110
+
111
+ /**
112
+ * Remove temporary run files (env and pgpass) if they exist.
113
+ * @param {string} runEnvPath - Path to .env.run
114
+ * @param {string} [pgpassRunPath] - Path to .pgpass.run
115
+ */
116
+ function cleanupRunFiles(runEnvPath, pgpassRunPath) {
117
+ try {
118
+ if (fs.existsSync(runEnvPath)) fs.unlinkSync(runEnvPath);
119
+ if (pgpassRunPath && fs.existsSync(pgpassRunPath)) fs.unlinkSync(pgpassRunPath);
120
+ } catch {
121
+ // Ignore unlink errors
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Starts Docker services and configures pgAdmin.
127
+ * Writes decrypted admin secrets to a temporary .env in infra dir, runs compose, then deletes the file (ISO 27K).
128
+ *
78
129
  * @async
79
130
  * @function startDockerServicesAndConfigure
80
131
  * @param {string} composePath - Compose file path
81
132
  * @param {string} devId - Developer ID
82
133
  * @param {number} idNum - Developer ID number
83
- * @param {string} adminSecretsPath - Admin secrets path
84
134
  * @param {string} infraDir - Infrastructure directory
85
135
  */
86
- async function startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir) {
87
- // Start Docker services
88
- const projectName = getInfraProjectName(devId);
89
- await startDockerServices(composePath, projectName, adminSecretsPath, infraDir);
90
-
91
- // Copy pgAdmin4 config files
92
- const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
93
- const serversJsonPath = path.join(infraDir, 'servers.json');
94
- const pgpassPath = path.join(infraDir, 'pgpass');
95
- await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
136
+ async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir) {
137
+ let runEnvPath;
138
+ let pgpassRunPath;
139
+ let adminObj;
140
+ try {
141
+ ({ adminObj, runEnvPath } = await prepareRunEnv(infraDir));
142
+ } catch (err) {
143
+ throw new Error(`Failed to prepare infra env: ${err.message}`);
144
+ }
96
145
 
97
- // Wait for services to be healthy
98
- await waitForServices(devId);
99
- logger.log('All services are healthy and ready');
146
+ try {
147
+ const projectName = getInfraProjectName(devId);
148
+ await startDockerServices(composePath, projectName, runEnvPath, infraDir);
149
+ pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
150
+ await waitForServices(devId);
151
+ logger.log('All services are healthy and ready');
152
+ } finally {
153
+ cleanupRunFiles(runEnvPath, pgpassRunPath);
154
+ }
100
155
  }
101
156
 
102
157
  /**
@@ -736,13 +736,13 @@
736
736
  "properties": {
737
737
  "envOutputPath": {
738
738
  "type": "string",
739
- "description": "Path where .env file is copied for local development (relative to builder/)",
739
+ "description": "Path where .env file is written for local development (relative to config dir); single .env for run",
740
740
  "pattern": "^[^/].*"
741
741
  },
742
742
  "localPort": {
743
743
  "type": "integer",
744
- "description": "Port for local development (different from Docker port)",
745
- "minimum": 1000,
744
+ "description": "Port for local development (host and .env PORT); when set and > 0, overrides port for run and local .env. Container/deployment still use port/containerPort.",
745
+ "minimum": 1,
746
746
  "maximum": 65535
747
747
  },
748
748
  "containerPort": {
@@ -768,6 +768,27 @@
768
768
  "type": "string",
769
769
  "description": "Dockerfile name (empty or missing = use auto-generated template)",
770
770
  "pattern": "^[^/].*"
771
+ },
772
+ "remoteSyncPath": {
773
+ "type": "string",
774
+ "description": "Relative path under user-mutagen-folder for remote sync and Docker -v; when unset, defaults to dev/<appKey>. No leading slash.",
775
+ "pattern": "^[^/].*"
776
+ },
777
+ "reloadStart": {
778
+ "type": "string",
779
+ "description": "When running with --reload, override the container command with this command (run from the mounted app root /app). Examples: TypeScript pnpm run reloadStart, Python make reloadStart."
780
+ },
781
+ "scripts": {
782
+ "type": "object",
783
+ "description": "Override CLI script commands (aifabrix test, install, lint, test-e2e, test-integration). When a key is missing, language default is used (TypeScript: pnpm test/install/lint/test:e2e/test:integration, Python: make test/install/lint/test:e2e/test-integration).",
784
+ "properties": {
785
+ "test": { "type": "string", "description": "Command for aifabrix test <app>" },
786
+ "install": { "type": "string", "description": "Command for aifabrix install <app>" },
787
+ "lint": { "type": "string", "description": "Command for aifabrix lint <app>" },
788
+ "testE2e": { "type": "string", "description": "Command for aifabrix test-e2e <app> (YAML may use test:e2e)" },
789
+ "testIntegration": { "type": "string", "description": "Command for aifabrix test-integration <app> (YAML may use test:integration). Defaults to testE2e when unset." }
790
+ },
791
+ "additionalProperties": true
771
792
  }
772
793
  },
773
794
  "additionalProperties": false