@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,76 @@
1
+ /**
2
+ * Build a minimal exposed.profiles row from metadataSchema (primitives + required fields).
3
+ *
4
+ * @fileoverview Minimal exposed.profiles row from metadataSchema (used by applyCapabilityCopy when basicExposure)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean']);
10
+
11
+ /**
12
+ * @param {object} prop - JSON Schema property
13
+ * @returns {boolean}
14
+ */
15
+ function isPrimitiveSchemaProperty(prop) {
16
+ if (!prop || typeof prop !== 'object') {
17
+ return false;
18
+ }
19
+ const t = prop.type;
20
+ if (typeof t === 'string') {
21
+ return PRIMITIVE.has(t);
22
+ }
23
+ if (Array.isArray(t)) {
24
+ return t.some((x) => PRIMITIVE.has(x));
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /**
30
+ * Attribute names for a minimal array-style profile: required primitives + all primitive properties.
31
+ *
32
+ * @param {object} parsed - Datasource JSON
33
+ * @returns {string[]} Sorted unique attribute names
34
+ */
35
+ function listBasicExposureAttributes(parsed) {
36
+ const ms = parsed.metadataSchema;
37
+ const properties = ms && typeof ms === 'object' ? ms.properties : null;
38
+ if (!properties || typeof properties !== 'object') {
39
+ return [];
40
+ }
41
+ const required = Array.isArray(ms.required) ? ms.required : [];
42
+ const names = new Set();
43
+ for (const key of required) {
44
+ if (properties[key] && isPrimitiveSchemaProperty(properties[key])) {
45
+ names.add(key);
46
+ }
47
+ }
48
+ for (const [key, prop] of Object.entries(properties)) {
49
+ if (isPrimitiveSchemaProperty(prop)) {
50
+ names.add(key);
51
+ }
52
+ }
53
+ return [...names].sort();
54
+ }
55
+
56
+ /**
57
+ * @param {object} parsed - Datasource JSON
58
+ * @returns {string[]}
59
+ * @throws {Error} When nothing can be derived
60
+ */
61
+ function buildBasicExposureProfileArray(parsed) {
62
+ const attrs = listBasicExposureAttributes(parsed);
63
+ if (attrs.length === 0) {
64
+ throw new Error(
65
+ 'Cannot build basic exposure: metadataSchema.properties has no primitive fields (or missing metadataSchema). ' +
66
+ 'Add primitive fields to metadataSchema, copy an existing exposed.profiles.<from> via capability copy, or choose another target key.'
67
+ );
68
+ }
69
+ return attrs;
70
+ }
71
+
72
+ module.exports = {
73
+ buildBasicExposureProfileArray,
74
+ listBasicExposureAttributes,
75
+ isPrimitiveSchemaProperty
76
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Extract a comparable subtree for `capability diff`.
3
+ *
4
+ * @fileoverview capability slice extraction for diff
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { deepClone } = require('./copy-operations');
10
+
11
+ /**
12
+ * @param {object} doc - Parsed datasource JSON
13
+ * @param {string} capabilityKey - Normalized capability key
14
+ * @param {string} [profileKey] - Optional exposed.profiles key
15
+ * @returns {object} Stable-shape slice for compareObjects
16
+ */
17
+ function extractCapabilitySliceForDiff(doc, capabilityKey, profileKey) {
18
+ const cap = capabilityKey;
19
+ const listed =
20
+ Boolean(Array.isArray(doc.capabilities) && doc.capabilities.includes(cap));
21
+ const openapiOp = doc.openapi?.operations?.[cap];
22
+ const cipOp = doc.execution?.cip?.operations?.[cap];
23
+ let exposedProfile;
24
+ if (profileKey && String(profileKey).trim()) {
25
+ const pk = String(profileKey).trim();
26
+ const raw = doc.exposed?.profiles?.[pk];
27
+ exposedProfile = raw !== undefined ? deepClone(raw) : undefined;
28
+ }
29
+
30
+ return {
31
+ capabilityKey: cap,
32
+ listedInCapabilities: listed,
33
+ openapiOperation: openapiOp !== undefined ? deepClone(openapiOp) : undefined,
34
+ cipOperation: cipOp !== undefined ? deepClone(cipOp) : undefined,
35
+ exposedProfile
36
+ };
37
+ }
38
+
39
+ module.exports = {
40
+ extractCapabilitySliceForDiff
41
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Normalize and validate external datasource capability keys.
3
+ *
4
+ * @fileoverview Capability key helpers for datasource capability CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ /** Pattern aligned with external-datasource.schema.json capabilities[].items */
10
+ const CAPABILITY_KEY_PATTERN = /^[a-z][a-zA-Z0-9_]*$/;
11
+
12
+ /**
13
+ * @param {string} raw - Raw CLI input
14
+ * @param {string} label - Option label for errors
15
+ * @returns {string} Normalized key
16
+ * @throws {Error} If empty or invalid
17
+ */
18
+ function normalizeCapabilityKey(raw, label = 'capability') {
19
+ const s = String(raw || '').trim();
20
+ if (!s) {
21
+ throw new Error(`${label} key is required`);
22
+ }
23
+ if (!CAPABILITY_KEY_PATTERN.test(s)) {
24
+ throw new Error(
25
+ `${label} key "${s}" is invalid: must match ${CAPABILITY_KEY_PATTERN}`
26
+ );
27
+ }
28
+ return s;
29
+ }
30
+
31
+ module.exports = {
32
+ normalizeCapabilityKey,
33
+ CAPABILITY_KEY_PATTERN
34
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Map logical capability names (capabilities[] / profiles) to openapi + CIP operation object keys.
3
+ *
4
+ * @fileoverview capabilities-first resolution when OpenAPI uses different casing
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ /**
10
+ * @param {object|undefined} operations
11
+ * @param {string} logicalKey
12
+ * @returns {string[]}
13
+ */
14
+ function findMatchingOpsKeys(operations, logicalKey) {
15
+ if (!operations || typeof operations !== 'object') {
16
+ return [];
17
+ }
18
+ if (operations[logicalKey] !== undefined) {
19
+ return [logicalKey];
20
+ }
21
+ const lower = String(logicalKey).toLowerCase();
22
+ return Object.keys(operations).filter((k) => k.toLowerCase() === lower);
23
+ }
24
+
25
+ /**
26
+ * @param {object|undefined} operations
27
+ * @param {string} logicalKey
28
+ * @param {string} label - For error messages
29
+ * @returns {string|null} Single match, or null
30
+ * @throws {Error} When multiple keys differ only by case
31
+ */
32
+ function resolveSingleOpsKey(operations, logicalKey, label) {
33
+ const matches = findMatchingOpsKeys(operations, logicalKey);
34
+ if (matches.length === 1) {
35
+ return matches[0];
36
+ }
37
+ if (matches.length === 0) {
38
+ return null;
39
+ }
40
+ throw new Error(
41
+ `Ambiguous ${label} operations keys for "${logicalKey}": ${matches.join(', ')}`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * For --from on copy: when capabilities[] is non-empty, the name must appear there (case-insensitive).
47
+ *
48
+ * @param {object} doc
49
+ * @param {string} userKey
50
+ * @returns {string} Canonical string from capabilities[] or userKey when no list
51
+ * @throws {Error} When capabilities is non-empty and no entry matches
52
+ */
53
+ function resolveSourceCapabilityForCopy(doc, userKey) {
54
+ const u = String(userKey).trim();
55
+ const caps = doc.capabilities;
56
+ if (Array.isArray(caps) && caps.length > 0) {
57
+ const hit = caps.find((c) => String(c).toLowerCase() === u.toLowerCase());
58
+ if (hit === undefined) {
59
+ throw new Error(
60
+ `Source capability "${userKey}" not found in capabilities[]. ` +
61
+ 'Use a name that appears in capabilities[] (case-insensitive match is allowed).'
62
+ );
63
+ }
64
+ return String(hit);
65
+ }
66
+ return u;
67
+ }
68
+
69
+ /**
70
+ * For remove: prefer canonical name from capabilities when present; otherwise use the user key.
71
+ *
72
+ * @param {object} doc
73
+ * @param {string} userKey
74
+ * @returns {string}
75
+ */
76
+ function resolveLogicalNameForRemove(doc, userKey) {
77
+ const u = String(userKey).trim();
78
+ const caps = doc.capabilities;
79
+ if (Array.isArray(caps) && caps.length > 0) {
80
+ const hit = caps.find((c) => String(c).toLowerCase() === u.toLowerCase());
81
+ if (hit !== undefined) {
82
+ return String(hit);
83
+ }
84
+ }
85
+ return u;
86
+ }
87
+
88
+ /**
89
+ * Openapi + CIP object keys for a logical capability (exact or single case-insensitive match per section).
90
+ *
91
+ * @param {object} doc
92
+ * @param {string} logicalKey
93
+ * @returns {{ openapiKey: string, cipKey: string }}
94
+ * @throws {Error} If a section is missing the operation
95
+ */
96
+ function resolveOpsKeysForCapability(doc, logicalKey) {
97
+ const openapiKey = resolveSingleOpsKey(doc.openapi?.operations, logicalKey, 'openapi');
98
+ const cipKey = resolveSingleOpsKey(doc.execution?.cip?.operations, logicalKey, 'cip');
99
+ if (!openapiKey) {
100
+ throw new Error(
101
+ `Missing openapi.operations entry for capability "${logicalKey}" ` +
102
+ '(no exact or case-insensitive key match).'
103
+ );
104
+ }
105
+ if (!cipKey) {
106
+ throw new Error(
107
+ `Missing execution.cip.operations entry for capability "${logicalKey}" ` +
108
+ '(no exact or case-insensitive key match).'
109
+ );
110
+ }
111
+ return { openapiKey, cipKey };
112
+ }
113
+
114
+ /**
115
+ * @param {object} doc
116
+ * @param {string} logicalKey
117
+ * @returns {string|null} profiles key to use, or null
118
+ * @throws {Error} On ambiguous profile keys
119
+ */
120
+ function resolveProfileKeyForLogical(doc, logicalKey) {
121
+ const prof = doc.exposed?.profiles;
122
+ if (!prof || typeof prof !== 'object') {
123
+ return null;
124
+ }
125
+ if (prof[logicalKey] !== undefined) {
126
+ return logicalKey;
127
+ }
128
+ const lower = String(logicalKey).toLowerCase();
129
+ const keys = Object.keys(prof).filter((k) => k.toLowerCase() === lower);
130
+ if (keys.length === 1) {
131
+ return keys[0];
132
+ }
133
+ if (keys.length > 1) {
134
+ throw new Error(
135
+ `Ambiguous exposed.profiles keys for "${logicalKey}": ${keys.join(', ')}`
136
+ );
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * True if the name appears in capabilities (case-insensitive) or in openapi or CIP operations (case-insensitive).
143
+ *
144
+ * @param {object} doc
145
+ * @param {string} name
146
+ * @returns {boolean}
147
+ */
148
+ function capabilityLogicalExists(doc, name) {
149
+ if (
150
+ Array.isArray(doc.capabilities) &&
151
+ doc.capabilities.some((c) => String(c).toLowerCase() === String(name).toLowerCase())
152
+ ) {
153
+ return true;
154
+ }
155
+ if (findMatchingOpsKeys(doc.openapi?.operations, name).length > 0) {
156
+ return true;
157
+ }
158
+ if (findMatchingOpsKeys(doc.execution?.cip?.operations, name).length > 0) {
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+
164
+ module.exports = {
165
+ findMatchingOpsKeys,
166
+ resolveSingleOpsKey,
167
+ resolveSourceCapabilityForCopy,
168
+ resolveLogicalNameForRemove,
169
+ resolveOpsKeysForCapability,
170
+ resolveProfileKeyForLogical,
171
+ capabilityLogicalExists
172
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Storage naming: openapi/CIP operation map keys and testPayload.scenarios.operation use lowercase.
3
+ * capabilities[] and exposed.profiles use logical camelCase names (--as).
4
+ *
5
+ * @fileoverview capability storage key normalization for copy pipeline
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ /**
11
+ * Lowercase key for openapi.operations / execution.cip.operations / scenario.operation.
12
+ *
13
+ * @param {string} logicalCapabilityKey - Normalized --as / capabilities[] name (e.g. updateCountry)
14
+ * @returns {string}
15
+ */
16
+ function storageOpsKey(logicalCapabilityKey) {
17
+ return String(logicalCapabilityKey ?? '').trim().toLowerCase();
18
+ }
19
+
20
+ module.exports = {
21
+ storageOpsKey
22
+ };
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Clone a capability within one datasource JSON document.
3
+ *
4
+ * @fileoverview capability copy / collision / profile sync
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { rewriteCapabilityReferences } = require('./reference-rewrite');
10
+ const { jsonPointerPath } = require('./json-pointer');
11
+ const { buildBasicExposureProfileArray } = require('./basic-exposure');
12
+ const {
13
+ resolveProfileKeyForLogical,
14
+ capabilityLogicalExists,
15
+ resolveOpsKeysForCapability,
16
+ resolveSourceCapabilityForCopy,
17
+ findMatchingOpsKeys
18
+ } = require('./capability-resolve');
19
+ const { copyTestPayloadScenarios } = require('./copy-test-payload');
20
+ const { storageOpsKey } = require('./capability-storage-keys');
21
+
22
+ /**
23
+ * @param {object} doc - Datasource object
24
+ * @param {string} name - Capability key
25
+ * @returns {boolean}
26
+ */
27
+ function capabilityExists(doc, name) {
28
+ return capabilityLogicalExists(doc, name);
29
+ }
30
+
31
+ /**
32
+ * @param {object} doc - Mutated datasource
33
+ * @param {string} logicalKey - Logical capability name (matches capabilities[] casing when listed)
34
+ * @returns {void}
35
+ */
36
+ function removeCapability(doc, logicalKey) {
37
+ const oo = doc.openapi?.operations;
38
+ const co = doc.execution?.cip?.operations;
39
+ const openapiKeys = findMatchingOpsKeys(oo, logicalKey);
40
+ const cipKeys = findMatchingOpsKeys(co, logicalKey);
41
+ if (openapiKeys.length > 1) {
42
+ throw new Error(
43
+ `Ambiguous openapi.operations keys for "${logicalKey}": ${openapiKeys.join(', ')}`
44
+ );
45
+ }
46
+ if (cipKeys.length > 1) {
47
+ throw new Error(
48
+ `Ambiguous execution.cip.operations keys for "${logicalKey}": ${cipKeys.join(', ')}`
49
+ );
50
+ }
51
+ const openapiKey = openapiKeys[0];
52
+ const cipKey = cipKeys[0];
53
+ const profileKey = resolveProfileKeyForLogical(doc, logicalKey);
54
+
55
+ if (Array.isArray(doc.capabilities)) {
56
+ doc.capabilities = doc.capabilities.filter(
57
+ (c) => String(c).toLowerCase() !== String(logicalKey).toLowerCase()
58
+ );
59
+ }
60
+ if (openapiKey && oo) {
61
+ delete oo[openapiKey];
62
+ }
63
+ if (cipKey && co) {
64
+ delete co[cipKey];
65
+ }
66
+ if (profileKey && doc.exposed?.profiles) {
67
+ delete doc.exposed.profiles[profileKey];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {object} doc
73
+ * @param {string} to - Desired target key
74
+ * @param {boolean} overwrite
75
+ * @returns {string} Same as `to` when free or when overwriting
76
+ * @throws {Error} When target exists and overwrite is false
77
+ */
78
+ function resolveTargetKey(doc, to, overwrite) {
79
+ if (overwrite) {
80
+ return to;
81
+ }
82
+ if (!capabilityExists(doc, to)) {
83
+ return to;
84
+ }
85
+ throw new Error(
86
+ `Capability "${to}" already exists (check capabilities[], openapi.operations, execution.cip.operations). ` +
87
+ 'Use --overwrite to replace it, or choose another --as name.'
88
+ );
89
+ }
90
+
91
+ /**
92
+ * @param {unknown} o
93
+ * @returns {unknown}
94
+ */
95
+ function deepClone(o) {
96
+ return JSON.parse(JSON.stringify(o));
97
+ }
98
+
99
+ /**
100
+ * When opts.basicExposure is true, refuse to clobber an existing profile row unless overwriting.
101
+ *
102
+ * @param {object} doc
103
+ * @param {string} resolvedAs
104
+ * @param {boolean} overwrite
105
+ * @param {boolean} basicExposure
106
+ * @returns {void}
107
+ * @throws {Error}
108
+ */
109
+ function assertBasicExposureSlot(doc, resolvedAs, overwrite, basicExposure) {
110
+ if (!basicExposure || overwrite) {
111
+ return;
112
+ }
113
+ if (doc.exposed?.profiles?.[resolvedAs]) {
114
+ throw new Error(
115
+ `exposed.profiles.${resolvedAs} already exists. Use --overwrite or choose another --as name.`
116
+ );
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Copy exposed.profiles[from] → exposed.profiles[resolvedAs], or synthesize basic profile at resolvedAs.
122
+ *
123
+ * @param {object} doc
124
+ * @param {object} opts
125
+ * @param {string} from - Source capability key (matches profiles[from])
126
+ * @param {string} resolvedAs - Target capability key (matches profiles[resolvedAs])
127
+ * @param {object[]} patchOperations
128
+ * @param {string[]} updatedSections
129
+ * @returns {void}
130
+ */
131
+ function copyExposedProfile(doc, opts, logicalFrom, resolvedAs, patchOperations, updatedSections) {
132
+ if (!doc.exposed) {
133
+ doc.exposed = {};
134
+ }
135
+ if (!doc.exposed.profiles) {
136
+ doc.exposed.profiles = {};
137
+ }
138
+
139
+ if (opts.basicExposure) {
140
+ const value = buildBasicExposureProfileArray(doc);
141
+ doc.exposed.profiles[resolvedAs] = value;
142
+ patchOperations.push({
143
+ op: 'add',
144
+ path: jsonPointerPath('exposed', 'profiles', resolvedAs),
145
+ value
146
+ });
147
+ updatedSections.push(`exposed.profiles.${resolvedAs} (basic from metadataSchema)`);
148
+ return;
149
+ }
150
+
151
+ const profileFrom = resolveProfileKeyForLogical(doc, logicalFrom);
152
+ if (profileFrom !== null && doc.exposed.profiles[profileFrom] !== undefined) {
153
+ doc.exposed.profiles[resolvedAs] = deepClone(doc.exposed.profiles[profileFrom]);
154
+ patchOperations.push({
155
+ op: 'add',
156
+ path: jsonPointerPath('exposed', 'profiles', resolvedAs),
157
+ value: doc.exposed.profiles[resolvedAs]
158
+ });
159
+ updatedSections.push(`exposed.profiles.${resolvedAs}`);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @param {object} doc
165
+ * @param {string} from
166
+ * @param {string} resolvedAs
167
+ * @param {object[]} patchOperations
168
+ * @param {string[]} updatedSections
169
+ * @returns {void}
170
+ */
171
+ function installClonedOperations(doc, fromKeys, resolvedAs, patchOperations, updatedSections) {
172
+ const { openapiKey, cipKey, logicalFrom } = fromKeys;
173
+ const targetOpsKey = storageOpsKey(resolvedAs);
174
+
175
+ const openapiClone = deepClone(doc.openapi.operations[openapiKey]);
176
+ rewriteCapabilityReferences(openapiClone, openapiKey, targetOpsKey);
177
+ if (logicalFrom !== openapiKey) {
178
+ rewriteCapabilityReferences(openapiClone, logicalFrom, targetOpsKey);
179
+ }
180
+ const cipClone = deepClone(doc.execution.cip.operations[cipKey]);
181
+ rewriteCapabilityReferences(cipClone, cipKey, targetOpsKey);
182
+ if (logicalFrom !== cipKey) {
183
+ rewriteCapabilityReferences(cipClone, logicalFrom, targetOpsKey);
184
+ }
185
+
186
+ if (!doc.openapi.operations) {
187
+ doc.openapi.operations = {};
188
+ }
189
+ if (!doc.execution.cip.operations) {
190
+ doc.execution.cip.operations = {};
191
+ }
192
+
193
+ doc.openapi.operations[targetOpsKey] = openapiClone;
194
+ patchOperations.push({
195
+ op: 'add',
196
+ path: jsonPointerPath('openapi', 'operations', targetOpsKey),
197
+ value: openapiClone
198
+ });
199
+ updatedSections.push(`openapi.operations.${targetOpsKey} (logical ${resolvedAs})`);
200
+
201
+ doc.execution.cip.operations[targetOpsKey] = cipClone;
202
+ patchOperations.push({
203
+ op: 'add',
204
+ path: jsonPointerPath('execution', 'cip', 'operations', targetOpsKey),
205
+ value: cipClone
206
+ });
207
+ updatedSections.push(`execution.cip.operations.${targetOpsKey} (logical ${resolvedAs})`);
208
+ }
209
+
210
+ /**
211
+ * Install already-cloned openapi + CIP slices under storage key (no source read).
212
+ *
213
+ * @param {object} doc
214
+ * @param {string} resolvedAs - Logical capability name (capabilities[])
215
+ * @param {{ targetOpsKey: string, openapiClone: object, cipClone: object }} slices
216
+ * @param {object[]} patchOperations
217
+ * @param {string[]} updatedSections
218
+ * @returns {void}
219
+ */
220
+ function installPreparedSlices(doc, resolvedAs, slices, patchOperations, updatedSections) {
221
+ const { targetOpsKey, openapiClone, cipClone } = slices;
222
+ if (!doc.openapi.operations) {
223
+ doc.openapi.operations = {};
224
+ }
225
+ if (!doc.execution.cip.operations) {
226
+ doc.execution.cip.operations = {};
227
+ }
228
+
229
+ doc.openapi.operations[targetOpsKey] = openapiClone;
230
+ patchOperations.push({
231
+ op: 'add',
232
+ path: jsonPointerPath('openapi', 'operations', targetOpsKey),
233
+ value: openapiClone
234
+ });
235
+ updatedSections.push(`openapi.operations.${targetOpsKey} (logical ${resolvedAs})`);
236
+
237
+ doc.execution.cip.operations[targetOpsKey] = cipClone;
238
+ patchOperations.push({
239
+ op: 'add',
240
+ path: jsonPointerPath('execution', 'cip', 'operations', targetOpsKey),
241
+ value: cipClone
242
+ });
243
+ updatedSections.push(`execution.cip.operations.${targetOpsKey} (logical ${resolvedAs})`);
244
+ }
245
+
246
+ /**
247
+ * @param {object} doc
248
+ * @param {string} resolvedAs
249
+ * @param {object[]} patchOperations
250
+ * @param {string[]} updatedSections
251
+ * @returns {void}
252
+ */
253
+ function appendCapabilityList(doc, resolvedAs, patchOperations, updatedSections) {
254
+ if (!Array.isArray(doc.capabilities)) {
255
+ doc.capabilities = [];
256
+ }
257
+ if (doc.capabilities.includes(resolvedAs)) {
258
+ return;
259
+ }
260
+ doc.capabilities.push(resolvedAs);
261
+ patchOperations.push({
262
+ op: 'add',
263
+ path: '/capabilities/-',
264
+ value: resolvedAs
265
+ });
266
+ updatedSections.push(`capabilities[]: ${resolvedAs}`);
267
+ }
268
+
269
+ /**
270
+ * Apply capability copy on a deep clone of the document.
271
+ *
272
+ * @param {object} originalDoc - Parsed datasource JSON
273
+ * @param {object} opts - Options
274
+ * @param {string} opts.from - Source capability key
275
+ * @param {string} opts.to - Desired target key
276
+ * @param {boolean} [opts.overwrite=false]
277
+ * @param {boolean} [opts.basicExposure=false] - Build minimal profile from metadataSchema at exposed.profiles[to]
278
+ * @param {boolean} [opts.includeTestPayload=false] - Clone matching testPayload.scenarios rows to target operation
279
+ * @returns {{
280
+ * doc: object,
281
+ * resolvedAs: string,
282
+ * patchOperations: object[],
283
+ * updatedSections: string[]
284
+ * }}
285
+ */
286
+ function applyCapabilityCopy(originalDoc, opts) {
287
+ const doc = deepClone(originalDoc);
288
+ const logicalFrom = resolveSourceCapabilityForCopy(doc, opts.from);
289
+ const { openapiKey, cipKey } = resolveOpsKeysForCapability(doc, logicalFrom);
290
+
291
+ const openapiFrom = doc.openapi?.operations?.[openapiKey];
292
+ const cipFrom = doc.execution?.cip?.operations?.[cipKey];
293
+
294
+ if (!openapiFrom) {
295
+ throw new Error(`Missing openapi.operations.${openapiKey}`);
296
+ }
297
+ if (!cipFrom) {
298
+ throw new Error(`Missing execution.cip.operations.${cipKey}`);
299
+ }
300
+
301
+ const resolvedAs = resolveTargetKey(doc, opts.to, Boolean(opts.overwrite));
302
+ assertBasicExposureSlot(doc, resolvedAs, Boolean(opts.overwrite), Boolean(opts.basicExposure));
303
+
304
+ const patchOperations = [];
305
+ const updatedSections = [];
306
+
307
+ if (opts.overwrite && capabilityLogicalExists(doc, resolvedAs)) {
308
+ removeCapability(doc, resolvedAs);
309
+ }
310
+
311
+ installClonedOperations(
312
+ doc,
313
+ { openapiKey, cipKey, logicalFrom },
314
+ resolvedAs,
315
+ patchOperations,
316
+ updatedSections
317
+ );
318
+ appendCapabilityList(doc, resolvedAs, patchOperations, updatedSections);
319
+ copyExposedProfile(doc, opts, logicalFrom, resolvedAs, patchOperations, updatedSections);
320
+ copyTestPayloadScenarios(
321
+ doc,
322
+ opts,
323
+ { openapiKey, cipKey, logicalFrom },
324
+ resolvedAs,
325
+ patchOperations,
326
+ updatedSections
327
+ );
328
+
329
+ return {
330
+ doc,
331
+ resolvedAs,
332
+ patchOperations,
333
+ updatedSections
334
+ };
335
+ }
336
+
337
+ module.exports = {
338
+ applyCapabilityCopy,
339
+ capabilityExists,
340
+ capabilityLogicalExists,
341
+ resolveTargetKey,
342
+ deepClone,
343
+ removeCapability,
344
+ installPreparedSlices,
345
+ appendCapabilityList,
346
+ copyExposedProfile,
347
+ assertBasicExposureSlot
348
+ };