@aifabrix/builder 2.51.1 → 2.52.1

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 (187) hide show
  1. package/jest.isolated-projects.js +25 -2
  2. package/jest.projects.js +4 -1
  3. package/lib/api/datasources-extended.api.js +8 -8
  4. package/lib/api/keycloak.api.js +92 -0
  5. package/lib/api/token-providers.api.js +77 -0
  6. package/lib/api/types/keycloak.types.js +23 -0
  7. package/lib/api/types/token-providers.types.js +65 -0
  8. package/lib/app/down.js +16 -1
  9. package/lib/app/run-container-start.js +4 -0
  10. package/lib/app/run-deploy-job.js +135 -27
  11. package/lib/app/run-env-compose.js +32 -0
  12. package/lib/app/run-env-recovery.js +35 -0
  13. package/lib/app/run-helpers.js +5 -6
  14. package/lib/app/run.js +11 -3
  15. package/lib/channels/add-channel.js +126 -19
  16. package/lib/channels/approval-guides/chatgpt.js +12 -9
  17. package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
  18. package/lib/channels/channel-add-ensure.js +128 -0
  19. package/lib/channels/channel-artifact-generator.js +11 -1
  20. package/lib/channels/channel-branding.js +94 -0
  21. package/lib/channels/channel-credential.js +8 -0
  22. package/lib/channels/channel-discovery.js +330 -0
  23. package/lib/channels/channel-external-system.js +1 -1
  24. package/lib/channels/channel-runtime-resolver.js +103 -14
  25. package/lib/channels/channel-status-display.js +7 -1
  26. package/lib/channels/channel-targets.js +18 -1
  27. package/lib/channels/chatgpt-runtime-contract.js +1 -1
  28. package/lib/channels/microsoft-copilot-channel-add.js +138 -0
  29. package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
  30. package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
  31. package/lib/channels/microsoft-copilot-icons.js +74 -0
  32. package/lib/channels/microsoft-copilot-limits.js +84 -0
  33. package/lib/channels/microsoft-copilot-package.js +130 -49
  34. package/lib/channels/microsoft-copilot-validate.js +197 -0
  35. package/lib/cli/index.js +2 -0
  36. package/lib/cli/infra-guided-footers.js +1 -1
  37. package/lib/cli/infra-guided-public-url-warn.js +55 -0
  38. package/lib/cli/infra-guided.js +78 -62
  39. package/lib/cli/setup-app.help.js +4 -0
  40. package/lib/cli/setup-app.test-commands.js +59 -28
  41. package/lib/cli/setup-external-system.js +50 -12
  42. package/lib/cli/setup-infra.js +31 -4
  43. package/lib/commands/channel-add-setup.js +35 -10
  44. package/lib/commands/channel.js +27 -12
  45. package/lib/commands/deploy-gate-orchestrator.js +3 -2
  46. package/lib/commands/governance-verify-external.js +2 -5
  47. package/lib/commands/integration-definition.js +158 -0
  48. package/lib/commands/lifecycle-external.js +14 -7
  49. package/lib/commands/repair-auth-apply.js +19 -5
  50. package/lib/commands/repair-datasource-sync-operations.js +4 -1
  51. package/lib/commands/role-assistant-test-output.js +200 -0
  52. package/lib/commands/role-assistant.js +131 -114
  53. package/lib/commands/test-governance-external.js +2 -5
  54. package/lib/commands/up-miso.js +61 -24
  55. package/lib/commands/upload-data-sync-finalize.js +20 -3
  56. package/lib/commands/verify-governance-external.js +2 -5
  57. package/lib/commands/verify-operations-external.js +2 -5
  58. package/lib/commands/verify-operations-step-plan.js +31 -0
  59. package/lib/commands/verify-trust-command-action.js +2 -5
  60. package/lib/datasource/capability/capability-resolve.js +15 -5
  61. package/lib/datasource/capability/copy-operations.js +14 -4
  62. package/lib/datasource/datasource-identifier.js +4 -3
  63. package/lib/datasource/integration-context.js +3 -3
  64. package/lib/datasource/resolve-app.js +90 -8
  65. package/lib/datasource/test-e2e.js +2 -2
  66. package/lib/datasource/unified-validation-run-body.js +15 -1
  67. package/lib/datasource/unified-validation-run-resolve.js +2 -2
  68. package/lib/external-system/deploy-helpers.js +9 -5
  69. package/lib/external-system/deploy.js +2 -1
  70. package/lib/external-system/test-system-level.js +21 -3
  71. package/lib/external-system/test.js +4 -4
  72. package/lib/generator/builders.js +9 -15
  73. package/lib/infrastructure/index.js +19 -4
  74. package/lib/integration-definition/apply.js +478 -0
  75. package/lib/integration-definition/canonical-json.js +51 -0
  76. package/lib/integration-definition/emit-suite-bid.js +94 -0
  77. package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
  78. package/lib/integration-definition/enrich-openapi-operations.js +434 -0
  79. package/lib/integration-definition/from-discovery.js +339 -0
  80. package/lib/integration-definition/hash.js +37 -0
  81. package/lib/integration-definition/index.js +37 -0
  82. package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
  83. package/lib/integration-definition/resource-type-catalog.js +59 -0
  84. package/lib/integration-definition/system-kind-enum.js +40 -0
  85. package/lib/integration-definition/validate.js +148 -0
  86. package/lib/programmatic/builder-api-request.js +5 -1
  87. package/lib/programmatic/builder-help-governance.js +3 -2
  88. package/lib/programmatic/capability-registry.js +19 -0
  89. package/lib/programmatic/openapi-component-schemas.js +6 -1
  90. package/lib/programmatic/openapi-descriptions.js +40 -0
  91. package/lib/programmatic/openapi-envelope-examples.js +269 -0
  92. package/lib/programmatic/openapi-operation-bindings.js +45 -0
  93. package/lib/programmatic/openapi-request-schemas.js +14 -0
  94. package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
  95. package/lib/programmatic/openapi-result-schemas.js +46 -99
  96. package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
  97. package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
  98. package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
  99. package/lib/programmatic/openapi-schema-helpers.js +9 -9
  100. package/lib/programmatic/openapi-spec.js +11 -1
  101. package/lib/programmatic/orchestration-openapi-meta.js +10 -0
  102. package/lib/programmatic/route-handler-map.js +29 -1
  103. package/lib/programmatic/run-channel-package.js +236 -0
  104. package/lib/programmatic/run-integration-definition.js +69 -0
  105. package/lib/role-assistant/knowledge-markdown-frontmatter.js +75 -0
  106. package/lib/role-assistant/knowledge-sync-logging.js +58 -0
  107. package/lib/role-assistant/knowledge-sync.js +104 -89
  108. package/lib/role-assistant/test-cases-bid.js +155 -0
  109. package/lib/role-assistant/test-cases-evaluate.js +9 -1
  110. package/lib/role-assistant/test-cases-honesty.js +24 -10
  111. package/lib/role-assistant/test-cases.js +28 -0
  112. package/lib/role-assistant/test-runner-bid-apply.js +122 -0
  113. package/lib/role-assistant/test-runner-bid-document.js +278 -0
  114. package/lib/role-assistant/test-runner-workhub-answers.js +289 -46
  115. package/lib/role-assistant/test-runner-workhub.js +121 -25
  116. package/lib/role-assistant/test-suite.js +119 -0
  117. package/lib/schema/application-schema.json +64 -32
  118. package/lib/schema/external-datasource.schema.json +103 -17
  119. package/lib/schema/external-system.schema.json +54 -3
  120. package/lib/schema/infra.parameter.yaml +0 -20
  121. package/lib/schema/integration-definition.schema.json +195 -0
  122. package/lib/utils/authentication-shape.js +6 -0
  123. package/lib/utils/certify-external-workspace.js +40 -0
  124. package/lib/utils/docker-build.js +3 -3
  125. package/lib/utils/docker-run-ephemeral.js +77 -17
  126. package/lib/utils/health-check-public-warn.js +31 -13
  127. package/lib/utils/health-check.js +11 -1
  128. package/lib/utils/help-builder.js +1 -0
  129. package/lib/utils/paths.js +13 -15
  130. package/lib/utils/promote-dev-docker-image.js +97 -18
  131. package/lib/utils/role-assistant-paths.js +76 -14
  132. package/lib/utils/transient-developer-id.js +28 -0
  133. package/lib/utils/upload-sync-options.js +7 -4
  134. package/lib/validation/reserved-system-key-prefixes.js +56 -0
  135. package/lib/validation/validate.js +54 -19
  136. package/package.json +1 -1
  137. package/scripts/pnpm/af.mjs +24 -9
  138. package/scripts/pnpm/help.mjs +1 -1
  139. package/templates/applications/builder-api/application.yaml +3 -3
  140. package/templates/applications/dataplane/application.yaml +5 -5
  141. package/templates/applications/dataplane/env.template +8 -7
  142. package/templates/applications/dataplane/rbac.yaml +10 -10
  143. package/templates/applications/keycloak/application.yaml +2 -2
  144. package/templates/applications/keycloak/env.template +5 -2
  145. package/templates/applications/miso-controller/application.yaml +25 -11
  146. package/templates/applications/miso-controller/env.template +24 -5
  147. package/templates/channels/README.md +11 -1
  148. package/templates/channels/brand/color-192.png +0 -0
  149. package/templates/channels/brand/dark/128.png +0 -0
  150. package/templates/channels/brand/dark/192.png +0 -0
  151. package/templates/channels/brand/dark/256.png +0 -0
  152. package/templates/channels/brand/dark/32.png +0 -0
  153. package/templates/channels/brand/dark/512.png +0 -0
  154. package/templates/channels/brand/dark/64.png +0 -0
  155. package/templates/channels/brand/light/128.png +0 -0
  156. package/templates/channels/brand/light/192.png +0 -0
  157. package/templates/channels/brand/light/256.png +0 -0
  158. package/templates/channels/brand/light/32.png +0 -0
  159. package/templates/channels/brand/light/512.png +0 -0
  160. package/templates/channels/brand/light/64.png +0 -0
  161. package/templates/channels/brand/mono-white/128.png +0 -0
  162. package/templates/channels/brand/mono-white/192.png +0 -0
  163. package/templates/channels/brand/mono-white/256.png +0 -0
  164. package/templates/channels/brand/mono-white/32.png +0 -0
  165. package/templates/channels/brand/mono-white/512.png +0 -0
  166. package/templates/channels/brand/mono-white/64.png +0 -0
  167. package/templates/channels/brand/outline-32.png +0 -0
  168. package/templates/channels/chatgpt/install.template.md.hbs +76 -36
  169. package/templates/channels/chatgpt/integration/README.md +4 -4
  170. package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
  171. package/templates/channels/chatgpt/manifest.template.json.hbs +3 -3
  172. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
  173. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
  174. package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
  175. package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
  176. package/templates/channels/microsoft-copilot/integration/README.md +2 -3
  177. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
  178. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
  179. package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
  180. package/templates/channels/slack/install.template.md.hbs +6 -1
  181. package/templates/channels/slack/integration/README.md +2 -4
  182. package/templates/channels/teams-bot/install.template.md.hbs +4 -0
  183. package/templates/channels/teams-bot/integration/README.md +3 -5
  184. package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
  185. package/templates/python/docker-compose.hbs +10 -0
  186. package/templates/typescript/docker-compose.hbs +10 -0
  187. /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
