@aifabrix/builder 2.46.0 → 2.47.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 (175) hide show
  1. package/.npmrc.token +1 -0
  2. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
  3. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
  4. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
  5. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
  6. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
  7. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
  8. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
  9. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
  10. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
  11. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
  12. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
  13. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
  14. package/.temp/validation/summary.txt +5 -0
  15. package/jest.isolated-projects.js +18 -2
  16. package/jest.projects.js +24 -0
  17. package/lib/api/dataplane-health.api.js +19 -7
  18. package/lib/api/lifecycle.api.js +22 -1
  19. package/lib/api/resource-types.api.js +107 -0
  20. package/lib/api/runtime-configuration.api.js +72 -0
  21. package/lib/api/types/resource-types.types.js +45 -0
  22. package/lib/api/user-groups.api.js +9 -9
  23. package/lib/api/users.api.js +8 -8
  24. package/lib/api/wizard.api.js +10 -2
  25. package/lib/app/down.js +58 -26
  26. package/lib/app/index.js +2 -2
  27. package/lib/app/run-resolve-image.js +2 -1
  28. package/lib/app/run-stop-containers.js +113 -0
  29. package/lib/app/run.js +1 -31
  30. package/lib/app/show-display.js +35 -0
  31. package/lib/app/show.js +32 -1
  32. package/lib/build/index.js +17 -5
  33. package/lib/build/miso-controller-npm-sdk-guard.js +25 -0
  34. package/lib/channels/add-channel.js +348 -0
  35. package/lib/channels/channel-artifact-generator.js +141 -0
  36. package/lib/channels/channel-audit.js +101 -0
  37. package/lib/channels/channel-auth-context.js +37 -0
  38. package/lib/channels/channel-credential.js +200 -0
  39. package/lib/channels/channel-display-name.js +64 -0
  40. package/lib/channels/channel-external-system.js +106 -0
  41. package/lib/channels/channel-integration-generator.js +274 -0
  42. package/lib/channels/channel-probe.js +89 -0
  43. package/lib/channels/channel-runtime-resolver.js +152 -0
  44. package/lib/channels/channel-status-display.js +141 -0
  45. package/lib/channels/channel-targets.js +115 -0
  46. package/lib/cli/index.js +16 -4
  47. package/lib/cli/setup-app.js +54 -3
  48. package/lib/cli/setup-infra-up-dataplane-action.js +2 -2
  49. package/lib/cli/setup-infra-up-platform-action.js +2 -1
  50. package/lib/cli/setup-platform.js +1 -1
  51. package/lib/cli/setup-utility-help-after.js +4 -1
  52. package/lib/cli/setup-utility-resolve.js +8 -5
  53. package/lib/commands/app-logs.js +54 -26
  54. package/lib/commands/app.js +41 -0
  55. package/lib/commands/auth-status-dataplane-version.js +97 -61
  56. package/lib/commands/auth-status.js +2 -7
  57. package/lib/commands/channel.js +58 -0
  58. package/lib/commands/datasource.js +1 -1
  59. package/lib/commands/identity-apply-sync.js +49 -8
  60. package/lib/commands/identity-group-cli.js +66 -26
  61. package/lib/commands/identity-membership-role-cache.js +58 -37
  62. package/lib/commands/identity-shared.js +22 -1
  63. package/lib/commands/identity-user-cli.js +80 -34
  64. package/lib/commands/repair-auth-apply.js +30 -4
  65. package/lib/commands/repair-auth-config.js +8 -13
  66. package/lib/commands/repair-datasource-com-v2.js +15 -0
  67. package/lib/commands/repair-datasource-service-contract.js +206 -0
  68. package/lib/commands/repair-datasource.js +53 -18
  69. package/lib/commands/repair-env-template.js +12 -12
  70. package/lib/commands/repair-openapi-sync.js +26 -4
  71. package/lib/commands/repair-steps.js +28 -13
  72. package/lib/commands/repair-system-alignment.js +7 -4
  73. package/lib/commands/resource-type.js +371 -0
  74. package/lib/commands/setup-modes.js +1 -1
  75. package/lib/commands/setup-prompts-platform-mode.js +24 -21
  76. package/lib/commands/test-trust-external.js +39 -90
  77. package/lib/commands/up-common.js +33 -1
  78. package/lib/commands/up-dataplane.js +8 -4
  79. package/lib/commands/upload.js +24 -3
  80. package/lib/commands/verify-trust-command-action.js +4 -1
  81. package/lib/commands/verify-trust-external.js +12 -20
  82. package/lib/core/config-setup-platform-mode.js +3 -3
  83. package/lib/core/setup-platform-mode.js +41 -6
  84. package/lib/datasource/capability/capability-diff-slice.js +2 -2
  85. package/lib/datasource/capability/capability-manifest.js +254 -0
  86. package/lib/datasource/capability/capability-resolve.js +3 -4
  87. package/lib/datasource/capability/copy-operations.js +5 -3
  88. package/lib/datasource/capability/remove-operations.js +2 -1
  89. package/lib/datasource/capability/run-capability-copy.js +32 -4
  90. package/lib/datasource/capability/run-capability-diff.js +3 -3
  91. package/lib/datasource/capability/run-capability-dimension.js +2 -4
  92. package/lib/datasource/capability/run-capability-edit.js +4 -7
  93. package/lib/datasource/capability/run-capability-relate.js +2 -4
  94. package/lib/datasource/capability/run-capability-remove.js +2 -4
  95. package/lib/datasource/capability/validate-capability-slice.js +2 -1
  96. package/lib/datasource/datasource-identifier.js +2 -1
  97. package/lib/datasource/datasource-validate-display.js +1 -1
  98. package/lib/datasource/field-reference-validator.js +24 -3
  99. package/lib/datasource/orphan-profiles-validator.js +98 -0
  100. package/lib/datasource/validate.js +41 -23
  101. package/lib/external-system/download-helpers.js +87 -0
  102. package/lib/external-system/download.js +61 -122
  103. package/lib/external-system/generator.js +1 -59
  104. package/lib/external-system/test.js +10 -15
  105. package/lib/generator/builders.js +11 -1
  106. package/lib/generator/external-controller-manifest.js +5 -2
  107. package/lib/generator/split.js +3 -1
  108. package/lib/generator/wizard.js +1 -28
  109. package/lib/identity/fk-reference.js +65 -0
  110. package/lib/identity/identity-apply-core.js +13 -58
  111. package/lib/identity/identity-apply-phases.js +5 -1
  112. package/lib/identity/identity-display.js +243 -0
  113. package/lib/identity/identity-response-helpers.js +52 -0
  114. package/lib/identity/identity-sync-errors.js +62 -0
  115. package/lib/identity/resolve-user-id.js +129 -0
  116. package/lib/identity/username-default.js +31 -0
  117. package/lib/lifecycle/certification-progress.js +2 -2
  118. package/lib/lifecycle/report-display-trust.js +70 -0
  119. package/lib/lifecycle/report-display.js +14 -13
  120. package/lib/resolvers/datasource-resolver.js +2 -3
  121. package/lib/resolvers/resource-type-file.js +159 -0
  122. package/lib/schema/application-schema.json +16 -0
  123. package/lib/schema/deployment-rules.yaml +2 -1
  124. package/lib/schema/external-datasource.schema.json +372 -58
  125. package/lib/schema/external-system.schema.json +331 -2
  126. package/lib/schema/type/document-storage.json +9 -5
  127. package/lib/utils/app-run-containers.js +25 -2
  128. package/lib/utils/authentication-shape.js +224 -0
  129. package/lib/utils/build-copy.js +36 -5
  130. package/lib/utils/build-resolve-image.js +66 -12
  131. package/lib/utils/celery-companion-apps.js +53 -0
  132. package/lib/utils/cli-utils.js +4 -0
  133. package/lib/utils/compose-celery-worker.js +23 -0
  134. package/lib/utils/compose-generate-docker-compose.js +9 -1
  135. package/lib/utils/configuration-env-resolver.js +36 -0
  136. package/lib/utils/credential-secrets-env-kv.js +216 -0
  137. package/lib/utils/credential-secrets-env.js +52 -262
  138. package/lib/utils/dataplane-resolver.js +4 -115
  139. package/lib/utils/declarative-url-ports.js +24 -1
  140. package/lib/utils/docker-build.js +1 -4
  141. package/lib/utils/external-env-template.js +10 -3
  142. package/lib/utils/external-system-validators.js +8 -2
  143. package/lib/utils/help-builder.js +2 -0
  144. package/lib/utils/paths.js +25 -0
  145. package/lib/utils/platform-controller-url.js +2 -1
  146. package/lib/utils/platform-kv-ref.js +57 -0
  147. package/lib/utils/runtime-configuration-download.js +189 -0
  148. package/lib/utils/runtime-configuration-values.js +119 -0
  149. package/lib/utils/schema-loader.js +19 -4
  150. package/lib/utils/worker-sidecars.js +142 -0
  151. package/lib/validation/external-manifest-validator.js +2 -0
  152. package/lib/validation/runtime-configuration-rules.js +98 -0
  153. package/lib/validation/validate-display-dimensions.js +154 -0
  154. package/lib/validation/validate-display-sections.js +276 -0
  155. package/lib/validation/validate-display-steps.js +67 -0
  156. package/lib/validation/validate-display.js +24 -402
  157. package/package.json +1 -1
  158. package/templates/applications/dataplane/application.yaml +5 -0
  159. package/templates/applications/dataplane/env.template +26 -13
  160. package/templates/applications/dataplane/rbac.yaml +108 -1
  161. package/templates/applications/miso-controller/application.yaml +2 -2
  162. package/templates/applications/miso-controller/env.template +37 -122
  163. package/templates/applications/miso-controller/rbac.yaml +28 -13
  164. package/templates/channels/chatgpt/install.template.md.hbs +49 -0
  165. package/templates/channels/chatgpt/manifest.template.json.hbs +9 -0
  166. package/templates/channels/microsoft-copilot/install.template.md.hbs +49 -0
  167. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +8 -0
  168. package/templates/channels/slack/install.template.md.hbs +49 -0
  169. package/templates/channels/slack/manifest.template.json.hbs +66 -0
  170. package/templates/channels/teams-bot/install.template.md.hbs +49 -0
  171. package/templates/channels/teams-bot/manifest.template.json.hbs +13 -0
  172. package/templates/channels/web-embed/install.template.md.hbs +49 -0
  173. package/templates/channels/web-embed/manifest.template.json.hbs +8 -0
  174. package/templates/python/docker-compose.hbs +56 -0
  175. package/templates/typescript/docker-compose.hbs +52 -0
