@aifabrix/builder 2.40.2 → 2.42.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 (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -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,8 +88,8 @@ 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);
86
- const { traefik = false } = options;
91
+ const { devId, idNum, ports, templatePath, infraDir } = await prepareInfrastructureEnvironment(developerId, options);
92
+ const { traefik = false, pgadmin = true, redisCommander = true } = options;
87
93
  const traefikConfig = buildTraefikConfig(traefik);
88
94
  const validation = validateTraefikConfig(traefikConfig);
89
95
  if (!validation.valid) {
@@ -94,18 +100,67 @@ async function startInfra(developerId = null, options = {}) {
94
100
  registerHandlebarsHelper();
95
101
 
96
102
  // Generate compose file
97
- const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, { traefik: traefikConfig });
103
+ const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, {
104
+ traefik: traefikConfig,
105
+ pgadmin: { enabled: !!pgadmin },
106
+ redisCommander: { enabled: !!redisCommander }
107
+ });
98
108
 
99
109
  try {
100
- await startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir);
110
+ await startDockerServicesAndConfigure(composePath, devId, idNum, infraDir, {
111
+ pgadmin: !!pgadmin,
112
+ redisCommander: !!redisCommander,
113
+ traefik: !!traefik
114
+ });
101
115
  } finally {
102
116
  // Keep the compose file for stop commands
103
117
  }
104
118
  }
105
119
 
