mirror of https://github.com/buster-so/buster.git
migrate cli over to ts
This commit is contained in:
parent
cbfcf25f0f
commit
68d93f1eba
File diff suppressed because it is too large
Load Diff
|
@ -75,7 +75,6 @@ web/playwright-tests/auth-utils/auth.json
|
|||
|
||||
.trigger
|
||||
|
||||
.claude/tasks
|
||||
.claude/commands
|
||||
.claude/settings.local.json
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
- Build: `cargo build`
|
||||
- Release build: `cargo build --release`
|
||||
- Run tests: `cargo test`
|
||||
- Run specific test: `cargo test test_name`
|
||||
- Run tests in file: `cargo test --test filename`
|
||||
- Run package tests: `cargo test -p <package_name>`
|
||||
- Format: `cargo fmt`
|
||||
- Lint: `cargo clippy`
|
||||
|
||||
## Code Style Guidelines
|
||||
- **Imports**: Group by std, external crates, internal modules; alphabetical order
|
||||
- **Formatting**: 4-space indentation (standard Rust)
|
||||
- **Error Handling**: Use `thiserror` for error types, `anyhow` for general propagation
|
||||
- **Naming**: Follow Rust conventions (`snake_case` for variables/functions, `PascalCase` for types)
|
||||
- **Types**: Define custom error types with descriptive messages
|
||||
- **Dependencies**: Use workspace dependencies with `{ workspace = true }`
|
||||
- **Testing**: Place in separate `/tests` directory; use `tempfile` for test directories
|
||||
- **Never log secrets or sensitive data**
|
||||
|
||||
## Project Info
|
||||
- CLI for managing semantic models in Buster
|
||||
- Uses semantic versioning (PR title conventions for bumps)
|
||||
- Major: PR title with "BREAKING CHANGE" or "major", or PR with "major" label
|
||||
- Minor: PR title with "feat", "feature", or "minor", or PR with "minor" label
|
||||
- Patch: Default for all other PRs
|
||||
- Cross-project references supported
|
||||
- Config defined in `buster.yml`
|
||||
- File and tag exclusions for deployment
|
||||
- Row limit of 5000 is enforced for database queries by default
|
|
@ -0,0 +1,370 @@
|
|||
# Buster CLI
|
||||
|
||||
A powerful command-line interface for managing semantic models in Buster. Deploy and manage your data models with ease, whether they're standalone or part of a dbt project.
|
||||
|
||||
## Installation
|
||||
|
||||
Choose the installation command for your operating system:
|
||||
|
||||
### macOS (x86_64)
|
||||
```bash
|
||||
mkdir -p ~/.local/bin && curl -L https://github.com/buster-so/buster/releases/latest/download/buster-cli-darwin-x86_64.tar.gz | tar xz && mv buster-cli ~/.local/bin/buster && chmod +x ~/.local/bin/buster
|
||||
```
|
||||
|
||||
### macOS (ARM/Apple Silicon)
|
||||
```bash
|
||||
mkdir -p ~/.local/bin && curl -L https://github.com/buster-so/buster/releases/latest/download/buster-cli-darwin-arm64.tar.gz | tar xz && mv buster-cli ~/.local/bin/buster && chmod +x ~/.local/bin/buster
|
||||
```
|
||||
|
||||
### Linux (x86_64)
|
||||
```bash
|
||||
mkdir -p ~/.local/bin && curl -L https://github.com/buster-so/buster/releases/latest/download/buster-cli-linux-x86_64.tar.gz | tar xz && mv buster-cli ~/.local/bin/buster && chmod +x ~/.local/bin/buster
|
||||
```
|
||||
|
||||
> **Note**: After installation, make sure `~/.local/bin` is in your PATH. Add this to your shell's config file (`.bashrc`, `.zshrc`, etc.):
|
||||
> ```bash
|
||||
> export PATH="$HOME/.local/bin:$PATH"
|
||||
> ```
|
||||
|
||||
### Windows (x86_64)
|
||||
1. Download the Windows binary:
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri https://github.com/buster-so/buster/releases/latest/download/buster-cli-windows-x86_64.zip -OutFile buster.zip
|
||||
```
|
||||
|
||||
2. Extract and install:
|
||||
```powershell
|
||||
Expand-Archive -Path buster.zip -DestinationPath $env:USERPROFILE\buster
|
||||
Move-Item -Path $env:USERPROFILE\buster\buster-cli.exe -Destination $env:LOCALAPPDATA\Microsoft\WindowsApps\buster.exe
|
||||
```
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
### 1. Authentication
|
||||
|
||||
First, authenticate with Buster using your API key:
|
||||
|
||||
```bash
|
||||
buster auth
|
||||
```
|
||||
|
||||
This will prompt you for:
|
||||
- API Key (required) - Get this from the Buster Platform
|
||||
- Host (optional) - Defaults to production if not specified
|
||||
|
||||
You can also configure authentication using environment variables:
|
||||
```bash
|
||||
# Set API key via environment variable
|
||||
export BUSTER_API_KEY=your_api_key_here
|
||||
|
||||
# Optional: Set custom host. For self-hosted instances.
|
||||
export BUSTER_HOST=your_custom_host
|
||||
```
|
||||
|
||||
The CLI will check for these environment variables in the following order:
|
||||
1. Command line arguments
|
||||
2. Environment variables
|
||||
3. Interactive prompt
|
||||
|
||||
This is particularly useful for:
|
||||
- CI/CD environments
|
||||
- Automated scripts
|
||||
- Development workflows where you don't want to enter credentials repeatedly
|
||||
|
||||
### 2. Generate Models
|
||||
|
||||
Generate Buster YAML models from your existing SQL files:
|
||||
|
||||
```bash
|
||||
buster generate
|
||||
```
|
||||
|
||||
Key flags for generation:
|
||||
- `--source-path`: Directory containing your SQL files (defaults to current directory)
|
||||
- `--destination-path`: Where to output the generated YAML files (defaults to current directory)
|
||||
- `--data-source-name`: Name of the data source to use in the models
|
||||
- `--schema`: Database schema name
|
||||
- `--database`: Database name
|
||||
- `--flat-structure`: Output YML files in a flat structure instead of maintaining directory hierarchy
|
||||
|
||||
The generate command will:
|
||||
- Scan the source directory for SQL files
|
||||
- Create corresponding YAML model files
|
||||
- Create a `buster.yml` configuration file if it doesn't exist
|
||||
- Preserve any existing model customizations
|
||||
|
||||
Example with all options:
|
||||
```bash
|
||||
buster generate \
|
||||
--source-path ./sql \
|
||||
--destination-path ./models \
|
||||
--data-source-name my_warehouse \
|
||||
--schema analytics \
|
||||
--database prod
|
||||
```
|
||||
|
||||
### 3. Deploy Models
|
||||
|
||||
Deploy your models to Buster:
|
||||
|
||||
```bash
|
||||
buster deploy
|
||||
```
|
||||
|
||||
Deploy options:
|
||||
- `--path`: Specific path to deploy (defaults to current directory)
|
||||
- `--dry-run`: Validate the deployment without actually deploying (defaults to false)
|
||||
- `--recursive`: Recursively search for model files in subdirectories (defaults to true)
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
# Deploy all models in current directory
|
||||
buster deploy
|
||||
|
||||
# Deploy a specific model or directory
|
||||
buster deploy --path ./models/customers.yml
|
||||
|
||||
# Validate deployment without applying changes
|
||||
buster deploy --dry-run
|
||||
|
||||
# Deploy only models in the specified directory (not recursively)
|
||||
buster deploy --path ./models --recursive=false
|
||||
```
|
||||
|
||||
The deploy command will:
|
||||
1. Discover all YAML model files in the specified path
|
||||
2. Load and validate the models
|
||||
3. Check for excluded models based on tags
|
||||
4. Validate cross-project references
|
||||
5. Deploy the models to Buster
|
||||
6. Provide detailed validation feedback and error messages
|
||||
|
||||
## Project Structure
|
||||
|
||||
A typical Buster project structure:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── buster.yml # Global configuration
|
||||
├── models/ # Your semantic model definitions
|
||||
│ ├── customers.yml
|
||||
│ ├── orders.yml
|
||||
│ └── products.yml
|
||||
└── sql/ # SQL definitions
|
||||
├── customers.sql
|
||||
├── orders.sql
|
||||
└── products.sql
|
||||
```
|
||||
|
||||
### Configuration (buster.yml)
|
||||
|
||||
```yaml
|
||||
# buster.yml
|
||||
data_source_name: "my_warehouse" # Your default data source
|
||||
schema: "analytics" # Default schema for models
|
||||
database: "prod" # Optional database name
|
||||
exclude_files: # Optional list of files to exclude from generation
|
||||
- "temp_*.sql" # Exclude all SQL files starting with temp_
|
||||
- "test/**/*.sql" # Exclude all SQL files in test directories
|
||||
- "customers.sql" # Exclude a specific file
|
||||
exclude_tags: # Optional list of tags to exclude from deployment
|
||||
- "staging" # Exclude models with the 'staging' tag
|
||||
- "test" # Exclude models with the 'test' tag
|
||||
```
|
||||
|
||||
The configuration supports the following fields:
|
||||
- `data_source_name`: (Required) Default data source for your models
|
||||
- `schema`: (Required) Default schema for your models
|
||||
- `database`: (Optional) Default database name
|
||||
- `exclude_files`: (Optional) List of glob patterns for files to exclude from generation
|
||||
- Supports standard glob patterns (*, **, ?, etc.)
|
||||
- Matches against relative paths from source directory
|
||||
- Common use cases:
|
||||
- Excluding temporary files: `temp_*.sql`
|
||||
- Excluding test files: `test/**/*.sql`
|
||||
- Excluding specific files: `customers.sql`
|
||||
- Excluding files in directories: `archive/**/*.sql`
|
||||
- `exclude_tags`: (Optional) List of tags to exclude from deployment
|
||||
- Looks for tags in SQL files in dbt format: `{{ config(tags=['tag1', 'tag2']) }}`
|
||||
- Useful for excluding staging models, test models, etc.
|
||||
- Case-insensitive matching
|
||||
|
||||
### Model Definition Example
|
||||
|
||||
```yaml
|
||||
# models/customers.yml
|
||||
version: 1
|
||||
models:
|
||||
- name: customers
|
||||
description: "Core customer data model"
|
||||
data_source_name: "my_warehouse" # Overrides buster.yml
|
||||
schema: "analytics" # Overrides buster.yml
|
||||
database: "prod" # Overrides buster.yml
|
||||
|
||||
entities:
|
||||
- name: customer_id
|
||||
expr: "id"
|
||||
type: "primary"
|
||||
description: "Primary customer identifier"
|
||||
- name: order
|
||||
expr: "order_id"
|
||||
type: "foreign"
|
||||
description: "Reference to order model"
|
||||
# Optional: reference to another model in a different project
|
||||
project_path: "path/to/other/project"
|
||||
# Optional: specify a different name for the referenced model
|
||||
ref: "orders"
|
||||
|
||||
dimensions:
|
||||
- name: email
|
||||
expr: "email"
|
||||
type: "string"
|
||||
description: "Customer email address"
|
||||
searchable: true # Optional: make this dimension searchable
|
||||
|
||||
measures:
|
||||
- name: total_customers
|
||||
expr: "customer_id"
|
||||
agg: "count_distinct"
|
||||
description: "Total number of unique customers"
|
||||
type: "integer" # Optional: specify the data type
|
||||
```
|
||||
|
||||
## Cross-Project References
|
||||
|
||||
Buster CLI supports referencing models across different projects, enabling you to build complex data relationships:
|
||||
|
||||
```yaml
|
||||
entities:
|
||||
- name: user_model
|
||||
expr: "user_id"
|
||||
type: "foreign"
|
||||
description: "Reference to user model in another project"
|
||||
project_path: "path/to/user/project"
|
||||
ref: "users" # Optional: specify a different name for the referenced model
|
||||
```
|
||||
|
||||
When using cross-project references, the CLI will:
|
||||
1. Validate that the referenced project exists
|
||||
2. Check for a valid buster.yml in the referenced project
|
||||
3. Verify that the data sources match between projects
|
||||
4. Confirm that the referenced model exists in the target project
|
||||
|
||||
This enables you to organize your models into logical projects while maintaining relationships between them.
|
||||
|
||||
## Tag-Based Exclusion
|
||||
|
||||
You can exclude models from deployment based on tags in your SQL files. This is useful for excluding staging models, test models, or any other models you don't want to deploy.
|
||||
|
||||
In your SQL files, add tags using the dbt format:
|
||||
|
||||
```sql
|
||||
{{ config(
|
||||
tags=['staging', 'test']
|
||||
) }}
|
||||
|
||||
SELECT * FROM source_table
|
||||
```
|
||||
|
||||
Then in your buster.yml, specify which tags to exclude:
|
||||
|
||||
```yaml
|
||||
exclude_tags:
|
||||
- "staging"
|
||||
- "test"
|
||||
```
|
||||
|
||||
During deployment, any model with matching tags will be automatically excluded.
|
||||
|
||||
## File and Tag Exclusions
|
||||
|
||||
Buster CLI provides a unified way to exclude files from processing across all commands. You can specify exclusions in your `buster.yml` file:
|
||||
|
||||
```yaml
|
||||
data_source_name: "my_data_source"
|
||||
schema: "my_schema"
|
||||
database: "my_database"
|
||||
exclude_files:
|
||||
- "**/*_temp.sql"
|
||||
- "staging/**/*.sql"
|
||||
- "tests/**/*.yml"
|
||||
exclude_tags:
|
||||
- "test"
|
||||
- "deprecated"
|
||||
- "wip"
|
||||
```
|
||||
|
||||
### Exclude Files
|
||||
|
||||
The `exclude_files` section allows you to specify glob patterns for files that should be excluded from processing. This works for any command that processes files.
|
||||
|
||||
Common patterns:
|
||||
- `"**/*_temp.sql"` - Exclude any SQL file ending with _temp.sql in any directory
|
||||
- `"staging/**/*.sql"` - Exclude all SQL files in the staging directory and its subdirectories
|
||||
- `"test_*.yml"` - Exclude all YAML files starting with test_
|
||||
|
||||
### Exclude Tags
|
||||
|
||||
The `exclude_tags` section allows you to exclude files based on tags specified in the file content. This is useful for excluding files that are marked as test, deprecated, etc.
|
||||
|
||||
Tags are specified in the SQL files using the format: `-- tags = ['tag1', 'tag2']`
|
||||
|
||||
When a file contains any of the excluded tags, it will be skipped by all commands.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Organization**
|
||||
- Keep YAML files in `models/`
|
||||
- Keep SQL files in `sql/`
|
||||
- Use `buster.yml` for shared settings
|
||||
- Group related models into subdirectories
|
||||
|
||||
2. **Model Generation**
|
||||
- Start with clean SQL files
|
||||
- Generate models first before customizing
|
||||
- Review generated models before deployment
|
||||
- Use tags to organize and filter models
|
||||
|
||||
3. **Deployment**
|
||||
- Use `--dry-run` to validate changes
|
||||
- Deploy frequently to catch issues early
|
||||
- Keep model and SQL files in sync
|
||||
- Use cross-project references for complex relationships
|
||||
|
||||
4. **Validation**
|
||||
- Ensure all models have descriptions
|
||||
- Validate cross-project references before deployment
|
||||
- Check for missing dependencies
|
||||
- Review validation errors carefully
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Authentication Issues**
|
||||
- Verify your API key is correct
|
||||
- Check if the host is properly specified (if using non-production)
|
||||
- Ensure network connectivity to Buster
|
||||
|
||||
2. **Generation Issues**
|
||||
- Verify SQL files are in the correct location
|
||||
- Check file permissions
|
||||
- Ensure SQL syntax is valid
|
||||
- Check for excluded files or tags
|
||||
|
||||
3. **Deployment Issues**
|
||||
- Validate YAML syntax
|
||||
- Check for missing dependencies
|
||||
- Verify data source connectivity
|
||||
- Look for cross-project reference errors
|
||||
- Check for tag-based exclusions
|
||||
|
||||
4. **Cross-Project Reference Issues**
|
||||
- Ensure the referenced project exists
|
||||
- Verify the referenced project has a valid buster.yml
|
||||
- Check that data sources match between projects
|
||||
- Confirm the referenced model exists in the target project
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
|
@ -1,34 +1,485 @@
|
|||
# CLAUDE.md
|
||||
# Buster CLI Development Guidelines
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
This document provides specific guidelines for developing the TypeScript-based Buster CLI in this monorepo.
|
||||
|
||||
## Build Commands
|
||||
- Build: `cargo build`
|
||||
- Release build: `cargo build --release`
|
||||
- Run tests: `cargo test`
|
||||
- Run specific test: `cargo test test_name`
|
||||
- Run tests in file: `cargo test --test filename`
|
||||
- Run package tests: `cargo test -p <package_name>`
|
||||
- Format: `cargo fmt`
|
||||
- Lint: `cargo clippy`
|
||||
## Architecture Overview
|
||||
|
||||
## Code Style Guidelines
|
||||
- **Imports**: Group by std, external crates, internal modules; alphabetical order
|
||||
- **Formatting**: 4-space indentation (standard Rust)
|
||||
- **Error Handling**: Use `thiserror` for error types, `anyhow` for general propagation
|
||||
- **Naming**: Follow Rust conventions (`snake_case` for variables/functions, `PascalCase` for types)
|
||||
- **Types**: Define custom error types with descriptive messages
|
||||
- **Dependencies**: Use workspace dependencies with `{ workspace = true }`
|
||||
- **Testing**: Place in separate `/tests` directory; use `tempfile` for test directories
|
||||
- **Never log secrets or sensitive data**
|
||||
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)
|
||||
|
||||
## Project Info
|
||||
- CLI for managing semantic models in Buster
|
||||
- Uses semantic versioning (PR title conventions for bumps)
|
||||
- Major: PR title with "BREAKING CHANGE" or "major", or PR with "major" label
|
||||
- Minor: PR title with "feat", "feature", or "minor", or PR with "minor" label
|
||||
- Patch: Default for all other PRs
|
||||
- Cross-project references supported
|
||||
- Config defined in `buster.yml`
|
||||
- File and tag exclusions for deployment
|
||||
- Row limit of 5000 is enforced for database queries by default
|
||||
**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.
|
|
@ -1,370 +1,69 @@
|
|||
# Buster CLI
|
||||
|
||||
A powerful command-line interface for managing semantic models in Buster. Deploy and manage your data models with ease, whether they're standalone or part of a dbt project.
|
||||
The official command-line interface for Buster, built with TypeScript, Commander.js, and Ink for a beautiful terminal experience.
|
||||
|
||||
## Installation
|
||||
## Architecture
|
||||
|
||||
Choose the installation command for your operating system:
|
||||
This CLI is designed as a thin client that communicates with the Buster server API. It handles:
|
||||
- File system operations (reading/writing YAML files)
|
||||
- API communication
|
||||
- Rich terminal UI using Ink (React for CLI)
|
||||
|
||||
### macOS (x86_64)
|
||||
```bash
|
||||
mkdir -p ~/.local/bin && curl -L https://github.com/buster-so/buster/releases/latest/download/buster-cli-darwin-x86_64.tar.gz | tar xz && mv buster-cli ~/.local/bin/buster && chmod +x ~/.local/bin/buster
|
||||
```
|
||||
## Development
|
||||
|
||||
### macOS (ARM/Apple Silicon)
|
||||
```bash
|
||||
mkdir -p ~/.local/bin && curl -L https://github.com/buster-so/buster/releases/latest/download/buster-cli-darwin-arm64.tar.gz | tar xz && mv buster-cli ~/.local/bin/buster && chmod +x ~/.local/bin/buster
|
||||
```
|
||||
|
||||
### Linux (x86_64)
|
||||
```bash
|
||||
mkdir -p ~/.local/bin && curl -L https://github.com/buster-so/buster/releases/latest/download/buster-cli-linux-x86_64.tar.gz | tar xz && mv buster-cli ~/.local/bin/buster && chmod +x ~/.local/bin/buster
|
||||
```
|
||||
|
||||
> **Note**: After installation, make sure `~/.local/bin` is in your PATH. Add this to your shell's config file (`.bashrc`, `.zshrc`, etc.):
|
||||
> ```bash
|
||||
> export PATH="$HOME/.local/bin:$PATH"
|
||||
> ```
|
||||
|
||||
### Windows (x86_64)
|
||||
1. Download the Windows binary:
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri https://github.com/buster-so/buster/releases/latest/download/buster-cli-windows-x86_64.zip -OutFile buster.zip
|
||||
```
|
||||
|
||||
2. Extract and install:
|
||||
```powershell
|
||||
Expand-Archive -Path buster.zip -DestinationPath $env:USERPROFILE\buster
|
||||
Move-Item -Path $env:USERPROFILE\buster\buster-cli.exe -Destination $env:LOCALAPPDATA\Microsoft\WindowsApps\buster.exe
|
||||
```
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
### 1. Authentication
|
||||
|
||||
First, authenticate with Buster using your API key:
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
buster auth
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run in development mode
|
||||
pnpm dev
|
||||
|
||||
# Build the CLI
|
||||
pnpm build
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
This will prompt you for:
|
||||
- API Key (required) - Get this from the Buster Platform
|
||||
- Host (optional) - Defaults to production if not specified
|
||||
### Commands
|
||||
|
||||
You can also configure authentication using environment variables:
|
||||
```bash
|
||||
# Set API key via environment variable
|
||||
export BUSTER_API_KEY=your_api_key_here
|
||||
- `auth` - Authenticate with Buster
|
||||
- `init` - Initialize a new Buster project
|
||||
- `deploy` - Deploy models to Buster
|
||||
- `parse` - Parse and validate YAML model files
|
||||
- `config` - Manage Buster configuration
|
||||
- `update` - Update CLI to the latest version
|
||||
- `start` - Start Buster services
|
||||
- `stop` - Stop Buster services
|
||||
- `reset` - Reset Buster services and data
|
||||
|
||||
# Optional: Set custom host. For self-hosted instances.
|
||||
export BUSTER_HOST=your_custom_host
|
||||
```
|
||||
|
||||
The CLI will check for these environment variables in the following order:
|
||||
1. Command line arguments
|
||||
2. Environment variables
|
||||
3. Interactive prompt
|
||||
|
||||
This is particularly useful for:
|
||||
- CI/CD environments
|
||||
- Automated scripts
|
||||
- Development workflows where you don't want to enter credentials repeatedly
|
||||
|
||||
### 2. Generate Models
|
||||
|
||||
Generate Buster YAML models from your existing SQL files:
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
buster generate
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run unit tests only
|
||||
pnpm test:unit
|
||||
|
||||
# Run integration tests
|
||||
pnpm test:integration
|
||||
|
||||
# Watch mode
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
Key flags for generation:
|
||||
- `--source-path`: Directory containing your SQL files (defaults to current directory)
|
||||
- `--destination-path`: Where to output the generated YAML files (defaults to current directory)
|
||||
- `--data-source-name`: Name of the data source to use in the models
|
||||
- `--schema`: Database schema name
|
||||
- `--database`: Database name
|
||||
- `--flat-structure`: Output YML files in a flat structure instead of maintaining directory hierarchy
|
||||
|
||||
The generate command will:
|
||||
- Scan the source directory for SQL files
|
||||
- Create corresponding YAML model files
|
||||
- Create a `buster.yml` configuration file if it doesn't exist
|
||||
- Preserve any existing model customizations
|
||||
|
||||
Example with all options:
|
||||
```bash
|
||||
buster generate \
|
||||
--source-path ./sql \
|
||||
--destination-path ./models \
|
||||
--data-source-name my_warehouse \
|
||||
--schema analytics \
|
||||
--database prod
|
||||
```
|
||||
|
||||
### 3. Deploy Models
|
||||
|
||||
Deploy your models to Buster:
|
||||
### Building Binaries
|
||||
|
||||
```bash
|
||||
buster deploy
|
||||
# Build binaries for all platforms
|
||||
pnpm build:binary
|
||||
```
|
||||
|
||||
Deploy options:
|
||||
- `--path`: Specific path to deploy (defaults to current directory)
|
||||
- `--dry-run`: Validate the deployment without actually deploying (defaults to false)
|
||||
- `--recursive`: Recursively search for model files in subdirectories (defaults to true)
|
||||
This creates standalone executables for Linux, macOS, and Windows using Bun's compile feature.
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
# Deploy all models in current directory
|
||||
buster deploy
|
||||
## Contributing
|
||||
|
||||
# Deploy a specific model or directory
|
||||
buster deploy --path ./models/customers.yml
|
||||
|
||||
# Validate deployment without applying changes
|
||||
buster deploy --dry-run
|
||||
|
||||
# Deploy only models in the specified directory (not recursively)
|
||||
buster deploy --path ./models --recursive=false
|
||||
```
|
||||
|
||||
The deploy command will:
|
||||
1. Discover all YAML model files in the specified path
|
||||
2. Load and validate the models
|
||||
3. Check for excluded models based on tags
|
||||
4. Validate cross-project references
|
||||
5. Deploy the models to Buster
|
||||
6. Provide detailed validation feedback and error messages
|
||||
|
||||
## Project Structure
|
||||
|
||||
A typical Buster project structure:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── buster.yml # Global configuration
|
||||
├── models/ # Your semantic model definitions
|
||||
│ ├── customers.yml
|
||||
│ ├── orders.yml
|
||||
│ └── products.yml
|
||||
└── sql/ # SQL definitions
|
||||
├── customers.sql
|
||||
├── orders.sql
|
||||
└── products.sql
|
||||
```
|
||||
|
||||
### Configuration (buster.yml)
|
||||
|
||||
```yaml
|
||||
# buster.yml
|
||||
data_source_name: "my_warehouse" # Your default data source
|
||||
schema: "analytics" # Default schema for models
|
||||
database: "prod" # Optional database name
|
||||
exclude_files: # Optional list of files to exclude from generation
|
||||
- "temp_*.sql" # Exclude all SQL files starting with temp_
|
||||
- "test/**/*.sql" # Exclude all SQL files in test directories
|
||||
- "customers.sql" # Exclude a specific file
|
||||
exclude_tags: # Optional list of tags to exclude from deployment
|
||||
- "staging" # Exclude models with the 'staging' tag
|
||||
- "test" # Exclude models with the 'test' tag
|
||||
```
|
||||
|
||||
The configuration supports the following fields:
|
||||
- `data_source_name`: (Required) Default data source for your models
|
||||
- `schema`: (Required) Default schema for your models
|
||||
- `database`: (Optional) Default database name
|
||||
- `exclude_files`: (Optional) List of glob patterns for files to exclude from generation
|
||||
- Supports standard glob patterns (*, **, ?, etc.)
|
||||
- Matches against relative paths from source directory
|
||||
- Common use cases:
|
||||
- Excluding temporary files: `temp_*.sql`
|
||||
- Excluding test files: `test/**/*.sql`
|
||||
- Excluding specific files: `customers.sql`
|
||||
- Excluding files in directories: `archive/**/*.sql`
|
||||
- `exclude_tags`: (Optional) List of tags to exclude from deployment
|
||||
- Looks for tags in SQL files in dbt format: `{{ config(tags=['tag1', 'tag2']) }}`
|
||||
- Useful for excluding staging models, test models, etc.
|
||||
- Case-insensitive matching
|
||||
|
||||
### Model Definition Example
|
||||
|
||||
```yaml
|
||||
# models/customers.yml
|
||||
version: 1
|
||||
models:
|
||||
- name: customers
|
||||
description: "Core customer data model"
|
||||
data_source_name: "my_warehouse" # Overrides buster.yml
|
||||
schema: "analytics" # Overrides buster.yml
|
||||
database: "prod" # Overrides buster.yml
|
||||
|
||||
entities:
|
||||
- name: customer_id
|
||||
expr: "id"
|
||||
type: "primary"
|
||||
description: "Primary customer identifier"
|
||||
- name: order
|
||||
expr: "order_id"
|
||||
type: "foreign"
|
||||
description: "Reference to order model"
|
||||
# Optional: reference to another model in a different project
|
||||
project_path: "path/to/other/project"
|
||||
# Optional: specify a different name for the referenced model
|
||||
ref: "orders"
|
||||
|
||||
dimensions:
|
||||
- name: email
|
||||
expr: "email"
|
||||
type: "string"
|
||||
description: "Customer email address"
|
||||
searchable: true # Optional: make this dimension searchable
|
||||
|
||||
measures:
|
||||
- name: total_customers
|
||||
expr: "customer_id"
|
||||
agg: "count_distinct"
|
||||
description: "Total number of unique customers"
|
||||
type: "integer" # Optional: specify the data type
|
||||
```
|
||||
|
||||
## Cross-Project References
|
||||
|
||||
Buster CLI supports referencing models across different projects, enabling you to build complex data relationships:
|
||||
|
||||
```yaml
|
||||
entities:
|
||||
- name: user_model
|
||||
expr: "user_id"
|
||||
type: "foreign"
|
||||
description: "Reference to user model in another project"
|
||||
project_path: "path/to/user/project"
|
||||
ref: "users" # Optional: specify a different name for the referenced model
|
||||
```
|
||||
|
||||
When using cross-project references, the CLI will:
|
||||
1. Validate that the referenced project exists
|
||||
2. Check for a valid buster.yml in the referenced project
|
||||
3. Verify that the data sources match between projects
|
||||
4. Confirm that the referenced model exists in the target project
|
||||
|
||||
This enables you to organize your models into logical projects while maintaining relationships between them.
|
||||
|
||||
## Tag-Based Exclusion
|
||||
|
||||
You can exclude models from deployment based on tags in your SQL files. This is useful for excluding staging models, test models, or any other models you don't want to deploy.
|
||||
|
||||
In your SQL files, add tags using the dbt format:
|
||||
|
||||
```sql
|
||||
{{ config(
|
||||
tags=['staging', 'test']
|
||||
) }}
|
||||
|
||||
SELECT * FROM source_table
|
||||
```
|
||||
|
||||
Then in your buster.yml, specify which tags to exclude:
|
||||
|
||||
```yaml
|
||||
exclude_tags:
|
||||
- "staging"
|
||||
- "test"
|
||||
```
|
||||
|
||||
During deployment, any model with matching tags will be automatically excluded.
|
||||
|
||||
## File and Tag Exclusions
|
||||
|
||||
Buster CLI provides a unified way to exclude files from processing across all commands. You can specify exclusions in your `buster.yml` file:
|
||||
|
||||
```yaml
|
||||
data_source_name: "my_data_source"
|
||||
schema: "my_schema"
|
||||
database: "my_database"
|
||||
exclude_files:
|
||||
- "**/*_temp.sql"
|
||||
- "staging/**/*.sql"
|
||||
- "tests/**/*.yml"
|
||||
exclude_tags:
|
||||
- "test"
|
||||
- "deprecated"
|
||||
- "wip"
|
||||
```
|
||||
|
||||
### Exclude Files
|
||||
|
||||
The `exclude_files` section allows you to specify glob patterns for files that should be excluded from processing. This works for any command that processes files.
|
||||
|
||||
Common patterns:
|
||||
- `"**/*_temp.sql"` - Exclude any SQL file ending with _temp.sql in any directory
|
||||
- `"staging/**/*.sql"` - Exclude all SQL files in the staging directory and its subdirectories
|
||||
- `"test_*.yml"` - Exclude all YAML files starting with test_
|
||||
|
||||
### Exclude Tags
|
||||
|
||||
The `exclude_tags` section allows you to exclude files based on tags specified in the file content. This is useful for excluding files that are marked as test, deprecated, etc.
|
||||
|
||||
Tags are specified in the SQL files using the format: `-- tags = ['tag1', 'tag2']`
|
||||
|
||||
When a file contains any of the excluded tags, it will be skipped by all commands.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Organization**
|
||||
- Keep YAML files in `models/`
|
||||
- Keep SQL files in `sql/`
|
||||
- Use `buster.yml` for shared settings
|
||||
- Group related models into subdirectories
|
||||
|
||||
2. **Model Generation**
|
||||
- Start with clean SQL files
|
||||
- Generate models first before customizing
|
||||
- Review generated models before deployment
|
||||
- Use tags to organize and filter models
|
||||
|
||||
3. **Deployment**
|
||||
- Use `--dry-run` to validate changes
|
||||
- Deploy frequently to catch issues early
|
||||
- Keep model and SQL files in sync
|
||||
- Use cross-project references for complex relationships
|
||||
|
||||
4. **Validation**
|
||||
- Ensure all models have descriptions
|
||||
- Validate cross-project references before deployment
|
||||
- Check for missing dependencies
|
||||
- Review validation errors carefully
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Authentication Issues**
|
||||
- Verify your API key is correct
|
||||
- Check if the host is properly specified (if using non-production)
|
||||
- Ensure network connectivity to Buster
|
||||
|
||||
2. **Generation Issues**
|
||||
- Verify SQL files are in the correct location
|
||||
- Check file permissions
|
||||
- Ensure SQL syntax is valid
|
||||
- Check for excluded files or tags
|
||||
|
||||
3. **Deployment Issues**
|
||||
- Validate YAML syntax
|
||||
- Check for missing dependencies
|
||||
- Verify data source connectivity
|
||||
- Look for cross-project reference errors
|
||||
- Check for tag-based exclusions
|
||||
|
||||
4. **Cross-Project Reference Issues**
|
||||
- Ensure the referenced project exists
|
||||
- Verify the referenced project has a valid buster.yml
|
||||
- Check that data sources match between projects
|
||||
- Confirm the referenced model exists in the target project
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
See [CLAUDE.md](./CLAUDE.md) for detailed development guidelines.
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["../../biome.json"]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/// <reference types="node" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
// API Configuration
|
||||
BUSTER_API_URL: string;
|
||||
BUSTER_API_KEY?: string;
|
||||
|
||||
|
||||
// CLI Configuration
|
||||
BUSTER_CONFIG_DIR?: string;
|
||||
BUSTER_CACHE_DIR?: string;
|
||||
|
||||
// Feature Flags
|
||||
BUSTER_AUTO_UPDATE?: string;
|
||||
BUSTER_TELEMETRY_DISABLED?: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "@buster-app/cli",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/main.ts"
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"buster": "./dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "[ \"$SKIP_ENV_CHECK\" = \"true\" ] || (tsx scripts/validate-env.ts && pnpm run typecheck)",
|
||||
"build": "tsup",
|
||||
"build:dry-run": "tsup",
|
||||
"build:binary": "bun build --compile --target=bun-linux-x64 --target=bun-darwin-x64 --target=bun-windows-x64 --outfile buster ./src/main.ts",
|
||||
"dev": "tsx --watch src/main.ts",
|
||||
"lint": "biome check --write",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --exclude '**/*.int.test.ts' --exclude '**/*.integration.test.ts'",
|
||||
"test:integration": "vitest run **/*.int.test.ts **/*.integration.test.ts",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:watch": "vitest --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@buster/server-shared": "workspace:*",
|
||||
"commander": "^14.0.0",
|
||||
"ink": "^6.0.1",
|
||||
"react": "^19.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"ink-table": "^3.1.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-confirm-input": "^2.0.0",
|
||||
"zod": "catalog:",
|
||||
"js-yaml": "^4.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"chalk": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@buster/typescript-config": "workspace:*",
|
||||
"@buster/vitest-config": "workspace:*",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"tsx": "^4.7.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
// Required for API communication
|
||||
BUSTER_API_URL: z.string().url(),
|
||||
|
||||
// Optional environment variables
|
||||
BUSTER_API_KEY: z.string().optional(),
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||
BUSTER_CONFIG_DIR: z.string().optional(),
|
||||
BUSTER_CACHE_DIR: z.string().optional(),
|
||||
BUSTER_AUTO_UPDATE: z.string().optional(),
|
||||
BUSTER_TELEMETRY_DISABLED: z.string().optional(),
|
||||
});
|
||||
|
||||
try {
|
||||
envSchema.parse(process.env);
|
||||
console.log('✅ Environment variables validated successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Environment validation failed:');
|
||||
if (error instanceof z.ZodError) {
|
||||
error.errors.forEach((err) => {
|
||||
console.error(` - ${err.path.join('.')}: ${err.message}`);
|
||||
});
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
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';
|
||||
import type { AuthArgs, Credentials } from './types.js';
|
||||
import { validateAndSaveCredentials } from './helpers.js';
|
||||
|
||||
interface AuthUIProps {
|
||||
args: AuthArgs;
|
||||
}
|
||||
|
||||
export const AuthUI: React.FC<AuthUIProps> = ({ args }) => {
|
||||
const [step, setStep] = useState<'input' | 'validating' | 'complete' | 'error'>('input');
|
||||
const [apiKey, setApiKey] = useState(args.apiKey || '');
|
||||
const [apiUrl, setApiUrl] = useState(args.host || process.env.BUSTER_API_URL || '');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setStep('validating');
|
||||
|
||||
try {
|
||||
const credentials: Credentials = {
|
||||
apiKey,
|
||||
apiUrl,
|
||||
environment: args.environment,
|
||||
};
|
||||
|
||||
await validateAndSaveCredentials(credentials);
|
||||
setStep('complete');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Authentication failed');
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (step === 'input') {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<Text>API URL: </Text>
|
||||
<TextInput
|
||||
value={apiUrl}
|
||||
onChange={setApiUrl}
|
||||
placeholder="https://api.buster.com"
|
||||
onSubmit={apiKey ? handleSubmit : undefined}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>API Key: </Text>
|
||||
<TextInput
|
||||
value={apiKey}
|
||||
onChange={setApiKey}
|
||||
placeholder="Enter your API key"
|
||||
mask="*"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'validating') {
|
||||
return (
|
||||
<Text>
|
||||
<Spinner type="dots" /> Validating credentials...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'complete') {
|
||||
return (
|
||||
<Box borderStyle="round" borderColor="green" padding={1}>
|
||||
<Text color="green">✓ Successfully authenticated with Buster!</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'error') {
|
||||
return (
|
||||
<Box borderStyle="round" borderColor="red" padding={1}>
|
||||
<Text color="red">❌ {error}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import type { Credentials } from './types.js';
|
||||
// TODO: Import from lib when created
|
||||
// import { apiClient } from '../../utils/api-client.js';
|
||||
// import { configManager } from '../../utils/config.js';
|
||||
|
||||
export async function validateAndSaveCredentials(credentials: Credentials): Promise<void> {
|
||||
// TODO: Implement actual validation via API
|
||||
// const isValid = await apiClient.validateAuth(credentials);
|
||||
|
||||
// Mock validation for now
|
||||
if (!credentials.apiKey || credentials.apiKey.length < 10) {
|
||||
throw new Error('Invalid API key');
|
||||
}
|
||||
|
||||
// TODO: Save credentials
|
||||
// await configManager.saveCredentials(credentials);
|
||||
|
||||
// Mock success
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { AuthUI } from './components.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')
|
||||
.option('-e, --environment <env>', 'Environment (local/cloud)', 'cloud')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = AuthArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<AuthUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// Command arguments schema
|
||||
export const AuthArgsSchema = z.object({
|
||||
host: z.string().url().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
environment: z.enum(['local', 'cloud']).default('cloud'),
|
||||
});
|
||||
|
||||
export type AuthArgs = z.infer<typeof AuthArgsSchema>;
|
||||
|
||||
// Credentials schema
|
||||
export const CredentialsSchema = z.object({
|
||||
apiKey: z.string(),
|
||||
apiUrl: z.string().url(),
|
||||
environment: z.enum(['local', 'cloud']),
|
||||
});
|
||||
|
||||
export type Credentials = z.infer<typeof CredentialsSchema>;
|
|
@ -0,0 +1,21 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { ConfigUI } from './components.js';
|
||||
import { ConfigArgsSchema } from './types.js';
|
||||
|
||||
export const configCommand = new Command('config')
|
||||
.description('Manage Buster configuration')
|
||||
.option('-s, --set <key=value>', 'Set a configuration value')
|
||||
.option('-g, --get <key>', 'Get a configuration value')
|
||||
.option('-l, --list', 'List all configuration values')
|
||||
.option('--reset', 'Reset configuration to defaults')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = ConfigArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<ConfigUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import type { DeployArgs } from './types.js';
|
||||
|
||||
interface DeployUIProps {
|
||||
args: DeployArgs;
|
||||
}
|
||||
|
||||
export const DeployUI: React.FC<DeployUIProps> = ({ args }) => {
|
||||
const [status, setStatus] = useState<'validating' | 'deploying' | 'complete' | 'error'>('validating');
|
||||
const [message, setMessage] = useState('Validating models...');
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate deployment process
|
||||
const timer = setTimeout(() => {
|
||||
if (args.dryRun) {
|
||||
setMessage('Dry run completed successfully');
|
||||
setStatus('complete');
|
||||
} else {
|
||||
setMessage('Models deployed successfully');
|
||||
setStatus('complete');
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [args.dryRun]);
|
||||
|
||||
if (status === 'complete') {
|
||||
return (
|
||||
<Box borderStyle="round" borderColor="green" padding={1}>
|
||||
<Text color="green">✓ {message}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text>
|
||||
<Spinner type="dots" /> {message}
|
||||
</Text>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { DeployUI } from './components.js';
|
||||
import { DeployArgsSchema } from './types.js';
|
||||
|
||||
export const deployCommand = new Command('deploy')
|
||||
.description('Deploy models to Buster')
|
||||
.option('-d, --dry-run', 'Perform a dry run without deploying')
|
||||
.option('-p, --path <path>', 'Path to models directory', './buster')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = DeployArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<DeployUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const DeployArgsSchema = z.object({
|
||||
dryRun: z.boolean().default(false),
|
||||
path: z.string().default('./buster'),
|
||||
});
|
||||
|
||||
export type DeployArgs = z.infer<typeof DeployArgsSchema>;
|
||||
|
||||
export const DeploymentResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
modelsDeployed: z.number(),
|
||||
errors: z.array(z.string()).optional(),
|
||||
warnings: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type DeploymentResult = z.infer<typeof DeploymentResultSchema>;
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import Spinner from 'ink-spinner';
|
||||
import type { InitArgs } from './types.js';
|
||||
import { initializeProject } from './helpers.js';
|
||||
|
||||
interface InitUIProps {
|
||||
args: InitArgs;
|
||||
}
|
||||
|
||||
type Step = 'input' | 'creating' | 'complete' | 'error';
|
||||
|
||||
export const InitUI: React.FC<InitUIProps> = ({ args }) => {
|
||||
const [step, setStep] = useState<Step>('input');
|
||||
const [projectName, setProjectName] = useState(args.name || '');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!projectName) return;
|
||||
|
||||
setStep('creating');
|
||||
|
||||
try {
|
||||
await initializeProject({
|
||||
name: projectName,
|
||||
path: args.path,
|
||||
});
|
||||
setStep('complete');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Initialization failed');
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (args.skipPrompts && !args.name) {
|
||||
return (
|
||||
<Box borderStyle="round" borderColor="red" padding={1}>
|
||||
<Text color="red">❌ Project name is required when using --skip-prompts</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (args.skipPrompts && args.name) {
|
||||
handleSubmit();
|
||||
}
|
||||
|
||||
if (step === 'input' && !args.skipPrompts) {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>Initialize a new Buster project</Text>
|
||||
<Box>
|
||||
<Text>Project name: </Text>
|
||||
<TextInput
|
||||
value={projectName}
|
||||
onChange={setProjectName}
|
||||
placeholder="my-buster-project"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'creating') {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
<Spinner type="dots" /> Creating project structure...
|
||||
</Text>
|
||||
<Text dimColor> → Creating buster folder</Text>
|
||||
<Text dimColor> → Creating docs folder</Text>
|
||||
<Text dimColor> → Creating metadata folder</Text>
|
||||
<Text dimColor> → Generating buster.yml</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'complete') {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box borderStyle="round" borderColor="green" padding={1}>
|
||||
<Text color="green">✓ Project initialized successfully!</Text>
|
||||
</Box>
|
||||
<Text>Created the following structure:</Text>
|
||||
<Text> └─ buster/</Text>
|
||||
<Text> ├─ buster.yml</Text>
|
||||
<Text> ├─ docs/</Text>
|
||||
<Text> └─ metadata/</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'error') {
|
||||
return (
|
||||
<Box borderStyle="round" borderColor="red" padding={1}>
|
||||
<Text color="red">❌ {error}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import yaml from 'js-yaml';
|
||||
import type { ProjectConfig } from './types.js';
|
||||
|
||||
interface InitOptions {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function initializeProject(options: InitOptions): Promise<void> {
|
||||
const projectPath = path.join(options.path, 'buster');
|
||||
|
||||
// Check if buster folder already exists
|
||||
try {
|
||||
await fs.access(projectPath);
|
||||
throw new Error('A buster folder already exists in this directory');
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create folder structure
|
||||
await fs.mkdir(projectPath, { recursive: true });
|
||||
await fs.mkdir(path.join(projectPath, 'docs'), { recursive: true });
|
||||
await fs.mkdir(path.join(projectPath, 'metadata'), { recursive: true });
|
||||
|
||||
// Create buster.yml
|
||||
const config: ProjectConfig = {
|
||||
version: '1.0',
|
||||
projectName: options.name,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const yamlContent = yaml.dump(config, {
|
||||
lineWidth: -1,
|
||||
quotingType: '"',
|
||||
});
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(projectPath, 'buster.yml'),
|
||||
yamlContent,
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
// Simulate some work
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { InitUI } from './components.js';
|
||||
import { InitArgsSchema } from './types.js';
|
||||
|
||||
export const initCommand = new Command('init')
|
||||
.description('Initialize a new Buster project')
|
||||
.option('-n, --name <name>', 'Project name')
|
||||
.option('-p, --path <path>', 'Project path', process.cwd())
|
||||
.option('--skip-prompts', 'Skip interactive prompts')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = InitArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<InitUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// Command arguments schema
|
||||
export const InitArgsSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
path: z.string().default(process.cwd()),
|
||||
skipPrompts: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type InitArgs = z.infer<typeof InitArgsSchema>;
|
||||
|
||||
// Project configuration schema
|
||||
export const ProjectConfigSchema = z.object({
|
||||
version: z.string().default('1.0'),
|
||||
projectName: z.string(),
|
||||
organization: z.string().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { ParseArgs } from './types.js';
|
||||
|
||||
interface ParseUIProps {
|
||||
args: ParseArgs;
|
||||
}
|
||||
|
||||
export const ParseUI: React.FC<ParseUIProps> = ({ args }) => {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>Parsing YAML files...</Text>
|
||||
<Text dimColor>Path: {args.path}</Text>
|
||||
{args.files.length > 0 && (
|
||||
<Text dimColor>Files: {args.files.join(', ')}</Text>
|
||||
)}
|
||||
<Box borderStyle="round" borderColor="green" padding={1} marginTop={1}>
|
||||
<Text color="green">✓ All files parsed successfully</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { ParseUI } from './components.js';
|
||||
import { ParseArgsSchema } from './types.js';
|
||||
|
||||
export const parseCommand = new Command('parse')
|
||||
.description('Parse and validate YAML model files')
|
||||
.argument('[files...]', 'Model files to parse')
|
||||
.option('-p, --path <path>', 'Path to models directory', './buster')
|
||||
.action(async (files, options) => {
|
||||
// Validate arguments
|
||||
const args = ParseArgsSchema.parse({ ...options, files });
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<ParseUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const ParseArgsSchema = z.object({
|
||||
path: z.string().default('./buster'),
|
||||
files: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export type ParseArgs = z.infer<typeof ParseArgsSchema>;
|
|
@ -0,0 +1,19 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { ResetUI } from './components.js';
|
||||
import { ResetArgsSchema } from './types.js';
|
||||
|
||||
export const resetCommand = new Command('reset')
|
||||
.description('Reset Buster services and data')
|
||||
.option('--hard', 'Remove all data and configurations')
|
||||
.option('--force', 'Skip confirmation prompts')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = ResetArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<ResetUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { StartUI } from './components.js';
|
||||
import { StartArgsSchema } from './types.js';
|
||||
|
||||
export const startCommand = new Command('start')
|
||||
.description('Start Buster services')
|
||||
.option('-d, --detached', 'Run services in detached mode')
|
||||
.option('--no-telemetry', 'Disable telemetry')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = StartArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<StartUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { StopUI } from './components.js';
|
||||
|
||||
export const stopCommand = new Command('stop')
|
||||
.description('Stop Buster services')
|
||||
.action(async () => {
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<StopUI />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { Command } from 'commander';
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { UpdateUI } from './components.js';
|
||||
import { UpdateArgsSchema } from './types.js';
|
||||
|
||||
export const updateCommand = new Command('update')
|
||||
.description('Update Buster CLI to the latest version')
|
||||
.option('--check', 'Check for updates without installing')
|
||||
.option('--force', 'Force update even if already on latest version')
|
||||
.action(async (options) => {
|
||||
// Validate arguments
|
||||
const args = UpdateArgsSchema.parse(options);
|
||||
|
||||
// Render Ink UI
|
||||
const { waitUntilExit } = render(<UpdateUI args={args} />);
|
||||
|
||||
await waitUntilExit();
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env node
|
||||
import { Command } from 'commander';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
// Import commands
|
||||
import { authCommand } from './commands/auth/index.js';
|
||||
import { initCommand } from './commands/init/index.js';
|
||||
import { deployCommand } from './commands/deploy/index.js';
|
||||
import { parseCommand } from './commands/parse/index.js';
|
||||
import { configCommand } from './commands/config/index.js';
|
||||
import { updateCommand } from './commands/update/index.js';
|
||||
import { startCommand } from './commands/start/index.js';
|
||||
import { stopCommand } from './commands/stop/index.js';
|
||||
import { resetCommand } from './commands/reset/index.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Read package.json for version
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
||||
);
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('buster')
|
||||
.description('Buster CLI - Your gateway to the Buster platform')
|
||||
.version(packageJson.version);
|
||||
|
||||
// Register commands
|
||||
program.addCommand(authCommand);
|
||||
program.addCommand(initCommand);
|
||||
program.addCommand(deployCommand);
|
||||
program.addCommand(parseCommand);
|
||||
program.addCommand(configCommand);
|
||||
program.addCommand(updateCommand);
|
||||
program.addCommand(startCommand);
|
||||
program.addCommand(stopCommand);
|
||||
program.addCommand(resetCommand);
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse(process.argv);
|
|
@ -0,0 +1,78 @@
|
|||
import { z } from 'zod';
|
||||
import { CLIError, ApiError } from './errors.js';
|
||||
|
||||
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 url = `${this.baseUrl}${path}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new ApiError(response.status, errorText || response.statusText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return responseSchema.parse(data);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new CLIError('Invalid API response format', 'VALIDATION_ERROR');
|
||||
}
|
||||
throw new CLIError('Network error', 'NETWORK_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
async validateAuth(): Promise<boolean> {
|
||||
try {
|
||||
await this.request({
|
||||
method: 'GET',
|
||||
path: '/api/v2/auth/validate',
|
||||
responseSchema: z.object({ valid: z.boolean() }),
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let apiClient: ApiClient;
|
||||
|
||||
export function initApiClient(baseUrl: string, apiKey?: string): void {
|
||||
apiClient = new ApiClient(baseUrl, apiKey);
|
||||
}
|
||||
|
||||
export function getApiClient(): ApiClient {
|
||||
if (!apiClient) {
|
||||
throw new CLIError('API client not initialized', 'NOT_INITIALIZED');
|
||||
}
|
||||
return apiClient;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import yaml from 'js-yaml';
|
||||
import { z } from 'zod';
|
||||
import { ConfigError, FileSystemError } from './errors.js';
|
||||
|
||||
// Configuration schemas
|
||||
export const GlobalConfigSchema = z.object({
|
||||
version: z.string(),
|
||||
defaultEnvironment: z.enum(['local', 'cloud']).default('cloud'),
|
||||
telemetry: z.boolean().default(true),
|
||||
autoUpdate: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type GlobalConfig = z.infer<typeof GlobalConfigSchema>;
|
||||
|
||||
export const CredentialsSchema = z.object({
|
||||
apiUrl: z.string().url(),
|
||||
apiKey: z.string(),
|
||||
environment: z.enum(['local', 'cloud']),
|
||||
});
|
||||
|
||||
export type Credentials = z.infer<typeof CredentialsSchema>;
|
||||
|
||||
export class ConfigManager {
|
||||
private configDir: string;
|
||||
private configPath: string;
|
||||
private credentialsPath: string;
|
||||
|
||||
constructor() {
|
||||
this.configDir = process.env.BUSTER_CONFIG_DIR || path.join(os.homedir(), '.buster');
|
||||
this.configPath = path.join(this.configDir, 'config.yml');
|
||||
this.credentialsPath = path.join(this.configDir, 'credentials');
|
||||
}
|
||||
|
||||
async ensureConfigDir(): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(this.configDir, { recursive: true });
|
||||
} catch (error) {
|
||||
throw new FileSystemError('Failed to create config directory');
|
||||
}
|
||||
}
|
||||
|
||||
async loadGlobalConfig(): Promise<GlobalConfig> {
|
||||
try {
|
||||
const content = await fs.readFile(this.configPath, 'utf-8');
|
||||
const parsed = yaml.load(content);
|
||||
return GlobalConfigSchema.parse(parsed);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// Return defaults if config doesn't exist
|
||||
return GlobalConfigSchema.parse({});
|
||||
}
|
||||
throw new ConfigError('Failed to load configuration');
|
||||
}
|
||||
}
|
||||
|
||||
async saveGlobalConfig(config: GlobalConfig): Promise<void> {
|
||||
await this.ensureConfigDir();
|
||||
const yamlContent = yaml.dump(config);
|
||||
await fs.writeFile(this.configPath, yamlContent, 'utf-8');
|
||||
}
|
||||
|
||||
async loadCredentials(): Promise<Credentials | null> {
|
||||
try {
|
||||
const content = await fs.readFile(this.credentialsPath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
return CredentialsSchema.parse(parsed);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw new ConfigError('Failed to load credentials');
|
||||
}
|
||||
}
|
||||
|
||||
async saveCredentials(credentials: Credentials): Promise<void> {
|
||||
await this.ensureConfigDir();
|
||||
// TODO: Encrypt credentials before saving
|
||||
const jsonContent = JSON.stringify(credentials, null, 2);
|
||||
await fs.writeFile(this.credentialsPath, jsonContent, 'utf-8');
|
||||
// Set restrictive permissions
|
||||
await fs.chmod(this.credentialsPath, 0o600);
|
||||
}
|
||||
|
||||
async clearCredentials(): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(this.credentialsPath);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw new ConfigError('Failed to clear credentials');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const configManager = new ConfigManager();
|
|
@ -0,0 +1,34 @@
|
|||
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');
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends CLIError {
|
||||
constructor(message: string) {
|
||||
super(message, 'VALIDATION_ERROR');
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigError extends CLIError {
|
||||
constructor(message: string) {
|
||||
super(message, 'CONFIG_ERROR');
|
||||
this.name = 'ConfigError';
|
||||
}
|
||||
}
|
||||
|
||||
export class FileSystemError extends CLIError {
|
||||
constructor(message: string) {
|
||||
super(message, 'FS_ERROR');
|
||||
this.name = 'FileSystemError';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "@buster/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/main.ts'],
|
||||
format: ['esm'],
|
||||
target: 'node22',
|
||||
platform: 'node',
|
||||
outDir: 'dist',
|
||||
clean: true,
|
||||
dts: false,
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
splitting: false,
|
||||
shims: false,
|
||||
external: [
|
||||
'@buster/server-shared',
|
||||
'@buster/typescript-config',
|
||||
'@buster/vitest-config',
|
||||
],
|
||||
esbuildOptions(options) {
|
||||
options.keepNames = true;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true,
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import baseConfig from '@buster/vitest-config';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
|
||||
export default mergeConfig(
|
||||
baseConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
},
|
||||
})
|
||||
);
|
1244
pnpm-lock.yaml
1244
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ packages:
|
|||
- "apps/electric-server"
|
||||
- "apps/trigger"
|
||||
- "apps/api"
|
||||
- "apps/cli"
|
||||
|
||||
catalog:
|
||||
"@mastra/core": "^0.10.8"
|
||||
|
@ -25,7 +26,7 @@ catalog:
|
|||
hono: "^4.8.0"
|
||||
pg: "^8.16.2"
|
||||
platejs: "^49.2.4"
|
||||
"remark-gfm": "^4.0.1"
|
||||
"remark-gfm": "^4.0.1"
|
||||
tsup: "^8.5.0"
|
||||
tsx: "^4.20.0"
|
||||
uuid: "^11.0.0"
|
||||
|
|
Loading…
Reference in New Issue