diff --git a/CLAUDE.md b/CLAUDE.md index b34afddcd..6656f319a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,34 @@ class UserService { // Never do this } ``` +### Import Patterns +- **Always use top-level imports** - Import all dependencies at the top of the file +- **No dynamic imports** - Avoid `await import()` in the middle of functions +- **Rationale** - Prioritize code simplicity and consistency over premature optimization +- **Exception** - Only use dynamic imports if you have clear evidence of measurable startup performance issues + +```typescript +// Good: Top-level imports +import { runHeadless } from '../services/headless-handler'; +import { render } from 'ink'; + +export function handleCommand(options) { + if (options.prompt) { + return runHeadless(options); + } + return render(
); +} + +// Bad: Dynamic imports without justification +export async function handleCommand(options) { + if (options.prompt) { + const { runHeadless } = await import('../services/headless-handler'); + return runHeadless(options); + } + return render(
); +} +``` + ## Cross-Cutting Concerns ### Environment Variables diff --git a/apps/cli/src/buster/program.tsx b/apps/cli/src/buster/program.tsx index f2e9677b7..b11cd880c 100644 --- a/apps/cli/src/buster/program.tsx +++ b/apps/cli/src/buster/program.tsx @@ -3,6 +3,7 @@ import { render } from 'ink'; import { Main } from '../commands/main/main'; import { getCurrentVersion } from '../commands/update/update-handler'; import { setupPreActionHook } from './hooks'; +import { runHeadless } from '../services/headless-handler'; interface RootOptions { cwd?: string; @@ -32,7 +33,6 @@ program.action(async (options: RootOptions) => { // Check if running in headless mode if (options.prompt) { try { - const { runHeadless } = await import('../services/headless-handler'); const chatId = await runHeadless({ prompt: options.prompt, ...(options.chatId && { chatId: options.chatId }), diff --git a/apps/cli/src/commands/auth/index.tsx b/apps/cli/src/commands/auth/index.tsx index 074aea164..21809f6a4 100644 --- a/apps/cli/src/commands/auth/index.tsx +++ b/apps/cli/src/commands/auth/index.tsx @@ -1,5 +1,7 @@ +import { createBusterSDK } from '@buster/sdk'; import { Command } from 'commander'; import { render } from 'ink'; +import { saveCredentials } from '../../utils/credentials'; import { Auth } from './auth'; /** @@ -33,9 +35,6 @@ export function createAuthCommand(): Command { // If we have an API key in CI, just validate and save it without interactive UI if (isCIEnvironment && (options.apiKey || process.env.BUSTER_API_KEY)) { - const { createBusterSDK } = await import('@buster/sdk'); - const { saveCredentials } = await import('../../utils/credentials'); - const apiKey = options.apiKey || process.env.BUSTER_API_KEY; const host = options.host || diff --git a/apps/cli/src/commands/deploy/index.tsx b/apps/cli/src/commands/deploy/index.tsx index 54df38438..af702eab4 100644 --- a/apps/cli/src/commands/deploy/index.tsx +++ b/apps/cli/src/commands/deploy/index.tsx @@ -1,7 +1,9 @@ import { Command } from 'commander'; import { render } from 'ink'; import { DeployCommand } from './deploy'; +import { deployHandler } from './deploy-handler.js'; import { DeployOptionsSchema } from './schemas'; +import { formatDeployError, getExitCode, isDeploymentValidationError } from './utils/errors.js'; /** * Creates the deploy command for deploying semantic models @@ -32,15 +34,9 @@ export function createDeployCommand(): Command { render(); } else { // Direct execution for cleaner CLI output - const { deployHandler } = await import('./deploy-handler.js'); await deployHandler(parsedOptions); } } catch (error) { - // Import the error formatter and type guard - const { isDeploymentValidationError, formatDeployError, getExitCode } = await import( - './utils/errors.js' - ); - // Check if it's a DeploymentValidationError to handle it specially if (isDeploymentValidationError(error)) { // The error message already contains the formatted output diff --git a/apps/cli/src/commands/hello.test.tsx b/apps/cli/src/commands/hello.test.tsx deleted file mode 100644 index affec0373..000000000 --- a/apps/cli/src/commands/hello.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { render } from 'ink-testing-library'; -import { describe, expect, it } from 'vitest'; -import { HelloCommand } from './hello'; - -describe('HelloCommand', () => { - it('should render greeting with default name', () => { - const { lastFrame } = render(); - - expect(lastFrame()).toContain('Hello, World!'); - expect(lastFrame()).toContain('Buster CLI'); - }); - - it('should render greeting in uppercase when flag is set', () => { - const { lastFrame } = render(); - - expect(lastFrame()).toContain('HELLO, CLAUDE!'); - }); - - it('should render greeting in normal case when uppercase flag is false', () => { - const { lastFrame } = render(); - - expect(lastFrame()).toContain('Hello, Claude!'); - expect(lastFrame()).not.toContain('HELLO, CLAUDE!'); - }); -}); diff --git a/apps/cli/src/commands/hello.tsx b/apps/cli/src/commands/hello.tsx deleted file mode 100644 index 95e0c9f3c..000000000 --- a/apps/cli/src/commands/hello.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import chalk from 'chalk'; -import { Box, Text } from 'ink'; -import type React from 'react'; -import { useEffect } from 'react'; - -interface HelloCommandProps { - name: string; - uppercase?: boolean; -} - -export const HelloCommand: React.FC = ({ name, uppercase }) => { - const greeting = `Hello, ${name}!`; - const displayText = uppercase ? greeting.toUpperCase() : greeting; - - useEffect(() => { - // Exit after rendering - setTimeout(() => { - process.exit(0); - }, 100); - }, []); - - return ( - - {chalk.bold('🚀 Buster CLI')} - {displayText} - - ); -}; diff --git a/apps/cli/src/commands/interactive.tsx b/apps/cli/src/commands/interactive.tsx deleted file mode 100644 index f5dc98d5b..000000000 --- a/apps/cli/src/commands/interactive.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import chalk from 'chalk'; -import { Box, Text, useApp, useInput } from 'ink'; -import type React from 'react'; -import { useState } from 'react'; - -export const InteractiveCommand: React.FC = () => { - const [selectedOption, setSelectedOption] = useState(0); - const { exit } = useApp(); - - const options = ['Create a new project', 'Deploy to production', 'Run tests', 'Exit']; - - useInput((input, key) => { - if (key.upArrow) { - setSelectedOption((prev) => Math.max(0, prev - 1)); - } else if (key.downArrow) { - setSelectedOption((prev) => Math.min(options.length - 1, prev + 1)); - } else if (key.return) { - if (selectedOption === options.length - 1) { - exit(); - } else { - // Handle selection - console.info(`\nYou selected: ${options[selectedOption]}`); - exit(); - } - } else if (input === 'q' || key.escape) { - exit(); - } - }); - - return ( - - - - 🚀 Buster CLI - Interactive Mode - - - - Use arrow keys to navigate, Enter to select, Q to quit - - {options.map((option, index) => ( - - - {selectedOption === index ? chalk.bold('▶ ') : ' '} - {option} - - - ))} - - - ); -}; diff --git a/apps/cli/src/commands/main/main.tsx b/apps/cli/src/commands/main/main.tsx index c67b74478..44081ff23 100644 --- a/apps/cli/src/commands/main/main.tsx +++ b/apps/cli/src/commands/main/main.tsx @@ -14,6 +14,7 @@ import { AgentMessageComponent } from '../../components/message'; import { SettingsForm } from '../../components/settings-form'; import { ExpansionContext } from '../../hooks/use-expansion'; import type { CliAgentMessage } from '../../services/analytics-engineer-handler'; +import { runAnalyticsEngineerAgent } from '../../services/analytics-engineer-handler'; import type { Conversation } from '../../utils/conversation-history'; import { loadConversation, saveModelMessages } from '../../utils/conversation-history'; import { getCurrentChatId, initNewSession, setSessionChatId } from '../../utils/session'; @@ -137,9 +138,6 @@ export function Main() { // Save to disk await saveModelMessages(chatId, cwd, updatedModelMessages); - // Import and run the analytics engineer agent - const { runAnalyticsEngineerAgent } = await import('../../services/analytics-engineer-handler'); - // Create AbortController for this agent execution const abortController = new AbortController(); abortControllerRef.current = abortController; diff --git a/apps/cli/src/commands/update/update-handler.ts b/apps/cli/src/commands/update/update-handler.ts index eaa892016..5d4652880 100644 --- a/apps/cli/src/commands/update/update-handler.ts +++ b/apps/cli/src/commands/update/update-handler.ts @@ -1,7 +1,7 @@ import { spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { createWriteStream, existsSync } from 'node:fs'; -import { chmod, mkdir, rename, unlink } from 'node:fs/promises'; +import { createReadStream, createWriteStream, existsSync } from 'node:fs'; +import { chmod, mkdir, readFile, rename, rm, unlink } from 'node:fs/promises'; import { arch, platform, tmpdir } from 'node:os'; import { join } from 'node:path'; import { pipeline } from 'node:stream/promises'; @@ -102,7 +102,6 @@ async function downloadFile(url: string, destination: string): Promise { * Verify file checksum */ async function verifyChecksum(filePath: string, expectedChecksum: string): Promise { - const { createReadStream } = await import('node:fs'); const hash = createHash('sha256'); const stream = createReadStream(filePath); @@ -315,7 +314,6 @@ export async function updateHandler(options: UpdateOptions): Promise { - const timer = setTimeout(() => { - exit(); - }, 5000); - - return () => clearTimeout(timer); - }, [exit]); - - return ( - - - - - - Welcome to Buster - - Type / to use slash commands - - - Type @ to mention files - - - Ctrl-C to exit - - - /help for more - - - "Run `buster` and fix all the errors" - - - - ); -} diff --git a/apps/cli/src/utils/ai-proxy.ts b/apps/cli/src/utils/ai-proxy.ts index fad40a6eb..825b0c15f 100644 --- a/apps/cli/src/utils/ai-proxy.ts +++ b/apps/cli/src/utils/ai-proxy.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { getCredentials } from './credentials'; const ProxyConfigSchema = z.object({ baseURL: z.string().url().describe('Base URL for the AI proxy endpoint'), @@ -18,7 +19,6 @@ export type ProxyConfig = z.infer; * API key comes from credentials (required) */ export async function getProxyConfig(): Promise { - const { getCredentials } = await import('./credentials'); const creds = await getCredentials(); if (!creds?.apiKey) { diff --git a/apps/cli/src/utils/conversation-history.ts b/apps/cli/src/utils/conversation-history.ts index 64faf2d84..fafdc8795 100644 --- a/apps/cli/src/utils/conversation-history.ts +++ b/apps/cli/src/utils/conversation-history.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { z } from 'zod'; @@ -205,7 +205,6 @@ export async function getLatestConversation( */ export async function deleteConversation(chatId: string, workingDirectory: string): Promise { const filePath = getConversationFilePath(chatId, workingDirectory); - const { unlink } = await import('node:fs/promises'); await unlink(filePath); } diff --git a/apps/cli/src/utils/version/version-cache.ts b/apps/cli/src/utils/version/version-cache.ts index 6d16177cd..bd09ecce3 100644 --- a/apps/cli/src/utils/version/version-cache.ts +++ b/apps/cli/src/utils/version/version-cache.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { type VersionCache, VersionCacheSchema } from './version-schemas'; @@ -72,7 +72,6 @@ export async function getCachedVersion(): Promise { */ export async function clearVersionCache(): Promise { try { - const { unlink } = await import('node:fs/promises'); await unlink(CACHE_FILE); } catch { // Cache file might not exist, that's fine