@aifabrix/builder 2.54.1 → 2.55.0

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 (210) hide show
  1. package/README.md +8 -0
  2. package/lib/agent-kit/apply-ops.js +82 -0
  3. package/lib/agent-kit/checksums.js +28 -0
  4. package/lib/agent-kit/classify.js +113 -0
  5. package/lib/agent-kit/claude-shim.js +61 -0
  6. package/lib/agent-kit/constants.js +55 -0
  7. package/lib/agent-kit/file-list.js +103 -0
  8. package/lib/agent-kit/host-approve.js +130 -0
  9. package/lib/agent-kit/hosting/github.js +63 -0
  10. package/lib/agent-kit/hosts.js +71 -0
  11. package/lib/agent-kit/init.js +319 -0
  12. package/lib/agent-kit/kit-root.js +17 -0
  13. package/lib/agent-kit/managed-section.js +79 -0
  14. package/lib/agent-kit/package-load.js +45 -0
  15. package/lib/agent-kit/plan-ops.js +69 -0
  16. package/lib/agent-kit/remote-prompts.js +239 -0
  17. package/lib/agent-kit/render-host-markdown.js +40 -0
  18. package/lib/agent-kit/repo-guard.js +47 -0
  19. package/lib/agent-kit/report.js +107 -0
  20. package/lib/agent-kit/run.js +82 -0
  21. package/lib/agent-kit/scm/git.js +122 -0
  22. package/lib/agent-kit/state-file.js +83 -0
  23. package/lib/agent-kit/status.js +50 -0
  24. package/lib/agent-kit/v1-detect.js +81 -0
  25. package/lib/agent-kit/validate-kit.js +87 -0
  26. package/lib/agent-kit/workspace-name.js +40 -0
  27. package/lib/agent-kit/workspace-skeleton.js +103 -0
  28. package/lib/api/role-assistant-test-pack.api.js +86 -0
  29. package/lib/api/work-search.api.js +31 -4
  30. package/lib/api/work.api.js +19 -12
  31. package/lib/app/list-external-systems.js +2 -1
  32. package/lib/app/show-display.js +33 -6
  33. package/lib/app/show.js +25 -2
  34. package/lib/channels/add-channel.js +1 -0
  35. package/lib/channels/approval-guides/claude.js +2 -2
  36. package/lib/channels/channel-add-failure.js +1 -0
  37. package/lib/channels/channel-add-remaining.js +15 -9
  38. package/lib/channels/channel-definition-status.js +115 -0
  39. package/lib/channels/channel-description.js +2 -1
  40. package/lib/channels/channel-install-envelope.js +4 -2
  41. package/lib/channels/channel-integration-generator.js +2 -0
  42. package/lib/channels/channel-runtime-resolver.js +14 -0
  43. package/lib/channels/channel-targets.js +31 -5
  44. package/lib/channels/claude-setup-values.js +4 -1
  45. package/lib/channels/slack-channel-add.js +59 -6
  46. package/lib/channels/slack-mcp-callback.js +51 -0
  47. package/lib/channels/slack-mcp-runtime.js +156 -0
  48. package/lib/channels/slack-provisioning/install-cli.js +14 -2
  49. package/lib/channels/slack-provisioning/with-curl-on-path.js +6 -1
  50. package/lib/cli/index.js +4 -0
  51. package/lib/cli/setup-certification-commands.js +2 -2
  52. package/lib/cli/setup-certification.help.js +3 -1
  53. package/lib/commands/agent-kit-help.js +79 -0
  54. package/lib/commands/agent-kit.js +346 -0
  55. package/lib/commands/channel-add-help.js +5 -5
  56. package/lib/commands/datasource-capability-relate-cli.js +5 -4
  57. package/lib/commands/repair-auth-apply.js +1 -1
  58. package/lib/commands/repair-datasource-key-skip.js +39 -0
  59. package/lib/commands/repair-datasource-keys.js +24 -12
  60. package/lib/commands/repair-rbac.js +16 -10
  61. package/lib/commands/role-assistant-copy-output.js +61 -0
  62. package/lib/commands/role-assistant-copy.js +155 -0
  63. package/lib/commands/role-assistant-test-output.js +8 -9
  64. package/lib/commands/role-assistant-test-result-rows.js +42 -0
  65. package/lib/commands/role-assistant.js +117 -48
  66. package/lib/commands/search.js +143 -0
  67. package/lib/commands/test-trust-external.js +7 -5
  68. package/lib/commands/upload-sidecar-followups.js +4 -1
  69. package/lib/commands/verify-operations-skip-e2e.js +131 -0
  70. package/lib/commands/verify-operations-step-plan.js +6 -33
  71. package/lib/commands/verify-operations-steps.js +3 -1
  72. package/lib/datasource/capability/relate-operations.js +28 -10
  73. package/lib/datasource/capability/run-capability-relate.js +48 -25
  74. package/lib/datasource/datasource-identifier.js +13 -5
  75. package/lib/deployment/deployer-poll-run.js +117 -35
  76. package/lib/external-system/deploy-dataplane-published.js +70 -0
  77. package/lib/external-system/deploy.js +12 -2
  78. package/lib/external-system/download-role-assistant.js +41 -15
  79. package/lib/external-system/download.js +7 -1
  80. package/lib/generator/wizard-datasource-key.js +54 -0
  81. package/lib/generator/wizard.js +55 -54
  82. package/lib/integration-definition/apply-datasource-keys.js +75 -0
  83. package/lib/integration-definition/apply-relationships.js +195 -0
  84. package/lib/integration-definition/apply-static-enterprise.js +229 -0
  85. package/lib/integration-definition/apply-system-file.js +71 -0
  86. package/lib/integration-definition/apply-test-endpoint.js +89 -0
  87. package/lib/integration-definition/apply-viewpoint-defaults.js +157 -0
  88. package/lib/integration-definition/apply.js +78 -59
  89. package/lib/integration-definition/enrich-openapi-operations.js +36 -10
  90. package/lib/integration-definition/enrich-openapi-pick.js +87 -0
  91. package/lib/lifecycle/role-assistant-governance.js +56 -21
  92. package/lib/programmatic/capability-registry.js +1 -0
  93. package/lib/programmatic/index.js +2 -0
  94. package/lib/programmatic/openapi-apply-result-schemas.js +8 -0
  95. package/lib/programmatic/openapi-channel-install-schemas.js +4 -1
  96. package/lib/programmatic/openapi-component-schemas.js +1 -0
  97. package/lib/programmatic/openapi-descriptions.js +9 -3
  98. package/lib/programmatic/openapi-envelope-examples.js +7 -0
  99. package/lib/programmatic/openapi-operation-bindings.js +5 -0
  100. package/lib/programmatic/openapi-request-schemas.js +5 -0
  101. package/lib/programmatic/openapi-schema-descriptions-channel.js +8 -5
  102. package/lib/programmatic/openapi-schema-descriptions-operations.js +19 -0
  103. package/lib/programmatic/openapi-spec.js +1 -0
  104. package/lib/programmatic/request-timeout.js +3 -0
  105. package/lib/programmatic/route-handler-map.js +7 -0
  106. package/lib/programmatic/run-channel-catalog.js +50 -8
  107. package/lib/programmatic/run-channel-install-options.js +5 -1
  108. package/lib/programmatic/run-download.js +41 -0
  109. package/lib/protection/auth-context.js +3 -1
  110. package/lib/protection/protection-display.js +3 -2
  111. package/lib/protection/report-exit.js +50 -0
  112. package/lib/protection/run-commands.js +2 -0
  113. package/lib/role-assistant/package-validate.js +175 -0
  114. package/lib/role-assistant/test-case-assertion-row.js +36 -0
  115. package/lib/role-assistant/test-case-copy-bind.js +77 -0
  116. package/lib/role-assistant/test-case-copy-expected.js +127 -0
  117. package/lib/role-assistant/test-case-copy-from-work.js +90 -0
  118. package/lib/role-assistant/test-case-copy-interactions.js +67 -0
  119. package/lib/role-assistant/test-case-copy.js +119 -0
  120. package/lib/role-assistant/test-case-expected-catalog.js +135 -0
  121. package/lib/role-assistant/test-case-validate.js +88 -0
  122. package/lib/role-assistant/test-cases-bid.js +117 -4
  123. package/lib/role-assistant/test-cases-evaluate.js +137 -30
  124. package/lib/role-assistant/test-cases-honesty.js +155 -6
  125. package/lib/role-assistant/test-cases-lifecycle.js +291 -0
  126. package/lib/role-assistant/test-cases-search-graph.js +122 -0
  127. package/lib/role-assistant/test-cases-search.js +20 -2
  128. package/lib/role-assistant/test-cases.js +34 -168
  129. package/lib/role-assistant/test-pack-map.js +180 -0
  130. package/lib/role-assistant/test-pack-sync.js +187 -0
  131. package/lib/role-assistant/test-runner-debug.js +54 -0
  132. package/lib/role-assistant/test-runner-respond-body.js +36 -0
  133. package/lib/role-assistant/test-runner-search.js +20 -5
  134. package/lib/role-assistant/test-runner-start-payload.js +63 -10
  135. package/lib/role-assistant/test-runner-workhub-answers.js +45 -16
  136. package/lib/role-assistant/test-runner-workhub.js +45 -21
  137. package/lib/role-assistant/test-runner.js +50 -10
  138. package/lib/schema/external-datasource.schema.json +36 -5
  139. package/lib/schema/external-system.schema.json +20 -2
  140. package/lib/schema/role-assistant-test-case.schema.json +408 -0
  141. package/lib/search/dataplane-context.js +23 -0
  142. package/lib/search/search-args.js +211 -0
  143. package/lib/search/search-display.js +238 -0
  144. package/lib/search/search-errors.js +50 -0
  145. package/lib/search/search-prompts.js +119 -0
  146. package/lib/search/search-roles.js +85 -0
  147. package/lib/search/search-run.js +241 -0
  148. package/lib/search/search-skills.js +198 -0
  149. package/lib/utils/authentication-slot-show.js +153 -0
  150. package/lib/utils/credential-display.js +43 -1
  151. package/lib/utils/external-system-readiness-compact-blocks.js +5 -1
  152. package/lib/utils/external-system-readiness-compact-display.js +30 -1
  153. package/lib/utils/external-system-validators.js +2 -1
  154. package/lib/utils/help-builder.js +3 -1
  155. package/lib/utils/schema-loader.js +37 -0
  156. package/lib/validation/validate-external-file.js +5 -1
  157. package/lib/validation/validate.js +3 -2
  158. package/package.json +1 -1
  159. package/templates/agent-kit/adapters/claude/nested.CLAUDE.md +1 -0
  160. package/templates/agent-kit/adapters/claude/root.CLAUDE.md +5 -0
  161. package/templates/agent-kit/adapters/cursor/commands/cust-0-plan.md +5 -0
  162. package/templates/agent-kit/adapters/cursor/commands/cust-1-validate.md +5 -0
  163. package/templates/agent-kit/adapters/cursor/commands/cust-2-build.md +5 -0
  164. package/templates/agent-kit/adapters/cursor/commands/cust-3-push.md +5 -0
  165. package/templates/agent-kit/adapters/cursor/commands/cust-biz-1-validate.md +5 -0
  166. package/templates/agent-kit/adapters/cursor/commands/cust-biz-2-build.md +5 -0
  167. package/templates/agent-kit/adapters/cursor/commands/cust-biz-3-prove.md +5 -0
  168. package/templates/agent-kit/agent-kit.yaml +45 -0
  169. package/templates/agent-kit/instructions/AGENTKIT.md +83 -0
  170. package/templates/agent-kit/instructions/integration.AGENTS.md +25 -0
  171. package/templates/agent-kit/instructions/role-assistant.AGENTS.md +22 -0
  172. package/templates/agent-kit/instructions/root.AGENTS.md +37 -0
  173. package/templates/agent-kit/parity-inventory.yaml +188 -0
  174. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +58 -0
  175. package/templates/agent-kit/skills/aifabrix-connected-system/references/contract-checklist.md +40 -0
  176. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +54 -0
  177. package/templates/agent-kit/skills/aifabrix-connected-system/references/production-safety.md +35 -0
  178. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/reserved-system-key.js +19 -0
  179. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +51 -0
  180. package/templates/agent-kit/skills/aifabrix-plan/references/challenge-checklist.md +108 -0
  181. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +34 -0
  182. package/templates/agent-kit/skills/aifabrix-plan/references/plan-template.md +182 -0
  183. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +50 -0
  184. package/templates/agent-kit/skills/aifabrix-prove/references/demo-template.md +65 -0
  185. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +68 -0
  186. package/templates/agent-kit/skills/aifabrix-prove/references/learning-template.md +66 -0
  187. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +42 -0
  188. package/templates/agent-kit/skills/aifabrix-role-assistant/references/package-boundary.md +21 -0
  189. package/templates/agent-kit/skills/shared/hosts.md +78 -0
  190. package/templates/agent-kit/workspace/.cursor/plans/README.md +22 -0
  191. package/templates/agent-kit/workspace/LICENSE.mit +21 -0
  192. package/templates/agent-kit/workspace/README.md +89 -0
  193. package/templates/applications/builder-api/Dockerfile +3 -1
  194. package/templates/applications/dataplane/application.yaml +2 -2
  195. package/templates/applications/dataplane/env.template +1 -1
  196. package/templates/applications/dataplane/rbac.yaml +28 -0
  197. package/templates/applications/keycloak/env.template +3 -3
  198. package/templates/applications/miso-controller/application.yaml +1 -1
  199. package/templates/applications/miso-controller/env.template +8 -9
  200. package/templates/channels/claude/TROUBLESHOOTING.md +3 -0
  201. package/templates/channels/claude/channel.json +1 -1
  202. package/templates/channels/claude/install.template.md.hbs +1 -0
  203. package/templates/channels/claude/integration/claude-system.json +1 -1
  204. package/templates/channels/slack/channel.json +11 -0
  205. package/templates/channels/slack/install.template.md.hbs +9 -0
  206. package/templates/channels/slack/integration/slack-system.json +11 -0
  207. package/templates/channels/slack/manifest.template.json.hbs +6 -3
  208. package/templates/channels/teams-bot/channel.json +1 -0
  209. package/templates/python/docker-compose.hbs +14 -0
  210. package/templates/typescript/docker-compose.hbs +14 -0
