2025-08-05 06:16:00 +08:00
|
|
|
# Buster CLI Development Guidelines
|
|
|
|
|
|
|
|
This document provides specific guidelines for developing the TypeScript-based Buster CLI in this monorepo.
|
|
|
|
|
|
|
|
## Architecture Overview
|
|
|
|
|
|
|
|
The Buster CLI is a **thin client** that serves as a gateway to the Buster server API. It handles:
|
|
|
|
- File system operations (reading/writing YAML files)
|
|
|
|
- API communication with the server
|
|
|
|
- Rich terminal UI using Ink (React for CLI)
|
|
|
|
|
|
|
|
**Important**: The CLI does NOT directly access databases or AI services. All business logic is handled by the server.
|
|
|
|
|
|
|
|
## Directory Structure
|
|
|
|
|
|
|
|
```
|
|
|
|
apps/cli/
|
|
|
|
├── src/
|
|
|
|
│ ├── commands/ # Command implementations
|
|
|
|
│ │ ├── auth/ # Each command in its own folder
|
|
|
|
│ │ │ ├── index.ts # Command definition and setup
|
|
|
|
│ │ │ ├── types.ts # Command-specific types (Zod schemas)
|
|
|
|
│ │ │ ├── helpers.ts # Command utilities
|
|
|
|
│ │ │ └── auth.test.ts
|
|
|
|
│ │ ├── init/
|
|
|
|
│ │ ├── deploy/
|
|
|
|
│ │ └── ...
|
|
|
|
│ ├── components/ # Reusable Ink UI components
|
|
|
|
│ │ ├── forms/ # Form components
|
|
|
|
│ │ ├── tables/ # Table display components
|
|
|
|
│ │ ├── progress/ # Progress indicators
|
|
|
|
│ │ ├── prompts/ # Input prompts
|
|
|
|
│ │ └── status/ # Status displays
|
|
|
|
│ ├── utils/ # Shared utilities
|
|
|
|
│ │ ├── api-client.ts # Server API communication
|
|
|
|
│ │ ├── config.ts # Configuration management
|
|
|
|
│ │ ├── errors.ts # Error handling
|
|
|
|
│ │ └── validation.ts # Zod validation utilities
|
|
|
|
│ ├── schemas/ # Zod schemas
|
|
|
|
│ │ ├── commands/ # Command argument schemas
|
|
|
|
│ │ ├── config/ # Configuration schemas
|
|
|
|
│ │ ├── models/ # Data model schemas
|
|
|
|
│ │ └── api/ # API request/response schemas
|
|
|
|
│ └── main.ts # CLI entry point
|
|
|
|
├── scripts/
|
|
|
|
│ └── validate-env.ts # Environment validation
|
|
|
|
└── tests/
|
|
|
|
├── unit/ # Unit tests
|
|
|
|
├── integration/ # Integration tests
|
|
|
|
└── utils/ # Test utilities
|
|
|
|
```
|
|
|
|
|
|
|
|
## Command Development Pattern
|
|
|
|
|
|
|
|
Each command follows a consistent structure:
|
|
|
|
|
|
|
|
### 1. Command Definition (`index.ts`)
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import { Command } from 'commander';
|
|
|
|
import { z } from 'zod';
|
|
|
|
import React from 'react';
|
|
|
|
import { render } from 'ink';
|
|
|
|
import { AuthUI } from './components.js';
|
|
|
|
import { authHandler } from './handlers.js';
|
|
|
|
import { AuthArgsSchema } from './types.js';
|
|
|
|
|
|
|
|
export const authCommand = new Command('auth')
|
|
|
|
.description('Authenticate with Buster')
|
|
|
|
.option('-h, --host <host>', 'API host URL')
|
|
|
|
.option('-k, --api-key <key>', 'API key')
|
|
|
|
.action(async (options) => {
|
|
|
|
// Validate arguments
|
|
|
|
const args = AuthArgsSchema.parse(options);
|
|
|
|
|
|
|
|
// Render Ink UI
|
|
|
|
const { waitUntilExit } = render(
|
|
|
|
<AuthUI args={args} onComplete={authHandler} />
|
|
|
|
);
|
|
|
|
|
|
|
|
await waitUntilExit();
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### 2. Type Definitions (`types.ts`)
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
// Command arguments schema
|
|
|
|
export const AuthArgsSchema = z.object({
|
|
|
|
host: z.string().url().optional(),
|
|
|
|
apiKey: z.string().optional(),
|
|
|
|
});
|
|
|
|
|
|
|
|
export type AuthArgs = z.infer<typeof AuthArgsSchema>;
|
|
|
|
|
|
|
|
// Internal types
|
|
|
|
export const CredentialsSchema = z.object({
|
|
|
|
apiKey: z.string(),
|
|
|
|
apiUrl: z.string().url(),
|
|
|
|
environment: z.enum(['local', 'cloud']),
|
|
|
|
});
|
|
|
|
|
|
|
|
export type Credentials = z.infer<typeof CredentialsSchema>;
|
|
|
|
```
|
|
|
|
|
|
|
|
### 3. Ink UI Components (`components.tsx`)
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
import { Box, Text } from 'ink';
|
|
|
|
import TextInput from 'ink-text-input';
|
|
|
|
import SelectInput from 'ink-select-input';
|
|
|
|
import Spinner from 'ink-spinner';
|
|
|
|
|
|
|
|
interface AuthUIProps {
|
|
|
|
args: AuthArgs;
|
|
|
|
onComplete: (credentials: Credentials) => Promise<void>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const AuthUI: React.FC<AuthUIProps> = ({ args, onComplete }) => {
|
|
|
|
const [step, setStep] = useState<'input' | 'validating' | 'complete'>('input');
|
|
|
|
const [apiKey, setApiKey] = useState(args.apiKey || '');
|
|
|
|
|
|
|
|
// UI implementation with Ink components
|
|
|
|
return (
|
|
|
|
<Box flexDirection="column">
|
|
|
|
{step === 'input' && (
|
|
|
|
<Box>
|
|
|
|
<Text>Enter your API key: </Text>
|
|
|
|
<TextInput value={apiKey} onChange={setApiKey} />
|
|
|
|
</Box>
|
|
|
|
)}
|
|
|
|
{step === 'validating' && (
|
|
|
|
<Text>
|
|
|
|
<Spinner type="dots" /> Validating credentials...
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
### 4. Command Logic (`helpers.ts`)
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import { apiClient } from '../../utils/api-client.js';
|
|
|
|
import { configManager } from '../../utils/config.js';
|
|
|
|
import type { Credentials } from './types.js';
|
|
|
|
|
|
|
|
export async function validateCredentials(credentials: Credentials): Promise<boolean> {
|
|
|
|
try {
|
|
|
|
await apiClient.validateAuth(credentials);
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function saveCredentials(credentials: Credentials): Promise<void> {
|
|
|
|
await configManager.saveCredentials(credentials);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
## Zod-First Type System
|
|
|
|
|
|
|
|
We use Zod schemas for all type definitions and runtime validation:
|
|
|
|
|
|
|
|
### 1. Define Schema First
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// Always define the schema first
|
|
|
|
const UserSchema = z.object({
|
|
|
|
id: z.string().uuid(),
|
|
|
|
email: z.string().email(),
|
|
|
|
name: z.string(),
|
|
|
|
});
|
|
|
|
|
|
|
|
// Then export the type
|
|
|
|
export type User = z.infer<typeof UserSchema>;
|
|
|
|
```
|
|
|
|
|
|
|
|
### 2. Validate User Input
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// In command actions
|
|
|
|
.action(async (options) => {
|
|
|
|
const args = ArgsSchema.parse(options); // Throws if invalid
|
|
|
|
// args is now fully typed and validated
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### 3. YAML File Validation
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import yaml from 'js-yaml';
|
|
|
|
import { BusterConfigSchema } from '../schemas/config/buster-config.js';
|
|
|
|
|
|
|
|
export async function loadBusterConfig(path: string): Promise<BusterConfig> {
|
|
|
|
const content = await fs.readFile(path, 'utf-8');
|
|
|
|
const parsed = yaml.load(content);
|
|
|
|
return BusterConfigSchema.parse(parsed); // Validates and types
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
## API Client Pattern
|
|
|
|
|
|
|
|
All server communication goes through the centralized API client:
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// utils/api-client.ts
|
|
|
|
import type { User } from '@buster/server-shared/users';
|
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
export class ApiClient {
|
|
|
|
constructor(private baseUrl: string, private apiKey?: string) {}
|
|
|
|
|
|
|
|
async request<T>({
|
|
|
|
method,
|
|
|
|
path,
|
|
|
|
body,
|
|
|
|
responseSchema,
|
|
|
|
}: {
|
|
|
|
method: string;
|
|
|
|
path: string;
|
|
|
|
body?: unknown;
|
|
|
|
responseSchema: z.ZodSchema<T>;
|
|
|
|
}): Promise<T> {
|
|
|
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
|
|
method,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
|
|
},
|
|
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new ApiError(response.status, await response.text());
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
return responseSchema.parse(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
## Ink UI Components Guidelines
|
|
|
|
|
|
|
|
### 1. Component Organization
|
|
|
|
|
|
|
|
- Place reusable components in `src/components/`
|
|
|
|
- Command-specific components stay in the command folder
|
|
|
|
- Export all components from index files
|
|
|
|
|
|
|
|
### 2. Common Patterns
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// Progress indicator
|
|
|
|
<Box>
|
|
|
|
<Spinner type="dots" />
|
|
|
|
<Text> {message}</Text>
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
// Form with validation
|
|
|
|
<Box flexDirection="column">
|
|
|
|
<TextInput
|
|
|
|
value={value}
|
|
|
|
onChange={setValue}
|
|
|
|
placeholder="Enter value"
|
|
|
|
/>
|
|
|
|
{error && <Text color="red">❌ {error}</Text>}
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
// Status display
|
|
|
|
<Box borderStyle="round" padding={1}>
|
|
|
|
<Text color="green">✓ Operation successful</Text>
|
|
|
|
</Box>
|
|
|
|
```
|
|
|
|
|
|
|
|
### 3. State Management
|
|
|
|
|
|
|
|
- Use React hooks for local state
|
|
|
|
- Pass callbacks for command completion
|
|
|
|
- Handle errors gracefully with try/catch
|
|
|
|
|
|
|
|
## Testing Strategy
|
|
|
|
|
|
|
|
### 1. Unit Tests
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// auth.test.ts
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
import { validateCredentials } from './helpers.js';
|
|
|
|
|
|
|
|
describe('auth helpers', () => {
|
|
|
|
it('should validate correct credentials', async () => {
|
|
|
|
const mockApiClient = vi.mocked(apiClient);
|
|
|
|
mockApiClient.validateAuth.mockResolvedValue(true);
|
|
|
|
|
|
|
|
const result = await validateCredentials({
|
|
|
|
apiKey: 'test-key',
|
|
|
|
apiUrl: 'https://api.buster.com',
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### 2. Integration Tests
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// auth.int.test.ts
|
|
|
|
import { testCLI } from '../../tests/utils/cli-tester.js';
|
|
|
|
|
|
|
|
describe('auth command integration', () => {
|
|
|
|
it('should authenticate with valid credentials', async () => {
|
|
|
|
const result = await testCLI(['auth', '--api-key', 'test-key']);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
expect(result.stdout).toContain('Successfully authenticated');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### 3. Ink Component Tests
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import { render } from 'ink-testing-library';
|
|
|
|
import { AuthUI } from './components.js';
|
|
|
|
|
|
|
|
it('should render auth form', () => {
|
|
|
|
const { lastFrame } = render(<AuthUI args={{}} onComplete={vi.fn()} />);
|
|
|
|
expect(lastFrame()).toContain('Enter your API key:');
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
## Error Handling
|
|
|
|
|
|
|
|
### 1. Custom Error Classes
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
export class CLIError extends Error {
|
|
|
|
constructor(message: string, public code: string) {
|
|
|
|
super(message);
|
|
|
|
this.name = 'CLIError';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ApiError extends CLIError {
|
|
|
|
constructor(public status: number, message: string) {
|
|
|
|
super(message, 'API_ERROR');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ValidationError extends CLIError {
|
|
|
|
constructor(message: string) {
|
|
|
|
super(message, 'VALIDATION_ERROR');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### 2. Error Display in Ink
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
interface ErrorDisplayProps {
|
|
|
|
error: Error;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error }) => (
|
|
|
|
<Box borderStyle="round" borderColor="red" padding={1}>
|
|
|
|
<Text color="red">
|
|
|
|
❌ {error.message}
|
|
|
|
{error instanceof CLIError && (
|
|
|
|
<Text dimColor> (Code: {error.code})</Text>
|
|
|
|
)}
|
|
|
|
</Text>
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
```
|
|
|
|
|
|
|
|
## Configuration Management
|
|
|
|
|
|
|
|
### 1. File Locations
|
|
|
|
|
|
|
|
- Global config: `~/.buster/config.yml`
|
|
|
|
- Project config: `./buster.yml`
|
|
|
|
- Credentials: `~/.buster/credentials` (encrypted)
|
|
|
|
|
|
|
|
### 2. Schema Validation
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
export const BusterConfigSchema = z.object({
|
|
|
|
version: z.string(),
|
|
|
|
projectName: z.string(),
|
|
|
|
organization: z.string().optional(),
|
|
|
|
settings: z.object({
|
|
|
|
autoUpdate: z.boolean().default(true),
|
|
|
|
telemetry: z.boolean().default(true),
|
|
|
|
}).optional(),
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
## Best Practices
|
|
|
|
|
|
|
|
1. **Keep Commands Simple**: Commands should only handle argument parsing and UI rendering
|
|
|
|
2. **Delegate to Handlers**: Business logic goes in handler functions
|
|
|
|
3. **Use Zod Everywhere**: All user input and file parsing should use Zod validation
|
|
|
|
4. **Server-First**: All operations should go through the server API
|
|
|
|
5. **Rich UI Feedback**: Use Ink components to provide clear, beautiful feedback
|
|
|
|
6. **Handle Errors Gracefully**: Show helpful error messages with recovery suggestions
|
|
|
|
7. **Test Everything**: Unit test logic, integration test commands, component test UI
|
|
|
|
|
|
|
|
## Common Patterns
|
|
|
|
|
|
|
|
### Loading Configuration
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
export async function loadProjectConfig(): Promise<BusterConfig | null> {
|
|
|
|
try {
|
|
|
|
const configPath = path.join(process.cwd(), 'buster.yml');
|
|
|
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
|
|
const parsed = yaml.load(content);
|
|
|
|
return BusterConfigSchema.parse(parsed);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 'ENOENT') {
|
|
|
|
return null; // No config file
|
|
|
|
}
|
|
|
|
throw new ValidationError('Invalid buster.yml configuration');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### API Request with Progress
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
const { unmount } = render(
|
|
|
|
<ProgressDisplay message="Deploying models..." />
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const result = await apiClient.deployModels(models);
|
|
|
|
unmount();
|
|
|
|
|
|
|
|
render(<SuccessDisplay result={result} />);
|
|
|
|
} catch (error) {
|
|
|
|
unmount();
|
|
|
|
render(<ErrorDisplay error={error} />);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Multi-Step Operations
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
export const InitUI: React.FC = () => {
|
|
|
|
const [step, setStep] = useState(0);
|
|
|
|
const steps = ['Create folders', 'Generate config', 'Validate setup'];
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box flexDirection="column">
|
|
|
|
<ProgressSteps steps={steps} currentStep={step} />
|
|
|
|
{/* Step-specific UI */}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
## Environment Variables
|
|
|
|
|
|
|
|
- `BUSTER_API_URL`: Base URL for the Buster API
|
|
|
|
- `BUSTER_API_KEY`: API authentication key
|
|
|
|
- `BUSTER_CONFIG_DIR`: Override config directory location
|
|
|
|
- `BUSTER_CACHE_DIR`: Override cache directory location
|
|
|
|
- `BUSTER_AUTO_UPDATE`: Enable/disable auto-updates
|
|
|
|
- `BUSTER_TELEMETRY_DISABLED`: Disable telemetry
|
|
|
|
|
|
|
|
## Distribution
|
|
|
|
|
|
|
|
The CLI is distributed as:
|
|
|
|
1. npm package: `npm install -g @buster/cli`
|
|
|
|
2. Homebrew: `brew install buster-cli`
|
|
|
|
3. Direct binary: Download from GitHub releases
|
|
|
|
|
|
|
|
Binaries are compiled using Bun's compile feature for all major platforms.
|