@aifabrix/builder 2.44.4 → 2.44.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +68 -17
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +44 -12
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Semantic validation for `datasource capability relate`.
3
+ *
4
+ * Pure validation module: no CLI/auth logic.
5
+ *
6
+ * @fileoverview relate semantic validator
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ /**
14
+ * @typedef {Object} RelateLocalContext
15
+ * @property {any} sourceDoc
16
+ * @property {any|null} targetDocLocal
17
+ * @property {string} targetDatasourceKey
18
+ * @property {string[]} fields
19
+ * @property {string[]|undefined} targetFields
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} RelateValidationInput
24
+ * @property {RelateLocalContext} localContext
25
+ * @property {any|null} remoteManifest - remote target datasource config (or null)
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} RelateValidationResult
30
+ * @property {boolean} ok
31
+ * @property {string[]} errors
32
+ * @property {string[]} warnings
33
+ * @property {{ targetDoc: any|null, resolvedTargetFields: string[] }} resolved
34
+ */
35
+
36
+ function _asObject(x) {
37
+ return x && typeof x === 'object' ? x : null;
38
+ }
39
+
40
+ function _getMetadataProp(doc, fieldName) {
41
+ const props = doc?.metadataSchema?.properties;
42
+ if (!props || typeof props !== 'object') return null;
43
+ const node = props[fieldName];
44
+ return _asObject(node);
45
+ }
46
+
47
+ function _getFieldMappingsAttributes(doc) {
48
+ const attrs = doc?.fieldMappings?.attributes;
49
+ return attrs && typeof attrs === 'object' ? attrs : null;
50
+ }
51
+
52
+ function _normalizeType(node) {
53
+ const t = node?.type;
54
+ if (typeof t === 'string') return t;
55
+ if (Array.isArray(t)) {
56
+ // common JSONSchema pattern: ["string","null"]
57
+ const nonNull = t.filter((x) => x && x !== 'null');
58
+ if (nonNull.length === 1 && typeof nonNull[0] === 'string') return nonNull[0];
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function _isNullable(node) {
64
+ // dataplane uses `nullable: true` convention in config metadataSchema
65
+ return node?.nullable === true;
66
+ }
67
+
68
+ /**
69
+ * @param {RelateValidationInput} input
70
+ * @returns {RelateValidationResult}
71
+ */
72
+ function validateRelateSemantics(input) {
73
+ const errors = [];
74
+ const warnings = [];
75
+ const local = input?.localContext;
76
+ const source = local?.sourceDoc;
77
+ const targetDatasourceKey = String(local?.targetDatasourceKey || '').trim();
78
+ const fields = Array.isArray(local?.fields) ? local.fields : [];
79
+ const resolvedTargetFields = _resolveTargetFields(local?.targetFields);
80
+
81
+ if (!_asObject(source)) {
82
+ errors.push('Local validation failed: sourceDoc missing or invalid.');
83
+ return { ok: false, errors, warnings, resolved: { targetDoc: null, resolvedTargetFields } };
84
+ }
85
+
86
+ const sourceAttrs = _getFieldMappingsAttributes(source);
87
+ const localFieldMeta = _validateSourceSide({ source, sourceAttrs, fields, errors });
88
+
89
+ const targetDoc = _resolveTargetDoc(local?.targetDocLocal, input?.remoteManifest);
90
+ if (!targetDoc) {
91
+ errors.push(
92
+ `Target datasource not found locally and remote validation did not provide a target config: ${targetDatasourceKey}`
93
+ );
94
+ return { ok: false, errors, warnings, resolved: { targetDoc: null, resolvedTargetFields } };
95
+ }
96
+
97
+ _validateJoinSemantics({
98
+ targetDoc,
99
+ targetDatasourceKey,
100
+ fields,
101
+ resolvedTargetFields,
102
+ localFieldMeta,
103
+ errors,
104
+ warnings
105
+ });
106
+
107
+ return { ok: errors.length === 0, errors, warnings, resolved: { targetDoc, resolvedTargetFields } };
108
+ }
109
+
110
+ function _resolveTargetFields(targetFieldsRaw) {
111
+ return Array.isArray(targetFieldsRaw) && targetFieldsRaw.length > 0
112
+ ? targetFieldsRaw.map((x) => String(x).trim()).filter(Boolean)
113
+ : ['externalId'];
114
+ }
115
+
116
+ function _resolveTargetDoc(targetDocLocal, remoteManifest) {
117
+ if (_asObject(targetDocLocal)) return targetDocLocal;
118
+ if (_asObject(remoteManifest)) return remoteManifest;
119
+ return null;
120
+ }
121
+
122
+ function _validateSourceSide({ source, sourceAttrs, fields, errors }) {
123
+ if (!fields || fields.length === 0) {
124
+ errors.push('Local validation failed: at least one local field is required.');
125
+ return {};
126
+ }
127
+ if (!sourceAttrs) {
128
+ errors.push('Source must declare fieldMappings.attributes (object).');
129
+ }
130
+
131
+ /** @type {Record<string, { type: string|null, nullable: boolean }>} */
132
+ const localFieldMeta = {};
133
+ for (const f of fields) {
134
+ const name = String(f || '').trim();
135
+ if (!name) {
136
+ errors.push('Local field names must be non-empty strings.');
137
+ continue;
138
+ }
139
+ if (sourceAttrs && !(name in sourceAttrs)) {
140
+ errors.push(`Source field not found in fieldMappings.attributes: ${name}`);
141
+ }
142
+ const node = _getMetadataProp(source, name);
143
+ if (!node) {
144
+ errors.push(`Source field not found in metadataSchema.properties: ${name}`);
145
+ continue;
146
+ }
147
+ localFieldMeta[name] = { type: _normalizeType(node), nullable: _isNullable(node) };
148
+ }
149
+ return localFieldMeta;
150
+ }
151
+
152
+ function _validateJoinSemantics({
153
+ targetDoc,
154
+ targetDatasourceKey,
155
+ fields,
156
+ resolvedTargetFields,
157
+ localFieldMeta,
158
+ errors,
159
+ warnings
160
+ }) {
161
+ const targetAttrs = _getFieldMappingsAttributes(targetDoc);
162
+ if (!targetAttrs) {
163
+ warnings.push(`Target datasource is missing fieldMappings.attributes: ${targetDatasourceKey}`);
164
+ }
165
+
166
+ if (resolvedTargetFields.length !== fields.length) {
167
+ errors.push(
168
+ `Foreign key cardinality mismatch: ${fields.length} local field(s) vs ${resolvedTargetFields.length} target field(s).`
169
+ );
170
+ return;
171
+ }
172
+
173
+ for (let i = 0; i < fields.length; i += 1) {
174
+ _validateJoinFieldPair({
175
+ targetDoc,
176
+ targetDatasourceKey,
177
+ localName: String(fields[i] || '').trim(),
178
+ targetName: String(resolvedTargetFields[i] || '').trim(),
179
+ localFieldMeta,
180
+ errors,
181
+ warnings
182
+ });
183
+ }
184
+ }
185
+
186
+ function _validateJoinFieldPair({
187
+ targetDoc,
188
+ targetDatasourceKey,
189
+ localName,
190
+ targetName,
191
+ localFieldMeta,
192
+ errors,
193
+ warnings
194
+ }) {
195
+ if (!targetName) {
196
+ errors.push('Target field names must be non-empty strings.');
197
+ return;
198
+ }
199
+ const targetNode = _getMetadataProp(targetDoc, targetName);
200
+ if (!targetNode) {
201
+ errors.push(`Target field not found in metadataSchema.properties: ${targetDatasourceKey}.${targetName}`);
202
+ return;
203
+ }
204
+ const lt = localFieldMeta[localName]?.type || null;
205
+ const tt = _normalizeType(targetNode);
206
+ if (lt && tt && lt !== tt) {
207
+ errors.push(`Type mismatch: ${localName} (${lt}) does not match ${targetDatasourceKey}.${targetName} (${tt}).`);
208
+ }
209
+ const ln = localFieldMeta[localName]?.nullable === true;
210
+ const tn = _isNullable(targetNode);
211
+ if (ln !== tn) {
212
+ warnings.push(`Nullable mismatch: ${localName} nullable=${ln} vs ${targetDatasourceKey}.${targetName} nullable=${tn}.`);
213
+ }
214
+ }
215
+
216
+ module.exports = {
217
+ validateRelateSemantics
218
+ };
219
+
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Remove a capability from one datasource JSON document (including profile + testPayload cleanup).
3
+ *
4
+ * @fileoverview capability remove — inverse of copy
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { jsonPointerPath } = require('./json-pointer');
10
+ const {
11
+ capabilityExists,
12
+ removeCapability,
13
+ deepClone
14
+ } = require('./copy-operations');
15
+ const {
16
+ resolveLogicalNameForRemove,
17
+ findMatchingOpsKeys,
18
+ resolveProfileKeyForLogical
19
+ } = require('./capability-resolve');
20
+
21
+ /**
22
+ * @param {unknown} capabilities
23
+ * @param {string} logicalName
24
+ * @returns {number} Index or -1
25
+ */
26
+ function findCapabilityIndex(capabilities, logicalName) {
27
+ if (!Array.isArray(capabilities)) {
28
+ return -1;
29
+ }
30
+ return capabilities.findIndex(
31
+ (c) => String(c).toLowerCase() === String(logicalName).toLowerCase()
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Planned RFC 6902 remove ops against `originalDoc` (before mutation). Paths use actual
37
+ * openapi/CIP object keys and scenario array indices (descending order for stability).
38
+ *
39
+ * @param {object} originalDoc
40
+ * @param {string} logicalName
41
+ * @param {string|undefined} openapiKey
42
+ * @param {string|undefined} cipKey
43
+ * @param {string|null} profileKey
44
+ * @returns {object[]}
45
+ */
46
+ function buildRemovePatchOperations(originalDoc, logicalName, openapiKey, cipKey, profileKey) {
47
+ const ops = [];
48
+
49
+ const capIdx = findCapabilityIndex(originalDoc.capabilities, logicalName);
50
+ if (capIdx >= 0) {
51
+ ops.push({ op: 'remove', path: `/capabilities/${capIdx}` });
52
+ }
53
+
54
+ const ooPath = openapiKey ?? logicalName;
55
+ if (originalDoc.openapi?.operations?.[ooPath] !== undefined) {
56
+ ops.push({ op: 'remove', path: jsonPointerPath('openapi', 'operations', ooPath) });
57
+ }
58
+
59
+ const cipPath = cipKey ?? logicalName;
60
+ if (originalDoc.execution?.cip?.operations?.[cipPath] !== undefined) {
61
+ ops.push({ op: 'remove', path: jsonPointerPath('execution', 'cip', 'operations', cipPath) });
62
+ }
63
+
64
+ if (
65
+ profileKey !== null &&
66
+ profileKey !== undefined &&
67
+ originalDoc.exposed?.profiles?.[profileKey] !== undefined
68
+ ) {
69
+ ops.push({ op: 'remove', path: jsonPointerPath('exposed', 'profiles', profileKey) });
70
+ }
71
+
72
+ const aliases = new Set(
73
+ [logicalName, openapiKey, cipKey].filter((x) => x !== undefined && x !== null && x !== '')
74
+ );
75
+ pushScenarioRemovePatches(originalDoc, aliases, ops);
76
+
77
+ return ops;
78
+ }
79
+
80
+ /**
81
+ * @param {object} originalDoc
82
+ * @param {Set<string>} aliases
83
+ * @param {object[]} ops
84
+ * @returns {void}
85
+ */
86
+ function pushScenarioRemovePatches(originalDoc, aliases, ops) {
87
+ const scenarios = originalDoc.testPayload?.scenarios;
88
+ if (!Array.isArray(scenarios)) {
89
+ return;
90
+ }
91
+ const idxs = [];
92
+ scenarios.forEach((s, i) => {
93
+ if (s && aliases.has(s.operation)) {
94
+ idxs.push(i);
95
+ }
96
+ });
97
+ idxs.sort((a, b) => b - a);
98
+ for (const i of idxs) {
99
+ ops.push({ op: 'remove', path: jsonPointerPath('testPayload', 'scenarios', String(i)) });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Drop testPayload.scenarios entries whose `operation` matches any alias (logical + openapi/cip keys).
105
+ *
106
+ * @param {object} doc - Mutated datasource
107
+ * @param {string} logicalKey - Canonical capability name
108
+ * @param {string|undefined} openapiKey - openapi.operations key removed
109
+ * @param {string|undefined} cipKey - cip.operations key removed
110
+ * @returns {boolean} True if the scenarios array changed
111
+ */
112
+ function pruneTestPayloadScenarios(doc, logicalKey, openapiKey, cipKey) {
113
+ const scenarios = doc.testPayload?.scenarios;
114
+ if (!Array.isArray(scenarios)) {
115
+ return false;
116
+ }
117
+ const aliases = new Set(
118
+ [logicalKey, openapiKey, cipKey].filter((x) => x !== undefined && x !== null && x !== '')
119
+ );
120
+ const next = scenarios.filter((s) => !s || !aliases.has(s.operation));
121
+ if (next.length === scenarios.length) {
122
+ return false;
123
+ }
124
+ doc.testPayload.scenarios = next;
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Drop empty `testPayload.scenarios`, then remove `testPayload` when it becomes `{}`.
130
+ *
131
+ * @param {object} doc - Mutated datasource
132
+ * @returns {boolean} True if something was deleted from the tree
133
+ */
134
+ function finalizeTestPayloadShape(doc) {
135
+ let changed = false;
136
+ const tp = doc.testPayload;
137
+ if (!tp || typeof tp !== 'object') {
138
+ return false;
139
+ }
140
+ if (Array.isArray(tp.scenarios) && tp.scenarios.length === 0) {
141
+ delete tp.scenarios;
142
+ changed = true;
143
+ }
144
+ if (Object.keys(tp).length === 0) {
145
+ delete doc.testPayload;
146
+ changed = true;
147
+ }
148
+ return changed;
149
+ }
150
+
151
+ /**
152
+ * Remove `exposed.profiles` when it is an empty object after profile deletion.
153
+ *
154
+ * @param {object} doc - Mutated datasource
155
+ * @returns {boolean} True if `exposed.profiles` was removed
156
+ */
157
+ function finalizeEmptyExposedProfiles(doc) {
158
+ if (!doc.exposed || typeof doc.exposed !== 'object') {
159
+ return false;
160
+ }
161
+ const prof = doc.exposed.profiles;
162
+ if (!prof || typeof prof !== 'object' || Object.keys(prof).length > 0) {
163
+ return false;
164
+ }
165
+ delete doc.exposed.profiles;
166
+ return true;
167
+ }
168
+
169
+ /**
170
+ * Mutate doc after capability slices removed: scenarios, empty containers, summary lines.
171
+ *
172
+ * @param {object} doc - Cloned datasource (mutated)
173
+ * @param {object} originalDoc - Original parsed JSON (for testPayload diff only)
174
+ * @param {string} logicalName
175
+ * @param {{ openapiKey?: string, cipKey?: string }} removedKeys - Actual openapi/CIP object keys removed
176
+ * @param {string[]} updatedSections - Human-readable sections changed
177
+ * @returns {void}
178
+ */
179
+ function applyCapabilityRemoveSideEffects(
180
+ doc,
181
+ originalDoc,
182
+ logicalName,
183
+ removedKeys,
184
+ updatedSections
185
+ ) {
186
+ const openapiKey = removedKeys?.openapiKey;
187
+ const cipKey = removedKeys?.cipKey;
188
+ const testPayloadBeforeJson = JSON.stringify(originalDoc.testPayload);
189
+ pruneTestPayloadScenarios(doc, logicalName, openapiKey, cipKey);
190
+ finalizeTestPayloadShape(doc);
191
+
192
+ const ooPath = openapiKey ?? logicalName;
193
+ const cipPath = cipKey ?? logicalName;
194
+ updatedSections.push(
195
+ 'removed: capabilities / openapi.operations.' +
196
+ ooPath +
197
+ ' / execution.cip.operations.' +
198
+ cipPath,
199
+ 'exposed.profiles / testPayload.scenarios (see JSON Patch paths)'
200
+ );
201
+
202
+ if (finalizeEmptyExposedProfiles(doc)) {
203
+ updatedSections.push('exposed.profiles (removed empty object)');
204
+ }
205
+
206
+ if (testPayloadBeforeJson !== JSON.stringify(doc.testPayload)) {
207
+ updatedSections.push('testPayload');
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Apply capability removal on a deep clone of the document.
213
+ *
214
+ * @param {object} originalDoc - Parsed datasource JSON
215
+ * @param {object} opts - Options
216
+ * @param {string} opts.capability - Capability key to remove
217
+ * @param {boolean} [opts.force=false] - If true and capability absent, return unchanged doc
218
+ * @returns {{
219
+ * doc: object,
220
+ * removed: boolean,
221
+ * patchOperations: object[],
222
+ * updatedSections: string[]
223
+ * }}
224
+ */
225
+ function applyCapabilityRemove(originalDoc, opts) {
226
+ const doc = deepClone(originalDoc);
227
+ const name = opts.capability;
228
+ const updatedSections = [];
229
+
230
+ if (!capabilityExists(doc, name)) {
231
+ if (!opts.force) {
232
+ throw new Error(
233
+ `Capability "${name}" not found in capabilities[], openapi.operations, or execution.cip.operations. ` +
234
+ 'Use --force to succeed when the capability is already absent.'
235
+ );
236
+ }
237
+ return {
238
+ doc,
239
+ removed: false,
240
+ patchOperations: [],
241
+ updatedSections: ['capability already absent (--force)']
242
+ };
243
+ }
244
+
245
+ const logicalName = resolveLogicalNameForRemove(doc, name);
246
+ const openapiKey = findMatchingOpsKeys(originalDoc.openapi?.operations, logicalName)[0];
247
+ const cipKey = findMatchingOpsKeys(originalDoc.execution?.cip?.operations, logicalName)[0];
248
+ const profileKey = resolveProfileKeyForLogical(originalDoc, logicalName);
249
+
250
+ const patchOperations = buildRemovePatchOperations(
251
+ originalDoc,
252
+ logicalName,
253
+ openapiKey,
254
+ cipKey,
255
+ profileKey
256
+ );
257
+
258
+ removeCapability(doc, logicalName);
259
+ applyCapabilityRemoveSideEffects(doc, originalDoc, logicalName, { openapiKey, cipKey }, updatedSections);
260
+
261
+ return {
262
+ doc,
263
+ removed: true,
264
+ patchOperations,
265
+ updatedSections
266
+ };
267
+ }
268
+
269
+ module.exports = {
270
+ applyCapabilityRemove,
271
+ buildRemovePatchOperations,
272
+ pruneTestPayloadScenarios,
273
+ finalizeTestPayloadShape,
274
+ finalizeEmptyExposedProfiles
275
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * File-backed capability copy with validation, backup, and atomic write.
3
+ *
4
+ * @fileoverview Run datasource capability copy CLI operation
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('path');
11
+ const { writeBackup } = require('../../utils/integration-file-backup');
12
+ const {
13
+ resolveValidateInputPath,
14
+ validateDatasourceParsed
15
+ } = require('../validate');
16
+ const { normalizeCapabilityKey } = require('./capability-key');
17
+ const { applyCapabilityCreate } = require('./create-operations');
18
+
19
+ /**
20
+ * Resolve an existing JSON file path using real disk (`node:fs`). Other suites often
21
+ * `jest.mock('fs')`; `require('fs')` would see the mock and break existsSync/read here.
22
+ * @param {string} identifier - Trimmed path or datasource key
23
+ * @returns {string} Absolute path to file
24
+ */
25
+ function resolveCapabilityCopyInputPath(identifier) {
26
+ const trimmed = String(identifier || '').trim();
27
+ if (!trimmed) {
28
+ throw new Error('File path is required and must be a string');
29
+ }
30
+ const candidates = [...new Set([trimmed, path.resolve(trimmed)])];
31
+ for (const p of candidates) {
32
+ try {
33
+ if (!fs.existsSync(p)) continue;
34
+ try {
35
+ const st = fs.statSync(p);
36
+ if (st && typeof st.isFile === 'function' && st.isFile()) {
37
+ return path.resolve(p);
38
+ }
39
+ } catch {
40
+ return path.resolve(p);
41
+ }
42
+ } catch {
43
+ /* ignore */
44
+ }
45
+ }
46
+ return resolveValidateInputPath(trimmed);
47
+ }
48
+
49
+ /**
50
+ * @param {string} filePath
51
+ * @param {object} obj
52
+ * @returns {void}
53
+ */
54
+ function atomicWriteJson(filePath, obj) {
55
+ const dir = path.dirname(filePath);
56
+ const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
57
+ const payload = `${JSON.stringify(obj, null, 2)}\n`;
58
+ fs.writeFileSync(tmp, payload, 'utf8');
59
+ fs.renameSync(tmp, filePath);
60
+ }
61
+
62
+ /**
63
+ * @typedef {object} RunCapabilityCopyOpts
64
+ * @property {string} fileOrKey - Path or datasource key
65
+ * @property {string} from
66
+ * @property {string} as - Target capability key
67
+ * @property {boolean} [dryRun=false]
68
+ * @property {boolean} [overwrite=false]
69
+ * @property {boolean} [noBackup=false]
70
+ * @property {boolean} [basicExposure=false] - Programmatic only: minimal exposed.profiles from metadataSchema (CLI always passes false)
71
+ * @property {boolean} [includeTestPayload=false] - Also clone testPayload.scenarios rows matching the source operation (`--test` on CLI)
72
+ * @property {string} [openApiOperationId] - Create without --from: match openapi.operations[].operationId
73
+ * @property {string} [template] - Create without --from: template name under capability/templates/
74
+ */
75
+
76
+ /**
77
+ * Execute capability copy on disk (or dry-run).
78
+ *
79
+ * @param {RunCapabilityCopyOpts} opts
80
+ * @returns {Promise<{
81
+ * dryRun: boolean,
82
+ * resolvedPath: string,
83
+ * resolvedAs: string,
84
+ * patchOperations: object[],
85
+ * updatedSections: string[],
86
+ * backupPath: string|null,
87
+ * validation: object
88
+ * }>}
89
+ */
90
+ async function runCapabilityCopy(opts) {
91
+ const resolvedPath = resolveCapabilityCopyInputPath(opts.fileOrKey.trim());
92
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
93
+ const parsed = JSON.parse(raw);
94
+
95
+ const to = normalizeCapabilityKey(opts.as, 'Target (--as)');
96
+ const fromRaw = opts.from !== undefined && opts.from !== null ? String(opts.from).trim() : '';
97
+ const from = fromRaw ? normalizeCapabilityKey(fromRaw, 'Source (--from)') : '';
98
+ const oidRaw =
99
+ opts.openApiOperationId !== undefined && opts.openApiOperationId !== null
100
+ ? String(opts.openApiOperationId).trim()
101
+ : '';
102
+ const tplRaw =
103
+ opts.template !== undefined && opts.template !== null ? String(opts.template).trim() : '';
104
+
105
+ const result = applyCapabilityCreate(parsed, {
106
+ from: from || undefined,
107
+ to,
108
+ openApiOperationId: oidRaw || undefined,
109
+ template: tplRaw || undefined,
110
+ overwrite: Boolean(opts.overwrite),
111
+ basicExposure: Boolean(opts.basicExposure),
112
+ includeTestPayload: Boolean(opts.includeTestPayload)
113
+ });
114
+
115
+ const validation = validateDatasourceParsed(result.doc);
116
+ if (!validation.valid) {
117
+ const err = new Error(validation.errors.join('\n'));
118
+ err.validationErrors = validation.errors;
119
+ throw err;
120
+ }
121
+
122
+ if (opts.dryRun) {
123
+ return {
124
+ dryRun: true,
125
+ resolvedPath,
126
+ resolvedAs: result.resolvedAs,
127
+ patchOperations: result.patchOperations,
128
+ updatedSections: result.updatedSections,
129
+ backupPath: null,
130
+ validation
131
+ };
132
+ }
133
+
134
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
135
+ atomicWriteJson(resolvedPath, result.doc);
136
+
137
+ return {
138
+ dryRun: false,
139
+ resolvedPath,
140
+ resolvedAs: result.resolvedAs,
141
+ patchOperations: result.patchOperations,
142
+ updatedSections: result.updatedSections,
143
+ backupPath,
144
+ validation
145
+ };
146
+ }
147
+
148
+ module.exports = {
149
+ runCapabilityCopy,
150
+ writeBackup,
151
+ atomicWriteJson
152
+ };