@aifabrix/builder 2.45.5 → 2.46.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 (231) hide show
  1. package/.cursor/rules/cli-layout.mdc +2 -0
  2. package/.cursor/rules/cli-product-neutral.mdc +43 -0
  3. package/.cursor/rules/project-rules.mdc +1 -0
  4. package/README.md +43 -17
  5. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  6. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  7. package/jest.config.default.js +2 -1
  8. package/jest.config.isolated.js +2 -1
  9. package/jest.global-live-fabrix-hooks.js +36 -0
  10. package/jest.isolated-projects.js +130 -0
  11. package/jest.projects.js +41 -136
  12. package/lib/api/api-response-errors.js +124 -0
  13. package/lib/api/governance-scenario-pack.api.js +159 -0
  14. package/lib/api/index.js +43 -3
  15. package/lib/api/lifecycle.api.js +77 -0
  16. package/lib/api/pipeline.api.js +3 -1
  17. package/lib/api/records-bulk.api.js +14 -1
  18. package/lib/api/validation-runner.js +3 -1
  19. package/lib/app/deploy-config.js +4 -1
  20. package/lib/app/down.js +20 -1
  21. package/lib/app/push.js +4 -5
  22. package/lib/app/register.js +7 -4
  23. package/lib/app/run-env-compose.js +84 -30
  24. package/lib/build/index.js +5 -0
  25. package/lib/certification/sync-system-certification.js +13 -6
  26. package/lib/cli/index.js +4 -0
  27. package/lib/cli/infra-guided-auth.js +109 -0
  28. package/lib/cli/infra-guided.js +84 -81
  29. package/lib/cli/setup-app.help.js +0 -31
  30. package/lib/cli/setup-app.js +28 -2
  31. package/lib/cli/setup-app.test-commands.js +5 -57
  32. package/lib/cli/setup-auth.js +25 -0
  33. package/lib/cli/setup-certification-commands.js +147 -0
  34. package/lib/cli/setup-certification.help.js +141 -0
  35. package/lib/cli/setup-environment.js +19 -2
  36. package/lib/cli/setup-external-system-help.js +62 -0
  37. package/lib/cli/setup-external-system.js +25 -3
  38. package/lib/cli/setup-infra.js +9 -1
  39. package/lib/cli/setup-platform.js +6 -1
  40. package/lib/cli/setup-secrets.js +33 -1
  41. package/lib/cli/setup-utility-help-after.js +22 -2
  42. package/lib/cli/setup-utility-repair.js +198 -0
  43. package/lib/cli/setup-utility-resolve.js +40 -4
  44. package/lib/cli/setup-utility-validate.js +197 -0
  45. package/lib/cli/setup-utility.js +4 -239
  46. package/lib/commands/auth-status-client-tokens.js +214 -0
  47. package/lib/commands/auth-status-dataplane-version.js +58 -20
  48. package/lib/commands/auth-status.js +41 -47
  49. package/lib/commands/datasource-test-trust-cli.js +27 -23
  50. package/lib/commands/datasource-unified-test-cli.js +6 -3
  51. package/lib/commands/datasource-unified-test-cli.options.js +5 -1
  52. package/lib/commands/datasource.js +7 -7
  53. package/lib/commands/dev-show-display.js +8 -1
  54. package/lib/commands/governance-cli-shared.js +94 -0
  55. package/lib/commands/governance-command-action.js +132 -0
  56. package/lib/commands/governance-download-external.js +34 -0
  57. package/lib/commands/governance-generate-external.js +71 -0
  58. package/lib/commands/governance-upload-external.js +36 -0
  59. package/lib/commands/governance-verify-external.js +64 -0
  60. package/lib/commands/governance.js +144 -0
  61. package/lib/commands/lifecycle-command-action.js +123 -0
  62. package/lib/commands/lifecycle-external.js +268 -0
  63. package/lib/commands/login.js +11 -2
  64. package/lib/commands/repair-auth-apply.js +127 -0
  65. package/lib/commands/repair-changed-files.js +41 -0
  66. package/lib/commands/repair-datasource-keys.js +4 -1
  67. package/lib/commands/repair-datasource-run.js +59 -0
  68. package/lib/commands/repair-datasource-test-payload.js +259 -0
  69. package/lib/commands/repair-datasource.js +1 -51
  70. package/lib/commands/repair-env-template.js +64 -13
  71. package/lib/commands/repair-external-run.js +216 -0
  72. package/lib/commands/repair-openapi-sync.js +23 -0
  73. package/lib/commands/repair-persist.js +4 -0
  74. package/lib/commands/repair-rbac.js +52 -1
  75. package/lib/commands/repair-steps.js +145 -0
  76. package/lib/commands/repair.js +29 -296
  77. package/lib/commands/secrets-get.js +184 -0
  78. package/lib/commands/secrets-remove-all.js +3 -1
  79. package/lib/commands/setup-image-refresh.js +146 -0
  80. package/lib/commands/setup-modes.js +158 -162
  81. package/lib/commands/setup-platform-auth.js +76 -0
  82. package/lib/commands/setup-prompts-admin.js +244 -0
  83. package/lib/commands/setup-prompts-platform-mode.js +67 -0
  84. package/lib/commands/setup-prompts-validators.js +36 -0
  85. package/lib/commands/setup-prompts.js +24 -76
  86. package/lib/commands/setup-run-context.js +45 -0
  87. package/lib/commands/setup-spinners.js +39 -0
  88. package/lib/commands/setup.js +27 -5
  89. package/lib/commands/teardown.js +39 -0
  90. package/lib/commands/test-e2e-external.js +24 -8
  91. package/lib/commands/test-governance-external.js +3 -24
  92. package/lib/commands/test-trust-external.js +20 -21
  93. package/lib/commands/up-common.js +25 -13
  94. package/lib/commands/up-dataplane.js +50 -13
  95. package/lib/commands/upload-publish-context.js +75 -0
  96. package/lib/commands/upload-sidecar-followups.js +89 -0
  97. package/lib/commands/upload.js +90 -108
  98. package/lib/commands/verify-governance-combined-external.js +61 -0
  99. package/lib/commands/verify-governance-command-action.js +64 -0
  100. package/lib/commands/verify-governance-db.js +102 -0
  101. package/lib/commands/verify-governance-external.js +77 -0
  102. package/lib/commands/verify-governance-result-builders.js +71 -0
  103. package/lib/commands/verify-operations-command-action.js +68 -0
  104. package/lib/commands/verify-operations-external.js +268 -0
  105. package/lib/commands/verify-operations-persist.js +75 -0
  106. package/lib/commands/verify-operations-step-plan.js +68 -0
  107. package/lib/commands/verify-operations-steps.js +111 -0
  108. package/lib/commands/verify-trust-command-action.js +70 -0
  109. package/lib/commands/verify-trust-external.js +67 -0
  110. package/lib/commands/wizard-config-normalizer.js +43 -23
  111. package/lib/commands/wizard-core.js +2 -0
  112. package/lib/core/admin-password-generator.js +38 -0
  113. package/lib/core/admin-secrets-bundle.js +111 -0
  114. package/lib/core/admin-secrets-env-overlay.js +62 -0
  115. package/lib/core/admin-secrets.js +32 -4
  116. package/lib/core/config-attach-extensions.js +3 -0
  117. package/lib/core/config-platform-controller.js +44 -0
  118. package/lib/core/config-setup-installation-profile.js +42 -0
  119. package/lib/core/config-setup-platform-mode.js +46 -0
  120. package/lib/core/config.js +46 -36
  121. package/lib/core/secrets-admin-env.js +6 -2
  122. package/lib/core/secrets-ensure.js +4 -0
  123. package/lib/core/secrets-env-content.js +54 -48
  124. package/lib/core/secrets-env-write.js +10 -1
  125. package/lib/core/secrets-infra-placeholder-sync.js +2 -0
  126. package/lib/core/setup-installation-profile.js +43 -0
  127. package/lib/core/setup-platform-mode.js +55 -0
  128. package/lib/datasource/agent-trust-debug-log.js +1 -1
  129. package/lib/datasource/agent-trust-run.js +5 -3
  130. package/lib/datasource/audit-evidence-extract-debug.js +30 -18
  131. package/lib/datasource/audit-evidence-extract.js +7 -4
  132. package/lib/datasource/audit-evidence-verify-matrix.js +54 -26
  133. package/lib/datasource/bulk-loader-run-batches.js +15 -3
  134. package/lib/datasource/datasource-load-export-context.js +3 -0
  135. package/lib/datasource/log-cleaner.js +2 -2
  136. package/lib/datasource/log-viewer.js +8 -8
  137. package/lib/datasource/test-e2e.js +4 -1
  138. package/lib/datasource/unified-validation-run-body.js +3 -1
  139. package/lib/datasource/unified-validation-run.js +5 -2
  140. package/lib/deployment/environment-poll-status.js +138 -0
  141. package/lib/deployment/environment.js +42 -34
  142. package/lib/external-system/download.js +21 -20
  143. package/lib/external-system/generator.js +5 -1
  144. package/lib/external-system/integration-auth-context.js +45 -0
  145. package/lib/external-system/test-auth.js +12 -5
  146. package/lib/external-system/test.js +6 -6
  147. package/lib/generator/external-controller-manifest.js +15 -0
  148. package/lib/governance/governance-cli-errors.js +96 -0
  149. package/lib/governance/governance-download-display.js +38 -0
  150. package/lib/governance/governance-run-display.js +215 -25
  151. package/lib/infrastructure/admin-secrets-apply.js +84 -0
  152. package/lib/infrastructure/admin-secrets-ensure.js +130 -0
  153. package/lib/infrastructure/admin-secrets-resolve.js +95 -0
  154. package/lib/infrastructure/admin-secrets-sync.js +91 -0
  155. package/lib/infrastructure/helpers.js +23 -129
  156. package/lib/infrastructure/index.js +5 -6
  157. package/lib/infrastructure/orphan-infra-docker-teardown.js +11 -5
  158. package/lib/lifecycle/certification-progress.js +231 -0
  159. package/lib/lifecycle/dataplane-ui-url.js +145 -0
  160. package/lib/lifecycle/exit-codes.js +24 -0
  161. package/lib/lifecycle/json-envelope.js +25 -0
  162. package/lib/lifecycle/lifecycle-governance-options.js +51 -0
  163. package/lib/lifecycle/logger-quiet.js +29 -0
  164. package/lib/lifecycle/open-api-published.js +59 -0
  165. package/lib/lifecycle/operations-readiness-metrics.js +101 -0
  166. package/lib/lifecycle/product-model.js +196 -0
  167. package/lib/lifecycle/report-display.js +471 -0
  168. package/lib/lifecycle/scenario-upload.js +191 -0
  169. package/lib/lifecycle/verify-step-progress.js +87 -0
  170. package/lib/parameters/infra-kv-discovery.js +27 -2
  171. package/lib/protection/auth-context.js +10 -4
  172. package/lib/schema/application-schema.json +1 -1
  173. package/lib/schema/datasource-test-run.schema.json +89 -6
  174. package/lib/schema/external-datasource.schema.json +2636 -2622
  175. package/lib/schema/external-system.schema.json +1089 -761
  176. package/lib/schema/infra.parameter.yaml +45 -0
  177. package/lib/schema/type/credential-field-registry.json +103 -97
  178. package/lib/utils/aifabrix-test-runtime-guard.js +125 -0
  179. package/lib/utils/api.js +55 -31
  180. package/lib/utils/app-relative-path-env.js +112 -0
  181. package/lib/utils/cli-secrets-error-format.js +83 -21
  182. package/lib/utils/cli-utils.js +16 -4
  183. package/lib/utils/compose-generate-docker-compose.js +4 -0
  184. package/lib/utils/compose-traefik-ingress-base.js +16 -1
  185. package/lib/utils/config-secrets-path.js +46 -0
  186. package/lib/utils/controller-url.js +88 -1
  187. package/lib/utils/credential-secrets-env.js +137 -9
  188. package/lib/utils/dataplane-resolver.js +129 -2
  189. package/lib/utils/declarative-url-ports.js +3 -3
  190. package/lib/utils/deployment-auth-mode.js +41 -0
  191. package/lib/utils/developer-volume-id.js +33 -0
  192. package/lib/utils/env-copy.js +14 -9
  193. package/lib/utils/env-kv-orphan-lines.js +43 -0
  194. package/lib/utils/error-formatters/network-errors.js +13 -4
  195. package/lib/utils/external-env-template.js +1 -0
  196. package/lib/utils/external-system-readiness-core.js +10 -0
  197. package/lib/utils/external-system-readiness-display.js +80 -23
  198. package/lib/utils/health-check.js +4 -1
  199. package/lib/utils/help-builder.js +11 -8
  200. package/lib/utils/keycloak-realm-ready.js +101 -0
  201. package/lib/utils/load-cip-capacity-display-config.js +61 -15
  202. package/lib/utils/manifest-source-emit.js +2 -0
  203. package/lib/utils/pipeline-upload-auth-retry.js +82 -0
  204. package/lib/utils/platform-controller-url.js +156 -0
  205. package/lib/utils/postgres-platform-bootstrap.js +199 -0
  206. package/lib/utils/postgres-wipe.js +9 -2
  207. package/lib/utils/resolve-env-mode.js +47 -0
  208. package/lib/utils/secrets-generator.js +31 -25
  209. package/lib/utils/secrets-missing-error.js +164 -0
  210. package/lib/utils/service-url-label.js +81 -0
  211. package/lib/utils/setup-quiet-output.js +25 -0
  212. package/lib/utils/template-helpers.js +15 -0
  213. package/lib/utils/token-manager-client-credentials.js +201 -0
  214. package/lib/utils/token-manager-messages.js +8 -0
  215. package/lib/utils/token-manager-refresh.js +12 -2
  216. package/lib/utils/token-manager.js +86 -105
  217. package/lib/utils/upload-pipeline-ui.js +12 -5
  218. package/lib/utils/upload-sync-options.js +42 -0
  219. package/lib/utils/url-declarative-public-base.js +21 -3
  220. package/lib/utils/url-declarative-vdir-inactive-env.js +14 -4
  221. package/lib/utils/validation-poll-ui.js +8 -0
  222. package/lib/utils/validation-run-stage.js +45 -0
  223. package/package.json +1 -1
  224. package/templates/applications/dataplane/application.yaml +2 -1
  225. package/templates/applications/dataplane/env.template +24 -1
  226. package/templates/applications/miso-controller/env.template +16 -0
  227. package/templates/external-system/README.md.hbs +74 -17
  228. package/templates/python/docker-compose.hbs +19 -18
  229. package/templates/typescript/docker-compose.hbs +18 -17
  230. package/lib/commands/test-governance-command-action.js +0 -55
  231. package/lib/commands/test-trust-command-action.js +0 -118
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Recreate platform Postgres databases and roles after setup "wipe data".
3
+ *
4
+ * Infra init scripts run only on first volume creation; dropping DBs in a
5
+ * running Postgres container requires explicit bootstrap before Keycloak/Miso start.
6
+ *
7
+ * @fileoverview Platform database bootstrap after postgres wipe
8
+ * @author AI Fabrix Team
9
+ * @version 1.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const config = require('../core/config');
15
+ const adminSecrets = require('../core/admin-secrets');
16
+ const logger = require('./logger');
17
+ const chalk = require('chalk');
18
+ const { successGlyph } = require('./cli-test-layout-chalk');
19
+ const { getPostgresContainerName } = require('./postgres-wipe');
20
+ const dockerExec = require('./docker-exec');
21
+ const { isSetupQuietOutput } = require('./setup-quiet-output');
22
+
23
+ const SUPERUSER_ROLE = 'pgadmin';
24
+
25
+ /** @type {{ dbName: string, secretKey: string }[]} */
26
+ const PLATFORM_DATABASE_SLOTS = [
27
+ { dbName: 'keycloak', secretKey: 'databases-keycloak-0-passwordKeyVault' },
28
+ { dbName: 'miso', secretKey: 'databases-miso-controller-0-passwordKeyVault' },
29
+ { dbName: 'miso-logs', secretKey: 'databases-miso-controller-1-passwordKeyVault' },
30
+ { dbName: 'dataplane', secretKey: 'databases-dataplane-0-passwordKeyVault' },
31
+ { dbName: 'dataplane-vector', secretKey: 'databases-dataplane-1-passwordKeyVault' },
32
+ { dbName: 'dataplane-logs', secretKey: 'databases-dataplane-2-passwordKeyVault' },
33
+ { dbName: 'dataplane-records', secretKey: 'databases-dataplane-3-passwordKeyVault' }
34
+ ];
35
+
36
+ /**
37
+ * @param {string} dbName
38
+ * @returns {string}
39
+ */
40
+ function pgRoleName(dbName) {
41
+ return `${String(dbName).replace(/-/g, '_')}_user`;
42
+ }
43
+
44
+ /**
45
+ * @param {string} s
46
+ * @returns {string}
47
+ */
48
+ function escapePgString(s) {
49
+ if (s === null || s === undefined || typeof s !== 'string') return '';
50
+ return s.replace(/'/g, '\'\'');
51
+ }
52
+
53
+ /**
54
+ * @param {string} urlOrPassword
55
+ * @returns {string|null}
56
+ */
57
+ function extractPasswordFromUrlOrValue(urlOrPassword) {
58
+ if (typeof urlOrPassword !== 'string' || urlOrPassword.length === 0) return null;
59
+ if (!urlOrPassword.includes('://')) return urlOrPassword;
60
+ try {
61
+ const u = new URL(
62
+ urlOrPassword.replace(/\$\{DB_HOST\}/g, 'postgres').replace(/\$\{DB_PORT\}/g, '5432')
63
+ );
64
+ return u.password || null;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Escape SQL for embedding in a double-quoted shell argument (-c "...").
72
+ * Bash expands `$` and `$$` (PID) inside double quotes; backslash-escape every `$`.
73
+ *
74
+ * @param {string} s
75
+ * @returns {string}
76
+ */
77
+ function escapeShellDoubleQuoted(s) {
78
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
79
+ }
80
+
81
+ /**
82
+ * @async
83
+ * @param {string} container
84
+ * @param {string} sql
85
+ * @param {string} adminPassword
86
+ * @returns {Promise<void>}
87
+ */
88
+ async function runPsqlCommand(container, sql, adminPassword, database = 'postgres') {
89
+ const escapedSql = escapeShellDoubleQuoted(sql);
90
+ const cmd =
91
+ `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d ${database} ` +
92
+ `-v ON_ERROR_STOP=1 -c "${escapedSql}"`;
93
+ await dockerExec.execWithDockerEnv(cmd, { env: { PGPASSWORD: adminPassword } });
94
+ }
95
+
96
+ /**
97
+ * @async
98
+ * @param {string} container
99
+ * @param {string} dbName
100
+ * @param {string} role
101
+ * @param {string} password
102
+ * @param {string} adminPassword
103
+ * @returns {Promise<void>}
104
+ */
105
+ async function ensureRoleAndDatabase(container, dbName, role, password, adminPassword) {
106
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(dbName) || !/^[A-Za-z_][A-Za-z0-9_-]*$/.test(role)) {
107
+ throw new Error(`Unsafe database or role name: ${dbName} / ${role}`);
108
+ }
109
+ const pwd = escapePgString(password);
110
+ await runPsqlCommand(
111
+ container,
112
+ `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${role}') THEN CREATE USER "${role}" WITH PASSWORD '${pwd}'; ELSE ALTER USER "${role}" WITH PASSWORD '${pwd}'; END IF; END $$;`,
113
+ adminPassword
114
+ );
115
+ const exists = await dockerExec.execWithDockerEnv(
116
+ `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '${dbName}'"`,
117
+ { env: { PGPASSWORD: adminPassword } }
118
+ );
119
+ const hasDb = String((exists && exists.stdout) || '').trim() === '1';
120
+ if (!hasDb) {
121
+ await runPsqlCommand(
122
+ container,
123
+ `CREATE DATABASE "${dbName}" OWNER "${role}";`,
124
+ adminPassword
125
+ );
126
+ }
127
+ await runPsqlCommand(
128
+ container,
129
+ `GRANT ALL ON SCHEMA public TO "${role}"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${role}";`,
130
+ adminPassword,
131
+ dbName
132
+ );
133
+ }
134
+
135
+ /**
136
+ * @async
137
+ * @param {string} container
138
+ * @param {{ dbName: string, secretKey: string }} slot
139
+ * @param {Object} secrets
140
+ * @param {string} adminPassword
141
+ * @returns {Promise<string>}
142
+ */
143
+ async function ensurePlatformDatabaseSlot(container, slot, secrets, adminPassword) {
144
+ const raw = secrets[slot.secretKey];
145
+ const password = extractPasswordFromUrlOrValue(raw);
146
+ if (!password || !password.trim()) {
147
+ throw new Error(
148
+ `Secret ${slot.secretKey} is missing or empty. Run "aifabrix up-infra" to ensure infra secrets.`
149
+ );
150
+ }
151
+ const role = pgRoleName(slot.dbName);
152
+ await ensureRoleAndDatabase(container, slot.dbName, role, password.trim(), adminPassword);
153
+ if (!isSetupQuietOutput()) {
154
+ logger.log(chalk.gray(` ${successGlyph()} Ensured database ${slot.dbName} (${role})`));
155
+ }
156
+ return slot.dbName;
157
+ }
158
+
159
+ /**
160
+ * @async
161
+ * @returns {Promise<{ container: string, adminPassword: string, secrets: Object }>}
162
+ */
163
+ async function resolveBootstrapContext() {
164
+ const devId = await config.getDeveloperId();
165
+ const container = getPostgresContainerName(devId);
166
+ const admin = await adminSecrets.readAndDecryptAdminSecrets();
167
+ const adminPassword = admin && admin.POSTGRES_PASSWORD;
168
+ if (!adminPassword) {
169
+ throw new Error('POSTGRES_PASSWORD not found in admin-secrets.env. Run "aifabrix up-infra" first.');
170
+ }
171
+ const secretsEnsure = require('../core/secrets-ensure');
172
+ await secretsEnsure.ensureInfraSecrets();
173
+ const { loadSecrets } = require('../core/secrets');
174
+ const secrets = await loadSecrets(undefined);
175
+ return { container, adminPassword, secrets };
176
+ }
177
+
178
+ /**
179
+ * Recreate platform DB users and databases from resolved kv secrets.
180
+ *
181
+ * @async
182
+ * @returns {Promise<string[]>} database names ensured
183
+ */
184
+ async function bootstrapPlatformPostgresDatabases() {
185
+ const { container, adminPassword, secrets } = await resolveBootstrapContext();
186
+ const ensured = [];
187
+ for (const slot of PLATFORM_DATABASE_SLOTS) {
188
+ ensured.push(await ensurePlatformDatabaseSlot(container, slot, secrets, adminPassword));
189
+ }
190
+ return ensured;
191
+ }
192
+
193
+ module.exports = {
194
+ PLATFORM_DATABASE_SLOTS,
195
+ bootstrapPlatformPostgresDatabases,
196
+ pgRoleName,
197
+ extractPasswordFromUrlOrValue,
198
+ escapeShellDoubleQuoted
199
+ };
@@ -22,6 +22,7 @@ const dockerExec = require('./docker-exec');
22
22
  const logger = require('./logger');
23
23
  const chalk = require('chalk');
24
24
  const { successGlyph } = require('./cli-test-layout-chalk');
25
+ const { isSetupQuietOutput } = require('./setup-quiet-output');
25
26
 
26
27
  /** Roles that must never be dropped (Postgres-managed predefined roles). */
27
28
  const PROTECTED_ROLES = new Set([
@@ -190,15 +191,21 @@ async function wipePostgresData() {
190
191
  }
191
192
 
192
193
  const databases = await listUserDatabases(container, password);
194
+ const quiet = isSetupQuietOutput();
195
+
193
196
  for (const dbName of databases) {
194
197
  await dropDatabase(container, dbName, password);
195
- logger.log(chalk.gray(` ${successGlyph()} Dropped database ${dbName}`));
198
+ if (!quiet) {
199
+ logger.log(chalk.gray(` ${successGlyph()} Dropped database ${dbName}`));
200
+ }
196
201
  }
197
202
 
198
203
  const roles = await listDropableRoles(container, password);
199
204
  for (const role of roles) {
200
205
  await dropRole(container, role, password);
201
- logger.log(chalk.gray(` ${successGlyph()} Dropped role ${role}`));
206
+ if (!quiet) {
207
+ logger.log(chalk.gray(` ${successGlyph()} Dropped role ${role}`));
208
+ }
202
209
  }
203
210
 
204
211
  return { databases, roles };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Chooses `local` vs `docker` for `generateEnvFile` / `resolve` (integration → local, no Docker rewrite).
3
+ *
4
+ * @fileoverview Resolve environment mode for env generation
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+
13
+ /** Platform builder apps that run in Docker and need docker host/port transforms on resolve. */
14
+ const PLATFORM_DOCKER_APP_KEYS = new Set(['keycloak', 'miso-controller', 'dataplane']);
15
+
16
+ /**
17
+ * @param {string} appPath - Resolved app directory from getResolveAppPath
18
+ * @returns {boolean}
19
+ */
20
+ function isIntegrationAppPath(appPath) {
21
+ if (!appPath || typeof appPath !== 'string') return false;
22
+ const normalized = path.resolve(appPath);
23
+ const marker = `${path.sep}integration${path.sep}`;
24
+ return normalized.includes(marker);
25
+ }
26
+
27
+ /**
28
+ * @param {string} appPath - Resolved application directory
29
+ * @param {string} [appName] - App or system key
30
+ * @returns {'local'|'docker'}
31
+ */
32
+ function resolveGenerateEnvEnvironment(appPath, appName) {
33
+ if (isIntegrationAppPath(appPath)) {
34
+ return 'local';
35
+ }
36
+ const key = String(appName || '').trim().toLowerCase();
37
+ if (PLATFORM_DOCKER_APP_KEYS.has(key)) {
38
+ return 'docker';
39
+ }
40
+ return 'docker';
41
+ }
42
+
43
+ module.exports = {
44
+ PLATFORM_DOCKER_APP_KEYS,
45
+ isIntegrationAppPath,
46
+ resolveGenerateEnvEnvironment
47
+ };
@@ -77,42 +77,44 @@ function loadYamlTolerantOfDuplicateKeys(content) {
77
77
  }
78
78
 
79
79
  /**
80
- * Skips commented or empty lines when scanning env.template
81
- * @param {string} line - Single line
80
+ * True when secrets map already has a non-empty value for a kv path (flat or nested).
81
+ * @param {Object} secrets
82
+ * @param {string} pathStr - Path after kv:// (e.g. hubspot-demo/token)
82
83
  * @returns {boolean}
83
84
  */
84
- function isCommentOrEmptyLine(line) {
85
- const t = line.trim();
86
- return t === '' || t.startsWith('#');
85
+ function secretPathExistsInStore(secrets, pathStr) {
86
+ if (!secrets || typeof secrets !== 'object' || !pathStr) {
87
+ return false;
88
+ }
89
+ const direct = secrets[pathStr];
90
+ if (direct !== undefined && direct !== null && String(direct).trim() !== '') {
91
+ return true;
92
+ }
93
+ if (!pathStr.includes('/')) {
94
+ return false;
95
+ }
96
+ let current = secrets;
97
+ for (const segment of pathStr.split('/')) {
98
+ if (current === null || current === undefined || typeof current !== 'object' || !(segment in current)) {
99
+ return false;
100
+ }
101
+ current = current[segment];
102
+ }
103
+ return current !== undefined && current !== null && String(current).trim() !== '';
87
104
  }
88
105
 
89
106
  /**
90
- * Finds missing secret keys from template (skips commented and empty lines)
107
+ * Finds missing secret keys from template (skips commented and empty lines).
108
+ * Uses full kv:// paths (e.g. hubspot-demo/token), aligned with collectMissingSecrets.
91
109
  * @function findMissingSecretKeys
92
110
  * @param {string} envTemplate - Environment template content
93
111
  * @param {Object} existingSecrets - Existing secrets object
94
112
  * @returns {string[]} Array of missing secret keys
95
113
  */
96
114
  function findMissingSecretKeys(envTemplate, existingSecrets) {
97
- const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
98
- const missingKeys = [];
99
- const seenKeys = new Set();
100
- const lines = envTemplate.split('\n');
101
-
102
- for (const line of lines) {
103
- if (isCommentOrEmptyLine(line)) continue;
104
- let match;
105
- kvPattern.lastIndex = 0;
106
- while ((match = kvPattern.exec(line)) !== null) {
107
- const secretKey = match[1];
108
- if (!seenKeys.has(secretKey) && !(secretKey in existingSecrets)) {
109
- missingKeys.push(secretKey);
110
- seenKeys.add(secretKey);
111
- }
112
- }
113
- }
114
-
115
- return missingKeys;
115
+ const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
116
+ const paths = collectUniqueKvPathStrings(envTemplate);
117
+ return paths.filter(pathStr => !secretPathExistsInStore(existingSecrets, pathStr));
116
118
  }
117
119
 
118
120
  /**
@@ -275,6 +277,8 @@ function loadExistingSecrets(resolvedPath) {
275
277
  * @throws {Error} If save fails
276
278
  */
277
279
  function saveSecretsFile(resolvedPath, secrets) {
280
+ const { assertWritableSecretsPathForTests } = require('./aifabrix-test-runtime-guard');
281
+ assertWritableSecretsPathForTests(resolvedPath);
278
282
  const dir = path.dirname(resolvedPath);
279
283
  if (!fs.existsSync(dir)) {
280
284
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -356,6 +360,8 @@ function appendSecretsToFile(resolvedPath, secrets) {
356
360
  if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {
357
361
  return;
358
362
  }
363
+ const { assertWritableSecretsPathForTests } = require('./aifabrix-test-runtime-guard');
364
+ assertWritableSecretsPathForTests(resolvedPath);
359
365
  const dir = path.dirname(resolvedPath);
360
366
  if (!fs.existsSync(dir)) {
361
367
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Build missing-secrets errors with remediation hints (no secret values).
3
+ *
4
+ * @fileoverview Missing kv:// error message for resolve and env generation
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
12
+
13
+ /**
14
+ * @param {string} line
15
+ * @returns {boolean}
16
+ */
17
+ function isCommentOrEmptyLine(line) {
18
+ const t = String(line).trim();
19
+ return t === '' || t.startsWith('#');
20
+ }
21
+
22
+ /**
23
+ * @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
24
+ * @returns {string}
25
+ */
26
+ function kvRefToSecretSetKey(kvRef) {
27
+ const s = String(kvRef || '').trim();
28
+ return s.replace(/^kv:\/\//i, '');
29
+ }
30
+
31
+ /**
32
+ * @param {string} envTemplate
33
+ * @param {string} kvRef
34
+ * @returns {string|null}
35
+ */
36
+ function findActiveEnvLineForKvRef(envTemplate, kvRef) {
37
+ if (!envTemplate || !kvRef) return null;
38
+ const needle = String(kvRef).trim();
39
+ for (const line of envTemplate.split('\n')) {
40
+ if (isCommentOrEmptyLine(line)) continue;
41
+ if (line.includes(needle)) return line.trim();
42
+ }
43
+ return null;
44
+ }
45
+
46
+ const OAUTH_STALE_KV_PATH_RE = /\/(clientId|clientSecret|tokenUrl|authorizationUrl)$/i;
47
+
48
+ /**
49
+ * @param {string[]} missing
50
+ * @returns {boolean}
51
+ */
52
+ function hasStaleOAuthTemplatePaths(missing) {
53
+ return missing.some((ref) => OAUTH_STALE_KV_PATH_RE.test(String(ref)));
54
+ }
55
+
56
+ /**
57
+ * @param {string} key
58
+ * @param {Object} secrets
59
+ * @returns {boolean}
60
+ */
61
+ function isPopulatedSecretKey(key, secrets) {
62
+ const value = secrets[key];
63
+ return value !== undefined && value !== null && String(value).trim() !== '';
64
+ }
65
+
66
+ /**
67
+ * Other populated secret paths that share the same path segment after the namespace.
68
+ *
69
+ * @param {string} pathStr - Secret path without kv:// (e.g. my-app-e2e/clientId)
70
+ * @param {Object} secrets
71
+ * @returns {string[]}
72
+ */
73
+ function findPopulatedAlternateKvPaths(pathStr, secrets) {
74
+ if (!secrets || typeof secrets !== 'object' || !pathStr) {
75
+ return [];
76
+ }
77
+ const slash = pathStr.indexOf('/');
78
+ if (slash <= 0) {
79
+ return [];
80
+ }
81
+ const missingNamespace = pathStr.slice(0, slash);
82
+ const segment = pathStr.slice(slash + 1);
83
+ return Object.keys(secrets).filter((key) => {
84
+ if (typeof key !== 'string' || !key.includes('/')) {
85
+ return false;
86
+ }
87
+ const keySlash = key.indexOf('/');
88
+ const namespace = key.slice(0, keySlash);
89
+ const keySegment = key.slice(keySlash + 1);
90
+ return (
91
+ keySegment === segment &&
92
+ namespace !== missingNamespace &&
93
+ isPopulatedSecretKey(key, secrets)
94
+ );
95
+ });
96
+ }
97
+
98
+ /**
99
+ * @param {string[]} missing - kv:// refs
100
+ * @param {Object} [secrets]
101
+ * @param {string} [appName]
102
+ * @returns {string[]}
103
+ */
104
+ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
105
+ const lines = [];
106
+ const seen = new Set();
107
+ for (const ref of missing) {
108
+ const pathStr = kvRefToSecretSetKey(ref);
109
+ for (const alt of findPopulatedAlternateKvPaths(pathStr, secrets)) {
110
+ const line = `Found kv://${alt} in secrets; env.template references kv://${pathStr}.`;
111
+ if (seen.has(line)) {
112
+ continue;
113
+ }
114
+ seen.add(line);
115
+ lines.push(line);
116
+ }
117
+ }
118
+ if (lines.length === 0) {
119
+ return [];
120
+ }
121
+ const repairTail = appName
122
+ ? `Use aifabrix secret set <key> for each missing path, or aifabrix repair ${appName} to align env.template.`
123
+ : 'Use aifabrix secret set <key> for each missing path, or repair to align env.template.';
124
+ lines.push(repairTail);
125
+ return lines;
126
+ }
127
+
128
+ /**
129
+ * @param {Object} opts
130
+ * @param {string[]} opts.missing
131
+ * @param {Object|string|null} [opts.secretsFilePaths]
132
+ * @param {string} [opts.appName]
133
+ * @param {string} [opts.envTemplatePath]
134
+ * @param {string} [opts.envTemplate]
135
+ * @param {Object} [opts.secrets]
136
+ * @returns {string}
137
+ */
138
+ function buildMissingSecretsErrorMessage(opts) {
139
+ const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets } = opts;
140
+ const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
141
+ const lines = [`Missing secrets: ${missing.join(', ')}`];
142
+ if (fileInfo) lines.push(fileInfo.trim());
143
+ if (envTemplatePath) lines.push(`Env-template-path: ${envTemplatePath}`);
144
+ for (const ref of missing) {
145
+ const templateLine = findActiveEnvLineForKvRef(envTemplate, ref);
146
+ if (templateLine) lines.push(`Env-template-line: ${ref}|${templateLine}`);
147
+ }
148
+ if (appName) lines.push(`App-name: ${appName}`);
149
+ if (hasStaleOAuthTemplatePaths(missing) && appName) {
150
+ lines.push(`Stale-template-hint: oauth-leftover | repair ${appName}`);
151
+ }
152
+ for (const hint of buildKvNamespaceAlternateHints(missing, secrets, appName)) {
153
+ lines.push(`Alternate-kv-hint: ${hint}`);
154
+ }
155
+ lines.push('Remediation-hint: comment-or-delete-template-line | secret-set');
156
+ return `${lines.join('\n')}\n`;
157
+ }
158
+
159
+ module.exports = {
160
+ kvRefToSecretSetKey,
161
+ findActiveEnvLineForKvRef,
162
+ findPopulatedAlternateKvPaths,
163
+ buildMissingSecretsErrorMessage
164
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Infer which platform service a request URL targets (CLI error copy).
3
+ *
4
+ * @fileoverview Service label heuristics for network error messages
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /** @type {readonly string[]} */
12
+ const DATAPLANE_PATH_PREFIXES = Object.freeze([
13
+ '/api/v1/external',
14
+ '/api/v1/validation',
15
+ '/api/v1/records',
16
+ '/api/v1/wizard',
17
+ '/api/v1/admin',
18
+ '/api/v1/health',
19
+ '/api/v1/dataplane',
20
+ '/api/v1/policy',
21
+ '/api/v1/mcp',
22
+ '/api/v1/ide',
23
+ '/api/v1/knowledgebase'
24
+ ]);
25
+
26
+ /** @type {readonly string[]} */
27
+ const CONTROLLER_PATH_PREFIXES = Object.freeze([
28
+ '/api/v1/auth',
29
+ '/api/v1/applications',
30
+ '/api/v1/pipeline',
31
+ '/api/v1/environments',
32
+ '/api/v1/deployments',
33
+ '/api/v1/datasources',
34
+ '/api/v1/external-systems',
35
+ '/api/controller'
36
+ ]);
37
+
38
+ /**
39
+ * @param {string} url - Full request URL
40
+ * @returns {'dataplane'|'controller'|'service'}
41
+ */
42
+ function inferServiceLabelFromUrl(url) {
43
+ if (!url || typeof url !== 'string') {
44
+ return 'service';
45
+ }
46
+ try {
47
+ const path = new URL(url.trim()).pathname || '';
48
+ for (const prefix of DATAPLANE_PATH_PREFIXES) {
49
+ if (path.startsWith(prefix)) {
50
+ return 'dataplane';
51
+ }
52
+ }
53
+ for (const prefix of CONTROLLER_PATH_PREFIXES) {
54
+ if (path.startsWith(prefix)) {
55
+ return 'controller';
56
+ }
57
+ }
58
+ } catch {
59
+ return 'service';
60
+ }
61
+ return 'service';
62
+ }
63
+
64
+ /**
65
+ * @param {'dataplane'|'controller'|'service'} label
66
+ * @returns {string}
67
+ */
68
+ function serviceLabelDisplayName(label) {
69
+ if (label === 'dataplane') {
70
+ return 'dataplane';
71
+ }
72
+ if (label === 'controller') {
73
+ return 'controller';
74
+ }
75
+ return 'service';
76
+ }
77
+
78
+ module.exports = {
79
+ inferServiceLabelFromUrl,
80
+ serviceLabelDisplayName
81
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Suppress verbose CLI detail during `aifabrix setup` (guided milestones only).
3
+ *
4
+ * @fileoverview Setup quiet output flag
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * @returns {boolean} True while `aifabrix setup` is running
13
+ */
14
+ function isSetupQuietOutput() {
15
+ try {
16
+ const { isSetupCommandFlow } = require('../commands/setup-run-context');
17
+ return isSetupCommandFlow();
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ module.exports = {
24
+ isSetupQuietOutput
25
+ };
@@ -110,6 +110,20 @@ function updateDatabaseConfig(variables, options, appName) {
110
110
  * @param {Object} options - CLI options
111
111
  * @param {Object} config - Final configuration
112
112
  */
113
+ async function patchEnvTemplateRelativePathKey(appPath, appName) {
114
+ const fsSync = require('fs');
115
+ const envTemplatePath = path.join(appPath, 'env.template');
116
+ if (!fsSync.existsSync(envTemplatePath)) {
117
+ return;
118
+ }
119
+ const { renameAppRelativePathEnvKeyInTemplate } = require('./app-relative-path-env');
120
+ const raw = await fs.readFile(envTemplatePath, 'utf8');
121
+ const patched = renameAppRelativePathEnvKeyInTemplate(raw, appName);
122
+ if (patched !== raw) {
123
+ await fs.writeFile(envTemplatePath, patched, 'utf8');
124
+ }
125
+ }
126
+
113
127
  async function updateTemplateVariables(appPath, appName, options, config) {
114
128
  const { resolveApplicationConfigPath } = require('./app-config-resolver');
115
129
  const { loadConfigFile, writeConfigFile } = require('./config-format');
@@ -123,6 +137,7 @@ async function updateTemplateVariables(appPath, appName, options, config) {
123
137
  updateDatabaseConfig(variables, options, appName);
124
138
 
125
139
  writeConfigFile(variablesPath, variables);
140
+ await patchEnvTemplateRelativePathKey(appPath, appName);
126
141
  } catch (error) {
127
142
  if (error.message && !error.message.includes('not found')) {
128
143
  logger.warn(chalk.yellow(`⚠ Warning: Could not update application config: ${error.message}`));