@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,377 @@
1
+ /**
2
+ * Interactive edit of one capability slice in a datasource JSON file.
3
+ *
4
+ * @fileoverview capability edit (inquirer + atomic write)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const { execSync } = require('child_process');
11
+ const inquirer = require('inquirer');
12
+ const {
13
+ resolveValidateInputPath,
14
+ validateDatasourceParsed
15
+ } = require('../validate');
16
+ const { normalizeCapabilityKey } = require('./capability-key');
17
+ const {
18
+ resolveLogicalNameForRemove,
19
+ resolveSingleOpsKey,
20
+ resolveProfileKeyForLogical
21
+ } = require('./capability-resolve');
22
+ const { writeBackup, atomicWriteJson } = require('./run-capability-copy');
23
+
24
+ /**
25
+ * @typedef {object} RunCapabilityEditOpts
26
+ * @property {string} fileOrKey
27
+ * @property {string} [capability]
28
+ * @property {'openapi'|'cip'|'profile'} [section]
29
+ * @property {string} [profile]
30
+ * @property {boolean} [noBackup=false]
31
+ */
32
+
33
+ /**
34
+ * @param {object} doc
35
+ * @returns {string[]}
36
+ */
37
+ function collectCapabilityKeys(doc) {
38
+ const s = new Set();
39
+ if (Array.isArray(doc.capabilities)) {
40
+ doc.capabilities.forEach((c) => s.add(String(c)));
41
+ }
42
+ if (doc.openapi?.operations) {
43
+ Object.keys(doc.openapi.operations).forEach((k) => s.add(k));
44
+ }
45
+ if (doc.execution?.cip?.operations) {
46
+ Object.keys(doc.execution.cip.operations).forEach((k) => s.add(k));
47
+ }
48
+ return [...s].sort();
49
+ }
50
+
51
+ /**
52
+ * @param {string[]} keys
53
+ * @param {string} [preset]
54
+ * @returns {Promise<string>}
55
+ */
56
+ async function promptCapabilityKey(keys, preset) {
57
+ if (preset && preset.trim()) {
58
+ return normalizeCapabilityKey(preset.trim(), '--capability');
59
+ }
60
+ const { key } = await inquirer.prompt([
61
+ {
62
+ type: 'list',
63
+ name: 'key',
64
+ message: 'Which capability?',
65
+ choices: keys,
66
+ pageSize: 15
67
+ }
68
+ ]);
69
+ return normalizeCapabilityKey(key, 'capability');
70
+ }
71
+
72
+ /**
73
+ * Resolve openapi/cip operations object key (exact or case-insensitive) for editing.
74
+ *
75
+ * @param {object} doc
76
+ * @param {string} logicalKey - Canonical name from capabilities[] when possible
77
+ * @param {'openapi'|'cip'} section
78
+ * @returns {string}
79
+ */
80
+ function resolveOpsKeyForEdit(doc, logicalKey, section) {
81
+ const ops =
82
+ section === 'openapi' ? doc.openapi?.operations : doc.execution?.cip?.operations;
83
+ const label = section === 'openapi' ? 'openapi.operations' : 'execution.cip.operations';
84
+ const k = resolveSingleOpsKey(ops, logicalKey, label);
85
+ if (!k) {
86
+ throw new Error(
87
+ `Missing ${label} entry for capability "${logicalKey}" (no exact or case-insensitive key match).`
88
+ );
89
+ }
90
+ return k;
91
+ }
92
+
93
+ /**
94
+ * @param {string} [preset]
95
+ * @returns {Promise<'openapi'|'cip'|'profile'>}
96
+ */
97
+ async function promptSection(preset) {
98
+ const allowed = ['openapi', 'cip', 'profile'];
99
+ if (preset && allowed.includes(preset)) {
100
+ return preset;
101
+ }
102
+ const { section } = await inquirer.prompt([
103
+ {
104
+ type: 'list',
105
+ name: 'section',
106
+ message: 'Which slice to edit?',
107
+ choices: [
108
+ { name: 'openapi.operations.<key> (JSON)', value: 'openapi' },
109
+ { name: 'execution.cip.operations.<key> (JSON)', value: 'cip' },
110
+ { name: 'exposed.profiles.<profile> (JSON)', value: 'profile' }
111
+ ]
112
+ }
113
+ ]);
114
+ return section;
115
+ }
116
+
117
+ /**
118
+ * If EDITOR/VISUAL unset, prefer nano when available on PATH (common on minimal Linux images).
119
+ *
120
+ * @returns {void}
121
+ */
122
+ function ensureDefaultEditorFallback() {
123
+ if (process.env.VISUAL && String(process.env.VISUAL).trim()) {
124
+ return;
125
+ }
126
+ if (process.env.EDITOR && String(process.env.EDITOR).trim()) {
127
+ return;
128
+ }
129
+ try {
130
+ execSync('command -v nano', { stdio: 'ignore' });
131
+ process.env.EDITOR = 'nano';
132
+ } catch {
133
+ /* fall through to vi / platform default */
134
+ }
135
+ }
136
+
137
+ /**
138
+ * @param {object} doc
139
+ * @param {string} [preset]
140
+ * @param {boolean} [capabilityFromCli=false] - If true and no profile matches the capability, throw (do not list).
141
+ * @param {string} [logicalCapKey]
142
+ * @returns {Promise<string>}
143
+ */
144
+ async function promptProfileKey(doc, preset, capabilityFromCli, logicalCapKey) {
145
+ const keys = doc.exposed?.profiles ? Object.keys(doc.exposed.profiles).sort() : [];
146
+ if (keys.length === 0) {
147
+ throw new Error('No exposed.profiles in this datasource');
148
+ }
149
+ if (preset && preset.trim()) {
150
+ const k = preset.trim();
151
+ if (!keys.includes(k)) {
152
+ throw new Error(`exposed.profiles.${k} not found (available: ${keys.join(', ')})`);
153
+ }
154
+ return k;
155
+ }
156
+ if (logicalCapKey) {
157
+ const pk = resolveProfileKeyForLogical(doc, logicalCapKey);
158
+ if (pk !== null && doc.exposed?.profiles?.[pk] !== undefined) {
159
+ return pk;
160
+ }
161
+ }
162
+ if (capabilityFromCli && logicalCapKey) {
163
+ throw new Error(
164
+ `No exposed.profiles row matches capability "${logicalCapKey}" for -c / --capability. ` +
165
+ `Pass --profile <key> or choose from: ${keys.join(', ')}`
166
+ );
167
+ }
168
+ const { pk } = await inquirer.prompt([
169
+ {
170
+ type: 'list',
171
+ name: 'pk',
172
+ message: 'Which exposure profile?',
173
+ choices: keys,
174
+ pageSize: 15
175
+ }
176
+ ]);
177
+ return pk;
178
+ }
179
+
180
+ /**
181
+ * @param {string} initialJson
182
+ * @returns {Promise<string>}
183
+ */
184
+ async function promptEditorJson(initialJson) {
185
+ ensureDefaultEditorFallback();
186
+ const { body } = await inquirer.prompt([
187
+ {
188
+ type: 'editor',
189
+ name: 'body',
190
+ message: 'Edit JSON (save and close editor to continue)',
191
+ default: initialJson,
192
+ postfix: '.json'
193
+ }
194
+ ]);
195
+ return body;
196
+ }
197
+
198
+ /**
199
+ * @param {object} doc
200
+ * @param {string} capKey
201
+ * @param {'openapi'|'cip'|'profile'} section
202
+ * @param {string} profileKey
203
+ * @returns {string}
204
+ */
205
+ function getEditableJson(doc, logicalCapKey, section, profileKey) {
206
+ if (section === 'openapi') {
207
+ const k = resolveOpsKeyForEdit(doc, logicalCapKey, 'openapi');
208
+ const v = doc.openapi.operations[k];
209
+ return JSON.stringify(v, null, 2);
210
+ }
211
+ if (section === 'cip') {
212
+ const k = resolveOpsKeyForEdit(doc, logicalCapKey, 'cip');
213
+ const v = doc.execution.cip.operations[k];
214
+ return JSON.stringify(v, null, 2);
215
+ }
216
+ const pk =
217
+ profileKey && profileKey.trim()
218
+ ? profileKey.trim()
219
+ : resolveProfileKeyForLogical(doc, logicalCapKey);
220
+ if (!pk || doc.exposed?.profiles?.[pk] === undefined) {
221
+ throw new Error(
222
+ `Missing exposed.profiles.${profileKey || logicalCapKey} (no matching profile key).`
223
+ );
224
+ }
225
+ const v = doc.exposed.profiles[pk];
226
+ return JSON.stringify(v, null, 2);
227
+ }
228
+
229
+ /**
230
+ * @param {string} jsonBody
231
+ * @returns {object}
232
+ */
233
+ function parseEditorJsonPayload(jsonBody) {
234
+ try {
235
+ return JSON.parse(jsonBody);
236
+ } catch (e) {
237
+ throw new Error(`Invalid JSON: ${e.message}`);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * @param {object} doc
243
+ * @param {string} capKey
244
+ * @param {object} value
245
+ * @returns {void}
246
+ */
247
+ function assignOpenapiOperation(doc, capKey, value) {
248
+ if (!doc.openapi) {
249
+ doc.openapi = {};
250
+ }
251
+ if (!doc.openapi.operations) {
252
+ doc.openapi.operations = {};
253
+ }
254
+ doc.openapi.operations[capKey] = value;
255
+ }
256
+
257
+ /**
258
+ * @param {object} doc
259
+ * @param {string} capKey
260
+ * @param {object} value
261
+ * @returns {void}
262
+ */
263
+ function assignCipOperation(doc, capKey, value) {
264
+ if (!doc.execution) {
265
+ doc.execution = {};
266
+ }
267
+ if (!doc.execution.cip) {
268
+ doc.execution.cip = {};
269
+ }
270
+ if (!doc.execution.cip.operations) {
271
+ doc.execution.cip.operations = {};
272
+ }
273
+ doc.execution.cip.operations[capKey] = value;
274
+ }
275
+
276
+ /**
277
+ * @param {object} doc
278
+ * @param {string} profileKey
279
+ * @param {object} value
280
+ * @returns {void}
281
+ */
282
+ function assignExposedProfile(doc, profileKey, value) {
283
+ if (!doc.exposed) {
284
+ doc.exposed = {};
285
+ }
286
+ if (!doc.exposed.profiles) {
287
+ doc.exposed.profiles = {};
288
+ }
289
+ doc.exposed.profiles[profileKey] = value;
290
+ }
291
+
292
+ /**
293
+ * @param {object} doc
294
+ * @param {string} capKey
295
+ * @param {'openapi'|'cip'|'profile'} section
296
+ * @param {string} profileKey
297
+ * @param {string} jsonBody
298
+ * @returns {void}
299
+ */
300
+ function applyEditedJson(doc, logicalCapKey, section, profileKey, jsonBody) {
301
+ const parsed = parseEditorJsonPayload(jsonBody);
302
+ if (section === 'openapi') {
303
+ const k = resolveOpsKeyForEdit(doc, logicalCapKey, 'openapi');
304
+ assignOpenapiOperation(doc, k, parsed);
305
+ return;
306
+ }
307
+ if (section === 'cip') {
308
+ const k = resolveOpsKeyForEdit(doc, logicalCapKey, 'cip');
309
+ assignCipOperation(doc, k, parsed);
310
+ return;
311
+ }
312
+ const pk =
313
+ profileKey && profileKey.trim()
314
+ ? profileKey.trim()
315
+ : resolveProfileKeyForLogical(doc, logicalCapKey);
316
+ if (!pk) {
317
+ throw new Error(`Cannot resolve exposed.profiles key for "${logicalCapKey}".`);
318
+ }
319
+ assignExposedProfile(doc, pk, parsed);
320
+ }
321
+
322
+ /**
323
+ * @param {object} doc
324
+ * @param {RunCapabilityEditOpts} opts
325
+ * @returns {Promise<{ capKey: string, section: string, profileKey: string }>}
326
+ */
327
+ async function resolveEditTargets(doc, opts) {
328
+ const keys = collectCapabilityKeys(doc);
329
+ if (keys.length === 0) {
330
+ throw new Error(
331
+ 'No capability keys found (check capabilities[], openapi.operations, execution.cip.operations)'
332
+ );
333
+ }
334
+ const capKeyRaw = await promptCapabilityKey(keys, opts.capability);
335
+ const capKey = resolveLogicalNameForRemove(doc, capKeyRaw);
336
+ const section = await promptSection(opts.section);
337
+ let profileKey = '';
338
+ if (section === 'profile') {
339
+ const capabilityFromCli = Boolean(opts.capability && String(opts.capability).trim());
340
+ profileKey = await promptProfileKey(doc, opts.profile, capabilityFromCli, capKey);
341
+ }
342
+ return { capKey, section, profileKey };
343
+ }
344
+
345
+ /**
346
+ * @param {RunCapabilityEditOpts} opts
347
+ * @returns {Promise<{ resolvedPath: string, backupPath: string|null }>}
348
+ */
349
+ async function runCapabilityEdit(opts) {
350
+ if (!process.stdin.isTTY) {
351
+ throw new Error('capability edit requires an interactive terminal (TTY). Use a terminal or SSH with -t.');
352
+ }
353
+ const resolvedPath = resolveValidateInputPath(opts.fileOrKey.trim());
354
+ const doc = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
355
+ const { capKey, section, profileKey } = await resolveEditTargets(doc, opts);
356
+ const initial = getEditableJson(doc, capKey, section, profileKey);
357
+ const body = await promptEditorJson(initial);
358
+ applyEditedJson(doc, capKey, section, profileKey, body);
359
+
360
+ const validation = validateDatasourceParsed(doc);
361
+ if (!validation.valid) {
362
+ const err = new Error(validation.errors.join('\n'));
363
+ err.validationErrors = validation.errors;
364
+ throw err;
365
+ }
366
+
367
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
368
+ atomicWriteJson(resolvedPath, doc);
369
+
370
+ return { resolvedPath, backupPath };
371
+ }
372
+
373
+ module.exports = {
374
+ runCapabilityEdit,
375
+ collectCapabilityKeys,
376
+ resolveOpsKeyForEdit
377
+ };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * File-backed capability relate (foreignKeys metadata).
3
+ *
4
+ * @fileoverview runCapabilityRelate
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const {
11
+ resolveValidateInputPath,
12
+ validateDatasourceParsed
13
+ } = require('../validate');
14
+ const { applyCapabilityRelate } = require('./relate-operations');
15
+ const { validateRelateSemantics } = require('./relate-validate');
16
+ const { writeBackup, atomicWriteJson } = require('./run-capability-copy');
17
+ const { tryResolveDatasourceKeyToLocalPath, readJsonFile } = require('../../resolvers/datasource-resolver');
18
+ const { tryFetchDatasourceConfig } = require('../../resolvers/manifest-resolver');
19
+
20
+ function resolveSystemKeyForAuth(sourceDoc) {
21
+ return typeof sourceDoc?.systemKey === 'string' ? sourceDoc.systemKey.trim() : '';
22
+ }
23
+
24
+ function loadTargetDocLocal(targetDatasourceKey) {
25
+ const localTarget = tryResolveDatasourceKeyToLocalPath(targetDatasourceKey);
26
+ return localTarget.ok ? readJsonFile(localTarget.path) : null;
27
+ }
28
+
29
+ async function maybeFetchRemoteTargetConfig({ systemKeyForAuth, targetDatasourceKey }) {
30
+ const remoteFetchMeta = { attempted: false, ok: false, notAuthenticated: false };
31
+ if (!systemKeyForAuth) {
32
+ return { remoteTargetConfig: null, remoteFetchMeta };
33
+ }
34
+ remoteFetchMeta.attempted = true;
35
+ const remote = await tryFetchDatasourceConfig(systemKeyForAuth, targetDatasourceKey, { silent: true });
36
+ if (remote.ok) {
37
+ return {
38
+ remoteTargetConfig: remote.datasourceConfig,
39
+ remoteFetchMeta: { attempted: true, ok: true, notAuthenticated: false }
40
+ };
41
+ }
42
+ if (remote.code === 'not_authenticated') {
43
+ remoteFetchMeta.notAuthenticated = true;
44
+ }
45
+ return { remoteTargetConfig: null, remoteFetchMeta };
46
+ }
47
+
48
+ function runSemanticValidation({ sourceDoc, targetDocLocal, remoteTargetConfig, opts, remoteFetchMeta }) {
49
+ const semantic = validateRelateSemantics({
50
+ localContext: {
51
+ sourceDoc,
52
+ targetDocLocal,
53
+ targetDatasourceKey: opts.targetDatasource,
54
+ fields: opts.fields,
55
+ targetFields: opts.targetFields
56
+ },
57
+ remoteManifest: remoteTargetConfig
58
+ });
59
+ if (!semantic.ok) {
60
+ const err = new Error(semantic.errors.join('\n'));
61
+ err.validationErrors = semantic.errors;
62
+ err.validationWarnings = semantic.warnings;
63
+ err.remoteValidation = remoteFetchMeta;
64
+ throw err;
65
+ }
66
+ return semantic;
67
+ }
68
+
69
+ function buildRelateResultEnvelope({ dryRun, resolvedPath, result, backupPath, validation, semanticWarnings, remoteValidation }) {
70
+ return {
71
+ dryRun,
72
+ resolvedPath,
73
+ patchOperations: result.patchOperations,
74
+ updatedSections: result.updatedSections,
75
+ backupPath,
76
+ validation,
77
+ semanticWarnings,
78
+ remoteValidation
79
+ };
80
+ }
81
+
82
+ function applyRelateAndValidateSchema(parsed, opts, semanticWarnings, remoteValidation) {
83
+ const result = applyCapabilityRelate(parsed, {
84
+ relationName: opts.relationName,
85
+ targetDatasource: opts.targetDatasource,
86
+ fields: opts.fields,
87
+ targetFields: opts.targetFields,
88
+ // Optional enrichment for metadataSchema.properties.<relationName> generation
89
+ targetDoc: opts.targetDoc,
90
+ resolvedTargetFields: opts.resolvedTargetFields,
91
+ required: opts.required,
92
+ description: opts.description,
93
+ overwrite: Boolean(opts.overwrite),
94
+ addMetadataProperty: opts.addMetadataProperty !== false
95
+ });
96
+
97
+ const validation = validateDatasourceParsed(result.doc);
98
+ if (!validation.valid) {
99
+ const err = new Error(validation.errors.join('\n'));
100
+ err.validationErrors = validation.errors;
101
+ err.validationWarnings = semanticWarnings;
102
+ err.remoteValidation = remoteValidation;
103
+ throw err;
104
+ }
105
+
106
+ return { result, validation };
107
+ }
108
+
109
+ /**
110
+ * @typedef {object} RunCapabilityRelateOpts
111
+ * @property {string} fileOrKey
112
+ * @property {string} relationName
113
+ * @property {string} targetDatasource
114
+ * @property {string[]} fields
115
+ * @property {string[]|undefined} targetFields
116
+ * @property {boolean|undefined} [required]
117
+ * @property {string|undefined} [description]
118
+ * @property {boolean} [dryRun=false]
119
+ * @property {boolean} [noBackup=false]
120
+ * @property {boolean} [overwrite=false]
121
+ * @property {boolean} [addMetadataProperty=true]
122
+ */
123
+
124
+ /**
125
+ * @param {RunCapabilityRelateOpts} opts
126
+ * @returns {Promise<{
127
+ * dryRun: boolean,
128
+ * resolvedPath: string,
129
+ * patchOperations: object[],
130
+ * updatedSections: string[],
131
+ * backupPath: string|null,
132
+ * validation: object
133
+ * }>}
134
+ */
135
+ async function runCapabilityRelate(opts) {
136
+ const resolvedPath = resolveValidateInputPath(opts.fileOrKey.trim());
137
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
138
+ const parsed = JSON.parse(raw);
139
+
140
+ const targetDocLocal = loadTargetDocLocal(opts.targetDatasource);
141
+ const systemKeyForAuth = resolveSystemKeyForAuth(parsed);
142
+ const { remoteTargetConfig, remoteFetchMeta } = await maybeFetchRemoteTargetConfig({
143
+ systemKeyForAuth,
144
+ targetDatasourceKey: opts.targetDatasource
145
+ });
146
+ const semantic = runSemanticValidation({
147
+ sourceDoc: parsed,
148
+ targetDocLocal,
149
+ remoteTargetConfig,
150
+ opts,
151
+ remoteFetchMeta
152
+ });
153
+
154
+ // Feed resolved target metadata into metadataSchema relation-property generation.
155
+ opts.targetDoc = semantic?.resolved?.targetDoc || null;
156
+ opts.resolvedTargetFields = semantic?.resolved?.resolvedTargetFields || null;
157
+
158
+ const { result, validation } = applyRelateAndValidateSchema(
159
+ parsed,
160
+ opts,
161
+ semantic.warnings,
162
+ remoteFetchMeta
163
+ );
164
+
165
+ if (opts.dryRun) {
166
+ return buildRelateResultEnvelope({
167
+ dryRun: true,
168
+ resolvedPath,
169
+ result,
170
+ backupPath: null,
171
+ validation,
172
+ semanticWarnings: semantic.warnings,
173
+ remoteValidation: remoteFetchMeta
174
+ });
175
+ }
176
+
177
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
178
+ atomicWriteJson(resolvedPath, result.doc);
179
+
180
+ return buildRelateResultEnvelope({
181
+ dryRun: false,
182
+ resolvedPath,
183
+ result,
184
+ backupPath,
185
+ validation,
186
+ semanticWarnings: semantic.warnings,
187
+ remoteValidation: remoteFetchMeta
188
+ });
189
+ }
190
+
191
+ module.exports = {
192
+ runCapabilityRelate
193
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * File-backed capability remove with validation, backup, and atomic write.
3
+ *
4
+ * @fileoverview Run datasource capability remove CLI operation
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const {
11
+ resolveValidateInputPath,
12
+ validateDatasourceParsed
13
+ } = require('../validate');
14
+ const { normalizeCapabilityKey } = require('./capability-key');
15
+ const { applyCapabilityRemove } = require('./remove-operations');
16
+ const { writeBackup, atomicWriteJson } = require('./run-capability-copy');
17
+
18
+ /**
19
+ * @param {{ valid: boolean, errors: string[] }} validation
20
+ * @returns {void}
21
+ * @throws {Error} When validation failed
22
+ */
23
+ function assertDatasourceValid(validation) {
24
+ if (!validation.valid) {
25
+ const err = new Error(validation.errors.join('\n'));
26
+ err.validationErrors = validation.errors;
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {string} resolvedPath
33
+ * @param {string} capability
34
+ * @param {object} result - applyCapabilityRemove result
35
+ * @param {object} validation
36
+ * @param {boolean} dryRun
37
+ * @param {string|null} backupPath
38
+ * @returns {object}
39
+ */
40
+ function makeRemoveResult(resolvedPath, capability, result, validation, dryRun, backupPath) {
41
+ return {
42
+ dryRun,
43
+ resolvedPath,
44
+ capability,
45
+ removed: result.removed,
46
+ patchOperations: result.patchOperations,
47
+ updatedSections: result.updatedSections,
48
+ backupPath,
49
+ validation
50
+ };
51
+ }
52
+
53
+ /**
54
+ * @typedef {object} RunCapabilityRemoveOpts
55
+ * @property {string} fileOrKey
56
+ * @property {string} capability
57
+ * @property {boolean} [dryRun=false]
58
+ * @property {boolean} [noBackup=false]
59
+ * @property {boolean} [force=false]
60
+ */
61
+
62
+ /**
63
+ * Execute capability remove on disk (or dry-run).
64
+ *
65
+ * @param {RunCapabilityRemoveOpts} opts
66
+ * @returns {Promise<object>}
67
+ */
68
+ async function runCapabilityRemove(opts) {
69
+ const resolvedPath = resolveValidateInputPath(opts.fileOrKey.trim());
70
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
71
+ const parsed = JSON.parse(raw);
72
+
73
+ const capability = normalizeCapabilityKey(opts.capability, 'Capability (--capability)');
74
+
75
+ const result = applyCapabilityRemove(parsed, {
76
+ capability,
77
+ force: Boolean(opts.force)
78
+ });
79
+
80
+ /** Skip AJV when already absent with --force: doc is unchanged clone; no disk write; avoids CI/schema flakes. */
81
+ let validation;
82
+ if (opts.force && !result.removed) {
83
+ validation = { valid: true, errors: [], warnings: [], summary: null };
84
+ } else {
85
+ validation = validateDatasourceParsed(result.doc);
86
+ assertDatasourceValid(validation);
87
+ }
88
+
89
+ if (opts.dryRun) {
90
+ return makeRemoveResult(resolvedPath, capability, result, validation, true, null);
91
+ }
92
+
93
+ if (!result.removed) {
94
+ return makeRemoveResult(resolvedPath, capability, result, validation, false, null);
95
+ }
96
+
97
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
98
+ atomicWriteJson(resolvedPath, result.doc);
99
+
100
+ return makeRemoveResult(resolvedPath, capability, result, validation, false, backupPath);
101
+ }
102
+
103
+ module.exports = {
104
+ runCapabilityRemove
105
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "openapiOperation": {
3
+ "operationId": "minimal-fetch-placeholder",
4
+ "method": "GET",
5
+ "path": "/minimal-fetch"
6
+ },
7
+ "cipOperation": {
8
+ "enabled": true,
9
+ "steps": [
10
+ {
11
+ "fetch": {
12
+ "source": "openapi",
13
+ "openapiRef": "__STORAGE_KEY__"
14
+ }
15
+ }
16
+ ]
17
+ }
18
+ }