106
120
  /**
107
- * Stops and removes local infrastructure services
108
- * Cleanly shuts down all infrastructure containers
121
+ * Stops and removes all app containers for the current developer (same network).
122
+ * @param {string} devId - Developer ID
123
+ * @returns {Promise<void>}
124
+ */
125
+ async function stopAllAppContainers(devId) {
126
+ const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
127
+ for (const name of containerNames) {
128
+ try {
129
+ await execAsyncWithCwd(`docker rm -f ${name}`);
130
+ logger.log(`Stopped and removed container: ${name}`);
131
+ } catch (err) {
132
+ logger.log(`Container ${name} not running or already removed`);
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Removes Docker volumes for the given app names (current developer).
139
+ * @param {string[]} appNames - Application names
140
+ * @param {string} devId - Developer ID
141
+ * @returns {Promise<void>}
142
+ */
143
+ async function removeAppVolumes(appNames, devId) {
144
+ const { getAppVolumeName } = require('../app/down');
145
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
146
+ for (const appName of appNames) {
147
+ const primaryName = getAppVolumeName(appName, devId);
148
+ const legacyDev0Name = idNum === 0 ? `aifabrix_dev0_${appName}_data` : null;
149
+ const candidates = Array.from(new Set([primaryName, legacyDev0Name].filter(Boolean)));
150
+ for (const volumeName of candidates) {
151
+ try {
152
+ await execAsyncWithCwd(`docker volume rm -f ${volumeName}`);
153
+ logger.log(`Removed volume: ${volumeName}`);
154
+ } catch {
155
+ logger.log(`Volume ${volumeName} not found or already removed`);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Stops and removes local infrastructure services and all application containers
163
+ * on the same network. Cleanly shuts down infra and app containers.
109
164
  *
110
165
  * @async
111
166
  * @function stopInfra
@@ -114,7 +169,7 @@ async function startInfra(developerId = null, options = {}) {
114
169
  *
115
170
  * @example
116
171
  * await stopInfra();
117
- * // All infrastructure containers are stopped and removed
172
+ * // All infrastructure and app containers on the same network are stopped and removed
118
173
  */
119
174
  async function stopInfra() {
120
175
  const devId = await config.getDeveloperId();
@@ -130,6 +185,8 @@ async function stopInfra() {
130
185
  }
131
186
 
132
187
  try {
188
+ logger.log('Stopping application containers on the same network...');
189
+ await stopAllAppContainers(devId);
133
190
  logger.log('Stopping infrastructure services...');
134
191
  const projectName = getInfraProjectName(devId);
135
192
  const composeCmd = await dockerUtils.getComposeCommand();
@@ -141,8 +198,35 @@ async function stopInfra() {
141
198
  }
142
199
 
143
200
  /**
144
- * Stops and removes local infrastructure services with volumes
145
- * Cleanly shuts down all infrastructure containers and removes all data
201
+ * Stops all app containers on the network and removes their volumes.
202
+ * @param {string} devId - Developer ID
203
+ * @returns {Promise<void>}
204
+ */
205
+ async function stopAllAppContainersAndVolumes(devId) {
206
+ const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
207
+ const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
208
+ for (const name of containerNames) {
209
+ try {
210
+ await execAsyncWithCwd(`docker rm -f ${name}`);
211
+ logger.log(`Stopped and removed container: ${name}`);
212
+ } catch (err) {
213
+ logger.log(`Container ${name} not running or already removed`);
214
+ }
215
+ }
216
+ const appNames = [...new Set(
217
+ containerNames
218
+ .map(n => statusHelpers.extractAppName(n, devIdNum, devId))
219
+ .filter(Boolean)
220
+ )];
221
+ if (appNames.length > 0) {
222
+ logger.log('Removing application volumes...');
223
+ await removeAppVolumes(appNames, devId);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Stops and removes local infrastructure services and all application containers
229
+ * on the same network, and removes all volumes (infra and app data).
146
230
  *
147
231
  * @async
148
232
  * @function stopInfraWithVolumes
@@ -151,7 +235,7 @@ async function stopInfra() {
151
235
  *
152
236
  * @example
153
237
  * await stopInfraWithVolumes();
154
- * // All infrastructure containers and data are removed
238
+ * // All infrastructure and app containers and volumes are removed
155
239
  */
156
240
  async function stopInfraWithVolumes() {
157
241
  const devId = await config.getDeveloperId();
@@ -167,6 +251,8 @@ async function stopInfraWithVolumes() {
167
251
  }
168
252
 
169
253
  try {
254
+ logger.log('Stopping application containers on the same network...');
255
+ await stopAllAppContainersAndVolumes(devId);
170
256
  logger.log('Stopping infrastructure services and removing all data...');
171
257
  const projectName = getInfraProjectName(devId);
172
258
  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,42 +75,102 @@ 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 (when enabled).
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
135
+ * @param {Object} [opts] - Options (pgadmin, redisCommander, traefik)
85
136
  */
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);
137
+ async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir, opts = {}) {
138
+ let runEnvPath;
139
+ let pgpassRunPath;
140
+ let adminObj;
141
+ const { pgadmin = true, redisCommander = true, traefik = false } = opts;
142
+ try {
143
+ ({ adminObj, runEnvPath } = await prepareRunEnv(infraDir));
144
+ } catch (err) {
145
+ throw new Error(`Failed to prepare infra env: ${err.message}`);
146
+ }
96
147
 
97
- // Wait for services to be healthy
98
- await waitForServices(devId);
99
- logger.log('All services are healthy and ready');
148
+ try {
149
+ const projectName = getInfraProjectName(devId);
150
+ await startDockerServices(composePath, projectName, runEnvPath, infraDir);
151
+ if (pgadmin) {
152
+ pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
153
+ }
154
+ await waitForServices(devId, { pgadmin, redisCommander, traefik });
155
+ logger.log('All services are healthy and ready');
156
+ } finally {
157
+ cleanupRunFiles(runEnvPath, pgpassRunPath);
158
+ }
100
159
  }
101
160
 
102
161
  /**
103
162
  * Waits for services to be healthy
104
163
  * @private
105
- * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
164
+ * @param {number|string|null} [devId] - Developer ID (optional, will be loaded from config if not provided)
165
+ * @param {Object} [opts] - Options (pgadmin, redisCommander, traefik) - which optional services to expect
106
166
  */
107
- async function waitForServices(devId = null) {
167
+ async function waitForServices(devId = null, opts = {}) {
108
168
  const maxAttempts = 30;
109
169
  const delay = 2000; // 2 seconds
170
+ const { pgadmin = true, redisCommander = true, traefik = false } = opts;
110
171
 
111
172
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
112
- const health = await checkInfraHealth(devId);
173
+ const health = await checkInfraHealth(devId, { pgadmin, redisCommander, traefik });
113
174
  const allHealthy = Object.values(health).every(status => status === 'healthy');
114
175
 
115
176
  if (allHealthy) {
@@ -127,15 +188,17 @@ async function waitForServices(devId = null) {
127
188
 
128
189
  /**
129
190
  * Checks if infrastructure services are running
130
- * Validates that all required services are healthy and accessible
191
+ * Validates that all expected services are healthy and accessible
131
192
  *
132
193
  * @async
133
194
  * @function checkInfraHealth
134
195
  * @param {number|string|null} [devId] - Developer ID (null = use current)
135
196
  * @param {Object} [options] - Options
136
197
  * @param {boolean} [options.strict=false] - When true, only consider current dev's containers (no fallback to dev 0); use for up-miso and status consistency
198
+ * @param {boolean} [options.pgadmin=true] - Include pgAdmin in health check
199
+ * @param {boolean} [options.redisCommander=true] - Include Redis Commander in health check
200
+ * @param {boolean} [options.traefik=false] - Include Traefik in health check
137
201
  * @returns {Promise<Object>} Health status of each service
138
- * @throws {Error} If health check fails
139
202
  *
140
203
  * @example
141
204
  * const health = await checkInfraHealth();
@@ -144,7 +207,10 @@ async function waitForServices(devId = null) {
144
207
  async function checkInfraHealth(devId = null, options = {}) {
145
208
  const developerId = devId || await config.getDeveloperId();
146
209
  const servicesWithHealthCheck = ['postgres', 'redis'];
147
- const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
210
+ const servicesWithoutHealthCheck = [];
211
+ if (options.pgadmin !== false) servicesWithoutHealthCheck.push('pgadmin');
212
+ if (options.redisCommander !== false) servicesWithoutHealthCheck.push('redis-commander');
213
+ if (options.traefik === true) servicesWithoutHealthCheck.push('traefik');
148
214
  const health = {};
149
215
  const lookupOptions = options.strict ? { strict: true } : {};
150
216
 
@@ -153,7 +219,7 @@ async function checkInfraHealth(devId = null, options = {}) {
153
219
  health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId, lookupOptions);
154
220
  }
155
221
 
156
- // Check if services without health checks are running
222
+ // Check if optional services without health checks are running
157
223
  for (const service of servicesWithoutHealthCheck) {
158
224
  health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId, lookupOptions);
159
225
  }
@@ -115,11 +115,6 @@
115
115
  "minimum": 1,
116
116
  "maximum": 65535
117
117
  },
118
- "deploymentKey": {
119
- "type": "string",
120
- "description": "SHA256 hash of deployment manifest (excluding deploymentKey field)",
121
- "pattern": "^[a-f0-9]{64}$"
122
- },
123
118
  "requiresDatabase": {
124
119
  "type": "boolean",
125
120
  "description": "Whether application requires database"
@@ -137,6 +132,14 @@
137
132
  "type": "string",
138
133
  "description": "Database name",
139
134
  "pattern": "^[a-z0-9_-]+$"
135
+ },
136
+ "extensions": {
137
+ "type": "array",
138
+ "description": "PostgreSQL extension names to create in this database during db-init (e.g. pgcrypto, uuid-ossp, vector, btree_gin, btree_gist). If the database name ends with 'vector', the vector extension is still added automatically if not listed.",
139
+ "items": {
140
+ "type": "string",
141
+ "pattern": "^[a-z0-9_-]+$"
142
+ }
140
143
  }
141
144
  },
142
145
  "additionalProperties": false
@@ -736,13 +739,13 @@
736
739
  "properties": {
737
740
  "envOutputPath": {
738
741
  "type": "string",
739
- "description": "Path where .env file is copied for local development (relative to builder/)",
742
+ "description": "Path where .env file is written for local development (relative to config dir); single .env for run",
740
743
  "pattern": "^[^/].*"
741
744
  },
742
745
  "localPort": {
743
746
  "type": "integer",
744
- "description": "Port for local development (different from Docker port)",
745
- "minimum": 1000,
747
+ "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.",
748
+ "minimum": 1,
746
749
  "maximum": 65535
747
750
  },
748
751
  "containerPort": {
@@ -768,6 +771,27 @@
768
771
  "type": "string",
769
772
  "description": "Dockerfile name (empty or missing = use auto-generated template)",
770
773
  "pattern": "^[^/].*"
774
+ },
775
+ "remoteSyncPath": {
776
+ "type": "string",
777
+ "description": "Relative path under user-mutagen-folder for remote sync and Docker -v; when unset, defaults to dev/<appKey>. No leading slash.",
778
+ "pattern": "^[^/].*"
779
+ },
780
+ "reloadStart": {
781
+ "type": "string",
782
+ "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."
783
+ },
784
+ "scripts": {
785
+ "type": "object",
786
+ "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).",
787
+ "properties": {
788
+ "test": { "type": "string", "description": "Command for aifabrix test <app>" },
789
+ "install": { "type": "string", "description": "Command for aifabrix install <app>" },
790
+ "lint": { "type": "string", "description": "Command for aifabrix lint <app>" },
791
+ "testE2e": { "type": "string", "description": "Command for aifabrix test-e2e <app> (YAML may use test:e2e)" },
792
+ "testIntegration": { "type": "string", "description": "Command for aifabrix test-integration <app> (YAML may use test:integration). Defaults to testE2e when unset." }
793
+ },
794
+ "additionalProperties": true
771
795
  }
772
796
  },
773
797
  "additionalProperties": false