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;
|
noSave?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_HOST = 'https://api2.buster.so';
|
const DEFAULT_HOST = 'api2.buster.so';
|
||||||
const LOCAL_HOST = 'http://localhost:3001';
|
const LOCAL_HOST = 'localhost:3001';
|
||||||
|
|
||||||
export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
||||||
const { exit } = useApp();
|
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 [apiKeyInput, setApiKeyInput] = useState('');
|
||||||
const [hostInput, setHostInput] = useState('');
|
const [hostInput, setHostInput] = useState('');
|
||||||
const [_error, setError] = useState<string | null>(null);
|
const [_error, setError] = useState<string | null>(null);
|
||||||
const [_existingCreds, setExistingCreds] = useState<Credentials | null>(null);
|
const [_existingCreds, setExistingCreds] = useState<Credentials | null>(null);
|
||||||
const [finalCreds, setFinalCreds] = 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
|
// Handle clear flag
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -45,49 +60,60 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
||||||
exit();
|
exit();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setStep('prompt');
|
// Determine initial step based on provided flags
|
||||||
}
|
|
||||||
}, [clear, exit]);
|
|
||||||
|
|
||||||
// Load existing credentials
|
|
||||||
useEffect(() => {
|
|
||||||
if (step === 'prompt') {
|
|
||||||
loadCredentials().then((creds) => {
|
loadCredentials().then((creds) => {
|
||||||
setExistingCreds(creds);
|
setExistingCreds(creds);
|
||||||
|
|
||||||
// Determine the host to use
|
// Determine the host to use
|
||||||
let targetHost = DEFAULT_HOST;
|
let targetHost = '';
|
||||||
if (local) targetHost = LOCAL_HOST;
|
if (local) {
|
||||||
else if (cloud) targetHost = DEFAULT_HOST;
|
targetHost = LOCAL_HOST;
|
||||||
else if (host) targetHost = host;
|
} else if (cloud) {
|
||||||
else if (creds?.apiUrl) targetHost = creds.apiUrl;
|
targetHost = DEFAULT_HOST;
|
||||||
|
} else if (host) {
|
||||||
|
targetHost = host;
|
||||||
|
}
|
||||||
|
|
||||||
setHostInput(targetHost);
|
// If we have both host and apiKey from flags, skip to validation
|
||||||
setApiKeyInput(apiKey || '');
|
if ((targetHost || host) && apiKey) {
|
||||||
|
const finalHost = normalizeHost(targetHost || host || DEFAULT_HOST);
|
||||||
// If we have all required info from args, skip to validation
|
setFinalCreds({ apiKey, apiUrl: finalHost });
|
||||||
if (apiKey) {
|
|
||||||
setFinalCreds({ apiKey, apiUrl: targetHost });
|
|
||||||
setStep('validate');
|
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
|
// Handle input completion
|
||||||
useInput((_input, key) => {
|
useInput((_input, key) => {
|
||||||
if (key.return && step === 'prompt') {
|
if (key.return) {
|
||||||
if (!hostInput) {
|
if (step === 'host') {
|
||||||
setError('Host URL is required');
|
// Use default host if empty
|
||||||
return;
|
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');
|
setStep(noSave ? 'done' : 'save');
|
||||||
} else {
|
} else {
|
||||||
setError('Invalid API key');
|
setError('Invalid API key');
|
||||||
setStep('prompt');
|
setStep('apikey');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
setError(`Validation failed: ${err.message}`);
|
setError(`Validation failed: ${err.message}`);
|
||||||
setStep('prompt');
|
setStep('apikey');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [step, finalCreds, noSave]);
|
}, [step, finalCreds, noSave]);
|
||||||
|
@ -135,11 +161,13 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step === 'done' && finalCreds) {
|
if (step === 'done' && finalCreds) {
|
||||||
const masked = finalCreds.apiKey.length > 6 ? `****${finalCreds.apiKey.slice(-6)}` : '****';
|
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("\n✅ You've successfully connected to Buster!\n");
|
||||||
console.log('Connection details:');
|
console.log('Connection details:');
|
||||||
console.log(` host: ${finalCreds.apiUrl}`);
|
console.log(` Host: ${displayHost}`);
|
||||||
console.log(` api_key: ${masked}`);
|
console.log(` API Key: ${masked}`);
|
||||||
|
|
||||||
if (!noSave && step === 'done') {
|
if (!noSave && step === 'done') {
|
||||||
console.log('\nCredentials saved successfully!');
|
console.log('\nCredentials saved successfully!');
|
||||||
|
@ -156,33 +184,48 @@ export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
||||||
return <Text>Clearing credentials...</Text>;
|
return <Text>Clearing credentials...</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === 'prompt') {
|
if (step === 'host') {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection='column'>
|
<Box flexDirection='column'>
|
||||||
{_existingCreds && (
|
{_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>
|
||||||
|
<Text>Enter your Buster API host (default: {DEFAULT_HOST}): </Text>
|
||||||
<Box marginY={1}>
|
|
||||||
<Text>Enter the URL of your Buster API (default: {DEFAULT_HOST}): </Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<TextInput value={hostInput} onChange={setHostInput} placeholder={DEFAULT_HOST} />
|
<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>
|
<Text>Enter your API key: </Text>
|
||||||
</Box>
|
</Box>
|
||||||
<TextInput value={apiKeyInput} onChange={setApiKeyInput} mask='*' />
|
<TextInput value={apiKeyInput} onChange={setApiKeyInput} mask='*' />
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text dimColor>
|
<Text dimColor>Find your API key at https://{displayHost}/app/settings/api-keys</Text>
|
||||||
Find your API key at {hostInput || DEFAULT_HOST}/app/settings/api-keys
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text dimColor>Press Enter when ready to continue</Text>
|
<Text dimColor>Press Enter to authenticate</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"@buster/typescript-config": "workspace:*",
|
"@buster/typescript-config": "workspace:*",
|
||||||
"@buster/vitest-config": "workspace:*",
|
"@buster/vitest-config": "workspace:*",
|
||||||
"@buster/web-tools": "workspace:*",
|
"@buster/web-tools": "workspace:*",
|
||||||
"@trigger.dev/sdk": "4.0.1",
|
"@trigger.dev/sdk": "4.0.2",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"braintrust": "catalog:",
|
"braintrust": "catalog:",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
|
@ -39,6 +39,6 @@
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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,
|
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 {
|
return {
|
||||||
config: validatedConfig,
|
config: validatedConfig,
|
||||||
healthcheck: () => get(validatedConfig, '/api/healthcheck'),
|
healthcheck: () => get(validatedConfig, '/healthcheck'),
|
||||||
auth: {
|
auth: {
|
||||||
validateApiKey: (apiKey?: string) => validateApiKey(validatedConfig, apiKey),
|
validateApiKey: (apiKey?: string) => validateApiKey(validatedConfig, apiKey),
|
||||||
isApiKeyValid: (apiKey?: string) => isApiKeyValid(validatedConfig, apiKey),
|
isApiKeyValid: (apiKey?: string) => isApiKeyValid(validatedConfig, apiKey),
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||||
// SDK Configuration Schema
|
// SDK Configuration Schema
|
||||||
export const SDKConfigSchema = z.object({
|
export const SDKConfigSchema = z.object({
|
||||||
apiKey: z.string().min(1, 'API key is required'),
|
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),
|
timeout: z.number().min(1000).max(60000).default(30000),
|
||||||
retryAttempts: z.number().min(0).max(5).default(3),
|
retryAttempts: z.number().min(0).max(5).default(3),
|
||||||
retryDelay: z.number().min(100).max(5000).default(1000),
|
retryDelay: z.number().min(100).max(5000).default(1000),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { DeployRequest, DeployResponse } from '@buster/server-shared';
|
import type { DeployRequest, DeployResponse } from '@buster/server-shared';
|
||||||
import type { SDKConfig } from '../config';
|
import type { SDKConfig } from '../config';
|
||||||
import { post } from '../http';
|
import { get, post } from '../http';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deploy semantic models to the Buster API
|
* Deploy semantic models to the Buster API
|
||||||
|
@ -10,7 +10,8 @@ export async function deployDatasets(
|
||||||
config: SDKConfig,
|
config: SDKConfig,
|
||||||
request: DeployRequest
|
request: DeployRequest
|
||||||
): Promise<DeployResponse> {
|
): 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,
|
config: SDKConfig,
|
||||||
dataSourceId?: string
|
dataSourceId?: string
|
||||||
): Promise<{ datasets: unknown[] }> {
|
): Promise<{ datasets: unknown[] }> {
|
||||||
const path = dataSourceId ? `/api/v2/datasets?dataSourceId=${dataSourceId}` : '/api/v2/datasets';
|
// The HTTP client will automatically add /api/v2 prefix
|
||||||
|
const params = dataSourceId ? { dataSourceId } : undefined;
|
||||||
const response = await fetch(new URL(path, config.apiUrl).toString(), {
|
return get<{ datasets: unknown[] }>(config, '/datasets', params);
|
||||||
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[] }>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import type { SDKConfig } from '../config';
|
import type { SDKConfig } from '../config';
|
||||||
import { NetworkError, SDKError } from '../errors';
|
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 {
|
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) {
|
if (params) {
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
|
Loading…
Reference in New Issue