@aifabrix/builder 2.44.4 → 2.44.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +68 -17
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +44 -12
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Walk ancestors of `startDir` for `<dir>/.aifabrix/config.yaml`.
3
+ *
4
+ * @fileoverview Config directory discovery from cwd
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+
13
+ /**
14
+ * @param {string} startDir
15
+ * @param {(p: string) => boolean} existsSyncFn
16
+ * @returns {string|null} Absolute path to `.aifabrix` directory containing `config.yaml`
17
+ */
18
+ function findAifabrixConfigDirFromAncestors(startDir, existsSyncFn) {
19
+ if (!startDir || typeof startDir !== 'string') {
20
+ return null;
21
+ }
22
+ let dir = path.resolve(startDir);
23
+ const maxSteps = 64;
24
+ for (let i = 0; i < maxSteps; i += 1) {
25
+ const candidate = path.join(dir, '.aifabrix', 'config.yaml');
26
+ if (existsSyncFn(candidate)) {
27
+ return path.join(dir, '.aifabrix');
28
+ }
29
+ const parent = path.dirname(dir);
30
+ if (parent === dir) {
31
+ break;
32
+ }
33
+ dir = parent;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ module.exports = {
39
+ findAifabrixConfigDirFromAncestors
40
+ };
@@ -3,8 +3,8 @@
3
3
  * Shared by `paths.getConfigDirForPaths` and `config.getConfigDir` (no circular imports).
4
4
  *
5
5
  * When `AIFABRIX_HOME` is set to the POSIX home (builder-server pattern) but the real
6
- * config file is under `~/.aifabrix/config.yaml`, use that nested directory so
7
- * `secrets.local.yaml` and auth config stay beside `config.yaml`.
6
+ * config file is under `~/.aifabrix/config.yaml`, use that nested directory.
7
+ * With no env override, walks up from cwd for `<ancestor>/.aifabrix/config.yaml`, else `~/.aifabrix`.
8
8
  *
9
9
  * Relative `AIFABRIX_HOME` / `AIFABRIX_CONFIG` values are anchored to the user home
10
10
  * directory (not `process.cwd()`), so a mistaken `aifabrix-training` does not become
@@ -21,6 +21,7 @@
21
21
  const { existsSync } = require('../internal/fs-real-sync');
22
22
  const path = require('path');
23
23
  const os = require('os');
24
+ const { findAifabrixConfigDirFromAncestors } = require('./aifabrix-config-dir-walk');
24
25
 
25
26
  /**
26
27
  * @returns {string}
@@ -102,6 +103,23 @@ function resolveAifabrixConfigEnvPath(raw) {
102
103
  return resolveAifabrixHomeLikePath(raw);
103
104
  }
104
105
 
106
+ /**
107
+ * When neither `AIFABRIX_CONFIG` nor `AIFABRIX_HOME` is set, look for `<ancestor>/.aifabrix/config.yaml`
108
+ * walking up from `process.cwd()`. Skipped under Jest so suites do not pick up the workspace config.
109
+ *
110
+ * @returns {string|null} Directory containing `config.yaml`, or null
111
+ */
112
+ function findAifabrixConfigDirWalkingUpFromCwd() {
113
+ if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
114
+ return null;
115
+ }
116
+ try {
117
+ return findAifabrixConfigDirFromAncestors(process.cwd(), existsSync);
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
105
123
  /**
106
124
  * @returns {string} Absolute directory containing `config.yaml`
107
125
  */
@@ -122,11 +140,16 @@ function getAifabrixRuntimeConfigDir() {
122
140
  }
123
141
  return homeDir;
124
142
  }
143
+ const fromCwd = findAifabrixConfigDirWalkingUpFromCwd();
144
+ if (fromCwd) {
145
+ return fromCwd;
146
+ }
125
147
  return path.join(safeHomedir(), '.aifabrix');
126
148
  }
127
149
 
128
150
  module.exports = {
129
151
  getAifabrixRuntimeConfigDir,
130
152
  resolveAifabrixHomeLikePath,
131
- resolveAifabrixConfigEnvPath
153
+ resolveAifabrixConfigEnvPath,
154
+ findAifabrixConfigDirWalkingUpFromCwd
132
155
  };
@@ -1,4 +1,4 @@
1
- const { formatSuccessLine } = require('./cli-test-layout-chalk');
1
+ const { formatSuccessLine, formatProgress } = require('./cli-test-layout-chalk');
2
2
  /**
3
3
  * AI Fabrix Builder - App Run Container Helpers
4
4
  *
@@ -118,7 +118,7 @@ async function checkContainerRunning(appName, developerId, debug = false, scopeO
118
118
  async function stopAndRemoveContainer(appName, developerId, debug = false, scopeOpts = null) {
119
119
  try {
120
120
  const containerName = getContainerName(appName, developerId, scopeOpts);
121
- logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
121
+ logger.log(formatProgress(`Stopping container ${containerName}…`));
122
122
  const stopCmd = `docker stop ${containerName}`;
123
123
  if (debug) {
124
124
  logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Map secrets keys `BASH_<NAME>` to process-style env `{ [NAME]: value }` for child_process env.
3
+ * Same naming rule as kv://BASH_* resolution (see secrets-bash-kv.js).
4
+ *
5
+ * @fileoverview BASH-prefixed secret → exported-style env for Docker/subprocess
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const secretsLoad = require('../core/secrets-load');
13
+
14
+ /**
15
+ * @param {string} suffix - Part after BASH_
16
+ * @returns {boolean}
17
+ */
18
+ function isValidExportedName(suffix) {
19
+ return Boolean(suffix && /^[A-Za-z_][A-Za-z0-9_]*$/.test(suffix));
20
+ }
21
+
22
+ /**
23
+ * Collect `BASH_*` entries from a flat or shallow secrets object.
24
+ *
25
+ * @param {Record<string, unknown>|null|undefined} secrets - Merged secrets map (decrypted)
26
+ * @returns {Record<string, string>} e.g. { NPM_TOKEN: '...' } from BASH_NPM_TOKEN
27
+ */
28
+ function collectBashPrefixedEnv(secrets) {
29
+ const out = {};
30
+ if (!secrets || typeof secrets !== 'object') return out;
31
+ for (const [k, v] of Object.entries(secrets)) {
32
+ if (typeof k !== 'string' || !k.startsWith('BASH_')) continue;
33
+ if (v === undefined || v === null) continue;
34
+ const str = typeof v === 'string' ? v.trim() : String(v).trim();
35
+ if (!str) continue;
36
+ const suffix = k.slice(5);
37
+ if (!isValidExportedName(suffix)) continue;
38
+ out[suffix] = str;
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /**
44
+ * Overlay for `child_process` `env`: values from user + shared + ancestor `secrets.local.yaml`
45
+ * for every `BASH_*` key (merged via {@link secretsLoad.loadSecrets}).
46
+ *
47
+ * @param {string|null} [secretsPath] - Optional explicit secrets file (same as resolve)
48
+ * @param {string|null} [appName] - Optional app name for loadSecrets second arg
49
+ * @returns {Promise<Record<string, string>>}
50
+ */
51
+ async function getBashPrefixedProcessEnvOverlay(secretsPath = null, appName = null) {
52
+ const secrets = await secretsLoad.loadSecrets(secretsPath, appName);
53
+ return collectBashPrefixedEnv(secrets);
54
+ }
55
+
56
+ module.exports = {
57
+ collectBashPrefixedEnv,
58
+ getBashPrefixedProcessEnvOverlay
59
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @fileoverview Missing-secrets CLI error lines (layout colors via cli-layout-chalk).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const { metadata, sectionTitle } = require('./cli-layout-chalk');
11
+
12
+ /**
13
+ * @param {string[]} messages - Output lines
14
+ * @param {RegExpMatchArray|null} missingSecretsMatch - Missing secrets capture
15
+ */
16
+ function pushMissingSecretsBodyLines(messages, missingSecretsMatch) {
17
+ if (missingSecretsMatch) {
18
+ const parts = missingSecretsMatch[1]
19
+ .trim()
20
+ .split(',')
21
+ .map((s) => s.trim())
22
+ .filter(Boolean);
23
+ if (parts.length > 1) {
24
+ messages.push(` ${sectionTitle('Missing secrets:')}`);
25
+ for (const ref of parts) {
26
+ messages.push(` ${chalk.cyan('-')} ${chalk.white(ref)}`);
27
+ }
28
+ return;
29
+ }
30
+ if (parts.length === 1) {
31
+ messages.push(` ${sectionTitle('Missing secrets:')} ${chalk.white(parts[0])}`);
32
+ return;
33
+ }
34
+ messages.push(` ${sectionTitle('Missing secrets:')}`);
35
+ return;
36
+ }
37
+ messages.push(` ${chalk.white('Missing secrets in secrets file.')}`);
38
+ }
39
+
40
+ /**
41
+ * @param {string[]} messages - Output lines
42
+ * @param {RegExpMatchArray|null} fileInfoMatch - File location capture
43
+ * @param {RegExpMatchArray|null} resolveMatch - Resolve command capture
44
+ */
45
+ function pushSecretsResolutionHintLines(messages, fileInfoMatch, resolveMatch) {
46
+ if (fileInfoMatch) {
47
+ messages.push(` ${metadata('Secrets file location:')} ${chalk.white(fileInfoMatch[1])}`);
48
+ }
49
+ if (resolveMatch) {
50
+ messages.push(
51
+ ` ${metadata('Run:')} ${chalk.yellow(`aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`)}`
52
+ );
53
+ return;
54
+ }
55
+ messages.push(` ${metadata('Run:')} ${chalk.yellow('aifabrix resolve <app-name> to generate missing secrets.')}`);
56
+ }
57
+
58
+ /**
59
+ * Format secrets-related errors
60
+ * @param {string} errorMsg - Error message
61
+ * @returns {string[]|null} Array of error message lines or null if not a secrets error
62
+ */
63
+ function formatSecretsError(errorMsg) {
64
+ if (!errorMsg.includes('Missing secrets')) {
65
+ return null;
66
+ }
67
+
68
+ const messages = [];
69
+ pushMissingSecretsBodyLines(messages, errorMsg.match(/Missing secrets: ([^\n]+)/));
70
+ pushSecretsResolutionHintLines(
71
+ messages,
72
+ errorMsg.match(/Secrets file location: ([^\n]+)/),
73
+ errorMsg.match(/Run "aifabrix resolve ([^"]+)"/)
74
+ );
75
+ return messages;
76
+ }
77
+
78
+ module.exports = { formatSecretsError };
@@ -7,6 +7,18 @@
7
7
 
8
8
  const chalk = require('chalk');
9
9
 
10
+ /**
11
+ * Invoke chalk[method](text) when the mock/real chalk exposes that method; otherwise return text.
12
+ * Keeps layout helpers working in Jest suites that only stub a subset of chalk.
13
+ * @param {string} method - chalk method name (e.g. 'white', 'bold', 'gray')
14
+ * @param {string} text
15
+ * @returns {string}
16
+ */
17
+ function chalkStyle(method, text) {
18
+ const fn = chalk[method];
19
+ return typeof fn === 'function' ? fn(text) : text;
20
+ }
21
+
10
22
  /** Canonical success glyph (green), layout §CORE / §3. */
11
23
  function successGlyph() {
12
24
  return chalk.green('āœ”');
@@ -35,6 +47,15 @@ function formatSuccessParagraph(message) {
35
47
  return chalk.green(`\nāœ” ${message}`);
36
48
  }
37
49
 
50
+ /**
51
+ * Non-blocking warning (layout §CORE): yellow ⚠ + white detail.
52
+ * @param {string} message - text after the glyph (do not prefix with ⚠)
53
+ * @returns {string}
54
+ */
55
+ function formatWarningLine(message) {
56
+ return `${chalkStyle('yellow', '⚠')} ${chalkStyle('white', message)}`;
57
+ }
58
+
38
59
  /**
39
60
  * Blocking error line: red āœ– + red message (layout §18).
40
61
  * @param {string} message
@@ -65,7 +86,7 @@ function formatIssue(title, hint) {
65
86
  */
66
87
  function formatNextActions(lines) {
67
88
  const body = (lines || [])
68
- .map(line => `${chalk.cyan('-')} ${chalk.white(line)}`)
89
+ .map(line => `${chalkStyle('cyan', '-')} ${chalkStyle('white', line)}`)
69
90
  .join('\n');
70
91
  return `${sectionTitle('Next actions:')}\n${body}`;
71
92
  }
@@ -87,7 +108,7 @@ function formatDocsLine(label, url) {
87
108
  * @returns {string}
88
109
  */
89
110
  function formatProgress(message) {
90
- return `${chalk.yellow('ā³')} ${chalk.white(message)}`;
111
+ return `${chalkStyle('yellow', 'ā³')} ${chalkStyle('white', message)}`;
91
112
  }
92
113
 
93
114
  /**
@@ -99,7 +120,7 @@ function formatProgress(message) {
99
120
  */
100
121
  function formatBulletSection(title, items, opts) {
101
122
  const bulletColor = opts && opts.bullet === 'red' ? chalk.red : chalk.cyan;
102
- const body = (items || []).map(line => `${bulletColor('-')} ${chalk.white(line)}`).join('\n');
123
+ const body = (items || []).map(line => `${bulletColor('-')} ${chalkStyle('white', line)}`).join('\n');
103
124
  return `${sectionTitle(title)}\n${body}`;
104
125
  }
105
126
 
@@ -109,7 +130,7 @@ function formatBulletSection(title, items, opts) {
109
130
  * @returns {string}
110
131
  */
111
132
  function sectionTitle(text) {
112
- return chalk.white.bold(text);
133
+ return chalkStyle('bold', chalkStyle('white', text));
113
134
  }
114
135
 
115
136
  /**
@@ -119,7 +140,7 @@ function sectionTitle(text) {
119
140
  * @returns {string}
120
141
  */
121
142
  function headerKeyValue(label, value) {
122
- return `${chalk.gray(label)} ${chalk.white.bold(value)}`;
143
+ return `${chalkStyle('gray', label)} ${chalkStyle('bold', chalkStyle('white', value))}`;
123
144
  }
124
145
 
125
146
  /**
@@ -201,7 +222,7 @@ function formatDatasourceListRow(rowStatus, name, statusHint) {
201
222
  ? chalk.red('āœ–')
202
223
  : chalk.gray('ā­');
203
224
  const hint = statusHint ? ` ${chalk.gray(`(${statusHint})`)}` : '';
204
- return ` ${sym} ${chalk.white(name)}${hint}`;
225
+ return ` ${sym} ${chalkStyle('white', name)}${hint}`;
205
226
  }
206
227
 
207
228
  /**
@@ -232,13 +253,13 @@ function colorRollupPrefixedLine(line) {
232
253
  const trimmed = line.trimStart();
233
254
  const first = trimmed[0];
234
255
  if (first === 'āœ”') {
235
- return chalk.green('āœ”') + chalk.white(trimmed.slice(1));
256
+ return chalk.green('āœ”') + chalkStyle('white', trimmed.slice(1));
236
257
  }
237
258
  if (first === '⚠') {
238
- return chalk.yellow('⚠') + chalk.white(trimmed.slice(1));
259
+ return chalk.yellow('⚠') + chalkStyle('white', trimmed.slice(1));
239
260
  }
240
261
  if (first === 'āœ–') {
241
- return chalk.red('āœ–') + chalk.white(trimmed.slice(1));
262
+ return chalk.red('āœ–') + chalkStyle('white', trimmed.slice(1));
242
263
  }
243
264
  return line;
244
265
  }
@@ -258,6 +279,7 @@ module.exports = {
258
279
  failureGlyph,
259
280
  formatSuccessLine,
260
281
  formatSuccessParagraph,
282
+ formatWarningLine,
261
283
  formatBlockingError,
262
284
  formatIssue,
263
285
  formatNextActions,
@@ -11,6 +11,8 @@
11
11
  const path = require('path');
12
12
  const chalk = require('chalk');
13
13
  const logger = require('./logger');
14
+ const { formatBlockingError, infoLine } = require('./cli-layout-chalk');
15
+ const { formatSecretsError } = require('./cli-secrets-error-format');
14
16
  const { getDockerDaemonStartHintSentence, getDockerApiOverTcpHintLines } = require('./docker-not-running-hint');
15
17
 
16
18
  /**
@@ -231,40 +233,6 @@ function formatAzureError(errorMsg) {
231
233
  return null;
232
234
  }
233
235
 
234
- /**
235
- * Format secrets-related errors
236
- * @param {string} errorMsg - Error message
237
- * @returns {string[]|null} Array of error message lines or null if not a secrets error
238
- */
239
- function formatSecretsError(errorMsg) {
240
- if (!errorMsg.includes('Missing secrets')) {
241
- return null;
242
- }
243
-
244
- const messages = [];
245
- const missingSecretsMatch = errorMsg.match(/Missing secrets: ([^\n]+)/);
246
- const fileInfoMatch = errorMsg.match(/Secrets file location: ([^\n]+)/);
247
- const resolveMatch = errorMsg.match(/Run "aifabrix resolve ([^"]+)"/);
248
-
249
- if (missingSecretsMatch) {
250
- messages.push(` Missing secrets: ${missingSecretsMatch[1]}`);
251
- } else {
252
- messages.push(' Missing secrets in secrets file.');
253
- }
254
-
255
- if (fileInfoMatch) {
256
- messages.push(` Secrets file location: ${fileInfoMatch[1]}`);
257
- }
258
-
259
- if (resolveMatch) {
260
- messages.push(` Run: aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`);
261
- } else {
262
- messages.push(' Run: aifabrix resolve <app-name> to generate missing secrets.');
263
- }
264
-
265
- return messages;
266
- }
267
-
268
236
  /**
269
237
  * Format deployment-related errors
270
238
  * @param {string} errorMsg - Error message
@@ -388,9 +356,9 @@ function formatError(error) {
388
356
  * @param {string[]} errorMessages - Error message lines
389
357
  */
390
358
  function logError(command, errorMessages) {
391
- logger.error(`\nāœ– Error in ${command} command:`);
359
+ logger.error(`\n${formatBlockingError(`Error in ${command} command:`)}`);
392
360
  errorMessages.forEach(msg => logger.error(msg));
393
- logger.error('\nšŸ’” Run "aifabrix doctor" for environment diagnostics.\n');
361
+ logger.log(`\n${infoLine('ℹ Run "aifabrix doctor" for environment diagnostics.')}\n`);
394
362
  }
395
363
 
396
364
  /**
@@ -300,12 +300,20 @@ function appendValidationIssueLines(lines, envelope, maxIssues = 5) {
300
300
  const code = iss && iss.code ? chalk.red(`[${iss.code}] `) : '';
301
301
  const msg = iss && iss.message ? String(iss.message) : JSON.stringify(iss);
302
302
  lines.push(` ${code}${chalk.yellow(msg)}`);
303
+ appendDpSec013Details(lines, iss);
303
304
  }
304
305
  if (issues.length > cap) {
305
306
  lines.push(chalk.gray(` … and ${issues.length - cap} more (see --json or debug full/raw)`));
306
307
  }
307
308
  }
308
309
 
310
+ function appendDpSec013Details(lines, iss) {
311
+ if (!iss || iss.code !== 'DP-SEC-013') return;
312
+ const perm = iss.details && iss.details.resolvedPermission ? String(iss.details.resolvedPermission) : '';
313
+ if (!perm) return;
314
+ lines.push(` ${chalk.gray('Missing permission:')} ${chalk.white(perm)}`);
315
+ }
316
+
309
317
  /**
310
318
  * @param {string[]} lines
311
319
  * @param {Object} envelope
@@ -12,6 +12,7 @@ const path = require('path');
12
12
  const readline = require('readline');
13
13
  const chalk = require('chalk');
14
14
  const { nodeFs } = require('../internal/node-fs');
15
+ const { formatSuccessLine } = require('./cli-layout-chalk');
15
16
 
16
17
  const IPV4_RE = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/;
17
18
 
@@ -238,7 +239,7 @@ async function resolveHostsIpForInit(hostname, hostsIp, logger) {
238
239
  async function appendHostsBlockOrPrintManual(hostsPath, block, line, logger) {
239
240
  try {
240
241
  await nodeFs().promises.appendFile(hostsPath, block, { encoding: 'utf8' });
241
- logger.log(chalk.green(` āœ” Updated ${hostsPath}\n`));
242
+ logger.log(chalk.green(' ') + formatSuccessLine(`Updated ${hostsPath}`) + '\n');
242
243
  } catch (e) {
243
244
  if (e.code === 'EACCES' || e.code === 'EPERM') {
244
245
  logger.log(chalk.yellow(` āœ– Could not write ${hostsPath} (permission denied).`));
@@ -261,7 +262,7 @@ async function appendHostsBlockOrPrintManual(hostsPath, block, line, logger) {
261
262
  async function tryWriteHostsEntry(hostsPath, hostnames, ip, skipConfirm, logger) {
262
263
  const missing = hostnames.filter((h) => !hostsFileHasHostname(hostsPath, h));
263
264
  if (missing.length === 0) {
264
- logger.log(chalk.green(` āœ” Required hostnames are already listed in ${hostsPath}. Nothing to do.\n`));
265
+ logger.log(chalk.green(' ') + formatSuccessLine(`Required hostnames are already listed in ${hostsPath}. Nothing to do.`) + '\n');
265
266
  return;
266
267
  }
267
268
  const line = `${ip} ${missing.join(' ')}`;
@@ -6,6 +6,7 @@ const chalk = require('chalk');
6
6
  const config = require('../core/config');
7
7
  const logger = require('./logger');
8
8
  const { ensureDevSshConfigBlock } = require('./dev-ssh-config-helper');
9
+ const { successGlyph } = require('./cli-layout-chalk');
9
10
 
10
11
  /**
11
12
  * Hostname from Builder Server URL (for sync-ssh-host fallback).
@@ -46,7 +47,7 @@ async function mergeDevSshConfigAfterInit(baseUrl, devId) {
46
47
  );
47
48
  } else {
48
49
  logger.log(
49
- chalk.green(' āœ” SSH config updated: ') +
50
+ `${chalk.green(' ')}${successGlyph()}${chalk.green(' SSH config updated: ')}` +
50
51
  chalk.cyan(`Host ${res.hostAlias}`) +
51
52
  chalk.gray(` → ${res.configPath}`)
52
53
  );
@@ -1,4 +1,4 @@
1
- const { formatSuccessLine } = require('./cli-test-layout-chalk');
1
+ const { formatSuccessLine, headerKeyValue, formatWarningLine } = require('./cli-test-layout-chalk');
2
2
  /**
3
3
  * Docker Build Utilities
4
4
  *
@@ -177,7 +177,6 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
177
177
  const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
178
178
  const fsSync = require('fs');
179
179
  const path = require('path');
180
- const { getRemoteDockerEnv } = require('./remote-docker-env');
181
180
  dockerfilePath = path.resolve(dockerfilePath);
182
181
  contextPath = path.resolve(contextPath);
183
182
 
@@ -196,7 +195,8 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
196
195
  }
197
196
 
198
197
  const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
199
- const remoteEnv = isTest ? {} : await getRemoteDockerEnv();
198
+ const { getDockerExecEnv } = require('./remote-docker-env');
199
+ const dockerCliEnv = isTest ? { ...process.env } : await getDockerExecEnv();
200
200
  const resolvedBuildArgs = buildArgs && typeof buildArgs === 'object' ? buildArgs : {};
201
201
  return new Promise((resolve, reject) => {
202
202
  runDockerBuildProcess({
@@ -207,7 +207,7 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
207
207
  spinner,
208
208
  resolve,
209
209
  reject,
210
- env: remoteEnv,
210
+ env: dockerCliEnv,
211
211
  buildArgs: resolvedBuildArgs,
212
212
  noCache
213
213
  });
@@ -258,14 +258,20 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
258
258
  */
259
259
  async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfilePath, contextPath, tag, options) {
260
260
  const logger = require('../utils/logger');
261
- const chalk = require('chalk');
262
261
 
263
- logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
264
- logger.log(chalk.blue(`Using build context: ${contextPath}`));
262
+ logger.log(headerKeyValue('Dockerfile:', dockerfilePath));
263
+ logger.log(headerKeyValue('Build context:', contextPath));
265
264
 
266
265
  await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
267
266
 
268
- // Back-compat: also tag the built dev image as the base image name
267
+ const wantCompatTag =
268
+ effectiveImageName !== imageName &&
269
+ options &&
270
+ options.base === true;
271
+ if (!wantCompatTag) {
272
+ return;
273
+ }
274
+
269
275
  try {
270
276
  const { promisify } = require('util');
271
277
  const { exec } = require('child_process');
@@ -275,7 +281,9 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
275
281
  await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`, { env });
276
282
  logger.log(formatSuccessLine(`Tagged image: ${imageName}:${tag}`));
277
283
  } catch (err) {
278
- logger.log(chalk.yellow(`⚠ Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
284
+ logger.log(
285
+ formatWarningLine(`Could not create compatibility tag ${imageName}:${tag} - ${err.message}`)
286
+ );
279
287
  }
280
288
  }
281
289