@aifabrix/builder 2.32.1 → 2.32.3

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.
@@ -0,0 +1,262 @@
1
+ /**
2
+ * AI Fabrix Builder - Auth Status Command
3
+ *
4
+ * Displays authentication status for the current controller and environment
5
+ *
6
+ * @fileoverview Authentication status command implementation
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const config = require('../core/config');
14
+ const { getConfig, getCurrentEnvironment } = config;
15
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
16
+ const { getAuthUser } = require('../api/auth.api');
17
+ const { resolveControllerUrl } = require('../utils/controller-url');
18
+
19
+ /**
20
+ * Format expiration date for display
21
+ * @param {string} expiresAt - ISO 8601 expiration timestamp
22
+ * @returns {string} Formatted expiration string
23
+ */
24
+ function formatExpiration(expiresAt) {
25
+ if (!expiresAt) {
26
+ return 'Unknown';
27
+ }
28
+ try {
29
+ const date = new Date(expiresAt);
30
+ return date.toISOString();
31
+ } catch {
32
+ return expiresAt;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Check and validate device token
38
+ * @async
39
+ * @param {string} controllerUrl - Controller URL
40
+ * @returns {Promise<Object|null>} Token validation result or null
41
+ */
42
+ async function checkDeviceToken(controllerUrl) {
43
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
44
+ if (!deviceToken || !deviceToken.token) {
45
+ return null;
46
+ }
47
+
48
+ try {
49
+ const authConfig = { type: 'bearer', token: deviceToken.token };
50
+ // Use getAuthUser instead of validateToken - it's more reliable and tests actual API access
51
+ const { getAuthUser } = require('../api/auth.api');
52
+ const response = await getAuthUser(controllerUrl, authConfig);
53
+
54
+ if (response.success && response.data) {
55
+ return {
56
+ type: 'Device Token',
57
+ token: deviceToken.token,
58
+ authenticated: response.data.authenticated !== false,
59
+ user: response.data.user,
60
+ expiresAt: deviceToken.expiresAt
61
+ };
62
+ }
63
+
64
+ return {
65
+ type: 'Device Token',
66
+ token: deviceToken.token,
67
+ authenticated: false,
68
+ error: response.error || response.formattedError || 'Token validation failed'
69
+ };
70
+ } catch (error) {
71
+ return {
72
+ type: 'Device Token',
73
+ token: deviceToken.token,
74
+ authenticated: false,
75
+ error: error.message || 'Token validation error'
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Decrypt token if encrypted
82
+ * @async
83
+ * @param {string} token - Token to decrypt
84
+ * @returns {Promise<string>} Decrypted token
85
+ */
86
+ async function decryptTokenIfNeeded(token) {
87
+ const { decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
88
+ const encryptionKey = await config.getSecretsEncryptionKey();
89
+
90
+ if (encryptionKey && isTokenEncrypted(token)) {
91
+ return await decryptToken(token, encryptionKey);
92
+ }
93
+ return token;
94
+ }
95
+
96
+ /**
97
+ * Validate client token and return result
98
+ * @async
99
+ * @param {string} token - Token to validate
100
+ * @param {string} controllerUrl - Controller URL
101
+ * @param {string} environment - Environment key
102
+ * @param {string} appName - Application name
103
+ * @param {string} expiresAt - Token expiration
104
+ * @returns {Promise<Object>} Token validation result
105
+ */
106
+ async function validateClientToken(token, controllerUrl, environment, appName, expiresAt) {
107
+ try {
108
+ const authConfig = { type: 'bearer', token: token };
109
+ // Use getAuthUser instead of validateToken - it's more reliable and tests actual API access
110
+ const response = await getAuthUser(controllerUrl, authConfig);
111
+
112
+ if (response.success && response.data) {
113
+ return {
114
+ type: 'Client Token',
115
+ token: token,
116
+ authenticated: response.data.authenticated !== false,
117
+ user: response.data.user,
118
+ expiresAt: expiresAt,
119
+ appName: appName
120
+ };
121
+ }
122
+
123
+ return {
124
+ type: 'Client Token',
125
+ token: token,
126
+ authenticated: false,
127
+ error: response.error || response.formattedError || 'Token validation failed',
128
+ appName: appName
129
+ };
130
+ } catch (error) {
131
+ return {
132
+ type: 'Client Token',
133
+ token: '***',
134
+ authenticated: false,
135
+ error: error.message || 'Token validation error',
136
+ appName: appName
137
+ };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Check and validate client token
143
+ * @async
144
+ * @param {string} controllerUrl - Controller URL
145
+ * @param {string} environment - Environment key
146
+ * @returns {Promise<Object|null>} Token validation result or null
147
+ */
148
+ async function checkClientToken(controllerUrl, environment) {
149
+ const configData = await getConfig();
150
+ const environments = configData.environments || {};
151
+ const envConfig = environments[environment];
152
+
153
+ if (!envConfig || !envConfig.clients) {
154
+ return null;
155
+ }
156
+
157
+ for (const [appName, tokenData] of Object.entries(envConfig.clients)) {
158
+ if (tokenData.controller === controllerUrl && tokenData.token) {
159
+ const token = await decryptTokenIfNeeded(tokenData.token);
160
+ return await validateClientToken(token, controllerUrl, environment, appName, tokenData.expiresAt);
161
+ }
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Display user information
169
+ * @param {Object} user - User object
170
+ */
171
+ function displayUserInfo(user) {
172
+ if (!user) {
173
+ return;
174
+ }
175
+
176
+ logger.log('');
177
+ logger.log(chalk.bold('User Information:'));
178
+ if (user.email) {
179
+ logger.log(` Email: ${chalk.cyan(user.email)}`);
180
+ }
181
+ if (user.username) {
182
+ logger.log(` Username: ${chalk.cyan(user.username)}`);
183
+ }
184
+ if (user.id) {
185
+ logger.log(` ID: ${chalk.gray(user.id)}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Display token information
191
+ * @param {Object} tokenInfo - Token information
192
+ */
193
+ function displayTokenInfo(tokenInfo) {
194
+ const statusIcon = tokenInfo.authenticated ? chalk.green('āœ“') : chalk.red('āœ—');
195
+ const statusText = tokenInfo.authenticated ? 'Authenticated' : 'Not authenticated';
196
+
197
+ logger.log(`Status: ${statusIcon} ${statusText}`);
198
+ logger.log(`Token Type: ${chalk.cyan(tokenInfo.type)}`);
199
+
200
+ if (tokenInfo.appName) {
201
+ logger.log(`Application: ${chalk.cyan(tokenInfo.appName)}`);
202
+ }
203
+
204
+ if (tokenInfo.expiresAt) {
205
+ logger.log(`Expires: ${chalk.gray(formatExpiration(tokenInfo.expiresAt))}`);
206
+ }
207
+
208
+ if (tokenInfo.error) {
209
+ logger.log(`Error: ${chalk.red(tokenInfo.error)}`);
210
+ }
211
+
212
+ displayUserInfo(tokenInfo.user);
213
+ }
214
+
215
+ /**
216
+ * Display authentication status
217
+ * @param {string} controllerUrl - Controller URL
218
+ * @param {string} environment - Environment key
219
+ * @param {Object|null} tokenInfo - Token information
220
+ */
221
+ function displayStatus(controllerUrl, environment, tokenInfo) {
222
+ logger.log(chalk.bold('\nšŸ” Authentication Status\n'));
223
+ logger.log(`Controller: ${chalk.cyan(controllerUrl)}`);
224
+ logger.log(`Environment: ${chalk.cyan(environment || 'Not specified')}\n`);
225
+
226
+ if (!tokenInfo) {
227
+ logger.log(`Status: ${chalk.red('āœ— Not authenticated')}`);
228
+ logger.log(`Token Type: ${chalk.gray('None')}\n`);
229
+ logger.log(chalk.yellow('šŸ’” Run "aifabrix login" to authenticate\n'));
230
+ return;
231
+ }
232
+
233
+ displayTokenInfo(tokenInfo);
234
+ logger.log('');
235
+ }
236
+
237
+ /**
238
+ * Handle auth status command
239
+ * @async
240
+ * @function handleAuthStatus
241
+ * @param {Object} options - Command options
242
+ * @param {string} [options.controller] - Controller URL (uses developer ID-based default if not provided)
243
+ * @param {string} [options.environment] - Environment key (uses current environment from config if not provided)
244
+ * @returns {Promise<void>} Resolves when status is displayed
245
+ */
246
+ async function handleAuthStatus(options) {
247
+ const configData = await getConfig();
248
+ const controllerUrl = await resolveControllerUrl(options, configData);
249
+ const environment = options.environment || await getCurrentEnvironment() || 'dev';
250
+
251
+ // Check device token first (preferred)
252
+ let tokenInfo = await checkDeviceToken(controllerUrl);
253
+
254
+ // If no device token, check client token
255
+ if (!tokenInfo) {
256
+ tokenInfo = await checkClientToken(controllerUrl, environment);
257
+ }
258
+
259
+ displayStatus(controllerUrl, environment, tokenInfo);
260
+ }
261
+
262
+ module.exports = { handleAuthStatus };
@@ -130,27 +130,32 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
130
130
 
131
131
  /**
132
132
  * Build scope string from options
133
- * @param {boolean} [offline] - Whether to request offline_access
133
+ * @param {boolean} [online] - Whether to exclude offline_access (default: false, meaning offline tokens are default)
134
134
  * @param {string} [customScope] - Custom scope string
135
135
  * @returns {string} Scope string
136
136
  */
137
- function buildScope(offline, customScope) {
137
+ function buildScope(online, customScope) {
138
138
  const defaultScope = 'openid profile email';
139
139
 
140
140
  if (customScope) {
141
- // If custom scope provided, use it and optionally add offline_access
142
- if (offline && !customScope.includes('offline_access')) {
141
+ // If custom scope provided, use it as-is
142
+ // If --online flag is used and scope contains offline_access, remove it
143
+ if (online && customScope.includes('offline_access')) {
144
+ return customScope.replace(/\s*offline_access\s*/g, ' ').trim().replace(/\s+/g, ' ');
145
+ }
146
+ // If not --online and scope doesn't have offline_access, add it (default behavior)
147
+ if (!online && !customScope.includes('offline_access')) {
143
148
  return `${customScope} offline_access`;
144
149
  }
145
150
  return customScope;
146
151
  }
147
152
 
148
- // Default scope with optional offline_access
149
- if (offline) {
150
- return `${defaultScope} offline_access`;
153
+ // Default scope: include offline_access unless --online is specified
154
+ if (online) {
155
+ return defaultScope;
151
156
  }
152
157
 
153
- return defaultScope;
158
+ return `${defaultScope} offline_access`;
154
159
  }
155
160
 
156
161
  /**
@@ -200,16 +205,16 @@ function convertDeviceCodeResponse(apiResponse) {
200
205
  * @async
201
206
  * @param {string} controllerUrl - Controller URL
202
207
  * @param {string} [environment] - Environment key from options
203
- * @param {boolean} [offline] - Whether to request offline_access scope
208
+ * @param {boolean} [online] - Whether to exclude offline_access scope (default: false, meaning offline tokens are default)
204
209
  * @param {string} [scope] - Custom scope string
205
210
  * @returns {Promise<{token: string, environment: string}>} Token and environment
206
211
  */
207
- async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
212
+ async function handleDeviceCodeLogin(controllerUrl, environment, online, scope) {
208
213
  const envKey = await getEnvironmentKey(environment);
209
- const requestScope = buildScope(offline, scope);
214
+ const requestScope = buildScope(online, scope);
210
215
 
211
216
  logger.log(chalk.blue('\nšŸ“± Initiating device code flow...\n'));
212
- if (offline) {
217
+ if (!online && requestScope.includes('offline_access')) {
213
218
  logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
214
219
  }
215
220
 
@@ -15,6 +15,7 @@ const { setCurrentEnvironment, saveClientToken } = require('../core/config');
15
15
  const logger = require('../utils/logger');
16
16
  const { handleCredentialsLogin } = require('./login-credentials');
17
17
  const { handleDeviceCodeLogin } = require('./login-device');
18
+ const { getDefaultControllerUrl } = require('../utils/controller-url');
18
19
 
19
20
  /**
20
21
  * Determine and validate authentication method
@@ -61,8 +62,8 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
61
62
  * @async
62
63
  * @function handleLogin
63
64
  * @param {Object} options - Login options
64
- * @param {string} [options.controller] - Controller URL (default: 'http://localhost:3000')
65
- * @param {string} [options.method] - Authentication method ('device' or 'credentials')
65
+ * @param {string} [options.controller] - Controller URL (default: calculated based on developer ID, e.g., 'http://localhost:3000' for dev ID 0, 'http://localhost:3100' for dev ID 1)
66
+ * @param {string} [options.method] - Authentication method ('device' or 'credentials', default: 'device')
66
67
  * @param {string} [options.app] - Application name (for credentials method, reads from secrets.local.yaml)
67
68
  * @param {string} [options.clientId] - Client ID (for credentials method, overrides secrets.local.yaml)
68
69
  * @param {string} [options.clientSecret] - Client Secret (for credentials method, overrides secrets.local.yaml)
@@ -72,12 +73,18 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
72
73
  */
73
74
  /**
74
75
  * Normalizes and logs controller URL
76
+ * Calculates default URL based on developer ID if not provided
77
+ * @async
75
78
  * @function normalizeControllerUrl
76
79
  * @param {Object} options - Login options
77
- * @returns {string} Normalized controller URL
80
+ * @returns {Promise<string>} Normalized controller URL
78
81
  */
79
- function normalizeControllerUrl(options) {
80
- const controllerUrl = (options.controller || options.url || 'http://localhost:3000').replace(/\/$/, '');
82
+ async function normalizeControllerUrl(options) {
83
+ let controllerUrl = options.controller || options.url;
84
+ if (!controllerUrl) {
85
+ controllerUrl = await getDefaultControllerUrl();
86
+ }
87
+ controllerUrl = controllerUrl.replace(/\/$/, '');
81
88
  logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
82
89
  return controllerUrl;
83
90
  }
@@ -108,8 +115,8 @@ async function handleEnvironmentConfig(options) {
108
115
  * @param {Object} options - Login options
109
116
  */
110
117
  function validateScopeOptions(method, options) {
111
- if (method === 'credentials' && (options.offline || options.scope)) {
112
- logger.log(chalk.yellow('āš ļø Warning: --offline and --scope options are only available for device flow'));
118
+ if (method === 'credentials' && (options.online || options.scope)) {
119
+ logger.log(chalk.yellow('āš ļø Warning: --online and --scope options are only available for device flow'));
113
120
  logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
114
121
  }
115
122
  }
@@ -141,13 +148,13 @@ async function handleCredentialsLoginFlow(controllerUrl, environment, options) {
141
148
  * @returns {Promise<{token: string, environment: string}>} Login result
142
149
  */
143
150
  async function handleDeviceCodeLoginFlow(controllerUrl, options) {
144
- return await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
151
+ return await handleDeviceCodeLogin(controllerUrl, options.environment, options.online, options.scope);
145
152
  }
146
153
 
147
154
  async function handleLogin(options) {
148
155
  logger.log(chalk.blue('\nšŸ” Logging in to Miso Controller...\n'));
149
156
 
150
- const controllerUrl = normalizeControllerUrl(options);
157
+ const controllerUrl = await normalizeControllerUrl(options);
151
158
  const environment = await handleEnvironmentConfig(options);
152
159
  const method = await determineAuthMethod(options.method);
153
160
 
@@ -10,11 +10,12 @@ const path = require('path');
10
10
  const fs = require('fs').promises;
11
11
  const logger = require('../utils/logger');
12
12
  const config = require('../core/config');
13
- const { getDeploymentAuth } = require('../utils/token-manager');
13
+ const { getDeviceOnlyAuth } = require('../utils/token-manager');
14
14
  const { getDataplaneUrl } = require('../datasource/deploy');
15
+ const { resolveControllerUrl } = require('../utils/controller-url');
15
16
  const {
16
- selectMode,
17
- selectSource,
17
+ createWizardSession,
18
+ updateWizardSession,
18
19
  parseOpenApi,
19
20
  detectType,
20
21
  generateConfig,
@@ -44,29 +45,23 @@ const { generateWizardFiles } = require('../generator/wizard');
44
45
  * @throws {Error} If validation fails
45
46
  */
46
47
  async function validateAndCheckAppDirectory(appName) {
47
- // Validate app name
48
48
  if (!/^[a-z0-9-_]+$/.test(appName)) {
49
49
  throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
50
50
  }
51
-
52
- // Check if app directory already exists
53
51
  const appPath = path.join(process.cwd(), 'integration', appName);
54
52
  try {
55
53
  await fs.access(appPath);
56
- const { overwrite } = await require('inquirer').prompt([
57
- {
58
- type: 'confirm',
59
- name: 'overwrite',
60
- message: `Directory ${appPath} already exists. Overwrite?`,
61
- default: false
62
- }
63
- ]);
54
+ const { overwrite } = await require('inquirer').prompt([{
55
+ type: 'confirm',
56
+ name: 'overwrite',
57
+ message: `Directory ${appPath} already exists. Overwrite?`,
58
+ default: false
59
+ }]);
64
60
  if (!overwrite) {
65
61
  logger.log(chalk.yellow('Wizard cancelled.'));
66
62
  return false;
67
63
  }
68
64
  } catch (error) {
69
- // Directory doesn't exist, continue
70
65
  if (error.code !== 'ENOENT') {
71
66
  throw error;
72
67
  }
@@ -75,37 +70,48 @@ async function validateAndCheckAppDirectory(appName) {
75
70
  }
76
71
 
77
72
  /**
78
- * Handle mode selection step
73
+ * Handle mode selection step - create wizard session
79
74
  * @async
80
75
  * @function handleModeSelection
81
76
  * @param {string} dataplaneUrl - Dataplane URL
82
77
  * @param {Object} authConfig - Authentication configuration
83
- * @returns {Promise<string>} Selected mode
78
+ * @returns {Promise<Object>} Object with mode and sessionId
84
79
  * @throws {Error} If mode selection fails
85
80
  */
86
81
  async function handleModeSelection(dataplaneUrl, authConfig) {
87
82
  logger.log(chalk.blue('\nšŸ“‹ Step 1: Mode Selection'));
88
83
  const mode = await promptForMode();
89
- const modeResponse = await selectMode(dataplaneUrl, authConfig, mode);
90
- if (!modeResponse.success) {
91
- throw new Error(`Mode selection failed: ${modeResponse.error || modeResponse.formattedError}`);
84
+ const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode);
85
+ if (!sessionResponse.success || !sessionResponse.data) {
86
+ const errorMsg = sessionResponse.formattedError ||
87
+ sessionResponse.error ||
88
+ sessionResponse.errorData?.detail ||
89
+ sessionResponse.message ||
90
+ (sessionResponse.status ? `HTTP ${sessionResponse.status}` : 'Unknown error');
91
+ throw new Error(`Failed to create wizard session: ${errorMsg}`);
92
+ }
93
+ const sessionId = sessionResponse.data.data?.sessionId || sessionResponse.data.sessionId;
94
+ if (!sessionId) {
95
+ throw new Error('Session ID not found in response');
92
96
  }
93
- return mode;
97
+ return { mode, sessionId };
94
98
  }
95
99
 
96
100
  /**
97
- * Handle source selection step
101
+ * Handle source selection step - update wizard session
98
102
  * @async
99
103
  * @function handleSourceSelection
100
104
  * @param {string} dataplaneUrl - Dataplane URL
105
+ * @param {string} sessionId - Wizard session ID
101
106
  * @param {Object} authConfig - Authentication configuration
102
107
  * @returns {Promise<Object>} Object with sourceType and sourceData
103
108
  * @throws {Error} If source selection fails
104
109
  */
105
- async function handleSourceSelection(dataplaneUrl, authConfig) {
110
+ async function handleSourceSelection(dataplaneUrl, sessionId, authConfig) {
106
111
  logger.log(chalk.blue('\nšŸ“‹ Step 2: Source Selection'));
107
112
  const sourceType = await promptForSourceType();
108
113
  let sourceData = null;
114
+ const updateData = { currentStep: 1 };
109
115
 
110
116
  if (sourceType === 'openapi-file') {
111
117
  const filePath = await promptForOpenApiFile();
@@ -113,17 +119,19 @@ async function handleSourceSelection(dataplaneUrl, authConfig) {
113
119
  } else if (sourceType === 'openapi-url') {
114
120
  const url = await promptForOpenApiUrl();
115
121
  sourceData = url;
122
+ updateData.openapiSpec = null; // Will be set after parsing
116
123
  } else if (sourceType === 'mcp-server') {
117
124
  const mcpDetails = await promptForMcpServer();
118
125
  sourceData = JSON.stringify(mcpDetails);
126
+ updateData.mcpServerUrl = mcpDetails.url || null;
119
127
  } else if (sourceType === 'known-platform') {
120
128
  const platform = await promptForKnownPlatform();
121
129
  sourceData = platform;
122
130
  }
123
131
 
124
- const sourceResponse = await selectSource(dataplaneUrl, authConfig, sourceType, sourceData);
125
- if (!sourceResponse.success) {
126
- throw new Error(`Source selection failed: ${sourceResponse.error || sourceResponse.formattedError}`);
132
+ const updateResponse = await updateWizardSession(dataplaneUrl, sessionId, authConfig, updateData);
133
+ if (!updateResponse.success) {
134
+ throw new Error(`Source selection failed: ${updateResponse.error || updateResponse.formattedError}`);
127
135
  }
128
136
 
129
137
  return { sourceType, sourceData };
@@ -379,11 +387,11 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
379
387
  * @throws {Error} If wizard flow fails
380
388
  */
381
389
  async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
382
- // Step 1: Mode Selection
383
- const mode = await handleModeSelection(dataplaneUrl, authConfig);
390
+ // Step 1: Mode Selection - Create wizard session
391
+ const { mode, sessionId } = await handleModeSelection(dataplaneUrl, authConfig);
384
392
 
385
- // Step 2: Source Selection
386
- const { sourceType, sourceData } = await handleSourceSelection(dataplaneUrl, authConfig);
393
+ // Step 2: Source Selection - Update session
394
+ const { sourceType, sourceData } = await handleSourceSelection(dataplaneUrl, sessionId, authConfig);
387
395
 
388
396
  // Step 3: Parse OpenAPI (if applicable)
389
397
  const openApiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
@@ -430,30 +438,25 @@ async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
430
438
  * @throws {Error} If wizard fails
431
439
  */
432
440
  async function handleWizard(options = {}) {
433
- try {
434
- logger.log(chalk.blue('\nšŸ§™ AI Fabrix External System Wizard\n'));
441
+ logger.log(chalk.blue('\nšŸ§™ AI Fabrix External System Wizard\n'));
435
442
 
436
- // Get or prompt for app name
437
- let appName = options.app;
438
- if (!appName) {
439
- appName = await promptForAppName();
440
- }
443
+ // Get or prompt for app name
444
+ let appName = options.app;
445
+ if (!appName) {
446
+ appName = await promptForAppName();
447
+ }
441
448
 
442
- // Validate app name and check directory
443
- const shouldContinue = await validateAndCheckAppDirectory(appName);
444
- if (!shouldContinue) {
445
- return;
446
- }
449
+ // Validate app name and check directory
450
+ const shouldContinue = await validateAndCheckAppDirectory(appName);
451
+ if (!shouldContinue) {
452
+ return;
453
+ }
447
454
 
448
- // Get dataplane URL and authentication
449
- const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, appName);
455
+ // Get dataplane URL and authentication
456
+ const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, appName);
450
457
 
451
- // Execute wizard flow
452
- await executeWizardFlow(appName, dataplaneUrl, authConfig);
453
- } catch (error) {
454
- logger.error(chalk.red(`\nāŒ Wizard failed: ${error.message}`));
455
- throw error;
456
- }
458
+ // Execute wizard flow
459
+ await executeWizardFlow(appName, dataplaneUrl, authConfig);
457
460
  }
458
461
 
459
462
  /**
@@ -468,29 +471,20 @@ async function handleWizard(options = {}) {
468
471
  async function setupDataplaneAndAuth(options, appName) {
469
472
  const configData = await config.getConfig();
470
473
  const environment = options.environment || 'dev';
471
- const controllerUrl = options.controller || configData.deployment?.controllerUrl || 'http://localhost:3000';
474
+ const controllerUrl = await resolveControllerUrl(options, configData);
475
+ // Wizard requires device token authentication (user-level), not client credentials
476
+ const authConfig = await getDeviceOnlyAuth(controllerUrl);
472
477
 
473
- // Get dataplane URL (either from option or from controller)
474
478
  let dataplaneUrl = options.dataplane;
475
479
  if (!dataplaneUrl) {
476
- // Get authentication first
477
- const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
478
- if (!authConfig.token && !authConfig.clientId) {
479
- throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
480
- }
481
-
482
- // Get dataplane URL from controller
483
480
  logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
484
- dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
485
- logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
486
-
487
- return { dataplaneUrl, authConfig };
488
- }
489
-
490
- // If dataplane URL provided directly, still need auth
491
- const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
492
- if (!authConfig.token && !authConfig.clientId) {
493
- throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
481
+ try {
482
+ dataplaneUrl = await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
483
+ logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
484
+ } catch (error) {
485
+ const example = `aifabrix wizard -a ${appName} --dataplane https://dataplane.example.com -e ${environment} -c ${controllerUrl}`;
486
+ throw new Error(`${error.message}\n\nšŸ’” For new applications, provide the dataplane URL using:\n --dataplane <dataplane-url>\n\n Example: ${example}`);
487
+ }
494
488
  }
495
489
 
496
490
  return { dataplaneUrl, authConfig };
@@ -40,9 +40,12 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
40
40
  }
41
41
 
42
42
  // Extract dataplane URL from application response
43
- // This is a placeholder - actual response structure may vary
43
+ // Try multiple possible locations for the URL
44
44
  const application = response.data.data || response.data;
45
- const dataplaneUrl = application.dataplaneUrl || application.dataplane?.url || application.configuration?.dataplaneUrl;
45
+ const dataplaneUrl = application.url ||
46
+ application.dataplaneUrl ||
47
+ application.dataplane?.url ||
48
+ application.configuration?.dataplaneUrl;
46
49
 
47
50
  if (!dataplaneUrl) {
48
51
  logger.error(chalk.red('āŒ Dataplane URL not found in application response'));
@@ -25,6 +25,7 @@ const logger = require('../utils/logger');
25
25
  const { getDataplaneUrl } = require('../datasource/deploy');
26
26
  const { detectAppType } = require('../utils/paths');
27
27
  const { generateExternalSystemApplicationSchema } = require('../generator/external');
28
+ const { resolveControllerUrl } = require('../utils/controller-url');
28
29
  const {
29
30
  loadVariablesYaml,
30
31
  validateSystemFiles,
@@ -109,7 +110,7 @@ async function prepareDeploymentConfig(appName, options) {
109
110
 
110
111
  const config = await getConfig();
111
112
  const environment = options.environment || 'dev';
112
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
113
+ const controllerUrl = await resolveControllerUrl(options, config);
113
114
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
114
115
 
115
116
  if (!authConfig.token && !authConfig.clientId) {
@@ -245,7 +246,7 @@ async function prepareDeploymentFiles(appName, options) {
245
246
 
246
247
  const config = await getConfig();
247
248
  const environment = options.environment || 'dev';
248
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
249
+ const controllerUrl = await resolveControllerUrl(options, config);
249
250
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
250
251
 
251
252
  if (!authConfig.token && !authConfig.clientId) {