@@ -60,8 +60,8 @@ async function runResolveJsonOutput(appName, appPath, envOnly, envPath, options,
60
60
  */
61
61
  async function runResolveHumanOutput(appName, envPath, envOnly, options, environment) {
62
62
  logger.log(`✔ Generated .env file: ${envPath}`);
63
- if (options.fresh) {
64
- logger.log(chalk.gray(' Wrote full resolved content (--fresh; did not merge with an existing .env).'));
63
+ if (options.fresh || options.force) {
64
+ logger.log(chalk.gray(' Wrote full resolved content (replaced existing .env from env.template; no merge).'));
65
65
  }
66
66
  if (environment === 'local') {
67
67
  logger.log(chalk.gray(' Resolved for local use (host ports; no Docker port rewrite).'));
@@ -130,7 +130,7 @@ async function runResolveAppAction(appName, options) {
130
130
  skipOutputPath: false,
131
131
  preserveFromPath: null,
132
132
  preferLocalEnvOutputPath,
133
- freshEnv: options.fresh === true
133
+ freshEnv: options.fresh === true || options.force === true
134
134
  }
135
135
  );
136
136
  if (jsonMode) {
@@ -148,10 +148,13 @@ function setupResolveCommand(program) {
148
148
  program.command('resolve <appKey|systemKey>')
149
149
  .description('Generate .env from template (builder/<appKey>/ or integration/<systemKey>/); optional validate after')
150
150
  .addHelpText('after', RESOLVE_HELP_AFTER)
151
- .option('-f, --force', 'Generate missing secret keys in secrets file')
151
+ .option(
152
+ '-f, --force',
153
+ 'Seed missing secret keys and replace .env from env.template (no merge with existing .env or envOutputPath)'
154
+ )
152
155
  .option(
153
156
  '--fresh',
154
- 'Replace .env from env.template (no merge with existing .env or envOutputPath; same as deleting .env first)'
157
+ 'Same full .env replace as --force without seeding missing secret keys in the secrets file'
155
158
  )
156
159
  .option('--skip-validation', 'Skip file validation after generating .env')
157
160
  .option('--json', 'Print JSON summary (envPath, manifestSource, optional validation)')
@@ -13,6 +13,10 @@ const logger = require('../utils/logger');
13
13
  const config = require('../core/config');
14
14
  const containerHelpers = require('../utils/app-run-containers');
15
15
  const { validateAppName } = require('../app/push');
16
+ const {
17
+ getCeleryCompanionAppKeys,
18
+ loadBuilderApplicationConfig
19
+ } = require('../utils/celery-companion-apps');
16
20
 
17
21
  const { execWithDockerEnv } = require('../utils/docker-exec');
18
22
  const { maskEnvLine } = require('../utils/log-redaction');
@@ -295,6 +299,36 @@ async function runDockerLogsFollow(containerName, tail, minLevel) {
295
299
  });
296
300
  }
297
301
 
302
+ function parseLogLevelOption(options) {
303
+ const rawLevel =
304
+ options.level !== undefined && options.level !== null && options.level !== ''
305
+ ? String(options.level).trim()
306
+ : undefined;
307
+ const level = rawLevel ? rawLevel.toLowerCase() : undefined;
308
+ if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
309
+ throw new Error(
310
+ `Invalid log level '${rawLevel}'; use one of: ${LOG_LEVELS.join(', ')}`
311
+ );
312
+ }
313
+ return level;
314
+ }
315
+
316
+ async function logFollowStream(companionKeys, containerName, tail, level) {
317
+ await runDockerLogsFollow(containerName, tail, level);
318
+ if (companionKeys.length > 0) {
319
+ const hint = companionKeys.map((key) => `aifabrix logs ${key}`).join(' or ');
320
+ logger.log(chalk.gray(`Tip: sidecar logs — ${hint}`));
321
+ }
322
+ }
323
+
324
+ async function logContainerSnapshot(containerName, tail, level) {
325
+ try {
326
+ await runDockerLogs(containerName, { tail, level });
327
+ } catch (err) {
328
+ logger.log(chalk.yellow(`No logs for ${containerName}: ${err.message}`));
329
+ }
330
+ }
331
+
298
332
  /**
299
333
  * Run app logs command: optional env dump (masked), then docker logs
300
334
  * @async
@@ -307,39 +341,33 @@ async function runDockerLogsFollow(containerName, tail, minLevel) {
307
341
  */
308
342
  async function runAppLogs(appKey, options = {}) {
309
343
  validateAppName(appKey);
310
- const rawLevel =
311
- options.level !== undefined && options.level !== null && options.level !== ''
312
- ? String(options.level).trim()
313
- : undefined;
314
- const level = rawLevel ? rawLevel.toLowerCase() : undefined;
315
- if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
316
- throw new Error(
317
- `Invalid log level '${rawLevel}'; use one of: ${LOG_LEVELS.join(', ')}`
318
- );
319
- }
320
-
344
+ const level = parseLogLevelOption(options);
321
345
  const developerId = await config.getDeveloperId();
322
- const containerName = containerHelpers.getContainerName(appKey, developerId);
323
-
324
346
  const follow = !!options.follow;
325
347
  const tail = typeof options.tail === 'number' ? options.tail : DEFAULT_TAIL_LINES;
348
+ const appConfig = loadBuilderApplicationConfig(appKey);
349
+ const companionKeys = getCeleryCompanionAppKeys(appKey, appConfig);
350
+ const appKeysToLog = follow ? [appKey] : [appKey, ...companionKeys];
326
351
 
327
- logger.log(chalk.blue(`Container: ${containerName}\n`));
352
+ for (const key of appKeysToLog) {
353
+ const containerName = containerHelpers.getContainerName(key, developerId);
328
354
 
329
- if (!follow) {
330
- await dumpMaskedEnv(containerName);
331
- }
355
+ logger.log(chalk.blue(`Container: ${containerName}\n`));
332
356
 
333
- if (follow) {
334
- await runDockerLogsFollow(containerName, tail, level);
335
- return;
336
- }
357
+ if (!follow) {
358
+ await dumpMaskedEnv(containerName);
359
+ }
337
360
 
338
- try {
339
- await runDockerLogs(containerName, { tail, level });
340
- } catch (err) {
341
- logger.log(chalk.red(`Error: ${err.message}`));
342
- throw new Error(`Failed to show logs: ${err.message}`);
361
+ if (follow) {
362
+ await logFollowStream(companionKeys, containerName, tail, level);
363
+ return;
364
+ }
365
+
366
+ await logContainerSnapshot(containerName, tail, level);
367
+
368
+ if (key !== appKeysToLog[appKeysToLog.length - 1]) {
369
+ logger.log('');
370
+ }
343
371
  }
344
372
  }
345
373
 
@@ -17,6 +17,8 @@ const { registerApplication } = require('../app/register');
17
17
  const { rotateSecret } = require('../app/rotate-secret');
18
18
  const { showApp } = require('../app/show');
19
19
  const { runAppDeploymentList } = require('./deployment-list');
20
+ const { isChannelTargetKey } = require('../channels/channel-targets');
21
+ const { addChannelCommand } = require('../channels/add-channel');
20
22
 
21
23
  const APP_HELP_AFTER = `
22
24
  Subcommands:
@@ -25,6 +27,7 @@ Subcommands:
25
27
  rotate-secret <key> New ClientSecret (shown once)
26
28
  show <key> From controller (like show --online)
27
29
  deployment <key> Deployment history for app
30
+ add-channel <target> Configure assistant channel install (after deploy)
28
31
 
29
32
  Requires: aifabrix login
30
33
  `;
@@ -118,6 +121,43 @@ function setupAppDeploymentCommand(app) {
118
121
  });
119
122
  }