@@ -117,7 +117,8 @@ async function runUploadSidecarFollowups(ctx) {
117
117
  governance: null,
118
118
  evidence: null,
119
119
  knowledge: null,
120
- documentStorageSettings: null
120
+ documentStorageSettings: null,
121
+ tests: null
121
122
  };
122
123
  const quietSidecar = isQuietMinimalSync(ctx.options);
123
124
  const model = buildRoleAssistantModel(ctx.payload);
@@ -131,6 +132,8 @@ async function runUploadSidecarFollowups(ctx) {
131
132
  throw err;
132
133
  }
133
134
  await runKnowledgeSidecar(ctx, model, sidecar, quietSidecar);
135
+ const { runTestsSidecar } = require('../role-assistant/test-pack-sync');
136
+ await runTestsSidecar(ctx, sidecar, quietSidecar);
134
137
  return sidecar;
135
138
  }
136
139
 
@@ -0,0 +1,131 @@
1
+ /**
2
+ * When verify-operations should skip live E2E (Role Assistant or static custom).
3
+ *
4
+ * @fileoverview Skip E2E for packages that have no live vendor pull
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const { loadConfigFile } = require('../utils/config-format');
11
+ const { discoverIntegrationFiles } = require('./repair-internal');
12
+
13
+ const STORAGE_ENTITY_TYPES = new Set(['recordStorage', 'documentStorage']);
14
+
15
+ /**
16
+ * Role Assistant packages use authType none + nested builder-api CIP.
17
+ * Live capacity E2E expects a system baseUrl credential and is not the RA certify path.
18
+ * @param {string} systemKey
19
+ * @param {Object} options
20
+ * @returns {boolean}
21
+ */
22
+ function shouldSkipE2eForRoleAssistant(systemKey, options) {
23
+ if (options.forceE2e === true) {
24
+ return false;
25
+ }
26
+ try {
27
+ const { resolveAppWorkspace } = require('../utils/app-path-resolve');
28
+ const { baseDir } = resolveAppWorkspace(systemKey, {});
29
+ return baseDir === 'role-assistant';
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * @param {string} filePath
37
+ * @returns {Object|null}
38
+ */
39
+ function readManifestObject(filePath) {
40
+ try {
41
+ const parsed = loadConfigFile(filePath);
42
+ return parsed && typeof parsed === 'object' ? parsed : null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {string} appPath
50
+ * @param {string} systemKey
51
+ * @returns {string|null}
52
+ */
53
+ function readLocalSystemType(appPath, systemKey) {
54
+ const candidates = [
55
+ path.join(appPath, `${systemKey}-system.json`),
56
+ path.join(appPath, `${systemKey}-system.yaml`),
57
+ path.join(appPath, `${systemKey}-system.yml`)
58
+ ];
59
+ for (const filePath of candidates) {
60
+ const doc = readManifestObject(filePath);
61
+ if (doc && typeof doc.type === 'string' && doc.type.trim()) {
62
+ return doc.type.trim().toLowerCase();
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * @param {string} appPath
70
+ * @returns {Object[]}
71
+ */
72
+ function loadStorageDatasources(appPath) {
73
+ const { datasourceFiles } = discoverIntegrationFiles(appPath);
74
+ const storage = [];
75
+ for (const name of datasourceFiles) {
76
+ const doc = readManifestObject(path.join(appPath, name));
77
+ if (!doc || !STORAGE_ENTITY_TYPES.has(String(doc.entityType || ''))) {
78
+ continue;
79
+ }
80
+ storage.push(doc);
81
+ }
82
+ return storage;
83
+ }
84
+
85
+ /**
86
+ * Static custom packages (syncBridge disabled) have no live vendor pull.
87
+ * @param {string} systemKey
88
+ * @param {Object} options
89
+ * @returns {boolean}
90
+ */
91
+ function shouldSkipE2eForStaticPackage(systemKey, options) {
92
+ if (options.forceE2e === true) {
93
+ return false;
94
+ }
95
+ try {
96
+ const { resolveAppWorkspace } = require('../utils/app-path-resolve');
97
+ const { baseDir, appPath } = resolveAppWorkspace(systemKey, options);
98
+ if (baseDir !== 'integration' || !appPath) {
99
+ return false;
100
+ }
101
+ if (readLocalSystemType(appPath, systemKey) !== 'custom') {
102
+ return false;
103
+ }
104
+ const storage = loadStorageDatasources(appPath);
105
+ if (storage.length === 0) {
106
+ return false;
107
+ }
108
+ return storage.every((ds) => String(ds.syncBridge || 'disabled').toLowerCase() === 'disabled');
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {string} systemKey
116
+ * @param {Object} options
117
+ * @returns {boolean}
118
+ */
119
+ function shouldSkipOperationsE2e(systemKey, options) {
120
+ if (options.skipE2e === true || options.minimal === true) {
121
+ return true;
122
+ }
123
+ return shouldSkipE2eForRoleAssistant(systemKey, options)
124
+ || shouldSkipE2eForStaticPackage(systemKey, options);
125
+ }
126
+
127
+ module.exports = {
128
+ shouldSkipOperationsE2e,
129
+ shouldSkipE2eForRoleAssistant,
130
+ shouldSkipE2eForStaticPackage
131
+ };
@@ -7,6 +7,7 @@
7
7
  const { validateExternalSystemComplete } = require('../validation/validate');
8
8
  const { testExternalSystem, testExternalSystemIntegration } = require('../external-system/test');
9
9
  const { runTestE2EForExternalSystem } = require('./test-e2e-external');
10
+ const { shouldSkipOperationsE2e } = require('./verify-operations-skip-e2e');
10
11
 
11
12
  /**
12
13
  * @param {string} systemKey
@@ -59,21 +60,13 @@ function buildAfterSyncSteps(systemKey, options) {
59
60
  * @returns {Object}
60
61
  */
61
62
  function buildE2eStep(systemKey, options) {
62
- if (options.skipE2e === true || options.minimal === true) {
63
+ if (shouldSkipOperationsE2e(systemKey, options)) {
64
+ const reason = options.skipE2e === true || options.minimal === true
65
+ ? 'minimal'
66
+ : 'static-or-role-assistant';
63
67
  return {
64
68
  key: 'e2e',
65
- run: async() => ({ success: true, skipped: true, reason: 'minimal' }),
66
- ok: () => true
67
- };
68
- }
69
- if (shouldSkipE2eForRoleAssistant(systemKey, options)) {
70
- return {
71
- key: 'e2e',
72
- run: async() => ({
73
- success: true,
74
- skipped: true,
75
- reason: 'role-assistant-auth-none'
76
- }),
69
+ run: async() => ({ success: true, skipped: true, reason }),
77
70
  ok: () => true
78
71
  };
79
72
  }
@@ -99,26 +92,6 @@ function buildE2eStep(systemKey, options) {
99
92
  };
100
93
  }
101
94
 
102
- /**
103
- * Role Assistant packages use authType none + nested builder-api CIP.
104
- * Live capacity E2E expects a system baseUrl credential and is not the RA certify path.
105
- * @param {string} systemKey
106
- * @param {Object} options
107
- * @returns {boolean}
108
- */
109
- function shouldSkipE2eForRoleAssistant(systemKey, options) {
110
- if (options.forceE2e === true) {
111
- return false;
112
- }
113
- try {
114
- const { resolveAppWorkspace } = require('../utils/app-path-resolve');
115
- const { baseDir } = resolveAppWorkspace(systemKey, {});
116
- return baseDir === 'role-assistant';
117
- } catch {
118
- return false;
119
- }
120
- }
121
-
122
95
  /**
123
96
  * @param {string} systemKey
124
97
  * @param {Object} options
@@ -86,7 +86,9 @@ async function runOperationsVerifySteps(systemKey, options) {
86
86
  }
87
87
  }
88
88
 
89
- if (!cliOptsSkipPublish(options)) {
89
+ // Upload is not publish. Verify against the already-deployed dataplane unless
90
+ // the caller opts in with upload: true (and did not pass --no-publish).
91
+ if (options.upload === true && !cliOptsSkipPublish(options)) {
90
92
  const uploadCtx = await syncLocalIfRequested(systemKey, options);
91
93
  attachUploadAuthToOptions(options, uploadCtx);
92
94
  }
@@ -78,23 +78,41 @@ function _buildRelationMetadataSchema({ relationName, targetDoc, resolvedTargetF
78
78
  return out;
79
79
  }
80
80
 
81
+ function relationNameFromCarrier(fields) {
82
+ const raw = String((Array.isArray(fields) && fields[0]) || '').trim();
83
+ if (!raw) {
84
+ return '';
85
+ }
86
+ if (/^[a-z][a-zA-Z0-9]*$/.test(raw)) {
87
+ return raw;
88
+ }
89
+ const camel = raw.replace(/[-_]([a-zA-Z0-9])/g, (_, ch) => String(ch).toUpperCase());
90
+ return /^[a-z][a-zA-Z0-9]*$/.test(camel) ? camel : '';
91
+ }
92
+
81
93
  /**
82
94
  * FK `name` must match schema: ^[a-z][a-zA-Z0-9]*$
95
+ * Defaults to the published carrier (fields[0]) when --relation-name is omitted.
83
96
  *
84
97
  * @param {string} raw
98
+ * @param {string[]} [fields]
85
99
  * @returns {string}
86
100
  */
87
- function normalizeRelationName(raw) {
101
+ function normalizeRelationName(raw, fields) {
88
102
  const s = String(raw || '').trim();
89
- if (!s) {
90
- throw new Error('--relation-name is required');
103
+ if (s) {
104
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(s)) {
105
+ throw new Error(
106
+ `--relation-name "${s}" must match ^[a-z][a-zA-Z0-9]*$ (camelCase)`
107
+ );
108
+ }
109
+ return s;
91
110
  }
92
- if (!/^[a-z][a-zA-Z0-9]*$/.test(s)) {
93
- throw new Error(
94
- `--relation-name "${s}" must match ^[a-z][a-zA-Z0-9]*$ (camelCase)`
95
- );
111
+ const fromField = relationNameFromCarrier(fields);
112
+ if (fromField) {
113
+ return fromField;
96
114
  }
97
- return s;
115
+ throw new Error('--relation-name is required when --field is not a camelCase carrier');
98
116
  }
99
117
 
100
118
  /**
@@ -287,9 +305,9 @@ function buildForeignKeyRow(name, fields, targetDatasource, targetFields, option
287
305
  * }}
288
306
  */
289
307
  function applyCapabilityRelate(doc, opts) {
290
- const name = normalizeRelationName(opts.relationName);
291
- const targetDatasource = normalizeTargetDatasource(opts.targetDatasource);
292
308
  const { fields, targetFields } = parseRelateFields(opts);
309
+ const name = normalizeRelationName(opts.relationName, fields);
310
+ const targetDatasource = normalizeTargetDatasource(opts.targetDatasource);
293
311
 
294
312
  const d = deepClone(doc);
295
313
  if (!Array.isArray(d.foreignKeys)) {
@@ -25,13 +25,18 @@ function loadTargetDocLocal(targetDatasourceKey) {
25
25
  return localTarget.ok ? readJsonFile(localTarget.path) : null;
26
26
  }
27
27
 
28
- async function maybeFetchRemoteTargetConfig({ systemKeyForAuth, targetDatasourceKey }) {
28
+ async function maybeFetchRemoteTargetConfig({
29
+ systemKeyForAuth,
30
+ targetSystemKey,
31
+ targetDatasourceKey
32
+ }) {
29
33
  const remoteFetchMeta = { attempted: false, ok: false, notAuthenticated: false };
30
- if (!systemKeyForAuth) {
34
+ const authKey = String(targetSystemKey || systemKeyForAuth || '').trim();
35
+ if (!authKey) {
31
36
  return { remoteTargetConfig: null, remoteFetchMeta };
32
37
  }
33
38
  remoteFetchMeta.attempted = true;
34
- const remote = await tryFetchDatasourceConfig(systemKeyForAuth, targetDatasourceKey, { silent: true });
39
+ const remote = await tryFetchDatasourceConfig(authKey, targetDatasourceKey, { silent: true });
35
40
  if (remote.ok) {
36
41
  return {
37
42
  remoteTargetConfig: remote.datasourceConfig,
@@ -105,6 +110,38 @@ function applyRelateAndValidateSchema(parsed, opts, semanticWarnings, remoteVali
105
110
  return { result, validation };
106
111
  }
107
112
 
113
+ function persistOrDryRunRelate({
114
+ opts,
115
+ resolvedPath,
116
+ result,
117
+ validation,
118
+ semantic,
119
+ remoteFetchMeta
120
+ }) {
121
+ if (opts.dryRun) {
122
+ return buildRelateResultEnvelope({
123
+ dryRun: true,
124
+ resolvedPath,
125
+ result,
126
+ backupPath: null,
127
+ validation,
128
+ semanticWarnings: semantic.warnings,
129
+ remoteValidation: remoteFetchMeta
130
+ });
131
+ }
132
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
133
+ atomicWriteJson(resolvedPath, result.doc);
134
+ return buildRelateResultEnvelope({
135
+ dryRun: false,
136
+ resolvedPath,
137
+ result,
138
+ backupPath,
139
+ validation,
140
+ semanticWarnings: semantic.warnings,
141
+ remoteValidation: remoteFetchMeta
142
+ });
143
+ }
144
+
108
145
  /**
109
146
  * @typedef {object} RunCapabilityRelateOpts
110
147
  * @property {string} fileOrKey
@@ -137,8 +174,10 @@ async function runCapabilityRelate(opts) {
137
174
 
138
175
  const targetDocLocal = loadTargetDocLocal(opts.targetDatasource);
139
176
  const systemKeyForAuth = resolveSystemKeyForAuth(parsed);
177
+ const targetSystemKey = resolveSystemKeyForAuth(targetDocLocal) || systemKeyForAuth;
140
178
  const { remoteTargetConfig, remoteFetchMeta } = await maybeFetchRemoteTargetConfig({
141
179
  systemKeyForAuth,
180
+ targetSystemKey,
142
181
  targetDatasourceKey: opts.targetDatasource
143
182
  });
144
183
  const semantic = runSemanticValidation({
@@ -159,33 +198,17 @@ async function runCapabilityRelate(opts) {
159
198
  semantic.warnings,
160
199
  remoteFetchMeta
161
200
  );
162
-
163
- if (opts.dryRun) {
164
- return buildRelateResultEnvelope({
165
- dryRun: true,
166
- resolvedPath,
167
- result,
168
- backupPath: null,
169
- validation,
170
- semanticWarnings: semantic.warnings,
171
- remoteValidation: remoteFetchMeta
172
- });
173
- }
174
-
175
- const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
176
- atomicWriteJson(resolvedPath, result.doc);
177
-
178
- return buildRelateResultEnvelope({
179
- dryRun: false,
201
+ return persistOrDryRunRelate({
202
+ opts,
180
203
  resolvedPath,
181
204
  result,
182
- backupPath,
183
205
  validation,
184
- semanticWarnings: semantic.warnings,
185
- remoteValidation: remoteFetchMeta
206
+ semantic,
207
+ remoteFetchMeta
186
208
  });
187
209
  }
188
210
 
189
211
  module.exports = {
190
- runCapabilityRelate
212
+ runCapabilityRelate,
213
+ resolveSystemKeyForAuth
191
214
  };
@@ -56,12 +56,20 @@ function pickIntegrationAppForDatasourceIdentifier(identifier, appNames) {
56
56
  if (!id) {
57
57
  return null;
58
58
  }
59
- const matches = appNames.filter(app => id === app || id.startsWith(`${app}-`));
60
- if (matches.length === 0) {
61
- return null;
59
+ const prefixMatches = (appNames || []).filter((app) => id === app || id.startsWith(`${app}-`));
60
+ if (prefixMatches.length > 0) {
61
+ prefixMatches.sort((a, b) => b.length - a.length);
62
+ return prefixMatches[0];
63
+ }
64
+ const keyMatches = (appNames || []).filter((app) => appHasDatasourceIdentifier(app, id));
65
+ if (keyMatches.length === 1) {
66
+ return keyMatches[0];
67
+ }
68
+ if (keyMatches.length > 1) {
69
+ keyMatches.sort((a, b) => b.length - a.length);
70
+ return keyMatches[0];
62
71
  }
63
- matches.sort((a, b) => b.length - a.length);
64
- return matches[0];
72
+ return null;
65
73
  }
66
74
 
67
75
  /**
@@ -18,52 +18,133 @@ const {
18
18
  const { resolvePollIntervalFromController } = require('./poll-interval');
19
19
  const { createDeployPollHandlers } = require('./deploy-poll-ui');
20
20
 
21
+ /** Same progress for this many polls + dataplane published → treat deploy as complete. */
22
+ const STUCK_PROGRESS_POLLS = 10;
23
+
24
+ function recordStuckProgress(state, snapshot) {
25
+ const progress = Number(snapshot && snapshot.progress);
26
+ const value = Number.isFinite(progress) ? progress : null;
27
+ if (value === state.lastProgress) {
28
+ state.stuckCount += 1;
29
+ } else {
30
+ state.lastProgress = value;
31
+ state.stuckCount = 1;
32
+ }
33
+ return state;
34
+ }
35
+
36
+ async function completeIfDataplanePublished(lastSeen, options) {
37
+ if (!lastSeen || typeof options.isDataplanePublished !== 'function') {
38
+ return null;
39
+ }
40
+ try {
41
+ if (await options.isDataplanePublished()) {
42
+ return {
43
+ ...lastSeen,
44
+ status: 'completed',
45
+ progress: 100,
46
+ completedViaDataplane: true
47
+ };
48
+ }
49
+ } catch {
50
+ return null;
51
+ }
52
+ return null;
53
+ }
54
+
21
55
  /**
22
- * Polls deployment status from controller.
23
- *
24
- * @async
25
- * @param {string} deploymentId
26
- * @param {string} controllerUrl
27
- * @param {string} envKey
28
- * @param {Object} authConfig
29
- * @param {Object} [options]
30
- * @returns {Promise<Object>}
56
+ * Poll until the controller job is terminal, or dataplane is already published.
57
+ * @param {Object} ctx
58
+ * @returns {Promise<Object|null>}
31
59
  */
60
+ async function pollUntilTerminal(ctx) {
61
+ const {
62
+ controllerUrl,
63
+ validatedEnvKey,
64
+ deploymentId,
65
+ pipelineAuthConfig,
66
+ interval,
67
+ maxAttempts,
68
+ onPollWait,
69
+ wrappedProgress,
70
+ stuckState,
71
+ options,
72
+ getLastSeen
73
+ } = ctx;
74
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
75
+ if (onPollWait) {
76
+ onPollWait(attempt, maxAttempts);
77
+ }
78
+ const response = await getPipelineDeployment(
79
+ controllerUrl,
80
+ validatedEnvKey,
81
+ deploymentId,
82
+ pipelineAuthConfig
83
+ );
84
+ const deploymentData = await processDeploymentStatusResponse(
85
+ response,
86
+ attempt,
87
+ maxAttempts,
88
+ interval,
89
+ deploymentId,
90
+ wrappedProgress
91
+ );
92
+ if (deploymentData) {
93
+ return deploymentData;
94
+ }
95
+ if (stuckState.stuckCount >= STUCK_PROGRESS_POLLS) {
96
+ const viaDataplane = await completeIfDataplanePublished(getLastSeen(), options);
97
+ if (viaDataplane) {
98
+ return viaDataplane;
99
+ }
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
32
105
  async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authConfig, options = {}) {
33
106
  const interval = options.interval || 5000;
34
107
  const maxAttempts = options.maxAttempts || 300;
35
-
36
108
  const validatedEnvKey = validateEnvironmentKey(envKey);
37
109
  const pipelineAuthConfig = convertToPipelineAuthConfig(authConfig);
38
-
39
110
  const onPollProgress = typeof options.onPollProgress === 'function' ? options.onPollProgress : null;
40
111
  const onPollWait = typeof options.onPollWait === 'function' ? options.onPollWait : null;
41
-
42
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
43
- try {
44
- if (onPollWait) {
45
- onPollWait(attempt, maxAttempts);
46
- }
47
- const response = await getPipelineDeployment(controllerUrl, validatedEnvKey, deploymentId, pipelineAuthConfig);
48
- const deploymentData = await processDeploymentStatusResponse(
49
- response,
50
- attempt,
51
- maxAttempts,
52
- interval,
53
- deploymentId,
54
- onPollProgress
55
- );
56
- if (deploymentData) {
57
- return deploymentData;
58
- }
59
- } catch (error) {
60
- if (error.message && error.message.includes('not found')) {
61
- throw error;
62
- }
112
+ const stuckState = { lastProgress: undefined, stuckCount: 0 };
113
+ let lastSeen = null;
114
+ const wrappedProgress = (data, attempt, max) => {
115
+ lastSeen = data;
116
+ recordStuckProgress(stuckState, data);
117
+ if (onPollProgress) {
118
+ onPollProgress(data, attempt, max);
119
+ }
120
+ };
121
+ try {
122
+ const terminal = await pollUntilTerminal({
123
+ controllerUrl,
124
+ validatedEnvKey,
125
+ deploymentId,
126
+ pipelineAuthConfig,
127
+ interval,
128
+ maxAttempts,
129
+ onPollWait,
130
+ wrappedProgress,
131
+ stuckState,
132
+ options,
133
+ getLastSeen: () => lastSeen
134
+ });
135
+ if (terminal) {
136
+ return terminal;
137
+ }
138
+ } catch (error) {
139
+ if (error.message && error.message.includes('not found')) {
63
140
  throw error;
64
141
  }
142
+ throw error;
143
+ }
144
+ const timedOutComplete = await completeIfDataplanePublished(lastSeen, options);
145
+ if (timedOutComplete) {
146
+ return timedOutComplete;
65
147
  }
66
-
67
148
  throw new Error('Deployment timeout: Maximum polling attempts reached');
68
149
  }
69
150
 
@@ -94,7 +175,8 @@ async function pollDeployment(result, url, validatedEnvKey, authConfig, options)
94
175
  interval: resolvedInterval,
95
176
  maxAttempts,
96
177
  onPollProgress: pollUi.onPollProgress,
97
- onPollWait: pollUi.onPollWait
178
+ onPollWait: pollUi.onPollWait,
179
+ isDataplanePublished: options.isDataplanePublished
98
180
  }
99
181
  );
100
182
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Treat a stuck controller deploy poll as complete when dataplane is already published.
3
+ *
4
+ * @fileoverview Dataplane published check for external deploy poll
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { getExternalSystem } = require('../api/external-systems.api');
10
+ const { unwrapApiData } = require('../utils/external-system-readiness-core');
11
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
12
+
13
+ /**
14
+ * @param {Object|null|undefined} system
15
+ * @returns {boolean}
16
+ */
17
+ function isDataplaneSystemPublished(system) {
18
+ const status = String((system && system.status) || '').toLowerCase();
19
+ return status === 'published' || status === 'deployed';
20
+ }
21
+
22
+ /**
23
+ * @param {string} controllerUrl
24
+ * @param {string} environment
25
+ * @param {Object} authConfig
26
+ * @param {string} systemKey
27
+ * @param {string} [dataplaneUrl]
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ async function checkDataplaneSystemPublished(
31
+ controllerUrl,
32
+ environment,
33
+ authConfig,
34
+ systemKey,
35
+ dataplaneUrl
36
+ ) {
37
+ let url = dataplaneUrl;
38
+ if (!url) {
39
+ url = await resolveDataplaneUrl(controllerUrl, environment, authConfig, { silent: true });
40
+ }
41
+ const getRes = await getExternalSystem(url, systemKey, authConfig);
42
+ return isDataplaneSystemPublished(unwrapApiData(getRes));
43
+ }
44
+
45
+ /**
46
+ * @param {Object} params
47
+ * @returns {() => Promise<boolean>}
48
+ */
49
+ function createDataplanePublishedCheck(params) {
50
+ const { controllerUrl, environment, authConfig, systemKey, dataplaneUrl } = params;
51
+ return async function isDataplanePublished() {
52
+ try {
53
+ return await checkDataplaneSystemPublished(
54
+ controllerUrl,
55
+ environment,
56
+ authConfig,
57
+ systemKey,
58
+ dataplaneUrl
59
+ );
60
+ } catch {
61
+ return false;
62
+ }
63
+ };
64
+ }
65
+
66
+ module.exports = {
67
+ isDataplaneSystemPublished,
68
+ checkDataplaneSystemPublished,
69
+ createDataplanePublishedCheck
70
+ };