@@ -8,8 +8,6 @@ const chalk = require('chalk');
8
8
  const logger = require('../utils/logger');
9
9
  const config = require('../core/config');
10
10
  const devConfig = require('../utils/dev-config');
11
- const pathsUtil = require('../utils/paths');
12
- const { loadConfigFile } = require('../utils/config-format');
13
11
  const { withMutedLogger } = require('../utils/with-muted-logger');
14
12
  const infra = require('../infrastructure');
15
13
  const { execWithDockerEnv } = require('../utils/docker-exec');
@@ -41,12 +39,72 @@ const {
41
39
  logUpPlatformForceCleanSummary,
42
40
  logStoppedServices
43
41
  } = require('./infra-guided-footers');
44
- const healthCheck = require('../utils/health-check');
45
42
  const { prepareUrlsLocalRegistryForUpPlatform } = require('../commands/up-common');
46
43
  const { assertDevInfraUp } = require('../commands/dev-infra-gate');
47
44
  const {
48
45
  emitSystemBuilderAppManifestLineIfTTY
49
46
  } = require('../utils/manifest-source-emit');
47
+ const { flushDeferredPublicUrlWarnings } = require('./infra-guided-public-url-warn');
48
+
49
+ /**
50
+ * Guided Keycloak → Miso Controller start with one spinner per app (no second health wait).
51
+ * @param {Object} options
52
+ * @param {Function} handleUpMiso
53
+ * @returns {Promise<string[]>} Deferred public-URL warning messages
54
+ */
55
+ async function runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso) {
56
+ const deferredWarnings = [];
57
+ const misoSpinnerRef = { current: null };
58
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'keycloak');
59
+ const kcSpin = startSpinner('Starting Keycloak...');
60
+ // Fully mute inner run logs — any newline (✔ / Manifest / blank) splits the ora line.
61
+ await withMutedLogger(
62
+ () =>
63
+ handleUpMiso({
64
+ ...options,
65
+ platformInstall: true,
66
+ onAfterKeycloak: () => {
67
+ stopSpinnerSuccess(kcSpin, 'Keycloak ready');
68
+ logger.log('');
69
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
70
+ misoSpinnerRef.current = startSpinner('Starting Miso Controller...');
71
+ },
72
+ onMisoPollText: (text) => {
73
+ const spin = misoSpinnerRef.current;
74
+ if (spin) {
75
+ // Keep app name; append health progress so the spinner does not look like a new step.
76
+ spin.text = `Starting Miso Controller… ${stripHealthPrefix(text)}`;
77
+ return;
78
+ }
79
+ logger.log(text);
80
+ },
81
+ onDeferredPublicUrlWarning: (message) => deferredWarnings.push(message)
82
+ }),
83
+ { allow: () => false }
84
+ );
85
+ if (misoSpinnerRef.current) {
86
+ stopSpinnerSuccess(misoSpinnerRef.current, 'Miso Controller ready');
87
+ } else {
88
+ stopSpinnerSuccess(kcSpin, 'Keycloak ready');
89
+ }
90
+ flushDeferredPublicUrlWarnings(deferredWarnings);
91
+ logger.log('');
92
+ return deferredWarnings;
93
+ }
94
+
95
+ /** ESC + CSI SGR — built without a control-char regex literal (no-control-regex). */
96
+ const ANSI_SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
97
+
98
+ /**
99
+ * Turn "Waiting for health check… (4/45)" into "(4/45)" for nested spinner text.
100
+ * @param {string} text
101
+ * @returns {string}
102
+ */
103
+ function stripHealthPrefix(text) {
104
+ const raw = String(text || '');
105
+ const match = raw.match(/\((\d+\/\d+)\)/);
106
+ return match ? `(${match[1]})` : raw.replace(ANSI_SGR_RE, '').trim();
107
+ }
50
108
 
