@aifabrix/builder 2.44.5 → 2.44.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +48 -2
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/validation-runner.js +46 -25
  17. package/lib/app/deploy-config.js +11 -1
  18. package/lib/app/deploy-status-display.js +3 -3
  19. package/lib/app/deploy.js +36 -14
  20. package/lib/app/display.js +15 -11
  21. package/lib/app/push.js +46 -23
  22. package/lib/app/register.js +1 -1
  23. package/lib/app/restart-display.js +95 -0
  24. package/lib/app/rotate-secret.js +1 -1
  25. package/lib/app/run-container-start.js +12 -6
  26. package/lib/app/run-env-compose.js +30 -1
  27. package/lib/app/run-helpers.js +44 -12
  28. package/lib/app/run-reload-sync.js +148 -0
  29. package/lib/app/run-resolve-image.js +51 -1
  30. package/lib/app/run.js +99 -73
  31. package/lib/build/index.js +75 -45
  32. package/lib/cli/doctor-check.js +117 -0
  33. package/lib/cli/index.js +8 -2
  34. package/lib/cli/infra-guided.js +445 -0
  35. package/lib/cli/setup-app.js +20 -2
  36. package/lib/cli/setup-auth.js +26 -0
  37. package/lib/cli/setup-dev-path-commands.js +50 -3
  38. package/lib/cli/setup-infra.js +134 -61
  39. package/lib/cli/setup-integration-client.js +182 -0
  40. package/lib/cli/setup-parameters.js +21 -2
  41. package/lib/cli/setup-platform.js +102 -0
  42. package/lib/cli/setup-secrets.js +18 -6
  43. package/lib/cli/setup-utility.js +78 -33
  44. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  45. package/lib/commands/datasource-capability-output.js +29 -0
  46. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  47. package/lib/commands/datasource-capability.js +411 -0
  48. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  49. package/lib/commands/datasource.js +53 -13
  50. package/lib/commands/dev-down.js +3 -3
  51. package/lib/commands/dev-infra-gate.js +32 -0
  52. package/lib/commands/dev-init.js +13 -7
  53. package/lib/commands/dimension-value.js +179 -0
  54. package/lib/commands/dimension.js +330 -0
  55. package/lib/commands/integration-client.js +430 -0
  56. package/lib/commands/login-device.js +65 -30
  57. package/lib/commands/login.js +21 -10
  58. package/lib/commands/parameters-validate.js +78 -13
  59. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  60. package/lib/commands/repair-datasource-keys.js +10 -5
  61. package/lib/commands/repair-datasource.js +19 -7
  62. package/lib/commands/repair-env-template.js +4 -1
  63. package/lib/commands/repair-openapi-sync.js +172 -0
  64. package/lib/commands/repair-persist.js +102 -0
  65. package/lib/commands/repair-rbac-extract.js +27 -0
  66. package/lib/commands/repair-rbac-migrate.js +186 -0
  67. package/lib/commands/repair-rbac.js +214 -31
  68. package/lib/commands/repair-system-alignment.js +246 -0
  69. package/lib/commands/repair-system-permissions.js +168 -0
  70. package/lib/commands/repair.js +120 -338
  71. package/lib/commands/secure.js +1 -1
  72. package/lib/commands/setup-modes.js +455 -0
  73. package/lib/commands/setup-prompts.js +388 -0
  74. package/lib/commands/setup.js +149 -0
  75. package/lib/commands/teardown.js +228 -0
  76. package/lib/commands/up-common.js +79 -19
  77. package/lib/commands/up-dataplane.js +33 -11
  78. package/lib/commands/up-miso.js +7 -11
  79. package/lib/commands/upload.js +109 -23
  80. package/lib/commands/wizard-core-helpers.js +14 -11
  81. package/lib/commands/wizard-core.js +6 -5
  82. package/lib/commands/wizard-dataplane.js +2 -2
  83. package/lib/commands/wizard-entity-selection.js +4 -3
  84. package/lib/commands/wizard-headless.js +2 -1
  85. package/lib/commands/wizard.js +2 -1
  86. package/lib/constants/infra-compose-service-names.js +40 -0
  87. package/lib/core/env-reader.js +16 -3
  88. package/lib/core/secrets-admin-env.js +101 -0
  89. package/lib/core/secrets-ensure-infra.js +34 -1
  90. package/lib/core/secrets-ensure.js +88 -66
  91. package/lib/core/secrets-env-content.js +432 -0
  92. package/lib/core/secrets-env-write.js +27 -1
  93. package/lib/core/secrets-load.js +248 -0
  94. package/lib/core/secrets-names.js +32 -0
  95. package/lib/core/secrets.js +17 -757
  96. package/lib/datasource/capability/basic-exposure.js +76 -0
  97. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  98. package/lib/datasource/capability/capability-key.js +34 -0
  99. package/lib/datasource/capability/capability-resolve.js +172 -0
  100. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  101. package/lib/datasource/capability/copy-operations.js +348 -0
  102. package/lib/datasource/capability/copy-test-payload.js +139 -0
  103. package/lib/datasource/capability/create-operations.js +235 -0
  104. package/lib/datasource/capability/dimension-operations.js +151 -0
  105. package/lib/datasource/capability/dimension-validate.js +219 -0
  106. package/lib/datasource/capability/json-pointer.js +31 -0
  107. package/lib/datasource/capability/reference-rewrite.js +51 -0
  108. package/lib/datasource/capability/relate-operations.js +325 -0
  109. package/lib/datasource/capability/relate-validate.js +219 -0
  110. package/lib/datasource/capability/remove-operations.js +275 -0
  111. package/lib/datasource/capability/run-capability-copy.js +152 -0
  112. package/lib/datasource/capability/run-capability-diff.js +135 -0
  113. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  114. package/lib/datasource/capability/run-capability-edit.js +377 -0
  115. package/lib/datasource/capability/run-capability-relate.js +193 -0
  116. package/lib/datasource/capability/run-capability-remove.js +105 -0
  117. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  118. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  119. package/lib/datasource/list.js +136 -23
  120. package/lib/datasource/log-viewer.js +2 -4
  121. package/lib/datasource/unified-validation-run.js +51 -16
  122. package/lib/datasource/validate.js +53 -1
  123. package/lib/deployment/deploy-poll-ui.js +60 -0
  124. package/lib/deployment/deployer-status.js +29 -3
  125. package/lib/deployment/deployer.js +48 -30
  126. package/lib/deployment/environment.js +7 -2
  127. package/lib/deployment/poll-interval.js +72 -0
  128. package/lib/deployment/push.js +11 -9
  129. package/lib/external-system/deploy.js +4 -1
  130. package/lib/external-system/download.js +61 -32
  131. package/lib/external-system/sync-deploy-manifest.js +33 -0
  132. package/lib/infrastructure/index.js +49 -19
  133. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  134. package/lib/parameters/infra-kv-discovery.js +29 -4
  135. package/lib/parameters/infra-parameter-catalog.js +6 -3
  136. package/lib/parameters/infra-parameter-validate.js +67 -19
  137. package/lib/resolvers/datasource-resolver.js +53 -0
  138. package/lib/resolvers/dimension-file.js +52 -0
  139. package/lib/resolvers/manifest-resolver.js +133 -0
  140. package/lib/schema/external-datasource.schema.json +183 -53
  141. package/lib/schema/external-system.schema.json +23 -10
  142. package/lib/schema/infra.parameter.yaml +26 -11
  143. package/lib/schema/wizard-config.schema.json +1 -1
  144. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  145. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  146. package/lib/utils/app-run-containers.js +2 -2
  147. package/lib/utils/bash-secret-env.js +59 -0
  148. package/lib/utils/cli-secrets-error-format.js +78 -0
  149. package/lib/utils/cli-test-layout-chalk.js +31 -9
  150. package/lib/utils/cli-utils.js +4 -36
  151. package/lib/utils/datasource-test-run-display.js +8 -0
  152. package/lib/utils/dev-hosts-helper.js +3 -2
  153. package/lib/utils/dev-init-ssh-merge.js +2 -1
  154. package/lib/utils/docker-build.js +17 -9
  155. package/lib/utils/docker-reload-mount.js +127 -0
  156. package/lib/utils/external-readme.js +71 -2
  157. package/lib/utils/external-system-local-test-tty.js +3 -2
  158. package/lib/utils/external-system-readiness-core.js +45 -12
  159. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  160. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  161. package/lib/utils/external-system-readiness-display.js +10 -1
  162. package/lib/utils/file-upload.js +40 -3
  163. package/lib/utils/health-check-db-init.js +107 -0
  164. package/lib/utils/health-check-public-warn.js +69 -0
  165. package/lib/utils/health-check-url.js +19 -4
  166. package/lib/utils/health-check.js +135 -105
  167. package/lib/utils/help-builder.js +5 -1
  168. package/lib/utils/image-name.js +34 -7
  169. package/lib/utils/integration-file-backup.js +74 -0
  170. package/lib/utils/mutagen-install.js +30 -3
  171. package/lib/utils/paths.js +108 -25
  172. package/lib/utils/postgres-wipe.js +212 -0
  173. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  174. package/lib/utils/remote-dev-auth.js +21 -5
  175. package/lib/utils/remote-docker-env.js +9 -1
  176. package/lib/utils/remote-secrets-loader.js +42 -3
  177. package/lib/utils/resolve-docker-image-ref.js +9 -3
  178. package/lib/utils/secrets-ancestor-paths.js +47 -0
  179. package/lib/utils/secrets-helpers.js +17 -10
  180. package/lib/utils/secrets-kv-refs.js +42 -0
  181. package/lib/utils/secrets-kv-scope.js +19 -2
  182. package/lib/utils/secrets-materialize-local.js +134 -0
  183. package/lib/utils/secrets-path.js +24 -10
  184. package/lib/utils/secrets-utils.js +2 -2
  185. package/lib/utils/system-builder-root.js +34 -0
  186. package/lib/utils/url-declarative-resolve-build.js +6 -1
  187. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  188. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  189. package/lib/utils/urls-local-registry.js +23 -12
  190. package/lib/utils/validation-poll-ui.js +81 -0
  191. package/lib/utils/validation-run-poll.js +29 -5
  192. package/lib/utils/with-muted-logger.js +53 -0
  193. package/package.json +1 -1
  194. package/templates/applications/dataplane/application.yaml +1 -1
  195. package/templates/applications/dataplane/rbac.yaml +10 -10
  196. package/templates/applications/keycloak/env.template +8 -6
  197. package/templates/applications/miso-controller/application.yaml +7 -0
  198. package/templates/applications/miso-controller/env.template +1 -1
  199. package/templates/applications/miso-controller/rbac.yaml +9 -9
  200. package/templates/external-system/README.md.hbs +83 -123
  201. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  202. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  203. package/.nyc_output/processinfo/index.json +0 -1
  204. package/lib/api/service-users.api.js +0 -150
  205. package/lib/api/types/service-users.types.js +0 -65
  206. package/lib/cli/setup-service-user.js +0 -187
  207. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,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(`\nError 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
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Decide whether `aifabrix run --reload` can bind-mount workspace without Mutagen.
3
+ * Mutagen is only needed when the CLI filesystem and the Docker engine host differ.
4
+ *
5
+ * @fileoverview Co-located Docker detection for reload mounts
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const os = require('os');
13
+
14
+ /**
15
+ * @param {string} host
16
+ * @returns {string}
17
+ */
18
+ function normalizeHost(host) {
19
+ return String(host || '')
20
+ .trim()
21
+ .toLowerCase()
22
+ .replace(/^\[|\]$/g, '');
23
+ }
24
+
25
+ /**
26
+ * @param {string} host
27
+ * @returns {boolean}
28
+ */
29
+ function isLocalLoopbackHost(host) {
30
+ const h = normalizeHost(host);
31
+ return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0:0:0:0:0:0:0:1';
32
+ }
33
+
34
+ /**
35
+ * @param {string} rest - after "tcp://"
36
+ * @returns {string|null}
37
+ */
38
+ function tcpHostFromRest(rest) {
39
+ if (rest.startsWith('[')) {
40
+ const end = rest.indexOf(']');
41
+ if (end === -1) return null;
42
+ return normalizeHost(rest.slice(1, end));
43
+ }
44
+ const hostPort = rest.split('/')[0];
45
+ const colonIdx = hostPort.lastIndexOf(':');
46
+ if (colonIdx === -1) {
47
+ return normalizeHost(hostPort);
48
+ }
49
+ const maybePort = hostPort.slice(colonIdx + 1);
50
+ if (/^\d+$/.test(maybePort)) {
51
+ return normalizeHost(hostPort.slice(0, colonIdx));
52
+ }
53
+ return normalizeHost(hostPort);
54
+ }
55
+
56
+ /**
57
+ * Extract host from docker-endpoint (tcp, optional URL form).
58
+ * @param {string} endpoint
59
+ * @returns {string|null} normalized host, or null for unix socket paths / empty
60
+ */
61
+ function extractHostFromDockerEndpoint(endpoint) {
62
+ const s = String(endpoint || '').trim();
63
+ if (!s) return null;
64
+ const lower = s.toLowerCase();
65
+ if (lower.startsWith('unix:')) {
66
+ return null;
67
+ }
68
+ if (lower.startsWith('tcp://')) {
69
+ return tcpHostFromRest(s.slice(6));
70
+ }
71
+ try {
72
+ const u = new URL(s.includes('://') ? s : `tcp://${s}`);
73
+ return normalizeHost(u.hostname);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * sync-ssh-host localhost check (same rules as run.js isLocalhostHost for reload gate).
81
+ * @param {string} host
82
+ * @returns {boolean}
83
+ */
84
+ function isLocalhostSyncSshHost(host) {
85
+ if (!host || typeof host !== 'string') return false;
86
+ const h = host.trim().toLowerCase();
87
+ return h === 'localhost' || h === '127.0.0.1';
88
+ }
89
+
90
+ /**
91
+ * True when docker API host is this machine (first label or FQDN match).
92
+ * @param {string} dockerHost - from extractHostFromDockerEndpoint
93
+ * @returns {boolean}
94
+ */
95
+ function hostnameMatchesDockerHost(dockerHost) {
96
+ const h = normalizeHost(dockerHost);
97
+ if (!h) return true;
98
+ if (isLocalLoopbackHost(h)) return true;
99
+ const hn = normalizeHost(os.hostname());
100
+ if (h === hn) return true;
101
+ const hnShort = hn.split('.')[0];
102
+ const hShort = h.split('.')[0];
103
+ if (h === hnShort || hn === hShort) return true;
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * When true, use a direct bind mount for --reload; Mutagen is not required.
109
+ * @param {string|null|undefined} dockerEndpoint - config `docker-endpoint`
110
+ * @returns {boolean}
111
+ */
112
+ function isReloadBindMountOnEngineHost(dockerEndpoint) {
113
+ const e = String(dockerEndpoint || '').trim();
114
+ if (!e) return true;
115
+ if (e.toLowerCase().startsWith('unix:')) return true;
116
+ const host = extractHostFromDockerEndpoint(e);
117
+ if (host === null) {
118
+ return false;
119
+ }
120
+ return hostnameMatchesDockerHost(host);
121
+ }
122
+
123
+ module.exports = {
124
+ extractHostFromDockerEndpoint,
125
+ isReloadBindMountOnEngineHost,
126
+ isLocalhostSyncSshHost
127
+ };
@@ -15,6 +15,72 @@ const path = require('path');
15
15
  const handlebars = require('handlebars');
16
16
  const { getProjectRoot } = require('./paths');
17
17
 
18
+ /**
19
+ * Extracts secret keys from integration/<appName>/env.template when present.
20
+ * Looks for values like `kv://systemKey/apiKey` and returns that key for
21
+ * `aifabrix secret set <key> <value>`.
22
+ *
23
+ * @param {object} args
24
+ * @param {string} args.projectRoot
25
+ * @param {string} args.appName
26
+ * @returns {Array<{path: string, description: string}>}
27
+ */
28
+ function extractSecretPathsFromEnvTemplate({ projectRoot, appName }) {
29
+ if (!projectRoot || !appName) return [];
30
+
31
+ const envTemplatePath = path.join(projectRoot, 'integration', appName, 'env.template');
32
+ if (!fs.existsSync(envTemplatePath)) return [];
33
+
34
+ const raw = _tryReadTextOrNull(envTemplatePath);
35
+ if (raw === null) return [];
36
+ if (typeof raw !== 'string') return [];
37
+
38
+ function parseSecretKey(value) {
39
+ if (!value || typeof value !== 'string') return null;
40
+ const kvIdx = value.indexOf('kv://');
41
+ if (kvIdx === -1) return null;
42
+ const after = value.slice(kvIdx + 'kv://'.length);
43
+ const key = after.split(/[ \t#]/)[0]?.trim();
44
+ return key || null;
45
+ }
46
+
47
+ function parseLine(line) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith('#')) return null;
50
+ const eqIdx = trimmed.indexOf('=');
51
+ if (eqIdx <= 0) return null;
52
+ const varName = trimmed.slice(0, eqIdx).trim();
53
+ const value = trimmed.slice(eqIdx + 1).trim();
54
+ const key = parseSecretKey(value);
55
+ if (!key) return null;
56
+ return { varName: varName || 'Secret', key };
57
+ }
58
+
59
+ const out = [];
60
+ const seen = new Set();
61
+
62
+ for (const line of raw.split('\n')) {
63
+ const parsed = parseLine(line);
64
+ if (!parsed) continue;
65
+ const dedupeKey = `${parsed.varName}::${parsed.key}`;
66
+ if (seen.has(dedupeKey)) continue;
67
+ seen.add(dedupeKey);
68
+ out.push({ path: parsed.key, description: parsed.varName });
69
+ }
70
+
71
+ return out;
72
+ }
73
+
74
+ function _tryReadTextOrNull(p) {
75
+ try {
76
+ return fs.readFileSync(p, 'utf8');
77
+ } catch (e) {
78
+ // Some Jest suites partially mock fs.existsSync; treat missing files as absent templates.
79
+ if (e && e.code === 'ENOENT') return null;
80
+ throw e;
81
+ }
82
+ }
83
+
18
84
  /**
19
85
  * Formats a display name from a key
20
86
  * @param {string} key - System or app key
@@ -179,7 +245,9 @@ function buildExternalReadmeContext(params = {}) {
179
245
  const normalizedExt = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
180
246
  const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
181
247
  const authType = params.authType || params.authentication?.type || params.authentication?.method || params.authentication?.authType;
182
- const secretPaths = buildSecretPaths(systemKey, authType);
248
+ const projectRoot = getProjectRoot();
249
+ const secretPathsFromEnv = extractSecretPathsFromEnvTemplate({ projectRoot, appName });
250
+ const secretPaths = secretPathsFromEnv.length > 0 ? secretPathsFromEnv : buildSecretPaths(systemKey, authType);
183
251
  const rbacOptionalFile = rbacOptionalFilename(normalizedExt);
184
252
 
185
253
  return {
@@ -238,5 +306,6 @@ module.exports = {
238
306
  buildExternalReadmeContext,
239
307
  generateExternalReadmeContent,
240
308
  wrapPlainTextForMarkdown,
241
- collapseConsecutiveBlankLines
309
+ collapseConsecutiveBlankLines,
310
+ extractSecretPathsFromEnvTemplate
242
311
  };