@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,411 @@
1
+ /**
2
+ * Datasource capability subcommands (copy, remove, create, diff, edit, validate).
3
+ *
4
+ * @fileoverview Nested `aifabrix datasource capability` CLI (copy, relate, …)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const logger = require('../utils/logger');
11
+ const {
12
+ formatBlockingError,
13
+ formatSuccessLine,
14
+ headerKeyValue,
15
+ infoLine,
16
+ metadata
17
+ } = require('../utils/cli-test-layout-chalk');
18
+ const { validateDatasourceFile, resolveValidateInputPath } = require('../datasource/validate');
19
+ const { runCapabilityCopy } = require('../datasource/capability/run-capability-copy');
20
+ const { runCapabilityRemove } = require('../datasource/capability/run-capability-remove');
21
+ const { runCapabilityDiff } = require('../datasource/capability/run-capability-diff');
22
+ const { runCapabilityEdit } = require('../datasource/capability/run-capability-edit');
23
+ const { checkCapabilitySlices } = require('../datasource/capability/validate-capability-slice');
24
+ const { logDatasourceValidateOutcome } = require('../datasource/datasource-validate-display');
25
+ const pathMod = require('path');
26
+ const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
27
+ const { setupCapabilityRelateCommand } = require('./datasource-capability-relate-cli');
28
+ const { setupCapabilityDimensionCommand } = require('./datasource-capability-dimension-cli');
29
+
30
+ const CAP_COPY_HELP = `
31
+ Examples:
32
+ $ aifabrix datasource capability copy test-e2e-hubspot-companies --from create --as createBasicTrial --dry-run
33
+ $ af datasource cap copy integration/myapp/x-datasource-y.json --from list --as listVerbose --dry-run
34
+
35
+ When exposed.profiles.<from> exists it is copied to exposed.profiles.<as>.
36
+ With --test, matching testPayload.scenarios rows (same operation as the source capability / openapi / CIP keys) are cloned with operation set to --as.
37
+
38
+ Next step after mutating: aifabrix datasource validate <file-or-key>
39
+ `;
40
+
41
+ const CAP_CREATE_HELP = `
42
+ Requires exactly one source: **--from**, **--template**, or **--openapi-operation**.
43
+
44
+ **--from** clones openapi/CIP/profile like **capability copy**. **--openapi-operation** finds an existing **openapi.operations** entry by **operationId** and clones it with a minimal CIP fetch step. **--template** loads **lib/datasource/capability/templates/[name].json** (built-in **minimal-fetch**).
45
+
46
+ $ aifabrix datasource capability create my-datasource-key --from list --as listAlt
47
+ $ af datasource cap create ./x.json --openapi-operation get-crm-v3-objects-contacts --as listContacts
48
+ $ af datasource cap create ./x.json --template minimal-fetch --as probeRead
49
+ `;
50
+
51
+ const CAP_VALIDATE_HELP = `
52
+ $ aifabrix datasource capability validate test-e2e-hubspot-companies
53
+ $ af datasource cap validate ./integration/x/y-datasource-z.json --capability create
54
+ `;
55
+
56
+ const CAP_REMOVE_HELP = `
57
+ Examples:
58
+ $ aifabrix datasource capability remove test-e2e-hubspot-companies --capability createCliGolden --dry-run
59
+ $ af datasource cap remove ./integration/x/datasource.json --capability list
60
+
61
+ Next: aifabrix datasource validate <file-or-key>
62
+ `;
63
+
64
+ const CAP_DIFF_HELP = `
65
+ Compare OpenAPI + CIP slices (and optional exposed.profiles) for one capability between two files.
66
+
67
+ $ aifabrix datasource capability diff ./before.json ./after.json --capability create
68
+ $ af datasource cap diff a.json b.json --capability-a list --capability-b listVerbose --profile list
69
+ `;
70
+
71
+ const CAP_EDIT_HELP = `
72
+ Uses $VISUAL or $EDITOR for the JSON editor; if both are unset and **nano** is on PATH, nano is used.
73
+
74
+ When **--section profile**, if **exposed.profiles** has a row for the capability you picked (interactive or **-c**), that row opens directly—no “which profile?” list. Use **--profile <key>** to edit a different row. Override the editor for one run: **--editor nano**.
75
+
76
+ $ aifabrix datasource capability edit my-datasource-key --capability create --section openapi
77
+ $ af datasource cap edit my-datasource-key -c updateAddress --section profile
78
+ `;
79
+
80
+ /**
81
+ * Copy/create from CLI: always calls copy with basicExposure false (not exposed on CLI).
82
+ *
83
+ * @param {string} fileOrKey
84
+ * @param {object} options - Commander options
85
+ * @returns {Promise<void>}
86
+ */
87
+ async function runCopyLikeAction(fileOrKey, options) {
88
+ const result = await runCapabilityCopy({
89
+ fileOrKey,
90
+ from: options.from,
91
+ as: options.as,
92
+ dryRun: Boolean(options.dryRun),
93
+ overwrite: Boolean(options.overwrite),
94
+ noBackup: Boolean(options.noBackup),
95
+ basicExposure: false,
96
+ includeTestPayload: Boolean(options.test),
97
+ openApiOperationId: options.openapiOperation,
98
+ template: options.template
99
+ });
100
+
101
+ if (result.dryRun) {
102
+ logger.log(infoLine('Dry run — planned JSON Patch operations:'));
103
+ logger.log('');
104
+ logger.log(JSON.stringify(result.patchOperations, null, 2));
105
+ return;
106
+ }
107
+
108
+ if (result.backupPath) {
109
+ logger.log(headerKeyValue('Backup:', result.backupPath));
110
+ }
111
+ printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections);
112
+ }
113
+
114
+ /**
115
+ * @param {string} fileOrKey
116
+ * @param {object} options - Commander options
117
+ * @returns {Promise<void>}
118
+ */
119
+ async function runRemoveAction(fileOrKey, options) {
120
+ const result = await runCapabilityRemove({
121
+ fileOrKey,
122
+ capability: options.capability,
123
+ dryRun: Boolean(options.dryRun),
124
+ noBackup: Boolean(options.noBackup),
125
+ force: Boolean(options.force)
126
+ });
127
+
128
+ if (result.dryRun) {
129
+ logger.log(infoLine('Dry run — planned JSON Patch operations:'));
130
+ logger.log('');
131
+ logger.log(JSON.stringify(result.patchOperations, null, 2));
132
+ if (!result.removed) {
133
+ logger.log(metadata('(no changes; capability already absent)'));
134
+ }
135
+ return;
136
+ }
137
+
138
+ if (result.backupPath) {
139
+ logger.log(headerKeyValue('Backup:', result.backupPath));
140
+ }
141
+ const heading = result.removed ? 'Removed' : 'Unchanged';
142
+ printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections, heading);
143
+ }
144
+
145
+ /**
146
+ * @param {import('commander').Command} cap - capability command group
147
+ * @returns {void}
148
+ */
149
+ function setupCapabilityCopyCommand(cap) {
150
+ cap.command('copy <file-or-key>')
151
+ .description(
152
+ 'Clone openapi + CIP operations (lowercase keys), profiles (--as casing); optional --test for scenarios'
153
+ )
154
+ .requiredOption('--from <key>', 'Source capability key (must exist)')
155
+ .requiredOption('--as <key>', 'Target capability key (must be unique unless --overwrite)')
156
+ .option('--dry-run', 'Print JSON Patch operations; do not write')
157
+ .option('--overwrite', 'Replace target capability and matching exposed.profiles.<as> if present')
158
+ .option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
159
+ .option(
160
+ '--test',
161
+ 'Also clone testPayload.scenarios rows whose operation matches the source capability'
162
+ )
163
+ .addHelpText('after', CAP_COPY_HELP)
164
+ .action(async(fileOrKey, options) => {
165
+ try {
166
+ await runCopyLikeAction(fileOrKey, options);
167
+ } catch (error) {
168
+ logger.error(formatBlockingError(`capability copy failed: ${error.message}`));
169
+ process.exit(1);
170
+ }
171
+ });
172
+ }
173
+
174
+ /**
175
+ * @param {import('commander').Command} cap
176
+ * @returns {void}
177
+ */
178
+ function setupCapabilityRemoveCommand(cap) {
179
+ cap.command('remove <file-or-key>')
180
+ .description(
181
+ 'Remove one capability (capabilities[], openapi.operations, execution.cip.operations), exposed.profiles.<key>, and matching testPayload.scenarios rows'
182
+ )
183
+ .requiredOption('-c, --capability <key>', 'Capability key to delete')
184
+ .option('--dry-run', 'Print JSON Patch operations; do not write')
185
+ .option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
186
+ .option('--force', 'Succeed if capability is already absent (no file change)')
187
+ .addHelpText('after', CAP_REMOVE_HELP)
188
+ .action(async(fileOrKey, options) => {
189
+ try {
190
+ await runRemoveAction(fileOrKey, options);
191
+ } catch (error) {
192
+ logger.error(formatBlockingError(`capability remove failed: ${error.message}`));
193
+ process.exit(1);
194
+ }
195
+ });
196
+ }
197
+
198
+ /**
199
+ * @param {import('commander').Command} cap
200
+ * @returns {void}
201
+ */
202
+ function setupCapabilityCreateCommand(cap) {
203
+ const createCmd = cap
204
+ .command('create <file-or-key>')
205
+ .description(
206
+ 'Create capability: exactly one of --from, --template, or --openapi-operation (alias: add)'
207
+ );
208
+ if (typeof createCmd.alias === 'function') {
209
+ createCmd.alias('add');
210
+ }
211
+ createCmd
212
+ .requiredOption('--as <key>', 'New capability key (must be unique unless --overwrite)')
213
+ .option('--from <key>', 'Clone from an existing capability (same as capability copy)')
214
+ .option(
215
+ '--openapi-operation <operationId>',
216
+ 'Match openapi.operations[].operationId in this file; minimal CIP fetch step'
217
+ )
218
+ .option('--template <name>', 'Built-in templates under capability/templates (e.g. minimal-fetch)')
219
+ .option('--dry-run', 'Print JSON Patch operations; do not write')
220
+ .option('--overwrite', 'Replace target capability and matching exposed.profiles.<as> if present')
221
+ .option('--no-backup', 'Skip backup')
222
+ .option(
223
+ '--test',
224
+ 'With --from: clone testPayload.scenarios rows whose operation matches the source capability'
225
+ )
226
+ .addHelpText('after', CAP_CREATE_HELP)
227
+ .action(async(fileOrKey, options) => {
228
+ try {
229
+ await runCopyLikeAction(fileOrKey, options);
230
+ } catch (error) {
231
+ logger.error(formatBlockingError(`capability create failed: ${error.message}`));
232
+ process.exit(1);
233
+ }
234
+ });
235
+ }
236
+
237
+ /**
238
+ * @param {import('commander').Command} cap
239
+ * @returns {void}
240
+ */
241
+ function setupCapabilityDiffCommand(cap) {
242
+ cap
243
+ .command('diff <file-a> <file-b>')
244
+ .description(
245
+ 'Compare capability slices between two datasource JSON files (optional exposed.profiles profile)'
246
+ )
247
+ .option(
248
+ '-c, --capability <key>',
249
+ 'Capability key on both sides (use --capability-a/b to compare different keys)'
250
+ )
251
+ .option('--capability-a <key>', 'Capability key in first file')
252
+ .option('--capability-b <key>', 'Capability key in second file')
253
+ .option('--profile <name>', 'Include exposed.profiles.<name> on both sides')
254
+ .option('--profile-a <name>', 'Profile key for first file')
255
+ .option('--profile-b <name>', 'Profile key for second file')
256
+ .addHelpText('after', CAP_DIFF_HELP)
257
+ .action(async(fileA, fileB, options) => {
258
+ try {
259
+ const { identical } = runCapabilityDiff({
260
+ fileA,
261
+ fileB,
262
+ capability: options.capability,
263
+ capabilityA: options.capabilityA,
264
+ capabilityB: options.capabilityB,
265
+ profile: options.profile,
266
+ profileA: options.profileA,
267
+ profileB: options.profileB
268
+ });
269
+ if (!identical) {
270
+ process.exit(1);
271
+ }
272
+ } catch (error) {
273
+ logger.error(formatBlockingError(`capability diff failed: ${error.message}`));
274
+ process.exit(1);
275
+ }
276
+ });
277
+ }
278
+
279
+ /**
280
+ * @param {import('commander').Command} cap
281
+ * @returns {void}
282
+ */
283
+ function setupCapabilityEditCommand(cap) {
284
+ cap
285
+ .command('edit <file-or-key>')
286
+ .description(
287
+ 'Interactively edit openapi/cip/exposed profile JSON for one capability (TTY + $EDITOR / nano)'
288
+ )
289
+ .option('-c, --capability <key>', 'Capability key (skip prompt)')
290
+ .option('--section <name>', 'openapi | cip | profile (skip prompt)')
291
+ .option('--profile <name>', 'When section=profile, exposed.profiles key (skip prompt)')
292
+ .option('--editor <cmd>', 'Editor command for this run (sets VISUAL and EDITOR)')
293
+ .option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
294
+ .addHelpText('after', CAP_EDIT_HELP)
295
+ .action(async(fileOrKey, options) => {
296
+ try {
297
+ const editorArg = options.editor !== undefined && options.editor !== null ? String(options.editor).trim() : '';
298
+ if (editorArg) {
299
+ process.env.VISUAL = editorArg;
300
+ process.env.EDITOR = editorArg;
301
+ }
302
+ const section = normalizeCapabilityEditSection(options.section);
303
+ const result = await runCapabilityEdit({
304
+ fileOrKey,
305
+ capability: options.capability,
306
+ section,
307
+ profile: options.profile,
308
+ noBackup: Boolean(options.noBackup)
309
+ });
310
+ if (result.backupPath) {
311
+ logger.log(headerKeyValue('Backup:', result.backupPath));
312
+ }
313
+ printCapabilitySuccessFooter(result.resolvedPath, ['Saved capability slice JSON'], 'Updated');
314
+ } catch (error) {
315
+ logger.error(formatBlockingError(`capability edit failed: ${error.message}`));
316
+ process.exit(1);
317
+ }
318
+ });
319
+ }
320
+
321
+ /**
322
+ * @param {import('commander').Command} cap
323
+ * @returns {void}
324
+ */
325
+ function setupCapabilityValidateCommand(cap) {
326
+ cap.command('validate <file-or-key>')
327
+ .description('Validate datasource JSON; optional --capability slice presence check')
328
+ .option('-c, --capability <key>', 'Ensure capability exists in openapi + cip + capabilities[]')
329
+ .addHelpText('after', CAP_VALIDATE_HELP)
330
+ .action(async(fileOrKey, options) => {
331
+ try {
332
+ const trimmed = fileOrKey.trim();
333
+ const result = await validateDatasourceFile(trimmed);
334
+ const resolvedPath = result.resolvedPath;
335
+ const argResolved = pathMod.resolve(trimmed);
336
+ const showMapping = resolvedPath && argResolved !== resolvedPath && trimmed !== resolvedPath;
337
+ logDatasourceValidateOutcome(result, trimmed, showMapping);
338
+
339
+ if (!result.valid) {
340
+ process.exit(1);
341
+ }
342
+
343
+ if (options.capability) {
344
+ const readPath = resolveValidateInputPath(trimmed);
345
+ const parsed = JSON.parse(fs.readFileSync(readPath, 'utf8'));
346
+ const slice = checkCapabilitySlices(parsed, options.capability);
347
+ if (slice.missing.length > 0) {
348
+ logger.error(
349
+ formatBlockingError(
350
+ `Capability "${slice.key}" incomplete: ${slice.missing.join('; ')}`
351
+ )
352
+ );
353
+ process.exit(1);
354
+ }
355
+ logger.log(formatSuccessLine(`Capability slice OK: ${slice.key}`));
356
+ }
357
+ } catch (error) {
358
+ logger.error(formatBlockingError(`capability validate failed: ${error.message}`));
359
+ process.exit(1);
360
+ }
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Normalize `--section` for capability edit (openapi | cip | profile).
366
+ *
367
+ * @param {unknown} raw - Commander option value
368
+ * @returns {'openapi'|'cip'|'profile'|undefined}
369
+ * @throws {Error} When value is non-empty but not allowed
370
+ */
371
+ function normalizeCapabilityEditSection(raw) {
372
+ const sec = raw !== undefined && raw !== null ? String(raw).trim().toLowerCase() : '';
373
+ if (sec && !['openapi', 'cip', 'profile'].includes(sec)) {
374
+ throw new Error('--section must be openapi, cip, or profile');
375
+ }
376
+ return sec || undefined;
377
+ }
378
+
379
+ /**
380
+ * Register nested datasource capability commands.
381
+ *
382
+ * @param {import('commander').Command} datasource - datasource command group
383
+ * @returns {void}
384
+ */
385
+ function setupDatasourceCapabilityCommands(datasource) {
386
+ const cap = datasource
387
+ .command('capability')
388
+ .description(
389
+ 'Copy, remove, relate (FK metadata), diff, edit, or validate per-capability slices in datasource JSON'
390
+ );
391
+ if (typeof cap.alias === 'function') {
392
+ cap.alias('cap');
393
+ }
394
+
395
+ setupCapabilityCopyCommand(cap);
396
+ setupCapabilityRemoveCommand(cap);
397
+ setupCapabilityCreateCommand(cap);
398
+ setupCapabilityRelateCommand(cap);
399
+ setupCapabilityDimensionCommand(cap);
400
+ setupCapabilityDiffCommand(cap);
401
+ setupCapabilityEditCommand(cap);
402
+ setupCapabilityValidateCommand(cap);
403
+ }
404
+
405
+ module.exports = {
406
+ setupDatasourceCapabilityCommands,
407
+ printCapabilitySuccessFooter,
408
+ runCopyLikeAction,
409
+ runRemoveAction,
410
+ normalizeCapabilityEditSection
411
+ };
@@ -125,7 +125,7 @@ function attachDatasourceTestCommonOptions(cmd, opts) {
125
125
  )
126
126
  .option(
127
127
  '--sync',
128
- 'Publish this datasource JSON from disk to the dataplane before running the test (requires login / same auth as upload)'
128
+ 'Publish local integration config to the dataplane before running the test (same as aifabrix upload <systemKey>; requires login)'
129
129
  )
130
130
  .option('--json', 'Print raw DatasourceTestRun JSON to stdout')
131
131
  .option('--summary', 'Print compact summary line')
@@ -23,16 +23,28 @@ const {
23
23
  setupDatasourceTestIntegrationCommand,
24
24
  setupDatasourceTestE2ECommand
25
25
  } = require('./datasource-unified-test-cli');
26
+ const { setupDatasourceCapabilityCommands } = require('./datasource-capability');
26
27
 
28
+ /**
29
+ * Parent help only. Commander already prints the full "Commands:" list; do not duplicate it here.
30
+ * Use examples + alias note so `af datasource -h` stays short and non-repetitive.
31
+ */
27
32
  const DATASOURCE_HELP_AFTER = `
28
- Subcommands:
29
- validate <file-or-key> Validate datasource JSON (path or datasource key under integration/<app>/)
30
- list List datasources (env from config)
31
- upload <file-or-key> Deploy one datasource JSON to the dataplane (path or key; systemKey in file)
32
- diff Compare two datasource JSON files
33
- test <key> Structural/policy validation via unified dataplane API (DatasourceTestRun)
34
- test-integration / test-e2e Integration or E2E run via the same unified validation API
35
- log-test / log-integration / log-e2e Show saved debug logs (structural, integration, E2E)
33
+
34
+ Examples:
35
+ $ aifabrix datasource validate integration/<app>/<app>-datasource-contacts.json
36
+ $ aifabrix datasource list
37
+ $ aifabrix datasource list test # only datasource keys starting with "test"
38
+ $ af ds upload <datasourceKey> # same as: aifabrix datasource upload
39
+ $ af ds test <datasourceKey> --app <integrationFolder> --debug
40
+ $ af ds test-e2e <datasourceKey> --app <integrationFolder> -v
41
+ $ aifabrix datasource capability copy <file-or-key> --from create --as createBasicTrial --dry-run
42
+ $ aifabrix datasource capability remove <file-or-key> --capability createTrial --dry-run
43
+ $ aifabrix datasource capability diff ./a.json ./b.json --capability list
44
+ $ aifabrix datasource capability edit <file-or-key> --capability create --section openapi
45
+
46
+ Per-command help: aifabrix datasource <command> --help
47
+ Shorthand: af ds (alias for datasource)
36
48
  `;
37
49
 
38
50
  const DATASOURCE_VALIDATE_HELP_AFTER = `
@@ -53,6 +65,25 @@ Examples:
53
65
  $ af ds upload ../integration/hubspot/hubspot-datasource-deals.json
54
66
  `;
55
67
 
68
+ const DATASOURCE_DIFF_HELP_AFTER = `
69
+ Examples:
70
+ $ aifabrix datasource diff integration/myapp/old-datasource.json integration/myapp/new-datasource.json
71
+ $ af ds diff ./before.json ./after.json
72
+ $ af ds diff /abs/path/a.json /abs/path/b.json
73
+
74
+ Note: Both arguments must be file paths. Use paths under integration/<app>/ or any two JSON files on disk.
75
+ `;
76
+
77
+ const DATASOURCE_LIST_HELP_AFTER = `
78
+ Examples:
79
+ $ aifabrix datasource list
80
+ $ aifabrix datasource list test # keys starting with "test" (e.g. test-e2e-hubspot-users)
81
+ $ af ds list hubspot # keys starting with "hubspot"
82
+
83
+ Filter applies to the datasource key field returned by the dataplane (prefix match, case-sensitive).
84
+ Omit [prefix] to list all datasources for the active environment.
85
+ `;
86
+
56
87
  function setupDatasourceValidateCommand(datasource) {
57
88
  datasource.command('validate <file-or-key>')
58
89
  .description('Validate datasource JSON (file path or datasource key under integration/<app>/)')
@@ -76,11 +107,18 @@ function setupDatasourceValidateCommand(datasource) {
76
107
  }
77
108
 
78
109
  function setupDatasourceListCommand(datasource) {
79
- datasource.command('list')
80
- .description('List datasources for environment in config')
81
- .action(async() => {
110
+ datasource.command('list [prefix]')
111
+ .description(
112
+ 'List datasources for environment in config (optional prefix filters datasource keys)'
113
+ )
114
+ .addHelpText('after', DATASOURCE_LIST_HELP_AFTER)
115
+ .action(async(prefix) => {
82
116
  try {
83
- await listDatasources({});
117
+ const opts = {};
118
+ if (typeof prefix === 'string' && prefix.trim()) {
119
+ opts.keyPrefix = prefix.trim();
120
+ }
121
+ await listDatasources(opts);
84
122
  } catch (error) {
85
123
  logger.error(formatBlockingError('Failed to list datasources:'), error.message);
86
124
  process.exit(1);
@@ -90,7 +128,8 @@ function setupDatasourceListCommand(datasource) {
90
128
 
91
129
  function setupDatasourceDiffCommand(datasource) {
92
130
  datasource.command('diff <file1> <file2>')
93
- .description('Diff two datasource JSON files')
131
+ .description('Diff two datasource JSON files (two file paths; not datasource keys)')
132
+ .addHelpText('after', DATASOURCE_DIFF_HELP_AFTER)
94
133
  .action(async(file1, file2) => {
95
134
  try {
96
135
  await compareDatasources(file1, file2);
@@ -193,6 +232,7 @@ function setupDatasourceCommands(program) {
193
232
  if (typeof datasource.alias === 'function') {
194
233
  datasource.alias('ds');
195
234
  }
235
+ setupDatasourceCapabilityCommands(datasource);
196
236
  setupDatasourceValidateCommand(datasource);
197
237
  setupDatasourceListCommand(datasource);
198
238
  setupDatasourceDiffCommand(datasource);
@@ -1,4 +1,4 @@
1
- const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
1
+ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
2
2
  /**
3
3
  * dev down – stop Mutagen sync sessions and optionally app containers for this developer.
4
4
  *
@@ -39,7 +39,7 @@ async function stopMutagenSessions(developerId) {
39
39
  const toTerminate = sessions.filter(name => name.startsWith(prefix));
40
40
  for (const name of toTerminate) {
41
41
  await execAsync(`"${mutagenPath}" sync terminate "${name}"`, { timeout: 5000 });
42
- logger.log(chalk.green(` Stopped sync session: ${name}`));
42
+ logger.log(chalk.green(' ') + formatSuccessLine(`Stopped sync session: ${name}`));
43
43
  }
44
44
  if (toTerminate.length === 0 && sessions.length > 0) {
45
45
  logger.log(chalk.gray('No sync sessions for this developer.'));
@@ -99,7 +99,7 @@ async function handleDevDown(options = {}) {
99
99
  if (!appName) continue;
100
100
  try {
101
101
  await appLib.downApp(appName, {});
102
- logger.log(chalk.green(` Stopped app: ${appName}`));
102
+ logger.log(chalk.green(' ') + formatSuccessLine(`Stopped app: ${appName}`));
103
103
  } catch (err) {
104
104
  logger.log(chalk.yellow(` ⚠ Could not stop ${appName}: ${err.message}`));
105
105
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @fileoverview Shared strict check that developer Postgres/Redis infra is running.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const logger = require('../utils/logger');
8
+ const infra = require('../infrastructure');
9
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
10
+
11
+ const DEV_INFRA_DOWN_MESSAGE = 'Infrastructure is not up. Run \'aifabrix up-infra\' first.';
12
+
13
+ /**
14
+ * Ensures this developer's Postgres/Redis (strict scope) are healthy.
15
+ *
16
+ * @param {{ quietSuccess?: boolean }} [opts] - When `quietSuccess`, omit the success log (e.g. guided spinner shows it).
17
+ * @returns {Promise<void>}
18
+ * @throws {Error} When infra is not healthy
19
+ */
20
+ async function assertDevInfraUp(opts = {}) {
21
+ const quietSuccess = Boolean(opts.quietSuccess);
22
+ const health = await infra.checkInfraHealth(undefined, { strict: true });
23
+ const allHealthy = Object.values(health).every((status) => status === 'healthy');
24
+ if (!allHealthy) {
25
+ throw new Error(DEV_INFRA_DOWN_MESSAGE);
26
+ }
27
+ if (!quietSuccess) {
28
+ logger.log(formatSuccessLine('Infrastructure is up'));
29
+ }
30
+ }
31
+
32
+ module.exports = { assertDevInfraUp, DEV_INFRA_DOWN_MESSAGE };
@@ -1,4 +1,4 @@
1
- const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
1
+ const { formatSuccessLine, formatSuccessParagraph, successGlyph } = require('../utils/cli-test-layout-chalk');
2
2
  /**
3
3
  * @fileoverview aifabrix dev init – onboard with Builder Server (issue-cert, save cert, get settings, add SSH key, SSH config alias).
4
4
  * Auth: first call (issue-cert) uses no client cert; other calls send the client cert (mTLS on https, X-Client-Cert header on http for getSettings/addSshKey).
@@ -195,12 +195,18 @@ async function saveCertAndConfig(configDir, devId, certificatePem, keyPem, caPem
195
195
  if (caPem && typeof caPem === 'string' && caPem.trim()) {
196
196
  const caNormalized = normalizePemNewlines(caPem.trim());
197
197
  await fs.writeFile(path.join(certDir, 'ca.pem'), caNormalized, { mode: 0o600 });
198
- logger.log(chalk.green(' ✔ Certificate and CA saved to ') + chalk.cyan(path.join(certDir, 'cert.pem')));
198
+ logger.log(
199
+ `${chalk.green(' ')}${successGlyph()}${chalk.green(' Certificate and CA saved to ')}${chalk.cyan(path.join(certDir, 'cert.pem'))}`
200
+ );
199
201
  } else {
200
- logger.log(chalk.green(' ✔ Certificate saved to ') + chalk.cyan(path.join(certDir, 'cert.pem')));
202
+ logger.log(
203
+ `${chalk.green(' ')}${successGlyph()}${chalk.green(' Certificate saved to ')}${chalk.cyan(path.join(certDir, 'cert.pem'))}`
204
+ );
201
205
  }
202
206
  await config.setDeveloperId(devId);
203
- logger.log(chalk.green(' ✔ Developer ID set to ') + chalk.cyan(devId));
207
+ logger.log(
208
+ `${chalk.green(' ')}${successGlyph()}${chalk.green(' Developer ID set to ')}${chalk.cyan(devId)}`
209
+ );
204
210
  }
205
211
 
206
212
  /**
@@ -217,7 +223,7 @@ async function registerSshKey(baseUrl, clientCertPem, clientKeyPem, devId, serve
217
223
  publicKey,
218
224
  label: 'aifabrix-init'
219
225
  }, clientKeyPem, serverCaPem);
220
- logger.log(chalk.green(' SSH key registered'));
226
+ logger.log(chalk.green(' ') + formatSuccessLine('SSH key registered'));
221
227
  } catch (err) {
222
228
  if (err.status === 409) {
223
229
  logger.log(chalk.yellow(' ⚠ SSH key already registered'));
@@ -262,7 +268,7 @@ async function applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, se
262
268
  const configDir = getConfigDirForPaths();
263
269
  if (issueResponse.settings && typeof issueResponse.settings === 'object') {
264
270
  await config.mergeRemoteSettings(issueResponse.settings);
265
- logger.log(chalk.green(' Config updated from server (issue-cert response)'));
271
+ logger.log(chalk.green(' ') + formatSuccessLine('Config updated from server (issue-cert response)'));
266
272
  return;
267
273
  }
268
274
  logger.log(chalk.gray(' Fetching settings...'));
@@ -272,7 +278,7 @@ async function applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, se
272
278
  }
273
279
  const settings = await devApi.getSettings(baseUrl, issueResponse.certificate, keyPem, serverCaPem);
274
280
  await config.mergeRemoteSettings(settings);
275
- logger.log(chalk.green(' Config updated from server'));
281
+ logger.log(chalk.green(' ') + formatSuccessLine('Config updated from server'));
276
282
  } catch (err) {
277
283
  const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
278
284
  logger.log(chalk.yellow(' ⚠ Could not fetch settings (server may not support cert yet): ' + msg));