51
109
  async function getRunningContainerNameForApp(appName) {
52
110
  const apps = await infra.getAppStatus();
@@ -86,31 +144,6 @@ async function validateAppErrorLogs(appNames) {
86
144
  logger.log('');
87
145
  }
88
146
 
89
- async function waitForAppReady(appName, opts = {}) {
90
- const timeoutSeconds = Number.isFinite(opts.timeoutSeconds) ? opts.timeoutSeconds : 90;
91
- const builderPath = pathsUtil.getBuilderPath(appName);
92
- const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
93
- const appConfig = loadConfigFile(configPath) || {};
94
-
95
- const apps = await infra.getAppStatus();
96
- const hit = (apps || []).find(a => a && a.name === appName && typeof a.url === 'string');
97
- const portMatch = hit && hit.url ? hit.url.match(/:(\d+)$/) : null;
98
- const hostPort = portMatch ? parseInt(portMatch[1], 10) : null;
99
- if (!hostPort) {
100
- throw new Error(`Could not determine host port for ${appName}`);
101
- }
102
-
103
- const userCfg = await config.getConfig();
104
- const healthRunOpts = { profile: 'docker' };
105
- if (userCfg && userCfg.traefik === true) {
106
- healthRunOpts.traefikEnabled = true;
107
- }
108
- if (typeof opts.onPollText === 'function') {
109
- healthRunOpts.onPollText = opts.onPollText;
110
- }
111
- await healthCheck.waitForHealthCheck(appName, timeoutSeconds, hostPort, appConfig, false, healthRunOpts);
112
- }
113
-
114
147
  function resolveInfraHost(remoteServer) {
115
148
  const raw = String(remoteServer || '').trim().replace(/\/+$/, '');
116
149
  if (!raw) return 'localhost';
@@ -179,16 +212,7 @@ async function runGuidedUpMiso(options, handleUpMiso) {
179
212
  logger.log('');
180
213
  logPlatformSetupDurationHint('platform');
181
214
  logger.log('');
182
- const kcSpin = startSpinner('Starting Keycloak...');
183
- await withMutedLogger(() => handleUpMiso({ ...options, platformInstall: true }));
184
- stopSpinnerSuccess(kcSpin, 'Keycloak ready');
185
- logger.log('');
186
- const mcSpin = startSpinner('Starting Miso Controller...');
187
- await waitForAppReady('miso-controller', {
188
- timeoutSeconds: 120,
189
- onPollText: createSpinnerTextUpdater(mcSpin)
190
- });
191
- stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
215
+ await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
192
216
  await logMisoReadyFooter();
193
217
  await validateAppErrorLogs(['miso-controller']);
194
218
  }
@@ -241,22 +265,6 @@ async function refreshDataplaneVersionAfterPlatformInstall() {
241
265
  }
242
266
  }
243
267
 
244
- async function runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso) {
245
- emitSystemBuilderAppManifestLineIfTTY(logger, 'keycloak');
246
- const kcSpin = startSpinner('Starting Keycloak...');
247
- await withMutedLogger(() => handleUpMiso({ ...options, platformInstall: true }));
248
- stopSpinnerSuccess(kcSpin, 'Keycloak ready');
249
- logger.log('');
250
- emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
251
- const mcSpin = startSpinner('Starting Miso Controller...');
252
- await waitForAppReady('miso-controller', {
253
- timeoutSeconds: 120,
254
- onPollText: createSpinnerTextUpdater(mcSpin)
255
- });
256
- stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
257
- logger.log('');
258
- }
259
-
260
268
  async function runGuidedDataplaneInstallPhase(options, handleUpDataplane, handleLoginFn, controllerUrl) {
261
269
  logger.log('');
262
270
  emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
@@ -310,7 +318,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
310
318
  await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
311
319
  const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
312
320
  await runGuidedDataplaneInstallPhase(options, handleUpDataplane, handleLogin, controllerUrl);
313
- const baSpin = startSpinner('Starting builder-api...');
321
+ const baSpin = startSpinner('Starting Builder API...');
314
322
  await withMutedLogger(() => handleUpBuilderApi({ ...options, platformInstall: true }));
315
323
  stopSpinnerSuccess(baSpin, 'Builder API ready');
316
324
  await validateAppErrorLogs(['miso-controller', 'dataplane', 'builder-api']);
@@ -323,23 +331,30 @@ async function runGuidedDownInfra(appName, options, infraMod, appLib) {
323
331
  logger.log(SEPARATOR);
324
332
  logger.log('');
325
333
 
334
+ const targetDeveloperId = options && options.developerId;
326
335
  const spin = startSpinner('Stopping infrastructure...');
327
336
  await withMutedLogger(async() => {
328
337
  if (typeof appName === 'string' && appName.trim().length > 0) {
329
- await appLib.downApp(appName, { volumes: !!options.volumes });
338
+ await appLib.downApp(appName, {
339
+ volumes: !!options.volumes,
340
+ developerId: targetDeveloperId
341
+ });
330
342
  return;
331
343
  }
332
- if (options.volumes) await infraMod.stopInfraWithVolumes();
333
- else await infraMod.stopInfra();
344
+ if (options.volumes) await infraMod.stopInfraWithVolumes(targetDeveloperId);
345
+ else await infraMod.stopInfra(targetDeveloperId);
334
346
  });
335
347
  stopSpinnerSuccess(spin, 'Infrastructure stopped');
336
348
 
337
- const developerId = await config.getDeveloperId();
338
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
349
+ const developerId = String(
350
+ targetDeveloperId !== undefined && targetDeveloperId !== null && targetDeveloperId !== ''
351
+ ? targetDeveloperId
352
+ : await config.getDeveloperId()
353
+ );
339
354
  const cfg = await config.getConfig();
340
355
 
341
356
  logFooterStart('Stopped');
342
- logSection('Developer', `dev${String(idNum).padStart(2, '0')} (id: ${idNum})`);
357
+ logSection('Developer', `dev${developerId.padStart(2, '0')} (id: ${developerId})`);
343
358
  logStoppedServices(cfg);
344
359
  if (options.volumes) {
345
360
  logger.log(chalk.yellow('⚠ Volumes removed: all local data deleted'));
@@ -353,5 +368,6 @@ module.exports = {
353
368
  runGuidedUpMiso,
354
369
  runGuidedUpDataplane,
355
370
  runGuidedUpPlatform,
356
- runGuidedDownInfra
371
+ runGuidedDownInfra,
372
+ stripHealthPrefix
357
373
  };
@@ -11,6 +11,10 @@ Examples:
11
11
  $ aifabrix test hubspot -v
12
12
  $ aifabrix test hubspot -d
13
13
 
14
+ # Role Assistant (role-assistant/ra-<roleKey>/) — lifecycle checks
15
+ $ aifabrix test ra-business-transformation
16
+ $ aifabrix role-assistant test business-transformation
17
+
14
18
  # Builder app (builder/<app>/) — runs in container
15
19
  $ aifabrix test myapp -e dev
16
20
  $ aifabrix test myapp -e tst
@@ -12,9 +12,56 @@ const {
12
12
  TEST_E2E_HELP_AFTER
13
13
  } = require('./setup-app.help');
14
14
 
15
+ /** External packages that use local unit validation (not builder container tests). */
16
+ function isExternalUnitTestWorkspace(baseDir) {
17
+ return baseDir === 'integration' || baseDir === 'enterprise-knowledge';
18
+ }
19
+
20
+ /**
21
+ * Dispatch aifabrix test by detected workspace (builder / integration / role-assistant).
22
+ * @param {string} appName
23
+ * @param {Object} options
24
+ * @param {Object} cmd
25
+ * @returns {Promise<void>}
26
+ */
27
+ async function runTestCommandAction(appName, options, cmd) {
28
+ const rawArgs = Array.isArray(cmd?.rawArgs) ? cmd.rawArgs : [];
29
+ const envExplicit = rawArgs.includes('-e') || rawArgs.includes('--env');
30
+ const pathsUtil = require('../utils/paths');
31
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
32
+ if (options.sync === true) {
33
+ throw new Error(
34
+ 'Option --sync is not supported for aifabrix test (local or container runs do not publish to the dataplane). ' +
35
+ 'Use: aifabrix upload <systemKey>, then aifabrix test-integration <systemKey> --sync or aifabrix datasource test-integration <datasourceKey> --sync.'
36
+ );
37
+ }
38
+ if (appType && appType.baseDir === 'role-assistant') {
39
+ const { runRoleAssistantTestAction } = require('../commands/role-assistant');
40
+ await runRoleAssistantTestAction(appName, options, cmd);
41
+ return;
42
+ }
43
+ if (appType && isExternalUnitTestWorkspace(appType.baseDir)) {
44
+ const test = require('../external-system/test');
45
+ const externalOpts = {
46
+ ...options,
47
+ // Keep help default but don't override auth/env behavior unless user explicitly set it.
48
+ env: envExplicit ? options.env : undefined
49
+ };
50
+ const results = await test.testExternalSystem(appName, externalOpts);
51
+ test.displayTestResults(results, options.verbose, appName);
52
+ if (!results.valid) process.exit(1);
53
+ return;
54
+ }
55
+ const { runAppTest } = require('../commands/app-test');
56
+ await runAppTest(appName, { env: options.env });
57
+ }
58
+
15
59
  function setupTestCommand(program) {
16
60
  program.command('test <appKey|systemKey>')
17
- .description('Tests: builder appKey in container; external systemKey = local validation')
61
+ .description(
62
+ 'Tests: builder appKey in container; integration/EK = local validation; ' +
63
+ 'role-assistant = lifecycle (same as role-assistant test)'
64
+ )
18
65
  .option('-e, --env <env>', 'For builder app: dev (running container) or tst (ephemeral)', 'dev')
19
66
  .option('-v, --verbose', 'Verbose output')
20
67
  .option('-d, --debug', 'Certification debug + log to integration/<systemKey>/logs/ (external only)')
@@ -25,30 +72,7 @@ function setupTestCommand(program) {
25
72
  .addHelpText('after', TEST_HELP_AFTER)
26
73
  .action(async(appName, options, cmd) => {
27
74
  try {
28
- const rawArgs = Array.isArray(cmd?.rawArgs) ? cmd.rawArgs : [];
29
- const envExplicit = rawArgs.includes('-e') || rawArgs.includes('--env');
30
- const pathsUtil = require('../utils/paths');
31
- const appType = await pathsUtil.detectAppType(appName).catch(() => null);
32
- if (options.sync === true) {
33
- throw new Error(
34
- 'Option --sync is not supported for aifabrix test (local or container runs do not publish to the dataplane). ' +
35
- 'Use: aifabrix upload <systemKey>, then aifabrix test-integration <systemKey> --sync or aifabrix datasource test-integration <datasourceKey> --sync.'
36
- );
37
- }
38
- if (appType && appType.baseDir === 'integration') {
39
- const test = require('../external-system/test');
40
- const externalOpts = {
41
- ...options,
42
- // Keep help default but don't override auth/env behavior unless user explicitly set it.
43
- env: envExplicit ? options.env : undefined
44
- };
45
- const results = await test.testExternalSystem(appName, externalOpts);
46
- test.displayTestResults(results, options.verbose, appName);
47
- if (!results.valid) process.exit(1);
48
- return;
49
- }
50
- const { runAppTest } = require('../commands/app-test');
51
- await runAppTest(appName, { env: options.env });
75
+ await runTestCommandAction(appName, options, cmd);
52
76
  } catch (error) {
53
77
  handleCommandError(error, 'test');
54
78
  process.exit(1);
@@ -106,7 +130,14 @@ async function runTestE2ECommand(appName, options, cmd) {
106
130
  'Remove --no-publish for builder app E2E.'
107
131
  );
108
132
  }
109
- if (appType && appType.baseDir === 'integration') {
133
+ if (appType && appType.baseDir === 'role-assistant') {
134
+ throw new Error(
135
+ `'${appName}' is a Role Assistant under role-assistant/. ` +
136
+ 'Datasource E2E does not apply. Use: aifabrix role-assistant test <roleKey> ' +
137
+ '(or: aifabrix test <roleKey>).'
138
+ );
139
+ }
140
+ if (appType && isExternalUnitTestWorkspace(appType.baseDir)) {
110
141
  await runExternalIntegrationE2EAndExit(appName, options, cmd);
111
142
  return;
112
143
  }
@@ -122,7 +153,7 @@ function setupInstallCommand(program) {
122
153
  try {
123
154
  const pathsUtil = require('../utils/paths');
124
155
  const appType = await pathsUtil.detectAppType(appName).catch(() => null);
125
- if (appType && appType.baseDir === 'integration') {
156
+ if (appType && (isExternalUnitTestWorkspace(appType.baseDir) || appType.baseDir === 'role-assistant')) {
126
157
  logger.log(
127
158
  chalk.gray('Install is for builder appKey only. Use aifabrix shell <appKey> for builder apps or External Systems commands for integrations.')
128
159
  );
@@ -186,7 +217,7 @@ function setupLintCommand(program) {
186
217
  try {
187
218
  const pathsUtil = require('../utils/paths');
188
219
  const appType = await pathsUtil.detectAppType(appName).catch(() => null);
189
- if (appType && appType.baseDir === 'integration') {
220
+ if (appType && (isExternalUnitTestWorkspace(appType.baseDir) || appType.baseDir === 'role-assistant')) {
190
221
  logger.log(chalk.gray('lint is for builder appKey only. Use aifabrix shell <appKey> then make lint or pnpm lint.'));
191
222
  return;
192
223
  }
@@ -159,10 +159,44 @@ function setupDeleteCommand(program) {
159
159
  * @param {Object} options - CLI options
160
160
  * @returns {Promise<boolean>} True if builder test was run, false otherwise
161
161
  */
162
- async function tryBuilderTestIntegration(appName, options) {
162
+ /**
163
+ * @param {string} appName
164
+ * @param {Object} options
165
+ * @returns {Promise<boolean>} True if a builder-app integration test was started
166
+ */
167
+ async function tryRunBuilderAppIntegration(appName, options) {
163
168
  const fsSync = require('fs');
169
+ const { getBuilderPath } = require('../utils/paths');
170
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
171
+ const builderPath = getBuilderPath(appName);
172
+ if (!fsSync.existsSync(builderPath)) {
173
+ return false;
174
+ }
175
+ let builderConfigPath;
176
+ try {
177
+ builderConfigPath = resolveApplicationConfigPath(builderPath);
178
+ } catch {
179
+ return false;
180
+ }
181
+ if (!fsSync.existsSync(builderConfigPath)) {
182
+ return false;
183
+ }
184
+ const { runAppTestIntegration } = require('../commands/app-test');
185
+ await runAppTestIntegration(appName, {
186
+ env: options.env || options.environment || 'dev'
187
+ });
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * Prefer builder-app container tests when no externalIntegration folder exists.
193
+ * @param {string} appName - App or system key
194
+ * @param {Object} options - CLI options
195
+ * @returns {Promise<boolean>} True if builder test was run, false otherwise
196
+ */
197
+ async function tryBuilderTestIntegration(appName, options) {
164
198
  const pathsUtil = require('../utils/paths');
165
- const { getIntegrationPath, getBuilderPath } = pathsUtil;
199
+ const { getIntegrationPath } = pathsUtil;
166
200
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
167
201
  const { loadConfigFile } = require('../utils/config-format');
168
202
  const integrationPath = getIntegrationPath(appName);
@@ -173,17 +207,10 @@ async function tryBuilderTestIntegration(appName, options) {
173
207
  } catch {
174
208
  // integration path or config missing
175
209
  }
176
- if (!hasExternalIntegration) {
177
- const builderPath = getBuilderPath(appName);
178
- const builderConfigPath = resolveApplicationConfigPath(builderPath);
179
- if (fsSync.existsSync(builderPath) && fsSync.existsSync(builderConfigPath)) {
180
- const { runAppTestIntegration } = require('../commands/app-test');
181
- const opts = { env: options.env || options.environment || 'dev' };
182
- await runAppTestIntegration(appName, opts);
183
- return true;
184
- }
210
+ if (hasExternalIntegration) {
211
+ return false;
185
212
  }
186
- return false;
213
+ return tryRunBuilderAppIntegration(appName, options);
187
214
  }
188
215
 
189
216
  /**
@@ -233,6 +260,17 @@ async function runTestIntegrationCommand(appName, options) {
233
260
  await runAppTestIntegration(appName, opts);
234
261
  return;
235
262
  }
263
+ // Role Assistant / EK / integration packages must use the dataplane path — do not
264
+ // probe builder/<app> (resolveApplicationConfigPath throws when the folder is absent).
265
+ if (
266
+ appType &&
267
+ (appType.baseDir === 'role-assistant' ||
268
+ appType.baseDir === 'enterprise-knowledge' ||
269
+ appType.baseDir === 'integration')
270
+ ) {
271
+ await runExternalSystemTestIntegration(appName, options);
272
+ return;
273
+ }
236
274
  const ranBuilder = await tryBuilderTestIntegration(appName, options);
237
275
  if (ranBuilder) {
238
276
  if (options.sync === true) {
@@ -31,6 +31,9 @@ const {
31
31
  getRestartableInfraServiceNames,
32
32
  buildRestartInfraHelpLines
33
33
  } = require('../constants/infra-compose-service-names');
34
+ const {
35
+ resolveTransientDeveloperIdFromOptions
36
+ } = require('../utils/transient-developer-id');
34
37
 
35
38
  const {
36
39
  computeEffectiveInfraOptionalFlags,
@@ -97,6 +100,18 @@ async function applyDeveloperIdFromUpInfra(options) {
97
100
  return id;
98
101
  }
99
102
 
103
+ const DOWN_INFRA_HELP_AFTER = `
104
+ Examples:
105
+ $ aifabrix down-infra
106
+ $ aifabrix down-infra --volumes
107
+ $ aifabrix down-infra --developer-id 02
108
+ $ aifabrix down-infra -d 02 --volumes
109
+ $ aifabrix down-infra myapp --developer-id 02
110
+
111
+ --developer-id / -d target another developer's stack for this command only.
112
+ They do not change developer-id in config.
113
+ `;
114
+
100
115
  /**
101
116
  * @param {Object} options
102
117
  * @param {Object} cfg - Mutable config from getConfig()
@@ -302,20 +317,32 @@ function setupUpBuilderApiCommand(program) {
302
317
  function setupDownInfraCommand(program) {
303
318
  program.command('down-infra [service|app]')
304
319
  .description('Stop all infra, or stop one app; use -v to remove volumes')
320
+ .addHelpText('after', DOWN_INFRA_HELP_AFTER)
305
321
  .option('-v, --volumes', 'Remove volumes (deletes all data)')
306
322
  .option('--verbose', 'Show full orchestration output (default is guided summary)')
323
+ .option(
324
+ '-d, --developer <id>',
325
+ 'Target developer ID for this stop only (does not change config)'
326
+ )
327
+ .option(
328
+ '--developer-id <id>',
329
+ 'Same as --developer; target another developer without changing config'
330
+ )
307
331
  .action(async(appName, options) => {
308
332
  try {
333
+ const developerId = resolveTransientDeveloperIdFromOptions(options);
334
+ const opts = { ...options, developerId };
309
335
  if (!options.verbose) {
310
- await runGuidedDownInfra(appName, options, infra, appLib);
336
+ await runGuidedDownInfra(appName, opts, infra, appLib);
311
337
  return;
312
338
  }
313
339
 
314
340
  if (typeof appName === 'string' && appName.trim().length > 0) {
315
- await appLib.downApp(appName, { volumes: !!options.volumes });
341
+ await appLib.downApp(appName, { volumes: !!options.volumes, developerId });
342
+ } else if (options.volumes) {
343
+ await infra.stopInfraWithVolumes(developerId);
316
344
  } else {
317
- if (options.volumes) await infra.stopInfraWithVolumes();
318
- else await infra.stopInfra();
345
+ await infra.stopInfra(developerId);
319
346
  }
320
347
  } catch (error) {
321
348
  handleCommandError(error, 'down-infra');
@@ -9,29 +9,45 @@
9
9
  const chalk = require('chalk');
10
10
  const logger = require('../utils/logger');
11
11
  const { formatBlockingError } = require('../utils/cli-test-layout-chalk');
12
- const { isChannelTargetKey } = require('../channels/channel-targets');
12
+ const {
13
+ formatSupportedChannelTargetsHelp,
14
+ isChannelTargetKey
15
+ } = require('../channels/channel-targets');
13
16
  const { addChannelCommand } = require('../channels/add-channel');
14
17
 
15
18
  const CHANNEL_ADD_HELP_AFTER = `
19
+ ${formatSupportedChannelTargetsHelp()}
20
+
21
+ One command covers scaffold → upload → install:
22
+ $ aifabrix channel add slack --name "Elsa" --probe
16
23
 
17
24
  Examples:
18
- # After upload/deploy of the communication external system
19
25
  $ aifabrix channel add slack --name "Elsa"
20
26
  $ aifabrix channel add teams-bot --name "Elsa" --description "Enterprise AI assistant for employees"
27
+ $ aifabrix channel add chatgpt --name "Your Assistant"
28
+ $ aifabrix channel add microsoft-copilot --name "Your Assistant" \\
29
+ --oauth-plugin-vault-reference-id "<Teams-Entra-SSO-auth-config-id>"
21
30
 
22
31
  # Slack slash-command prefix (registers /{prefix}-help in manifest)
23
32
  $ aifabrix channel add slack --name "Elsa" --command-prefix af
24
33
 
25
- # Generate artifacts only; no dataplane patch
34
+ # Generate artifacts only; no dataplane patch / upload
26
35
  $ aifabrix channel add slack --name "Elsa" --dry-run
27
36
 
28
- # Regenerate artifacts and patch config (does not rotate secrets)
37
+ # Re-upload communication system + regenerate channel config (does not rotate secrets)
29
38
  $ aifabrix channel add slack --name "Elsa" --force
30
39
 
31
40
  # Validate callbacks and assistant catalog after install
32
41
  $ aifabrix channel add slack --name "Elsa" --probe
33
42
 
34
- Prerequisites: aifabrix upload <target> (or deploy) and aifabrix login
43
+ Behavior:
44
+ - Scaffolds integration/<target>/ when missing (does not overwrite existing files)
45
+ - Uploads when the system is not on the dataplane; with --force uploads every time
46
+ - Then configures the channel install (artifacts, credentials, admin guide)
47
+ - microsoft-copilot uses Entra SSO (trustedIssuer); pass --oauth-plugin-vault-reference-id
48
+ from Teams Developer Portal. Does not create Fabrix oauth2 channel credentials.
49
+
50
+ Requires: aifabrix login
35
51
  `;
36
52
 
37
53
  /**
@@ -49,7 +65,7 @@ async function runChannelAddAction(channelTarget, options, meta = {}) {
49
65
  }
50
66
  if (!isChannelTargetKey(channelTarget)) {
51
67
  throw new Error(
52
- `Unknown channel target "${channelTarget}". Use teams-bot, slack, microsoft-copilot, or chatgpt.`
68
+ `Unknown channel target "${channelTarget}". ${formatSupportedChannelTargetsHelp()}`
53
69
  );
54
70
  }
55
71
  await addChannelCommand(channelTarget, {
@@ -58,6 +74,7 @@ async function runChannelAddAction(channelTarget, options, meta = {}) {
58
74
  icon: options.icon,
59
75
  template: options.template,
60
76
  commandPrefix: options.commandPrefix,
77
+ oauthPluginVaultReferenceId: options.oauthPluginVaultReferenceId,
61
78
  dryRun: options.dryRun === true,
62
79
  force: options.force === true,
63
80
  probe: options.probe === true
@@ -71,7 +88,7 @@ async function runChannelAddAction(channelTarget, options, meta = {}) {
71
88
  */
72
89
  function registerChannelAddCommand(parent, meta = {}) {
73
90
  const description =
74
- 'Configure channel install (manifest, credentials, admin guide) after upload/deploy';
91
+ 'Scaffold (if needed), upload (if needed/--force), then configure channel install';
75
92
  const commandName = meta.commandName || 'add <channelTarget>';
76
93
  const cmd = meta.hidden
77
94
  ? parent.command(commandName, { hidden: true })
@@ -82,12 +99,19 @@ function registerChannelAddCommand(parent, meta = {}) {
82
99
  .option('-d, --description <text>', 'Channel description (default generated)')
83
100
  .option('--icon <path>', 'Path to channel icon asset')
84
101
  .option('--template <name>', 'Template override (default from channel target)')
85
- .option('--dry-run', 'Generate artifacts only; do not patch external system or credentials')
86
- .option('--force', 'Regenerate artifacts and patch config (does not rotate secrets)')
102
+ .option('--dry-run', 'Generate artifacts only; do not upload, patch, or set credentials')
103
+ .option(
104
+ '--force',
105
+ 'Re-upload communication system and regenerate channel config (does not rotate secrets)'
106
+ )
87
107
  .option(
88
108
  '--command-prefix <key>',
89
109
  'Slack slash alias prefix (default af) — registers /{prefix}-help etc. in manifest'
90
110
  )
111
+ .option(
112
+ '--oauth-plugin-vault-reference-id <id>',
113
+ 'Teams Developer Portal Entra SSO OAuth auth-config id (microsoft-copilot)'
114
+ )
91
115
  .option('--probe', 'After install, validate callbacks and assistant catalog')
92
116
  .addHelpText('after', CHANNEL_ADD_HELP_AFTER)
93
117
  .action(async(channelTarget, options) => {
@@ -95,7 +119,8 @@ function registerChannelAddCommand(parent, meta = {}) {
95
119
  await runChannelAddAction(channelTarget, options, meta);
96
120
  } catch (error) {
97
121
  if (!error.message?.includes('Authentication required')) {
98
- logger.error(formatBlockingError('Channel install failed:'), error.message);
122
+ const detail = error.message || String(error);
123
+ logger.error(formatBlockingError(`Channel install failed:\n${detail}`));
99
124
  }
100
125
  process.exit(1);
101
126
  }