120
123
 
124
+ function setupAppAddChannelCommand(app) {
125
+ app.command('add-channel <channelTarget>')
126
+ .description(
127
+ 'Configure assistant channel install (teams-bot, slack, …) after aifabrix deploy <target>'
128
+ )
129
+ .requiredOption('-n, --name <name>', 'Customer-visible channel name')
130
+ .option('-d, --description <text>', 'Channel description (default generated)')
131
+ .option('--icon <path>', 'Path to channel icon asset')
132
+ .option('--template <name>', 'Template override (default from channel target)')
133
+ .option('--dry-run', 'Generate artifacts only; do not patch external system or credentials')
134
+ .option('--force', 'Regenerate artifacts and patch config (does not rotate secrets)')
135
+ .option('--probe', 'After install, validate callbacks and assistant catalog')
136
+ .action(async(channelTarget, options) => {
137
+ try {
138
+ if (!isChannelTargetKey(channelTarget)) {
139
+ throw new Error(
140
+ `Unknown channel target "${channelTarget}". Use teams-bot, slack, microsoft-copilot, or chatgpt.`
141
+ );
142
+ }
143
+ await addChannelCommand(channelTarget, {
144
+ name: options.name,
145
+ description: options.description,
146
+ icon: options.icon,
147
+ template: options.template,
148
+ dryRun: options.dryRun === true,
149
+ force: options.force === true,
150
+ probe: options.probe === true
151
+ });
152
+ } catch (error) {
153
+ if (!error.message?.includes('Authentication required')) {
154
+ logger.error(formatBlockingError('Channel install failed:'), error.message);
155
+ }
156
+ process.exit(1);
157
+ }
158
+ });
159
+ }
160
+
121
161
  /**
122
162
  * Setup application management commands
123
163
  * @param {Command} program - Commander program instance
@@ -129,6 +169,7 @@ function setupAppCommands(program) {
129
169
  setupAppRotateSecretCommand(app);
130
170
  setupAppShowCommand(app);
131
171
  setupAppDeploymentCommand(app);
172
+ setupAppAddChannelCommand(app);
132
173
  }
133
174
 
134
175
  module.exports = { setupAppCommands };
@@ -17,7 +17,8 @@
17
17
  const { fetchDataplaneGeneralHealth } = require('../api/dataplane-health.api');
18
18
  const {
19
19
  updateDeviceDataplaneVersions,
20
- getDeviceDataplaneVersions
20
+ getDeviceDataplaneVersions,
21
+ isCacheStale
21
22
  } = require('../core/config-device-dataplane');
22
23
  const {
23
24
  getInstalledCliVersion
@@ -25,10 +26,10 @@ const {
25
26
  const { isAtLeast, isValidSemver } = require('../utils/semver-compare');
26
27
 
27
28
  /** Max wall time for post-login version cache refresh (silent after success banner). */
