mirror of https://github.com/buster-so/buster.git
top level imports and such moved
This commit is contained in:
parent
f9df968a15
commit
d123494372
28
CLAUDE.md
28
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(<Main />);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(<Main />);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Cross-Cutting Concerns
|
## Cross-Cutting Concerns
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { render } from 'ink';
|
||||||
import { Main } from '../commands/main/main';
|
import { Main } from '../commands/main/main';
|
||||||
import { getCurrentVersion } from '../commands/update/update-handler';
|
import { getCurrentVersion } from '../commands/update/update-handler';
|
||||||
import { setupPreActionHook } from './hooks';
|
import { setupPreActionHook } from './hooks';
|
||||||
|
import { runHeadless } from '../services/headless-handler';
|
||||||
|
|
||||||
interface RootOptions {
|
interface RootOptions {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
@ -32,7 +33,6 @@ program.action(async (options: RootOptions) => {
|
||||||
// Check if running in headless mode
|
// Check if running in headless mode
|
||||||
if (options.prompt) {
|
if (options.prompt) {
|
||||||
try {
|
try {
|
||||||
const { runHeadless } = await import('../services/headless-handler');
|
|
||||||
const chatId = await runHeadless({
|
const chatId = await runHeadless({
|
||||||
prompt: options.prompt,
|
prompt: options.prompt,
|
||||||
...(options.chatId && { chatId: options.chatId }),
|
...(options.chatId && { chatId: options.chatId }),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { createBusterSDK } from '@buster/sdk';
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { render } from 'ink';
|
import { render } from 'ink';
|
||||||
|
import { saveCredentials } from '../../utils/credentials';
|
||||||
import { Auth } from './auth';
|
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 we have an API key in CI, just validate and save it without interactive UI
|
||||||
if (isCIEnvironment && (options.apiKey || process.env.BUSTER_API_KEY)) {
|
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 apiKey = options.apiKey || process.env.BUSTER_API_KEY;
|
||||||
const host =
|
const host =
|
||||||
options.host ||
|
options.host ||
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { render } from 'ink';
|
import { render } from 'ink';
|
||||||
import { DeployCommand } from './deploy';
|
import { DeployCommand } from './deploy';
|
||||||
|
import { deployHandler } from './deploy-handler.js';
|
||||||
import { DeployOptionsSchema } from './schemas';
|
import { DeployOptionsSchema } from './schemas';
|
||||||
|
import { formatDeployError, getExitCode, isDeploymentValidationError } from './utils/errors.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the deploy command for deploying semantic models
|
* Creates the deploy command for deploying semantic models
|
||||||
|
@ -32,15 +34,9 @@ export function createDeployCommand(): Command {
|
||||||
render(<DeployCommand {...parsedOptions} />);
|
render(<DeployCommand {...parsedOptions} />);
|
||||||
} else {
|
} else {
|
||||||
// Direct execution for cleaner CLI output
|
// Direct execution for cleaner CLI output
|
||||||
const { deployHandler } = await import('./deploy-handler.js');
|
|
||||||
await deployHandler(parsedOptions);
|
await deployHandler(parsedOptions);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
// Check if it's a DeploymentValidationError to handle it specially
|
||||||
if (isDeploymentValidationError(error)) {
|
if (isDeploymentValidationError(error)) {
|
||||||
// The error message already contains the formatted output
|
// The error message already contains the formatted output
|
||||||
|
|
|
@ -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(<HelloCommand name='World' />);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain('Hello, World!');
|
|
||||||
expect(lastFrame()).toContain('Buster CLI');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render greeting in uppercase when flag is set', () => {
|
|
||||||
const { lastFrame } = render(<HelloCommand name='Claude' uppercase={true} />);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain('HELLO, CLAUDE!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render greeting in normal case when uppercase flag is false', () => {
|
|
||||||
const { lastFrame } = render(<HelloCommand name='Claude' uppercase={false} />);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain('Hello, Claude!');
|
|
||||||
expect(lastFrame()).not.toContain('HELLO, CLAUDE!');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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<HelloCommandProps> = ({ name, uppercase }) => {
|
|
||||||
const greeting = `Hello, ${name}!`;
|
|
||||||
const displayText = uppercase ? greeting.toUpperCase() : greeting;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Exit after rendering
|
|
||||||
setTimeout(() => {
|
|
||||||
process.exit(0);
|
|
||||||
}, 100);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexDirection='column'>
|
|
||||||
<Text color='green'>{chalk.bold('🚀 Buster CLI')}</Text>
|
|
||||||
<Text>{displayText}</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 (
|
|
||||||
<Box flexDirection='column'>
|
|
||||||
<Box marginBottom={1}>
|
|
||||||
<Text color='cyan' bold>
|
|
||||||
🚀 Buster CLI - Interactive Mode
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Text dimColor>Use arrow keys to navigate, Enter to select, Q to quit</Text>
|
|
||||||
<Box marginTop={1} flexDirection='column'>
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<Box key={option}>
|
|
||||||
<Text {...(selectedOption === index ? { color: 'green' } : {})}>
|
|
||||||
{selectedOption === index ? chalk.bold('▶ ') : ' '}
|
|
||||||
{option}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -14,6 +14,7 @@ import { AgentMessageComponent } from '../../components/message';
|
||||||
import { SettingsForm } from '../../components/settings-form';
|
import { SettingsForm } from '../../components/settings-form';
|
||||||
import { ExpansionContext } from '../../hooks/use-expansion';
|
import { ExpansionContext } from '../../hooks/use-expansion';
|
||||||
import type { CliAgentMessage } from '../../services/analytics-engineer-handler';
|
import type { CliAgentMessage } from '../../services/analytics-engineer-handler';
|
||||||
|
import { runAnalyticsEngineerAgent } from '../../services/analytics-engineer-handler';
|
||||||
import type { Conversation } from '../../utils/conversation-history';
|
import type { Conversation } from '../../utils/conversation-history';
|
||||||
import { loadConversation, saveModelMessages } from '../../utils/conversation-history';
|
import { loadConversation, saveModelMessages } from '../../utils/conversation-history';
|
||||||
import { getCurrentChatId, initNewSession, setSessionChatId } from '../../utils/session';
|
import { getCurrentChatId, initNewSession, setSessionChatId } from '../../utils/session';
|
||||||
|
@ -137,9 +138,6 @@ export function Main() {
|
||||||
// Save to disk
|
// Save to disk
|
||||||
await saveModelMessages(chatId, cwd, updatedModelMessages);
|
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
|
// Create AbortController for this agent execution
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { createWriteStream, existsSync } from 'node:fs';
|
import { createReadStream, createWriteStream, existsSync } from 'node:fs';
|
||||||
import { chmod, mkdir, rename, unlink } from 'node:fs/promises';
|
import { chmod, mkdir, readFile, rename, rm, unlink } from 'node:fs/promises';
|
||||||
import { arch, platform, tmpdir } from 'node:os';
|
import { arch, platform, tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
@ -102,7 +102,6 @@ async function downloadFile(url: string, destination: string): Promise<void> {
|
||||||
* Verify file checksum
|
* Verify file checksum
|
||||||
*/
|
*/
|
||||||
async function verifyChecksum(filePath: string, expectedChecksum: string): Promise<boolean> {
|
async function verifyChecksum(filePath: string, expectedChecksum: string): Promise<boolean> {
|
||||||
const { createReadStream } = await import('node:fs');
|
|
||||||
const hash = createHash('sha256');
|
const hash = createHash('sha256');
|
||||||
const stream = createReadStream(filePath);
|
const stream = createReadStream(filePath);
|
||||||
|
|
||||||
|
@ -315,7 +314,6 @@ export async function updateHandler(options: UpdateOptions): Promise<UpdateResul
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Read and validate checksum
|
// Read and validate checksum
|
||||||
const { readFile } = await import('node:fs/promises');
|
|
||||||
const checksumContent = await readFile(checksumPath, 'utf-8');
|
const checksumContent = await readFile(checksumPath, 'utf-8');
|
||||||
|
|
||||||
// SHA256 checksums are 64 hex characters
|
// SHA256 checksums are 64 hex characters
|
||||||
|
@ -346,7 +344,6 @@ export async function updateHandler(options: UpdateOptions): Promise<UpdateResul
|
||||||
await replaceBinary(extractedBinary);
|
await replaceBinary(extractedBinary);
|
||||||
|
|
||||||
// Clean up temp directory
|
// Clean up temp directory
|
||||||
const { rm } = await import('node:fs/promises');
|
|
||||||
await rm(tempDir, { recursive: true, force: true });
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -361,7 +358,6 @@ export async function updateHandler(options: UpdateOptions): Promise<UpdateResul
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clean up on error
|
// Clean up on error
|
||||||
try {
|
try {
|
||||||
const { rm } = await import('node:fs/promises');
|
|
||||||
await rm(tempDir, { recursive: true, force: true });
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { Box, Text, useApp } from 'ink';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { AnimatedLogo } from '../components/animated-logo';
|
|
||||||
|
|
||||||
export function Welcome() {
|
|
||||||
const { exit } = useApp();
|
|
||||||
|
|
||||||
// Auto-exit after a few seconds
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
exit();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [exit]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box paddingY={2} paddingX={2}>
|
|
||||||
<Box marginRight={4}>
|
|
||||||
<AnimatedLogo color='#7C3AED' />
|
|
||||||
</Box>
|
|
||||||
<Box flexDirection='column' justifyContent='center'>
|
|
||||||
<Text bold>Welcome to Buster</Text>
|
|
||||||
<Box marginTop={1}>
|
|
||||||
<Text dimColor>Type / to use slash commands</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text dimColor>Type @ to mention files</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text dimColor>Ctrl-C to exit</Text>
|
|
||||||
</Box>
|
|
||||||
<Box marginTop={2}>
|
|
||||||
<Text dimColor>/help for more</Text>
|
|
||||||
</Box>
|
|
||||||
<Box marginTop={2}>
|
|
||||||
<Text color='#7C3AED'>"Run `buster` and fix all the errors"</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getCredentials } from './credentials';
|
||||||
|
|
||||||
const ProxyConfigSchema = z.object({
|
const ProxyConfigSchema = z.object({
|
||||||
baseURL: z.string().url().describe('Base URL for the AI proxy endpoint'),
|
baseURL: z.string().url().describe('Base URL for the AI proxy endpoint'),
|
||||||
|
@ -18,7 +19,6 @@ export type ProxyConfig = z.infer<typeof ProxyConfigSchema>;
|
||||||
* API key comes from credentials (required)
|
* API key comes from credentials (required)
|
||||||
*/
|
*/
|
||||||
export async function getProxyConfig(): Promise<ProxyConfig> {
|
export async function getProxyConfig(): Promise<ProxyConfig> {
|
||||||
const { getCredentials } = await import('./credentials');
|
|
||||||
const creds = await getCredentials();
|
const creds = await getCredentials();
|
||||||
|
|
||||||
if (!creds?.apiKey) {
|
if (!creds?.apiKey) {
|
||||||
|
|
|
@ -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 { homedir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -205,7 +205,6 @@ export async function getLatestConversation(
|
||||||
*/
|
*/
|
||||||
export async function deleteConversation(chatId: string, workingDirectory: string): Promise<void> {
|
export async function deleteConversation(chatId: string, workingDirectory: string): Promise<void> {
|
||||||
const filePath = getConversationFilePath(chatId, workingDirectory);
|
const filePath = getConversationFilePath(chatId, workingDirectory);
|
||||||
const { unlink } = await import('node:fs/promises');
|
|
||||||
await unlink(filePath);
|
await unlink(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { homedir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { type VersionCache, VersionCacheSchema } from './version-schemas';
|
import { type VersionCache, VersionCacheSchema } from './version-schemas';
|
||||||
|
@ -72,7 +72,6 @@ export async function getCachedVersion(): Promise<VersionCache | null> {
|
||||||
*/
|
*/
|
||||||
export async function clearVersionCache(): Promise<void> {
|
export async function clearVersionCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { unlink } = await import('node:fs/promises');
|
|
||||||
await unlink(CACHE_FILE);
|
await unlink(CACHE_FILE);
|
||||||
} catch {
|
} catch {
|
||||||
// Cache file might not exist, that's fine
|
// Cache file might not exist, that's fine
|
||||||
|
|
Loading…
Reference in New Issue