@aifabrix/builder 2.38.0 → 2.39.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 (49) hide show
  1. package/.cursor/rules/project-rules.mdc +3 -0
  2. package/integration/hubspot/hubspot-deploy.json +0 -3
  3. package/integration/hubspot/hubspot-system.json +0 -3
  4. package/lib/api/applications.api.js +8 -2
  5. package/lib/api/auth.api.js +14 -0
  6. package/lib/api/credentials.api.js +1 -1
  7. package/lib/api/datasources-core.api.js +16 -1
  8. package/lib/api/datasources-extended.api.js +18 -1
  9. package/lib/api/deployments.api.js +6 -1
  10. package/lib/api/environments.api.js +11 -0
  11. package/lib/api/external-systems.api.js +16 -1
  12. package/lib/api/pipeline.api.js +12 -4
  13. package/lib/api/service-users.api.js +41 -0
  14. package/lib/api/types/service-users.types.js +24 -0
  15. package/lib/api/wizard.api.js +19 -0
  16. package/lib/app/deploy-status-display.js +78 -0
  17. package/lib/app/deploy.js +66 -21
  18. package/lib/app/rotate-secret.js +3 -1
  19. package/lib/app/run-helpers.js +7 -2
  20. package/lib/app/show-display.js +30 -11
  21. package/lib/app/show.js +34 -8
  22. package/lib/cli/index.js +2 -0
  23. package/lib/cli/setup-app.js +8 -0
  24. package/lib/cli/setup-infra.js +3 -3
  25. package/lib/cli/setup-service-user.js +61 -0
  26. package/lib/commands/app.js +2 -1
  27. package/lib/commands/service-user.js +199 -0
  28. package/lib/commands/up-common.js +74 -5
  29. package/lib/commands/up-dataplane.js +13 -7
  30. package/lib/commands/up-miso.js +17 -24
  31. package/lib/core/templates.js +0 -1
  32. package/lib/external-system/deploy.js +79 -15
  33. package/lib/generator/builders.js +0 -24
  34. package/lib/schema/application-schema.json +0 -12
  35. package/lib/schema/external-system.schema.json +0 -16
  36. package/lib/utils/app-register-config.js +10 -12
  37. package/lib/utils/deployment-errors.js +10 -0
  38. package/lib/utils/environment-checker.js +25 -6
  39. package/lib/utils/variable-transformer.js +6 -14
  40. package/package.json +1 -1
  41. package/templates/applications/dataplane/README.md +23 -7
  42. package/templates/applications/dataplane/env.template +31 -2
  43. package/templates/applications/dataplane/rbac.yaml +1 -1
  44. package/templates/applications/dataplane/variables.yaml +2 -1
  45. package/templates/applications/keycloak/env.template +6 -3
  46. package/templates/applications/keycloak/variables.yaml +1 -0
  47. package/templates/applications/miso-controller/env.template +22 -15
  48. package/templates/applications/miso-controller/rbac.yaml +15 -0
  49. package/templates/applications/miso-controller/variables.yaml +24 -23
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AI Fabrix Builder - Up Miso Command
3
3
  *
4
- * Installs keycloak, miso-controller, and dataplane from images (no build).
4
+ * Installs keycloak and miso-controller from images (no build). For dataplane, use up-dataplane.
5
5
  * Assumes infra is up; sets dev secrets and resolves (no force; existing .env values preserved).
6
6
  *
7
7
  * @fileoverview up-miso command implementation
@@ -19,19 +19,16 @@ const secrets = require('../core/secrets');
19
19
  const infra = require('../infrastructure');
20
20
  const app = require('../app');
21
21
  const { saveLocalSecret } = require('../utils/local-secrets');
22
- const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly } = require('./up-common');
22
+ const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly, validateEnvOutputPathFolderOrNull } = require('./up-common');
23
23
 
24
24
  /** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
25
25
  const KEYCLOAK_BASE_PORT = 8082;
26
26
  /** Miso controller base port (dev-config app base) */
27
27
  const MISO_BASE_PORT = 3000;