28
- const LOGIN_POST_REFRESH_BUDGET_MS = 12000;
29
+ const LOGIN_POST_REFRESH_BUDGET_MS = 8000;
29
30
 
30
- /** Per health probe during login refresh (two endpoints may be tried). */
31
- const LOGIN_HEALTH_PROBE_TIMEOUT_MS = 6000;
31
+ /** Per health probe during login refresh (legacy /health first, then v1). */
32
+ const LOGIN_HEALTH_PROBE_TIMEOUT_MS = 3000;
32
33
 
33
34
  /**
34
35
  * Resolve when `promise` settles or when `timeoutMs` elapses (returns undefined).
@@ -79,29 +80,32 @@ async function safeReadCache(controllerUrl) {
79
80
  }
80
81
 
81
82
  /**
82
- * Refresh dataplane health, persist `device.<controllerUrl>` version cache,
83
- * and derive the compatibility verdict against the installed Builder CLI.
84
- * Falls back to the cached entry when the live probe fails so `auth status`
85
- * still shows the last-known values offline.
86
- *
87
- * @async
88
- * @param {string} controllerUrl - Controller URL (cache key)
89
- * @param {string} dataplaneUrl - Dataplane base URL for the health probe
90
- * @returns {Promise<{
91
- * dataplaneVersion: string|undefined,
92
- * minBuilderCliVersion: string|undefined,
93
- * cliVersion: string,
94
- * compatible: boolean
95
- * }>}
83
+ * Build optional fetch args for {@link fetchDataplaneGeneralHealth}.
84
+ * @param {Object} [options]
85
+ * @returns {Object}
96
86
  */
