mirror of https://github.com/buster-so/buster.git
cli
This commit is contained in:
parent
e165a37b36
commit
b5e931dcb8
|
@ -20,17 +20,32 @@ interface AuthProps {
|
|||
noSave?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_HOST = 'https://api2.buster.so';
|
||||
const LOCAL_HOST = 'http://localhost:3001';
|
||||
const DEFAULT_HOST = 'api2.buster.so';
|
||||
const LOCAL_HOST = 'localhost:3001';
|
||||
|
||||
export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
||||
const { exit } = useApp();
|
||||
const [step, setStep] = useState<'clear' | 'prompt' | 'validate' | 'save' | 'done'>('clear');
|
||||
const [step, setStep] = useState<'clear' | 'host' | 'apikey' | 'validate' | 'save' | 'done'>(
|
||||
'clear'
|
||||
);
|
||||
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||
const [hostInput, setHostInput] = useState('');
|
||||
const [_error, setError] = useState<string | null>(null);
|
||||
const [_existingCreds, setExistingCreds] = useState<Credentials | null>(null);
|
||||
const [finalCreds, setFinalCreds] = useState<Credentials | null>(null);
|
||||
const [_promptStage, setPromptStage] = useState<'host' | 'apikey'>('host');
|
||||
|
||||
// Normalize host URL (add https:// if missing, unless localhost)
|
||||
const normalizeHost = (h: string): string => {
|
||||
const trimmed = h.trim();
|
||||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.includes('localhost') || trimmed.includes('127.0.0.1')) {
|
||||
return `http://${trimmed}`;
|
||||
}
|
||||
return `https://${trimmed}`;
|
||||
};
|
||||
|
||||
// Handle clear flag
|
||||
useEffect(() => {
|
||||
|
@ -45,49 +60,60 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
|||
exit();
|
||||
});
|
||||
} else {
|
||||
setStep('prompt');
|
||||
}
|
||||
}, [clear, exit]);
|
||||
|
||||
// Load existing credentials
|
||||
useEffect(() => {
|
||||
if (step === 'prompt') {
|
||||
// Determine initial step based on provided flags
|
||||
loadCredentials().then((creds) => {
|
||||
setExistingCreds(creds);
|
||||
|
||||
// Determine the host to use
|
||||
let targetHost = DEFAULT_HOST;
|
||||
if (local) targetHost = LOCAL_HOST;
|
||||
else if (cloud) targetHost = DEFAULT_HOST;
|
||||
else if (host) targetHost = host;
|
||||
else if (creds?.apiUrl) targetHost = creds.apiUrl;
|
||||
let targetHost = '';
|
||||
if (local) {
|
||||
targetHost = LOCAL_HOST;
|
||||
} else if (cloud) {
|
||||
targetHost = DEFAULT_HOST;
|
||||
} else if (host) {
|
||||
targetHost = host;
|
||||
}
|
||||
|
||||
setHostInput(targetHost);
|
||||
setApiKeyInput(apiKey || '');
|
||||
|
||||
// If we have all required info from args, skip to validation
|
||||
if (apiKey) {
|
||||
setFinalCreds({ apiKey, apiUrl: targetHost });
|
||||
// If we have both host and apiKey from flags, skip to validation
|
||||
if ((targetHost || host) && apiKey) {
|
||||
const finalHost = normalizeHost(targetHost || host || DEFAULT_HOST);
|
||||
setFinalCreds({ apiKey, apiUrl: finalHost });
|
||||
setStep('validate');
|
||||
} else if (targetHost || host) {
|
||||
// If we only have host, set it and prompt for API key
|
||||
setHostInput(targetHost || host || '');
|
||||
setApiKeyInput(apiKey || '');
|
||||
setStep('apikey');
|
||||
setPromptStage('apikey');
|
||||
} else {
|
||||
// No flags provided, start with host prompt
|
||||
setHostInput('');
|
||||
setApiKeyInput(apiKey || '');
|
||||
setStep('host');
|
||||
setPromptStage('host');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [step, apiKey, host, local, cloud]);
|
||||
}, [clear, apiKey, host, local, cloud, exit]);
|
||||
|
||||
// Handle input completion
|
||||
useInput((_input, key) => {
|
||||
if (key.return && step === 'prompt') {
|
||||
if (!hostInput) {
|
||||
setError('Host URL is required');
|
||||
return;
|
||||
if (key.return) {
|
||||
if (step === 'host') {
|
||||
// Use default host if empty
|
||||
const finalHost = hostInput.trim() || DEFAULT_HOST;
|
||||
setHostInput(finalHost);
|
||||
setStep('apikey');
|
||||
setPromptStage('apikey');
|
||||
} else if (step === 'apikey') {
|
||||
if (!apiKeyInput.trim()) {
|
||||
setError('API key is required');
|
||||
return;
|
||||
}
|
||||
const finalHost = normalizeHost(hostInput || DEFAULT_HOST);
|
||||
setFinalCreds({ apiKey: apiKeyInput.trim(), apiUrl: finalHost });
|
||||
setStep('validate');
|
||||
}
|
||||
if (!apiKeyInput) {
|
||||
setError('API key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setFinalCreds({ apiKey: apiKeyInput, apiUrl: hostInput });
|
||||
setStep('validate');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -107,12 +133,12 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
|||
setStep(noSave ? 'done' : 'save');
|
||||
} else {
|
||||
setError('Invalid API key');
|
||||
setStep('prompt');
|
||||
setStep('apikey');
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
setError(`Validation failed: ${err.message}`);
|
||||
setStep('prompt');
|
||||
setStep('apikey');
|
||||
});
|
||||
}
|
||||
}, [step, finalCreds, noSave]);
|
||||
|
@ -135,11 +161,13 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
|||
useEffect(() => {
|
||||
if (step === 'done' && finalCreds) {
|
||||
const masked = finalCreds.apiKey.length > 6 ? `****${finalCreds.apiKey.slice(-6)}` : '****';
|
||||
// Remove protocol for display
|
||||
const displayHost = finalCreds.apiUrl.replace(/^https?:\/\//, '');
|
||||
|
||||
console.log("\n✅ You've successfully connected to Buster!\n");
|
||||
console.log('Connection details:');
|
||||
console.log(` host: ${finalCreds.apiUrl}`);
|
||||
console.log(` api_key: ${masked}`);
|
||||
console.log(` Host: ${displayHost}`);
|
||||
console.log(` API Key: ${masked}`);
|
||||
|
||||
if (!noSave && step === 'done') {
|
||||
console.log('\nCredentials saved successfully!');
|
||||
|
@ -156,33 +184,48 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
|||
return <Text>Clearing credentials...</Text>;
|
||||
}
|
||||
|
||||
if (step === 'prompt') {
|
||||
if (step === 'host') {
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
{_existingCreds && (
|
||||
<Text color='yellow'>⚠️ Existing credentials found. They will be overwritten.</Text>
|
||||
<Box marginBottom={1}>
|
||||
<Text color='yellow'>⚠️ Existing credentials found. They will be overwritten.</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{_error && <Text color='red'>❌ {_error}</Text>}
|
||||
|
||||
<Box marginY={1}>
|
||||
<Text>Enter the URL of your Buster API (default: {DEFAULT_HOST}): </Text>
|
||||
<Box>
|
||||
<Text>Enter your Buster API host (default: {DEFAULT_HOST}): </Text>
|
||||
</Box>
|
||||
<TextInput value={hostInput} onChange={setHostInput} placeholder={DEFAULT_HOST} />
|
||||
|
||||
<Box marginY={1}>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press Enter to continue (leave empty for default)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'apikey') {
|
||||
const displayHost = (hostInput || DEFAULT_HOST).replace(/^https?:\/\//, '');
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
{_error && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color='red'>❌ {_error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text>Enter your API key: </Text>
|
||||
</Box>
|
||||
<TextInput value={apiKeyInput} onChange={setApiKeyInput} mask='*' />
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Find your API key at {hostInput || DEFAULT_HOST}/app/settings/api-keys
|
||||
</Text>
|
||||
<Text dimColor>Find your API key at https://{displayHost}/app/settings/api-keys</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press Enter when ready to continue</Text>
|
||||
<Text dimColor>Press Enter to authenticate</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"@buster/typescript-config": "workspace:*",
|
||||
"@buster/vitest-config": "workspace:*",
|
||||
"@buster/web-tools": "workspace:*",
|
||||
"@trigger.dev/sdk": "4.0.1",
|
||||
"@trigger.dev/sdk": "4.0.2",
|
||||
"ai": "catalog:",
|
||||
"braintrust": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
|
@ -39,6 +39,6 @@
|
|||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trigger.dev/build": "4.0.1"
|
||||
"@trigger.dev/build": "4.0.2"
|
||||
}
|
||||
}
|
|
@ -16,9 +16,10 @@ export async function validateApiKey(
|
|||
apiKey: apiKey || config.apiKey,
|
||||
};
|
||||
|
||||
console.info(`Validating API key with endpoint: ${config.apiUrl}/api/v2/auth/validate-api-key`);
|
||||
// The HTTP client will automatically add /api/v2 prefix
|
||||
console.info(`Validating API key with host: ${config.apiUrl}`);
|
||||
|
||||
return post<ValidateApiKeyResponse>(config, '/api/v2/auth/validate-api-key', request);
|
||||
return post<ValidateApiKeyResponse>(config, '/auth/validate-api-key', request);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,7 @@ export function createBusterSDK(config: Partial<SDKConfig>): BusterSDK {
|
|||
|
||||
return {
|
||||
config: validatedConfig,
|
||||
healthcheck: () => get(validatedConfig, '/api/healthcheck'),
|
||||
healthcheck: () => get(validatedConfig, '/healthcheck'),
|
||||
auth: {
|
||||
validateApiKey: (apiKey?: string) => validateApiKey(validatedConfig, apiKey),
|
||||
isApiKeyValid: (apiKey?: string) => isApiKeyValid(validatedConfig, apiKey),
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
|||
// SDK Configuration Schema
|
||||
export const SDKConfigSchema = z.object({
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
apiUrl: z.string().url().default('https://api2.buster.so'),
|
||||
apiUrl: z.string().url().default('https://api2.buster.so'), // Base URL without /api/v2
|
||||
timeout: z.number().min(1000).max(60000).default(30000),
|
||||
retryAttempts: z.number().min(0).max(5).default(3),
|
||||
retryDelay: z.number().min(100).max(5000).default(1000),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { DeployRequest, DeployResponse } from '@buster/server-shared';
|
||||
import type { SDKConfig } from '../config';
|
||||
import { post } from '../http';
|
||||
import { get, post } from '../http';
|
||||
|
||||
/**
|
||||
* Deploy semantic models to the Buster API
|
||||
|
@ -10,7 +10,8 @@ export async function deployDatasets(
|
|||
config: SDKConfig,
|
||||
request: DeployRequest
|
||||
): Promise<DeployResponse> {
|
||||
return post<DeployResponse>(config, '/api/v2/datasets/deploy', request);
|
||||
// The HTTP client will automatically add /api/v2 prefix
|
||||
return post<DeployResponse>(config, '/datasets/deploy', request);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,20 +21,7 @@ export async function getDatasets(
|
|||
config: SDKConfig,
|
||||
dataSourceId?: string
|
||||
): Promise<{ datasets: unknown[] }> {
|
||||
const path = dataSourceId ? `/api/v2/datasets?dataSourceId=${dataSourceId}` : '/api/v2/datasets';
|
||||
|
||||
const response = await fetch(new URL(path, config.apiUrl).toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
...config.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get datasets: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<{ datasets: unknown[] }>;
|
||||
// The HTTP client will automatically add /api/v2 prefix
|
||||
const params = dataSourceId ? { dataSourceId } : undefined;
|
||||
return get<{ datasets: unknown[] }>(config, '/datasets', params);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import type { SDKConfig } from '../config';
|
||||
import { NetworkError, SDKError } from '../errors';
|
||||
|
||||
// Build URL with query params
|
||||
// Build URL with query params and /api/v2 prefix for Buster API routes
|
||||
export function buildUrl(baseUrl: string, path: string, params?: Record<string, string>): string {
|
||||
const url = new URL(path, baseUrl);
|
||||
// If the path doesn't start with /api, assume it's a Buster API route and add /api/v2 prefix
|
||||
const finalPath = path.startsWith('/api')
|
||||
? path
|
||||
: `/api/v2${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const url = new URL(finalPath, baseUrl);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
|
|
Loading…
Reference in New Issue