From b5e931dcb87a204255c25dd8ed83f30a9dca4148 Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 3 Sep 2025 09:41:52 -0600 Subject: [PATCH] cli --- apps/cli/src/commands/auth.tsx | 139 ++++++++++++------ apps/trigger/package.json | 6 +- packages/sdk/src/lib/auth/validate-api-key.ts | 5 +- packages/sdk/src/lib/client/buster-sdk.ts | 2 +- packages/sdk/src/lib/config/config-schema.ts | 2 +- packages/sdk/src/lib/datasets/deploy.ts | 24 +-- packages/sdk/src/lib/http/http-client.ts | 8 +- 7 files changed, 111 insertions(+), 75 deletions(-) diff --git a/apps/cli/src/commands/auth.tsx b/apps/cli/src/commands/auth.tsx index 7c9e593f6..7f7cdca1c 100644 --- a/apps/cli/src/commands/auth.tsx +++ b/apps/cli/src/commands/auth.tsx @@ -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(null); const [_existingCreds, setExistingCreds] = useState(null); const [finalCreds, setFinalCreds] = useState(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 Clearing credentials...; } - if (step === 'prompt') { + if (step === 'host') { return ( {_existingCreds && ( - ⚠️ Existing credentials found. They will be overwritten. + + ⚠️ Existing credentials found. They will be overwritten. + )} - {_error && ❌ {_error}} - - - Enter the URL of your Buster API (default: {DEFAULT_HOST}): + + Enter your Buster API host (default: {DEFAULT_HOST}): - + + Press Enter to continue (leave empty for default) + + + ); + } + + if (step === 'apikey') { + const displayHost = (hostInput || DEFAULT_HOST).replace(/^https?:\/\//, ''); + return ( + + {_error && ( + + ❌ {_error} + + )} + + Enter your API key: - - Find your API key at {hostInput || DEFAULT_HOST}/app/settings/api-keys - + Find your API key at https://{displayHost}/app/settings/api-keys - Press Enter when ready to continue + Press Enter to authenticate ); diff --git a/apps/trigger/package.json b/apps/trigger/package.json index 51d279376..865c275a0 100644 --- a/apps/trigger/package.json +++ b/apps/trigger/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/packages/sdk/src/lib/auth/validate-api-key.ts b/packages/sdk/src/lib/auth/validate-api-key.ts index b3488fe32..9b81379ef 100644 --- a/packages/sdk/src/lib/auth/validate-api-key.ts +++ b/packages/sdk/src/lib/auth/validate-api-key.ts @@ -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(config, '/api/v2/auth/validate-api-key', request); + return post(config, '/auth/validate-api-key', request); } /** diff --git a/packages/sdk/src/lib/client/buster-sdk.ts b/packages/sdk/src/lib/client/buster-sdk.ts index 0f6ed7d59..5c8d7f15b 100644 --- a/packages/sdk/src/lib/client/buster-sdk.ts +++ b/packages/sdk/src/lib/client/buster-sdk.ts @@ -25,7 +25,7 @@ export function createBusterSDK(config: Partial): 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), diff --git a/packages/sdk/src/lib/config/config-schema.ts b/packages/sdk/src/lib/config/config-schema.ts index 23cfcf5bc..3ccba2c99 100644 --- a/packages/sdk/src/lib/config/config-schema.ts +++ b/packages/sdk/src/lib/config/config-schema.ts @@ -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), diff --git a/packages/sdk/src/lib/datasets/deploy.ts b/packages/sdk/src/lib/datasets/deploy.ts index c956c8d62..d61dfce0c 100644 --- a/packages/sdk/src/lib/datasets/deploy.ts +++ b/packages/sdk/src/lib/datasets/deploy.ts @@ -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 { - return post(config, '/api/v2/datasets/deploy', request); + // The HTTP client will automatically add /api/v2 prefix + return post(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); } diff --git a/packages/sdk/src/lib/http/http-client.ts b/packages/sdk/src/lib/http/http-client.ts index bb85b6cd0..3e85c4c96 100644 --- a/packages/sdk/src/lib/http/http-client.ts +++ b/packages/sdk/src/lib/http/http-client.ts @@ -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 { - 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]) => {