97
- async function refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl, options = {}) {
98
- const cliVersion = getInstalledCliVersion();
99
- let dataplaneVersion;
100
- let minBuilderCliVersion;
87
+ function buildHealthFetchOptions(options = {}) {
101
88
  const healthOpts = {};
102
89
  if (options.healthTimeoutMs !== undefined && options.healthTimeoutMs !== null) {
103
90
  healthOpts.timeoutMs = options.healthTimeoutMs;
104
91
  }
92
+ if (options.endpointOrder) {
93
+ healthOpts.endpointOrder = options.endpointOrder;
94
+ }
95
+ return healthOpts;
96
+ }
97
+
98
+ /**
99
+ * Probe dataplane health and persist version fields when available.
100
+ * @async
101
+ * @param {string} controllerUrl
102
+ * @param {string} dataplaneUrl
103
+ * @param {Object} healthOpts
104
+ * @returns {Promise<{ dataplaneVersion: string|undefined, minBuilderCliVersion: string|undefined }>}
105
+ */
106
+ async function fetchAndPersistHealthSnapshot(controllerUrl, dataplaneUrl, healthOpts) {
107
+ let dataplaneVersion;
108
+ let minBuilderCliVersion;
105
109
 
106
110
  try {
107
111
  const snapshot = await fetchDataplaneGeneralHealth(dataplaneUrl, healthOpts);
@@ -121,14 +125,63 @@ async function refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl, options
121
125
  // ignore — fall through to cached values
122
126
  }
123
127
 
124
- if (!dataplaneVersion && !minBuilderCliVersion) {
125
- const cached = await safeReadCache(controllerUrl);
126
- if (cached) {
127
- dataplaneVersion = cached.version;
128
- minBuilderCliVersion = cached.minBuilderCliVersion;
129
- }
128
+ return { dataplaneVersion, minBuilderCliVersion };
129
+ }
130
+
131
+ /**
132
+ * Fill missing version fields from the device cache when live probe failed.
133
+ * @async
134
+ * @param {string} controllerUrl
135
+ * @param {string|undefined} dataplaneVersion
136
+ * @param {string|undefined} minBuilderCliVersion
137
+ * @returns {Promise<{ dataplaneVersion: string|undefined, minBuilderCliVersion: string|undefined }>}
138
+ */
139
+ async function resolveVersionFieldsFromCache(controllerUrl, dataplaneVersion, minBuilderCliVersion) {
140
+ if (dataplaneVersion || minBuilderCliVersion) {
141
+ return { dataplaneVersion, minBuilderCliVersion };
130
142
  }
131
143
 
144
+ const cached = await safeReadCache(controllerUrl);
145
+ if (!cached) {
146
+ return { dataplaneVersion, minBuilderCliVersion };
147
+ }
148
+
149
+ return {
150
+ dataplaneVersion: cached.version,
151
+ minBuilderCliVersion: cached.minBuilderCliVersion
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Refresh dataplane health, persist `device.<controllerUrl>` version cache,
157
+ * and derive the compatibility verdict against the installed Builder CLI.
158
+ * Falls back to the cached entry when the live probe fails so `auth status`
159
+ * still shows the last-known values offline.
160
+ *
161
+ * @async
162
+ * @param {string} controllerUrl - Controller URL (cache key)
163
+ * @param {string} dataplaneUrl - Dataplane base URL for the health probe
164
+ * @returns {Promise<{
165
+ * dataplaneVersion: string|undefined,
166
+ * minBuilderCliVersion: string|undefined,
167
+ * cliVersion: string,
168
+ * compatible: boolean
169
+ * }>}
170
+ */
171
+ async function refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl, options = {}) {
172
+ const cliVersion = getInstalledCliVersion();
173
+ const healthOpts = buildHealthFetchOptions(options);
174
+ let { dataplaneVersion, minBuilderCliVersion } = await fetchAndPersistHealthSnapshot(
175
+ controllerUrl,
176
+ dataplaneUrl,
177
+ healthOpts
178
+ );
179
+ ({ dataplaneVersion, minBuilderCliVersion } = await resolveVersionFieldsFromCache(
180
+ controllerUrl,
181
+ dataplaneVersion,
182
+ minBuilderCliVersion
183
+ ));
184
+
132
185
  const compatible = computeCompatibility(cliVersion, minBuilderCliVersion);
133
186
  return { dataplaneVersion, minBuilderCliVersion, cliVersion, compatible };
134
187
  }
@@ -184,33 +237,6 @@ async function resolveAuthConfigAfterLogin(controllerUrl, environment, loginOpti
184
237
  }
185
238
  }
186
239
 
187
- /**
188
- * Resolve dataplane URL after login (mirrors auth-status silent resolver).
189
- * @private
190
- * @async
191
- * @param {string} controllerUrl
192
- * @param {string} environment
193
- * @param {Object} authConfig
194
- * @returns {Promise<string|null>}
195
- */
196
- async function resolveDataplaneUrlAfterLogin(controllerUrl, environment, authConfig) {
197
- const { findDataplaneServiceAppKey } = require('./wizard-dataplane');
198
- const { getDataplaneUrl } = require('../datasource/deploy');
199
- try {
200
- const dataplaneAppKey = await findDataplaneServiceAppKey(
201
- controllerUrl,
202
- environment,
203
- authConfig
204
- );
205
- if (dataplaneAppKey) {
206
- return await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
207
- }
208
- return await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
209
- } catch (_resolveError) {
210
- return null;
211
- }
212
- }
213
-
214
240
  /**
215
241
  * Best-effort health refresh after login when dataplane URL is discoverable.
216
242
  * Never throws — cache failures must not block login success.
@@ -222,7 +248,13 @@ async function resolveDataplaneUrlAfterLogin(controllerUrl, environment, authCon
222
248
  * @returns {Promise<void>}
223
249
  */
224
250
  async function tryRefreshDataplaneVersionAfterLoginWork(controllerUrl, loginOptions = {}) {
251
+ const cached = await safeReadCache(controllerUrl);
252
+ if (cached && !isCacheStale(cached)) {
253
+ return;
254
+ }
255
+
225
256
  const { resolveEnvironment } = require('../core/config');
257
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
226
258
  const environment = await resolveEnvironment();
227
259
  const authConfig = await resolveAuthConfigAfterLogin(
228
260
  controllerUrl,
@@ -232,14 +264,18 @@ async function tryRefreshDataplaneVersionAfterLoginWork(controllerUrl, loginOpti
232
264
  if (!authConfig) {
233
265
  return;
234
266
  }
235
- const dataplaneUrl = await resolveDataplaneUrlAfterLogin(
236
- controllerUrl,
237
- environment,
238
- authConfig
239
- );
267
+ let dataplaneUrl = null;
268
+ try {
269
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig, {
270
+ silent: true
271
+ });
272
+ } catch (_resolveError) {
273
+ return;
274
+ }
240
275
  if (dataplaneUrl) {
241
276
  await refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl, {
242
- healthTimeoutMs: LOGIN_HEALTH_PROBE_TIMEOUT_MS
277
+ healthTimeoutMs: LOGIN_HEALTH_PROBE_TIMEOUT_MS,
278
+ endpointOrder: 'legacy-first'
243
279
  });
244
280
  }
245
281
  }
@@ -14,8 +14,7 @@ const logger = require('../utils/logger');
14
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
15
15
  const { getAuthUser } = require('../api/auth.api');
16
16
  const { resolveControllerUrl } = require('../utils/controller-url');
17
- const { findDataplaneServiceAppKey } = require('./wizard-dataplane');
18
- const { getDataplaneUrl } = require('../datasource/deploy');
17
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
19
18
  const { checkDataplaneHealth } = require('../utils/dataplane-health');
20
19
  const {
21
20
  displayDataplaneVersionSection,
@@ -211,11 +210,7 @@ function displayTokenInfo(tokenInfo) {
211
210
  */
212
211
  async function resolveDataplaneUrlSilent(controllerUrl, environment, authConfig) {
213
212
  try {
214
- const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
215
- if (dataplaneAppKey) {
216
- return await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
217
- }
218
- return await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
213
+ return await resolveDataplaneUrl(controllerUrl, environment, authConfig, { silent: true });
219
214
  } catch {
220
215
  return null;
221
216
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Channel CLI commands (plan 152).
3
+ *
4
+ * @fileoverview Scaffold communication external systems for channel deploy step 1
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const { formatSuccessLine, formatBlockingError, sectionTitle, metadata } = require('../utils/cli-test-layout-chalk');
14
+ const { CHANNEL_TARGET_KEYS, isChannelTargetKey } = require('../channels/channel-targets');
15
+ const { scaffoldChannelIntegration } = require('../channels/channel-integration-generator');
16
+
17
+ /**
18
+ * @param {import('commander').Command} program
19
+ */
20
+ function setupChannelCommands(program) {
21
+ const channel = program
22
+ .command('channel')
23
+ .description('Assistant channel integration scaffolds (plan 152)');
24
+
25
+ channel
26
+ .command('scaffold <channelTarget>')
27
+ .description('Generate integration/<target>/ communication external system for deploy step 1')
28
+ .option('--force', 'Overwrite generated scaffold files when folder exists')
29
+ .option('--allow-experimental', 'Allow web-embed scaffold')
30
+ .action(async(channelTarget, options) => {
31
+ try {
32
+ if (!isChannelTargetKey(channelTarget)) {
33
+ throw new Error(
34
+ `Unknown channel target "${channelTarget}". Expected: ${CHANNEL_TARGET_KEYS.join(', ')}`
35
+ );
36
+ }
37
+ const result = scaffoldChannelIntegration(channelTarget, {
38
+ force: options.force === true,
39
+ allowExperimental: options.allowExperimental === true
40
+ });
41
+ logger.log(sectionTitle(`${channelTarget} channel scaffold`));
42
+ logger.log('');
43
+ logger.log(formatSuccessLine(`Created integration at ${result.integrationDir}`));
44
+ for (const file of result.files) {
45
+ logger.log(metadata(file));
46
+ }
47
+ logger.log('');
48
+ logger.log(chalk.white('Next steps:'));
49
+ logger.log(chalk.gray(` aifabrix deploy ${channelTarget}`));
50
+ logger.log(chalk.gray(` aifabrix app add-channel ${channelTarget} --name "Your Assistant"`));
51
+ } catch (error) {
52
+ logger.error(formatBlockingError(error.message || String(error)));
53
+ process.exit(1);
54
+ }
55
+ });
56
+ }
57
+
58
+ module.exports = { setupChannelCommands };
@@ -97,7 +97,7 @@ Omit [prefix] to list all datasources for the active environment.
97
97
 
98
98
  function setupDatasourceValidateCommand(datasource) {
99
99
  datasource.command('validate <file-or-key>')
100
- .description('Validate datasource JSON (file path or datasource key under integration/<app>/)')
100
+ .description('Validate datasource YAML/JSON (file path or datasource key under integration/<app>/)')
101
101
  .addHelpText('after', DATASOURCE_VALIDATE_HELP_AFTER)
102
102
  .action(async(fileOrKey) => {
103
103
  try {
@@ -7,14 +7,15 @@
7
7
  'use strict';
8
8
 
9
9
  const logger = require('../utils/logger');
10
- const { formatBlockingError, formatSuccessLine } = require('../utils/cli-layout-chalk');
10
+ const { formatSuccessLine, headerKeyValue } = require('../utils/cli-layout-chalk');
11
11
  const {
12
12
  resolveControllerAndAuth,
13
13
  resolveEnvKey,
14
14
  requireNonEmpty,
15
15
  unwrapControllerData,
16
16
  throwIfApiFailed,
17
- assertSyncStatsOk
17
+ assertSyncStatsOk,
18
+ logIdentityCliError
18
19
  } = require('./identity-shared');
19
20
  const { clearAuthCache } = require('../api/auth-cache.api');
20
21
  const { fullSyncToDataplane } = require('../api/dataplane-sync.api');
@@ -70,7 +71,7 @@ function setupIdentityApplyCommand(identityCmd) {
70
71
  );
71
72
  }
72
73
  } catch (e) {
73
- logger.error(formatBlockingError(e.message));
74
+ logIdentityCliError(e.message);
74
75
  process.exit(1);
75
76
  }
76
77
  });
@@ -103,6 +104,46 @@ async function maybePurgeCache(controllerUrl, authConfig, purgeCache) {
103
104
  logger.log(formatSuccessLine('Controller cache cleared'));
104
105
  }
105
106
 
107
+ /**
108
+ * @param {string} controllerUrl
109
+ * @param {string} envKey
110
+ * @param {Object} authConfig
111
+ * @returns {Promise<string>}
112
+ */
113
+ async function resolveSyncDataplaneUrl(controllerUrl, envKey, authConfig) {
114
+ try {
115
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
116
+ return await resolveDataplaneUrl(controllerUrl, envKey, authConfig, { silent: true });
117
+ } catch {
118
+ return '—';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * @param {Object} params
124
+ * @param {string} params.controllerUrl
125
+ * @param {string} params.envKey
126
+ * @param {Object} params.authConfig
127
+ * @param {Object|null|undefined} params.stats
128
+ */
129
+ async function printIdentitySyncSuccess({ controllerUrl, envKey, authConfig, stats }) {
130
+ const dataplaneUrl = await resolveSyncDataplaneUrl(controllerUrl, envKey, authConfig);
131
+ const usersProcessed = Number(stats?.usersProcessed ?? 0);
132
+ const groupsProcessed = Number(stats?.groupsProcessed ?? 0);
133
+ const syncErrors = Number(stats?.errors ?? 0);
134
+
135
+ logger.log('');
136
+ logger.log(formatSuccessLine(`Identity synced to dataplane (${envKey})`));
137
+ logger.log(headerKeyValue(' Controller:', controllerUrl));
138
+ logger.log(headerKeyValue(' Dataplane:', dataplaneUrl));
139
+ logger.log('');
140
+ logger.log(` Users: ${usersProcessed} processed`);
141
+ logger.log(` Groups: ${groupsProcessed} processed`);
142
+ if (syncErrors > 0) {
143
+ logger.log(` Errors: ${syncErrors}`);
144
+ }
145
+ }
146
+
106
147
  async function runIdentitySyncAction(options) {
107
148
  try {
108
149
  const { controllerUrl, authConfig } = await resolveControllerAndAuth();
@@ -116,16 +157,16 @@ async function runIdentitySyncAction(options) {
116
157
  printJson(stats);
117
158
  return;
118
159
  }
119
- logger.log(formatSuccessLine(`Identity synced to dataplane (${envKey})`));
120
- logger.log(` usersProcessed=${stats?.usersProcessed ?? 0}`);
121
- logger.log(` groupsProcessed=${stats?.groupsProcessed ?? 0}`);
160
+ await printIdentitySyncSuccess({ controllerUrl, envKey, authConfig, stats });
122
161
  } catch (e) {
123
- logger.error(formatBlockingError(e.message));
162
+ logIdentityCliError(e.message);
124
163
  process.exit(1);
125
164
  }
126
165
  }
127
166
 
128
167
  module.exports = {
129
168
  setupIdentityApplyCommand,
130
- setupIdentitySyncCommand
169
+ setupIdentitySyncCommand,
170
+ printIdentitySyncSuccess,
171
+ resolveSyncDataplaneUrl
131
172
  };