@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,179 @@
1
+ /**
2
+ * Dimension value commands (Controller).
3
+ *
4
+ * Commands:
5
+ * - aifabrix dimension-value create
6
+ * - aifabrix dimension-value list
7
+ * - aifabrix dimension-value delete
8
+ *
9
+ * @fileoverview Dimension value CLI commands
10
+ * @author AI Fabrix Team
11
+ * @version 2.0.0
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const chalk = require('chalk');
17
+ const logger = require('../utils/logger');
18
+ const { resolveControllerUrl } = require('../utils/controller-url');
19
+ const { normalizeControllerUrl } = require('../core/config');
20
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
21
+ const { formatBlockingError, formatSuccessLine, headerKeyValue } = require('../utils/cli-layout-chalk');
22
+ const {
23
+ listDimensionValues,
24
+ createDimensionValue,
25
+ deleteDimensionValue
26
+ } = require('../api/dimension-values.api');
27
+
28
+ const DIMENSION_VALUE_CREATE_HELP_AFTER = `
29
+
30
+ Examples:
31
+ $ aifabrix dimension-value create customerRegion --value emea --display-name "EMEA"
32
+ $ aifabrix dimension-value create customerRegion --value na --display-name "North America"
33
+
34
+ `;
35
+
36
+ const DIMENSION_VALUE_LIST_HELP_AFTER = `
37
+
38
+ Examples:
39
+ $ aifabrix dimension-value list customerRegion
40
+ $ aifabrix dimension-value list customerRegion --page 1 --page-size 50
41
+ $ aifabrix dimension-value list customerRegion --search emea
42
+
43
+ `;
44
+
45
+ const DIMENSION_VALUE_DELETE_HELP_AFTER = `
46
+
47
+ Examples:
48
+ $ aifabrix dimension-value delete clx1234567890abcdef
49
+
50
+ Tip: Find ids via "aifabrix dimension get <key>" (it prints values) or "aifabrix dimension-value list <key>".
51
+
52
+ `;
53
+
54
+ /**
55
+ * @param {string} raw
56
+ * @param {string} label
57
+ * @returns {string}
58
+ */
59
+ function requireNonEmpty(raw, label) {
60
+ const s = String(raw || '').trim();
61
+ if (!s) throw new Error(`${label} is required.`);
62
+ return s;
63
+ }
64
+
65
+ async function resolveControllerAndAuth() {
66
+ const controllerUrl = await resolveControllerUrl();
67
+ if (!controllerUrl) {
68
+ throw new Error('Controller URL is required. Run "aifabrix login" first.');
69
+ }
70
+ const normalized = normalizeControllerUrl(controllerUrl);
71
+ const deviceToken = await getOrRefreshDeviceToken(normalized);
72
+ if (!deviceToken || !deviceToken.token) {
73
+ throw new Error(`Not authenticated for controller: ${controllerUrl}. Run "aifabrix login" and try again.`);
74
+ }
75
+ return {
76
+ controllerUrl: deviceToken.controller || normalized,
77
+ authConfig: { type: 'bearer', token: deviceToken.token }
78
+ };
79
+ }
80
+
81
+ function unwrapControllerData(response) {
82
+ return response?.data?.data ?? response?.data ?? response;
83
+ }
84
+
85
+ function setupDimensionValueCreateCommand(cmd) {
86
+ cmd
87
+ .command('create <dimensionIdOrKey>')
88
+ .description('Create a value under a dimension')
89
+ .addHelpText('after', DIMENSION_VALUE_CREATE_HELP_AFTER)
90
+ .requiredOption('--value <value>', 'Value (unique within the dimension)')
91
+ .option('--display-name <name>', 'Display name')
92
+ .option('--description <text>', 'Description')
93
+ .action(async(dimensionIdOrKey, options) => {
94
+ try {
95
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth();
96
+ const dimKey = requireNonEmpty(dimensionIdOrKey, 'dimensionIdOrKey');
97
+ const value = requireNonEmpty(options.value, 'value');
98
+ const res = await createDimensionValue(controllerUrl, authConfig, dimKey, {
99
+ value,
100
+ displayName: options.displayName,
101
+ description: options.description
102
+ });
103
+ const row = unwrapControllerData(res);
104
+ logger.log(formatSuccessLine(`Dimension value created: ${dimKey}.${row.value}`));
105
+ } catch (e) {
106
+ logger.error(formatBlockingError(e.message));
107
+ process.exit(1);
108
+ }
109
+ });
110
+ }
111
+
112
+ function setupDimensionValueListCommand(cmd) {
113
+ cmd
114
+ .command('list <dimensionIdOrKey>')
115
+ .description('List values for a dimension')
116
+ .addHelpText('after', DIMENSION_VALUE_LIST_HELP_AFTER)
117
+ .option('--page <n>', 'Page', (v) => parseInt(v, 10))
118
+ .option('--page-size <n>', 'Page size', (v) => parseInt(v, 10))
119
+ .option('--search <text>', 'Search by value/displayName/description')
120
+ .action(async(dimensionIdOrKey, options) => {
121
+ try {
122
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth();
123
+ const dimKey = requireNonEmpty(dimensionIdOrKey, 'dimensionIdOrKey');
124
+ const res = await listDimensionValues(controllerUrl, authConfig, dimKey, {
125
+ page: options.page,
126
+ pageSize: options.pageSize,
127
+ search: options.search
128
+ });
129
+ const payload = unwrapControllerData(res);
130
+ const items = payload?.data ?? payload ?? [];
131
+ logger.log(chalk.bold('\nšŸ· Dimension values:\n'));
132
+ logger.log(headerKeyValue('Dimension:', dimKey));
133
+ logger.log('');
134
+ if (!Array.isArray(items) || items.length === 0) {
135
+ logger.log(chalk.gray(' No values found.\n'));
136
+ return;
137
+ }
138
+ items.forEach((v) => {
139
+ const value = v?.value ? String(v.value) : '—';
140
+ const display = v?.displayName ? String(v.displayName) : '';
141
+ logger.log(` ${chalk.white(value)}${display ? ` ${chalk.gray(`(${display})`)}` : ''}`);
142
+ });
143
+ logger.log('');
144
+ } catch (e) {
145
+ logger.error(formatBlockingError(e.message));
146
+ process.exit(1);
147
+ }
148
+ });
149
+ }
150
+
151
+ function setupDimensionValueDeleteCommand(cmd) {
152
+ cmd
153
+ .command('delete <dimensionValueId>')
154
+ .description('Delete a dimension value by id')
155
+ .addHelpText('after', DIMENSION_VALUE_DELETE_HELP_AFTER)
156
+ .action(async(dimensionValueId) => {
157
+ try {
158
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth();
159
+ const id = requireNonEmpty(dimensionValueId, 'dimensionValueId');
160
+ await deleteDimensionValue(controllerUrl, authConfig, id);
161
+ logger.log(formatSuccessLine(`Dimension value deleted: ${id}`));
162
+ } catch (e) {
163
+ logger.error(formatBlockingError(e.message));
164
+ process.exit(1);
165
+ }
166
+ });
167
+ }
168
+
169
+ function setupDimensionValueCommands(program) {
170
+ const cmd = program.command('dimension-value').description('Manage dimension values (static dimensions)');
171
+ setupDimensionValueCreateCommand(cmd);
172
+ setupDimensionValueListCommand(cmd);
173
+ setupDimensionValueDeleteCommand(cmd);
174
+ }
175
+
176
+ module.exports = {
177
+ setupDimensionValueCommands
178
+ };
179
+
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Dimension catalog commands (Controller).
3
+ *
4
+ * Commands:
5
+ * - aifabrix dimension create
6
+ * - aifabrix dimension get
7
+ * - aifabrix dimension list
8
+ *
9
+ * @fileoverview Dimension CLI commands
10
+ * @author AI Fabrix Team
11
+ * @version 2.0.0
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const chalk = require('chalk');
17
+ const logger = require('../utils/logger');
18
+ const { resolveControllerUrl } = require('../utils/controller-url');
19
+ const { normalizeControllerUrl, resolveEnvironment } = require('../core/config');
20
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
21
+ const { readDimensionCreateFile } = require('../resolvers/dimension-file');
22
+ const {
23
+ formatBlockingError,
24
+ formatSuccessLine,
25
+ infoLine,
26
+ headerKeyValue
27
+ } = require('../utils/cli-layout-chalk');
28
+ const {
29
+ listDimensions,
30
+ getDimension,
31
+ createDimensionIdempotent
32
+ } = require('../api/dimensions.api');
33
+ const { createDimensionValue } = require('../api/dimension-values.api');
34
+
35
+ const DIMENSION_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9-_]*$/;
36
+ const DATA_TYPES = new Set(['string', 'number', 'boolean']);
37
+
38
+ const DIMENSION_CREATE_HELP_AFTER = `
39
+
40
+ Examples:
41
+ $ aifabrix dimension create --key customerRegion --display-name "Customer Region" --data-type string
42
+ $ aifabrix dimension create --key dataClassification --display-name "Data Classification" --data-type string --required
43
+ $ aifabrix dimension create --file ./customer-region.json
44
+
45
+ `;
46
+
47
+ const DIMENSION_GET_HELP_AFTER = `
48
+
49
+ Examples:
50
+ $ aifabrix dimension get customerRegion
51
+ $ aifabrix dimension get clx1234567890abcdef
52
+
53
+ `;
54
+
55
+ const DIMENSION_LIST_HELP_AFTER = `
56
+
57
+ Examples:
58
+ $ aifabrix dimension list
59
+ $ aifabrix dimension list --page 1 --page-size 50
60
+ $ aifabrix dimension list --search region
61
+
62
+ `;
63
+
64
+ /**
65
+ * @param {string} raw
66
+ * @returns {string}
67
+ */
68
+ function requireDimensionKey(raw) {
69
+ const key = String(raw || '').trim();
70
+ if (!key) {
71
+ throw new Error('Dimension key is required (--key <key> or --file <path>).');
72
+ }
73
+ if (!DIMENSION_KEY_PATTERN.test(key)) {
74
+ throw new Error(
75
+ 'Dimension key must start with a letter and contain only letters, numbers, hyphens, and underscores.'
76
+ );
77
+ }
78
+ return key;
79
+ }
80
+
81
+ /**
82
+ * @param {string} raw
83
+ * @returns {'string'|'number'|'boolean'}
84
+ */
85
+ function requireDataType(raw) {
86
+ const dt = String(raw || '').trim();
87
+ if (!dt) {
88
+ throw new Error('dataType is required (--data-type string|number|boolean or in --file).');
89
+ }
90
+ if (!DATA_TYPES.has(dt)) {
91
+ throw new Error('--data-type must be one of: string, number, boolean');
92
+ }
93
+ return /** @type {any} */ (dt);
94
+ }
95
+
96
+ /**
97
+ * @param {Object} options
98
+ * @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
99
+ */
100
+ async function resolveControllerAndAuth(_options) {
101
+ const controllerUrl = await resolveControllerUrl();
102
+ if (!controllerUrl) {
103
+ throw new Error('Controller URL is required. Run "aifabrix login" first.');
104
+ }
105
+ const normalized = normalizeControllerUrl(controllerUrl);
106
+ const deviceToken = await getOrRefreshDeviceToken(normalized);
107
+ if (!deviceToken || !deviceToken.token) {
108
+ throw new Error(
109
+ `Not authenticated for controller: ${controllerUrl}. ` +
110
+ 'Run "aifabrix login" and try again.'
111
+ );
112
+ }
113
+ return {
114
+ controllerUrl: deviceToken.controller || normalized,
115
+ authConfig: { type: 'bearer', token: deviceToken.token }
116
+ };
117
+ }
118
+
119
+ async function resolveHeaderContext() {
120
+ const env = await resolveEnvironment();
121
+ const controllerUrl = await resolveControllerUrl();
122
+ const normalized = normalizeControllerUrl(controllerUrl);
123
+ return { environment: env || 'dev', controllerUrl: normalized };
124
+ }
125
+
126
+ /**
127
+ * @param {Object} options
128
+ * @returns {Object}
129
+ */
130
+ function buildCreatePayload(options) {
131
+ let base = {};
132
+ if (options.file) {
133
+ base = readDimensionCreateFile(options.file);
134
+ }
135
+ const payload = {
136
+ ...base
137
+ };
138
+ if (options.key) payload.key = options.key;
139
+ if (options.displayName) payload.displayName = options.displayName;
140
+ if (options.description !== undefined) payload.description = options.description;
141
+ if (options.dataType) payload.dataType = options.dataType;
142
+ if (options.required !== undefined && options.required !== null) {
143
+ payload.isRequired = Boolean(options.required);
144
+ }
145
+ payload.key = requireDimensionKey(payload.key);
146
+ if (!payload.displayName) {
147
+ throw new Error('displayName is required (--display-name <name> or in --file).');
148
+ }
149
+ payload.dataType = requireDataType(payload.dataType);
150
+ return payload;
151
+ }
152
+
153
+ async function maybeCreateValuesFromFile(controllerUrl, authConfig, payload) {
154
+ const values = payload?.values;
155
+ if (!Array.isArray(values) || values.length === 0) return;
156
+ const dimKey = String(payload.key || '').trim();
157
+ for (const v of values) {
158
+ const value = String(v?.value || '').trim();
159
+ if (!value) {
160
+ throw new Error('values[].value must be a non-empty string');
161
+ }
162
+ try {
163
+ await createDimensionValue(controllerUrl, authConfig, dimKey, {
164
+ value,
165
+ displayName: v?.displayName,
166
+ description: v?.description
167
+ });
168
+ } catch (e) {
169
+ // Idempotent behavior: if the value already exists (409), treat as success.
170
+ const msg = e?.message || String(e);
171
+ if (/409|Conflict/i.test(msg)) {
172
+ continue;
173
+ }
174
+ throw e;
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {Object} response
181
+ * @returns {any}
182
+ */
183
+ function unwrapControllerData(response) {
184
+ return response?.data?.data ?? response?.data ?? response;
185
+ }
186
+
187
+ /**
188
+ * @param {import('commander').Command} dim
189
+ */
190
+ function setupDimensionCreateCommand(dim) {
191
+ dim
192
+ .command('create')
193
+ .description('Create dimension (idempotent; success if it already exists)')
194
+ .addHelpText('after', DIMENSION_CREATE_HELP_AFTER)
195
+ .option('--file <path>', 'Read dimension create payload from JSON file')
196
+ .option('--key <key>', 'Dimension key (letters, digits, hyphens, underscores)')
197
+ .option('--display-name <name>', 'Display name')
198
+ .option('--description <text>', 'Description')
199
+ .option('--data-type <type>', 'string | number | boolean')
200
+ .option('--required', 'Mark dimension required (isRequired=true)')
201
+ .option('--no-required', 'Mark dimension not required (isRequired=false)')
202
+ .action(async(options) => {
203
+ try {
204
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
205
+ const payload = buildCreatePayload(options);
206
+ const out = await createDimensionIdempotent(controllerUrl, authConfig, payload);
207
+ const dimRow = unwrapControllerData(out.response);
208
+ await maybeCreateValuesFromFile(controllerUrl, authConfig, payload);
209
+ if (out.created) {
210
+ logger.log(formatSuccessLine(`Dimension created: ${dimRow.key}`));
211
+ return;
212
+ }
213
+ logger.log(formatSuccessLine(`Dimension exists: ${dimRow.key}`));
214
+ logger.log(infoLine('No changes were needed.'));
215
+ } catch (e) {
216
+ logger.error(formatBlockingError(e.message));
217
+ process.exit(1);
218
+ }
219
+ });
220
+ }
221
+
222
+ function setupDimensionGetCommand(dim) {
223
+ dim
224
+ .command('get <dimensionIdOrKey>')
225
+ .description('Get dimension by id or key')
226
+ .addHelpText('after', DIMENSION_GET_HELP_AFTER)
227
+ .action(async(dimensionIdOrKey, options) => {
228
+ try {
229
+ const { environment, controllerUrl: headerControllerUrl } = await resolveHeaderContext();
230
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
231
+ const res = await getDimension(controllerUrl, authConfig, String(dimensionIdOrKey).trim(), {
232
+ includeValues: true
233
+ });
234
+ const row = unwrapControllerData(res);
235
+ logger.log(chalk.bold(`\nšŸ“ Dimension in ${environment} environment (${headerControllerUrl}):\n`));
236
+ logger.log(headerKeyValue('Key:', row.key || '—'));
237
+ logger.log(headerKeyValue('Display:', row.displayName || '—'));
238
+ logger.log(headerKeyValue('Type:', row.dataType || '—'));
239
+ logger.log(headerKeyValue('Required:', String(row.isRequired)));
240
+ if (row.description) {
241
+ logger.log(headerKeyValue('Description:', row.description));
242
+ }
243
+ if (Array.isArray(row.dimensionValues) && row.dimensionValues.length > 0) {
244
+ logger.log('');
245
+ logger.log(chalk.white.bold('Values:'));
246
+ row.dimensionValues.forEach((v) => {
247
+ const value = v?.value ? String(v.value) : '—';
248
+ const display = v?.displayName ? String(v.displayName) : '';
249
+ logger.log(` ${chalk.white(value)}${display ? ` ${chalk.gray(`(${display})`)}` : ''}`);
250
+ });
251
+ }
252
+ logger.log('');
253
+ } catch (e) {
254
+ logger.error(formatBlockingError(e.message));
255
+ process.exit(1);
256
+ }
257
+ });
258
+ }
259
+
260
+ function setupDimensionListCommand(dim) {
261
+ function displayDimensionList(items, environment, controllerUrl) {
262
+ logger.log(chalk.bold(`\nšŸ“ Dimensions in ${environment} environment (${controllerUrl}):\n`));
263
+ if (!Array.isArray(items) || items.length === 0) {
264
+ logger.log(chalk.gray(' No dimensions found.\n'));
265
+ return;
266
+ }
267
+
268
+ const KEY_WIDTH = 26;
269
+ const DISPLAY_WIDTH = 34;
270
+ const TYPE_WIDTH = 10;
271
+ const REQ_WIDTH = 10;
272
+ const SEP_LEN = KEY_WIDTH + DISPLAY_WIDTH + TYPE_WIDTH + REQ_WIDTH;
273
+
274
+ const keyCol = 'Key'.padEnd(KEY_WIDTH);
275
+ const displayCol = 'Display'.padEnd(DISPLAY_WIDTH);
276
+ const typeCol = 'Type'.padEnd(TYPE_WIDTH);
277
+ const reqCol = 'Required'.padEnd(REQ_WIDTH);
278
+ logger.log(chalk.gray(`${keyCol}${displayCol}${typeCol}${reqCol}`));
279
+ logger.log(chalk.gray('-'.repeat(SEP_LEN)));
280
+
281
+ items.forEach((d) => {
282
+ const key = (d?.key ?? '—').toString().padEnd(KEY_WIDTH);
283
+ const displayName = (d?.displayName ?? '—').toString().padEnd(DISPLAY_WIDTH);
284
+ const dataType = (d?.dataType ?? '—').toString().padEnd(TYPE_WIDTH);
285
+ const required = (d?.isRequired ?? '—').toString().padEnd(REQ_WIDTH);
286
+ logger.log(`${key}${displayName}${dataType}${required}`);
287
+ });
288
+ logger.log('');
289
+ }
290
+
291
+ dim
292
+ .command('list')
293
+ .description('List dimensions')
294
+ .addHelpText('after', DIMENSION_LIST_HELP_AFTER)
295
+ .option('--page <n>', 'Page', (v) => parseInt(v, 10))
296
+ .option('--page-size <n>', 'Page size', (v) => parseInt(v, 10))
297
+ .option('--search <text>', 'Search by key/displayName/description')
298
+ .action(async(options) => {
299
+ try {
300
+ const { environment, controllerUrl: headerControllerUrl } = await resolveHeaderContext();
301
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
302
+ const res = await listDimensions(controllerUrl, authConfig, {
303
+ page: options.page,
304
+ pageSize: options.pageSize,
305
+ search: options.search
306
+ });
307
+ const payload = unwrapControllerData(res);
308
+ const items = payload?.data ?? payload ?? [];
309
+ displayDimensionList(items, environment, headerControllerUrl);
310
+ } catch (e) {
311
+ logger.error(formatBlockingError(e.message));
312
+ process.exit(1);
313
+ }
314
+ });
315
+ }
316
+
317
+ /**
318
+ * @param {import('commander').Command} program
319
+ */
320
+ function setupDimensionCommands(program) {
321
+ const dim = program.command('dimension').description('Manage the Controller Dimension Catalog');
322
+ setupDimensionCreateCommand(dim);
323
+ setupDimensionGetCommand(dim);
324
+ setupDimensionListCommand(dim);
325
+ }
326
+
327
+ module.exports = {
328
+ setupDimensionCommands
329
+ };
330
+