28
- /** Dataplane base port (from templates/applications/dataplane/variables.yaml) */
29
- const _DATAPLANE_BASE_PORT = 3001;
30
-
31
28
  /**
32
- * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string, dataplane?: string }
33
- * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1', 'dataplane=reg/d:v1']
34
- * @returns {{ keycloak?: string, 'miso-controller'?: string, dataplane?: string }}
29
+ * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
30
+ * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1']
31
+ * @returns {{ keycloak?: string, 'miso-controller'?: string }}
35
32
  */
36
33
  function parseImageOptions(imageOpts) {
37
34
  const map = {};
@@ -50,7 +47,7 @@ function parseImageOptions(imageOpts) {
50
47
 
51
48
  /**
52
49
  * Build full image ref from registry and app variables (registry/name:tag)
53
- * @param {string} appName - keycloak, miso-controller, or dataplane
50
+ * @param {string} appName - keycloak or miso-controller
54
51
  * @param {string} registry - Registry URL
55
52
  * @returns {string} Full image reference
56
53
  */
@@ -67,7 +64,7 @@ function buildImageRefFromRegistry(appName, registry) {
67
64
  }
68
65
 
69
66
  /**
70
- * Set URL secrets and resolve keycloak + miso-controller + dataplane (no force; existing .env preserved)
67
+ * Set URL secrets and resolve keycloak + miso-controller (no force; existing .env preserved)
71
68
  * @async
72
69
  * @param {number} devIdNum - Developer ID number
73
70
  */
@@ -79,12 +76,11 @@ async function setMisoSecretsAndResolve(devIdNum) {
79
76
  logger.log(chalk.green('✓ Set keycloak and miso-controller URL secrets'));
80
77
  await secrets.generateEnvFile('keycloak', undefined, 'docker', false, true);
81
78
  await secrets.generateEnvFile('miso-controller', undefined, 'docker', false, true);
82
- await secrets.generateEnvFile('dataplane', undefined, 'docker', false, true);
83
- logger.log(chalk.green('✓ Resolved keycloak, miso-controller, and dataplane'));
79
+ logger.log(chalk.green(' Resolved keycloak and miso-controller'));
84
80
  }
85
81
 
86
82
  /**
87
- * Build run options and run keycloak, miso-controller, then dataplane
83
+ * Build run options and run keycloak, then miso-controller
88
84
  * @async
89
85
  * @param {Object} options - Commander options (image, registry, registryMode)
90
86
  */
@@ -92,27 +88,23 @@ async function runMisoApps(options) {
92
88
  const imageMap = parseImageOptions(options.image);
93
89
  const keycloakImage = imageMap.keycloak || (options.registry ? buildImageRefFromRegistry('keycloak', options.registry) : undefined);
94
90
  const misoImage = imageMap['miso-controller'] || (options.registry ? buildImageRefFromRegistry('miso-controller', options.registry) : undefined);
95
- const dataplaneImage = imageMap.dataplane || (options.registry ? buildImageRefFromRegistry('dataplane', options.registry) : undefined);
96
91
  const keycloakRunOpts = { image: keycloakImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
97
92
  const misoRunOpts = { image: misoImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
98
- const dataplaneRunOpts = { image: dataplaneImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
99
93
  logger.log(chalk.blue('Starting keycloak...'));
100
94
  await app.runApp('keycloak', keycloakRunOpts);
101
95
  logger.log(chalk.blue('Starting miso-controller...'));
102
96
  await app.runApp('miso-controller', misoRunOpts);
103
- logger.log(chalk.blue('Starting dataplane...'));
104
- await app.runApp('dataplane', dataplaneRunOpts);
105
97
  }
106
98
 
107
99
  /**
108
- * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak, miso-controller, then dataplane.
100
+ * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak and miso-controller.
109
101
  *
110
102
  * @async
111
103
  * @function handleUpMiso
112
104
  * @param {Object} options - Commander options
113
105
  * @param {string} [options.registry] - Override registry for all apps
114
106
  * @param {string} [options.registryMode] - Override registry mode (acr|external)
115
- * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1, miso-controller=reg/m:v1, dataplane=reg/d:v1
107
+ * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1, miso-controller=reg/m:v1
116
108
  * @returns {Promise<void>}
117
109
  * @throws {Error} If infra not up or any step fails
118
110
  */
@@ -121,7 +113,7 @@ async function handleUpMiso(options = {}) {
121
113
  if (builderDir) {
122
114
  process.env.AIFABRIX_BUILDER_DIR = builderDir;
123
115
  }
124
- logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller + dataplane from images)...\n'));
116
+ logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
125
117
  // Strict: only this developer's infra (same as status), so up-miso and status agree
126
118
  const health = await infra.checkInfraHealth(undefined, { strict: true });
127
119
  const allHealthy = Object.values(health).every(status => status === 'healthy');
@@ -131,17 +123,18 @@ async function handleUpMiso(options = {}) {
131
123
  logger.log(chalk.green('✓ Infrastructure is up'));
132
124
  await ensureAppFromTemplate('keycloak');
133
125
  await ensureAppFromTemplate('miso-controller');
134
- await ensureAppFromTemplate('dataplane');
126
+ // If envOutputPath target folder does not exist, set envOutputPath to null
127
+ validateEnvOutputPathFolderOrNull('keycloak');
128
+ validateEnvOutputPathFolderOrNull('miso-controller');
135
129
  // Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
136
130
  patchEnvOutputPathForDeployOnly('keycloak');
137
131
  patchEnvOutputPathForDeployOnly('miso-controller');
138
- patchEnvOutputPathForDeployOnly('dataplane');
139
132
  const developerId = await config.getDeveloperId();
140
133
  const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
141
134
  await setMisoSecretsAndResolve(devIdNum);
142
135
  await runMisoApps(options);
143
- logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.') +
144
- chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed.'));
136
+ logger.log(chalk.green('\n✓ up-miso complete. Keycloak and miso-controller are running.') +
137
+ chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed. Use \'aifabrix up-dataplane\' for dataplane.'));
145
138
  }
146
139
 
147
140
  module.exports = { handleUpMiso, parseImageOptions };
@@ -35,7 +35,6 @@ function generateExternalSystemVariables(appName, displayName, config) {
35
35
  type: 'external'
36
36
  },
37
37
  deployment: {
38
- controllerUrl: '',
39
38
  environment: 'dev'
40
39
  },
41
40
  externalIntegration: {
@@ -13,10 +13,87 @@ const chalk = require('chalk');
13
13
  const { getDeploymentAuth } = require('../utils/token-manager');
14
14
  const logger = require('../utils/logger');
15
15
  const { resolveControllerUrl } = require('../utils/controller-url');
16
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
17
+ const { getExternalSystem } = require('../api/external-systems.api');
16
18
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
17
19
  const { validateExternalSystemComplete } = require('../validation/validate');
18
20
  const { displayValidationResults } = require('../validation/validate-display');
19
21
 
22
+ /**
23
+ * Displays API and MCP documentation URLs from dataplane when available
24
+ * @async
25
+ * @function displayDeploymentDocs
26
+ * @param {string} controllerUrl - Controller base URL
27
+ * @param {string} environment - Environment key
28
+ * @param {Object} authConfig - Authentication configuration
29
+ * @param {string} systemKey - External system key
30
+ */
31
+ async function displayDeploymentDocs(controllerUrl, environment, authConfig, systemKey) {
32
+ try {
33
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
34
+ const res = await getExternalSystem(dataplaneUrl, systemKey, authConfig);
35
+ const sys = res?.data || res;
36
+ if (!sys) return;
37
+
38
+ const apiDocumentUrl = sys.apiDocumentUrl;
39
+ const mcpServerUrl = sys.mcpServerUrl;
40
+ const openApiDocsPageUrl = sys.openApiDocsPageUrl;
41
+
42
+ const urls = [];
43
+ if (apiDocumentUrl && typeof apiDocumentUrl === 'string') {
44
+ urls.push({ label: 'API Docs', url: apiDocumentUrl });
45
+ }
46
+ if (mcpServerUrl && typeof mcpServerUrl === 'string') {
47
+ urls.push({ label: 'MCP Server', url: mcpServerUrl });
48
+ }
49
+ if (openApiDocsPageUrl && typeof openApiDocsPageUrl === 'string') {
50
+ urls.push({ label: 'OpenAPI Docs Page', url: openApiDocsPageUrl });
51
+ }
52
+
53
+ if (urls.length > 0) {
54
+ logger.log(chalk.blue('\nDocumentation:'));
55
+ urls.forEach(({ label, url }) => {
56
+ logger.log(chalk.blue(` ${label}: ${url}`));
57
+ });
58
+ }
59
+ } catch (_err) {
60
+ // Silently ignore: dataplane may be unreachable or docs not configured
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Deploys via controller and displays success summary with docs
66
+ * @async
67
+ * @function executeDeployAndDisplay
68
+ * @param {Object} manifest - Controller manifest
69
+ * @param {string} controllerUrl - Controller base URL
70
+ * @param {string} environment - Environment key
71
+ * @param {Object} authConfig - Authentication configuration
72
+ * @param {Object} options - Deployment options
73
+ * @returns {Promise<Object>} Deployment result
74
+ */
75
+ async function executeDeployAndDisplay(manifest, controllerUrl, environment, authConfig, options) {
76
+ const deployer = require('../deployment/deployer');
77
+ const pollOpts = {
78
+ poll: options.poll,
79
+ pollInterval: options.pollInterval !== undefined ? options.pollInterval : 500,
80
+ pollMaxAttempts: options.pollMaxAttempts,
81
+ ...options
82
+ };
83
+ const result = await deployer.deployToController(
84
+ manifest,
85
+ controllerUrl,
86
+ environment,
87
+ authConfig,
88
+ pollOpts
89
+ );
90
+ logger.log(chalk.green('\n✅ External system deployed successfully!'));
91
+ logger.log(chalk.blue(`System: ${manifest.key}`));
92
+ logger.log(chalk.blue(`Datasources: ${manifest.dataSources.length}`));
93
+ await displayDeploymentDocs(controllerUrl, environment, authConfig, manifest.key);
94
+ return result;
95
+ }
96
+
20
97
  /**
21
98
  * Prepares deployment configuration (auth, controller URL, environment)
22
99
  * @async
@@ -75,26 +152,13 @@ async function deployExternalSystem(appName, options = {}) {
75
152
  const { environment, controllerUrl, authConfig } = await prepareDeploymentConfig(appName, options);
76
153
 
77
154
  // Step 3: Deploy via controller pipeline (same as regular apps)
78
- // Use 500ms polling for external systems (faster than web apps which use 5000ms)
79
- const deployer = require('../deployment/deployer');
80
- const result = await deployer.deployToController(
155
+ const result = await executeDeployAndDisplay(
81
156
  manifest,
82
157
  controllerUrl,
83
158
  environment,
84
159
  authConfig,
85
- {
86
- poll: options.poll,
87
- pollInterval: options.pollInterval !== undefined ? options.pollInterval : 500,
88
- pollMaxAttempts: options.pollMaxAttempts,
89
- ...options
90
- }
160
+ options
91
161
  );
92
-
93
- // Display success summary
94
- logger.log(chalk.green('\n✅ External system deployed successfully!'));
95
- logger.log(chalk.blue(`System: ${manifest.key}`));
96
- logger.log(chalk.blue(`Datasources: ${manifest.dataSources.length}`));
97
-
98
162
  return result;
99
163
  } catch (error) {
100
164
  let message = `Failed to deploy external system: ${error.message}`;
@@ -262,25 +262,6 @@ function validateBuildFields(build) {
262
262
  return Object.keys(buildConfig).length > 0 ? buildConfig : null;
263
263
  }
264
264
 
265
- /**
266
- * Validates and transforms deployment fields
267
- * @function validateDeploymentFields
268
- * @param {Object} deployment - Deployment configuration
269
- * @returns {Object|null} Validated deployment config or null
270
- */
271
- function validateDeploymentFields(deployment) {
272
- if (!deployment) {
273
- return null;
274
- }
275
-
276
- const deploymentConfig = {};
277
- if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
278
- deploymentConfig.controllerUrl = deployment.controllerUrl;
279
- }
280
-
281
- return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
282
- }
283
-
284
265
  /**
285
266
  * Adds optional fields to deployment manifest
286
267
  * @function buildOptionalFields
@@ -339,11 +320,6 @@ function addValidatedConfigSections(deployment, variables) {
339
320
  if (build) {
340
321
  deployment.build = build;
341
322
  }
342
-
343
- const deploymentConfig = validateDeploymentFields(variables.deployment);
344
- if (deploymentConfig) {
345
- deployment.deployment = deploymentConfig;
346
- }
347
323
  }
348
324
 
349
325
  /**
@@ -772,18 +772,6 @@
772
772
  },
773
773
  "additionalProperties": false
774
774
  },
775
- "deployment": {
776
- "type": "object",
777
- "description": "Deployment configuration for pipeline API",
778
- "properties": {
779
- "controllerUrl": {
780
- "type": "string",
781
- "description": "Controller API URL for deployment",
782
- "pattern": "^https://.*$"
783
- }
784
- },
785
- "additionalProperties": false
786
- },
787
775
  "system": {
788
776
  "type": "object",
789
777
  "description": "Optional: Inline external system configuration for atomic deployment. Uses external-system.schema.json structure via $ref. Alternative to externalIntegration.systems file-based approach.",
@@ -102,22 +102,6 @@
102
102
  "type": "boolean",
103
103
  "default": true
104
104
  },
105
- "environment": {
106
- "type": "object",
107
- "description": "Environment-level configuration values used by dataplane and external data sources.",
108
- "properties": {
109
- "baseUrl": {
110
- "type": "string",
111
- "description": "Base API URL or MCP server URL",
112
- "pattern": "^(http|https)://.*$"
113
- },
114
- "region": {
115
- "type": "string",
116
- "description": "Optional region setting for API routing"
117
- }
118
- },
119
- "additionalProperties": true
120
- },
121
105
  "authentication": {
122
106
  "type": "object",
123
107
  "description": "Authentication configuration for external system",
@@ -112,19 +112,14 @@ function resolveSystemFilePath(appPath, schemaBasePath, systemFileName) {
112
112
  }
113
113
 
114
114
  /**
115
- * Extracts URL from system JSON
115
+ * Base URL is no longer read from manifest; Controller resolves from configuration at deploy time.
116
116
  * @function extractUrlFromSystemJson
117
- * @param {Object} systemJson - System JSON object
118
- * @param {string} systemFileName - System file name
119
- * @returns {string} Base URL
120
- * @throws {Error} If URL is missing
117
+ * @param {Object} _systemJson - System JSON object (unused; kept for call-site compatibility)
118
+ * @param {string} _systemFileName - System file name (unused)
119
+ * @returns {undefined} URL is not supplied from manifest
121
120
  */
122
- function extractUrlFromSystemJson(systemJson, systemFileName) {
123
- const url = systemJson.environment?.baseUrl;
124
- if (!url) {
125
- throw new Error(`Missing environment.baseUrl in ${systemFileName}`);
126
- }
127
- return url;
121
+ function extractUrlFromSystemJson(_systemJson, _systemFileName) {
122
+ return undefined;
128
123
  }
129
124
 
130
125
  /**
@@ -209,7 +204,10 @@ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
209
204
  */
210
205
  async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile, displayName, description) {
211
206
  const { url, apiKey } = await extractExternalIntegrationUrl(appKey, variables.externalIntegration);
212
- const externalIntegration = { url };
207
+ const externalIntegration = {};
208
+ if (url !== undefined && url !== null) {
209
+ externalIntegration.url = url;
210
+ }
213
211
  if (apiKey) {
214
212
  externalIntegration.apiKey = apiKey;
215
213
  }
@@ -206,6 +206,16 @@ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false
206
206
  // Extract error message
207
207
  const errorMessage = extractErrorMessage(error);
208
208
 
209
+ // Deployment failed/cancelled (from deploy.js): show message as-is, do not treat as HTTP error
210
+ if (errorMessage.startsWith('Deployment failed:') || errorMessage.startsWith('Deployment cancelled:')) {
211
+ if (!alreadyLogged) {
212
+ await logDeploymentFailure(appName, url, error);
213
+ }
214
+ const err = new Error(errorMessage);
215
+ err.formatted = (error.formatted || errorMessage).trim();
216
+ throw err;
217
+ }
218
+
209
219
  // For validation errors (like URL validation), just re-throw them directly
210
220
  if (isValidationError(errorMessage)) {
211
221
  throwValidationError(error, errorMessage);
@@ -13,6 +13,8 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const dockerUtils = require('./docker');
15
15
  const { getActualSecretsPath } = require('./secrets-path');
16
+ const config = require('../core/config');
17
+ const devConfig = require('./dev-config');
16
18
 
17
19
  /**
18
20
  * Checks if Docker is installed and available
@@ -33,17 +35,28 @@ async function checkDocker() {
33
35
 
34
36
  /**
35
37
  * Checks if required ports are available
38
+ * Uses developer-specific ports when developer-id greater than 0 (basePort + developerId * 100)
36
39
  *
37
40
  * @async
38
41
  * @function checkPorts
42
+ * @param {number[]} [requiredPorts] - Ports to check. If omitted, uses ports from config developer-id
39
43
  * @returns {Promise<string>} 'ok' if all ports are available, 'warning' otherwise
40
44
  */
41
- async function checkPorts() {
42
- const requiredPorts = [5432, 6379, 5050, 8081];
45
+ async function checkPorts(requiredPorts) {
43
46
  const netstat = require('net');
47
+ let portsToCheck = requiredPorts;
48
+
49
+ if (!portsToCheck || portsToCheck.length === 0) {
50
+ const devId = await config.getDeveloperId();
51
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
52
+ const id = Number.isNaN(idNum) || idNum < 0 ? 0 : idNum;
53
+ const ports = devConfig.getDevPorts(id);
54
+ portsToCheck = [ports.postgres, ports.redis, ports.pgadmin, ports.redisCommander];
55
+ }
56
+
44
57
  let portIssues = 0;
45
58
 
46
- for (const port of requiredPorts) {
59
+ for (const port of portsToCheck) {
47
60
  try {
48
61
  await new Promise((resolve, reject) => {
49
62
  const server = netstat.createServer();
@@ -128,10 +141,16 @@ async function checkEnvironment() {
128
141
  result.recommendations.push('Install Docker and Docker Compose');
129
142
  }
130
143
 
131
- // Check ports
132
- result.ports = await checkPorts();
144
+ // Check ports (developer-specific: dev 0 = base ports, dev N = base + N*100)
145
+ const devId = await config.getDeveloperId();
146
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
147
+ const id = Number.isNaN(idNum) || idNum < 0 ? 0 : idNum;
148
+ const ports = devConfig.getDevPorts(id);
149
+ const requiredPorts = [ports.postgres, ports.redis, ports.pgadmin, ports.redisCommander];
150
+
151
+ result.ports = await checkPorts(requiredPorts);
133
152
  if (result.ports === 'warning') {
134
- result.recommendations.push('Some required ports (5432, 6379, 5050, 8081) are in use');
153
+ result.recommendations.push(`Some required ports (${requiredPorts.join(', ')}) are in use`);
135
154
  }
136
155
 
137
156
  // Check secrets
@@ -198,22 +198,14 @@ function validateBuildConfig(build) {
198
198
  }
199
199
 
200
200
  /**
201
- * Validates and transforms deployment configuration
201
+ * Validates and transforms deployment configuration.
202
+ * Manifest is generic: deployment URL/env are resolved outside the manifest (user/config).
202
203
  * @function validateDeploymentConfig
203
- * @param {Object} deployment - Deployment configuration
204
- * @returns {Object|null} Validated deployment config or null
204
+ * @param {Object} _deployment - Deployment configuration (unused; manifest is generic)
205
+ * @returns {null} No deployment block emitted from variables
205
206
  */
206
- function validateDeploymentConfig(deployment) {
207
- if (!deployment) {
208
- return null;
209
- }
210
-
211
- const deploymentConfig = {};
212
- if (deployment.controllerUrl && deployment.controllerUrl.trim() !== '' && /^https:\/\/.*$/.test(deployment.controllerUrl)) {
213
- deploymentConfig.controllerUrl = deployment.controllerUrl;
214
- }
215
-
216
- return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
207
+ function validateDeploymentConfig(_deployment) {
208
+ return null;
217
209
  }
218
210
 
219
211
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.38.0",
3
+ "version": "2.39.1",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -18,17 +18,27 @@ npm install -g @aifabrix/builder
18
18
  # Check your environment
19
19
  aifabrix doctor
20
20
 
21
- # Login to controller (change your own port)
22
- aifabrix login --method device --environment dev --controller http://localhost:3100
21
+ # Login to controller (debug mode -c http://localhost:3010 - change your own port)
22
+ aifabrix login
23
23
 
24
24
  # Register your application (gets you credentials automatically)
25
25
  aifabrix app register dataplane
26
+
27
+ # Rotate credentials if needed:
28
+ aifabrix app rotate-secret dataplane
29
+
30
+ # Run locally
31
+ aifabrix run dataplane
32
+
33
+ # Deploy to miso-controller
34
+ aifabrix deploy dataplane
35
+
26
36
  ```
27
37
 
28
38
  ### 3. Build & Run Locally
29
39
 
30
40
  ```bash
31
- # Build the Docker image
41
+ # Build the Docker image (latest)
32
42
  aifabrix build dataplane
33
43
 
34
44
  # Run locally
@@ -41,7 +51,7 @@ aifabrix run dataplane
41
51
 
42
52
  ## Testing dataplane (use DATAPLANE_TEST_GUIDE)
43
53
 
44
- **Use the builder's Dataplane Test Guide** for auth, health, wizard, external systems, and pipeline checks:
54
+ **Use the builders Dataplane Test Guide** for auth, health, wizard, external systems, and pipeline checks:
45
55
 
46
56
  - **In aifabrix-builder:** `integration/hubspot/DATAPLANE_TEST_GUIDE.md`
47
57
  - **Dataplane base URL:** `http://localhost:3111`
@@ -53,13 +63,13 @@ Keep `build.localPort` in `variables.yaml` at **3111** so it matches that guide.
53
63
  **View logs:**
54
64
 
55
65
  ```bash
56
- docker logs aifabrix-dataplane -f
66
+ docker logs aifabrix-dev06-dataplane -f
57
67
  ```
58
68
 
59
69
  **Stop:**
60
70
 
61
71
  ```bash
62
- docker stop aifabrix-dataplane
72
+ docker stop aifabrix-dev06-dataplane
63
73
  ```
64
74
 
65
75
  ### 4. Deploy to Azure
@@ -73,6 +83,7 @@ aifabrix push dataplane --registry myacr.azurecr.io --tag "v1.0.0,latest"
73
83
 
74
84
  # Deploy to miso-controller
75
85
  aifabrix deploy dataplane
86
+
76
87
  ```
77
88
 
78
89
  ---
@@ -103,7 +114,8 @@ aifabrix dockerfile dataplane --force # Generate Dockerfile
103
114
  aifabrix resolve dataplane # Generate .env file
104
115
 
105
116
  # Deployment
106
- aifabrix json dataplane # Generate deployment manifest
117
+ aifabrix json dataplane # Preview deployment JSON
118
+ aifabrix genkey dataplane # Generate deployment key
107
119
  aifabrix push dataplane --registry myacr.azurecr.io # Push to ACR
108
120
  aifabrix deploy dataplane --controller <url> # Deploy to Azure
109
121
 
@@ -167,6 +179,8 @@ export AIFABRIX_HOME=/custom/path
167
179
  export AIFABRIX_SECRETS=/path/to/secrets.yaml
168
180
  ```
169
181
 
182
+ **Default OAuth callback URL:** Set `DATAPLANE_WEB_SERVER_URL` (e.g. in `env.template` as `http://localhost:${PORT}`) so the dataplane can build the default OAuth2 callback URL when `redirectUri` is omitted. The callback URL is `{DATAPLANE_WEB_SERVER_URL}/auth/callback`. When you change the domain (e.g. from localhost to a production URL), update this single variable and register the same callback URL in your OAuth app (e.g. HubSpot).
183
+
170
184
  ---
171
185
 
172
186
  ## Troubleshooting
@@ -177,12 +191,14 @@ export AIFABRIX_SECRETS=/path/to/secrets.yaml
177
191
  - **"Authentication failed"** → Run `aifabrix login` again
178
192
  - **"Build fails"** → Check Docker is running and `variables.yaml` → `build.secrets` path is correct
179
193
  - **"Can't connect"** → Verify infrastructure is running and PostgreSQL is accessible
194
+ - **Wizard / API 401 after `rotate-secret`** → The wizard may write `.env` to a different path (e.g. `../../.env`). Ensure the **project root** `.env` has the new `MISO_CLIENTID` and `MISO_CLIENTSECRET` (copy from the rotate-secret output or run `make resolve`), then **restart the backend** (`make dev` or restart the process) so it loads the new credentials.
180
195
 
181
196
  **Regenerate files:**
182
197
 
183
198
  ```bash
184
199
  aifabrix resolve dataplane --force
185
200
  aifabrix json dataplane
201
+ aifabrix genkey dataplane
186
202
  ```
187
203
 
188
204
  ---