2025-09-16 05:06:41 +08:00
|
|
|
# CLI Application
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
This is the command-line interface application built with TypeScript and Bun. It provides terminal-based access to Buster functionality.
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Core Responsibility
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
`@buster-app/cli` is responsible for:
|
|
|
|
- Command-line interface for Buster operations
|
|
|
|
- Developer tools and utilities
|
|
|
|
- Administrative tasks
|
|
|
|
- Data import/export operations
|
|
|
|
- Communication with server via server-shared types
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Architecture
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
```
|
|
|
|
CLI Commands → @buster-app/cli → @buster/server-shared → API Server
|
|
|
|
↓
|
|
|
|
Packages
|
|
|
|
(Direct package usage)
|
|
|
|
```
|
|
|
|
|
|
|
|
## Command Structure
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Directory Organization
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```
|
2025-09-16 05:06:41 +08:00
|
|
|
cli/
|
2025-08-28 04:33:42 +08:00
|
|
|
├── src/
|
2025-09-16 05:06:41 +08:00
|
|
|
│ ├── commands/
|
|
|
|
│ │ ├── auth/
|
|
|
|
│ │ │ ├── login.ts
|
|
|
|
│ │ │ ├── logout.ts
|
|
|
|
│ │ │ └── status.ts
|
|
|
|
│ │ ├── data/
|
|
|
|
│ │ │ ├── import.ts
|
|
|
|
│ │ │ ├── export.ts
|
|
|
|
│ │ │ └── sync.ts
|
|
|
|
│ │ ├── dashboard/
|
|
|
|
│ │ │ ├── create.ts
|
|
|
|
│ │ │ ├── list.ts
|
|
|
|
│ │ │ └── delete.ts
|
|
|
|
│ │ └── admin/
|
|
|
|
│ │ ├── users.ts
|
|
|
|
│ │ ├── organizations.ts
|
|
|
|
│ │ └── system.ts
|
|
|
|
│ ├── lib/
|
|
|
|
│ │ ├── api-client.ts
|
|
|
|
│ │ ├── config.ts
|
|
|
|
│ │ └── utils.ts
|
|
|
|
│ └── index.ts
|
2025-08-28 04:33:42 +08:00
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Command Implementation
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Basic Command Pattern
|
|
|
|
|
|
|
|
Commands are pure functions using Commander.js:
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```typescript
|
2025-09-16 05:06:41 +08:00
|
|
|
import { Command } from 'commander';
|
2025-08-28 04:33:42 +08:00
|
|
|
import { z } from 'zod';
|
2025-09-16 05:06:41 +08:00
|
|
|
import type { CreateDashboardRequest } from '@buster/server-shared';
|
|
|
|
import { apiClient } from '../lib/api-client';
|
|
|
|
|
|
|
|
// Command options schema
|
|
|
|
const CreateDashboardOptionsSchema = z.object({
|
|
|
|
name: z.string().describe('Dashboard name'),
|
|
|
|
description: z.string().optional().describe('Dashboard description'),
|
|
|
|
datasource: z.string().uuid().describe('Data source ID'),
|
|
|
|
public: z.boolean().default(false).describe('Make dashboard public')
|
2025-08-28 04:33:42 +08:00
|
|
|
});
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
type CreateDashboardOptions = z.infer<typeof CreateDashboardOptionsSchema>;
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export const createDashboardCommand = new Command('create')
|
|
|
|
.description('Create a new dashboard')
|
|
|
|
.requiredOption('-n, --name <name>', 'Dashboard name')
|
|
|
|
.option('-d, --description <desc>', 'Dashboard description')
|
|
|
|
.requiredOption('-s, --datasource <id>', 'Data source ID')
|
|
|
|
.option('-p, --public', 'Make dashboard public', false)
|
|
|
|
.action(async (options: unknown) => {
|
|
|
|
try {
|
|
|
|
const validated = CreateDashboardOptionsSchema.parse(options);
|
|
|
|
|
|
|
|
// Create request
|
|
|
|
const request: CreateDashboardRequest = {
|
|
|
|
name: validated.name,
|
|
|
|
description: validated.description,
|
|
|
|
dataSourceId: validated.datasource,
|
|
|
|
isPublic: validated.public
|
|
|
|
};
|
|
|
|
|
|
|
|
// Call API
|
|
|
|
const response = await apiClient.createDashboard(request);
|
|
|
|
|
|
|
|
// Display result
|
|
|
|
console.info(`✅ Dashboard created: ${response.dashboard.id}`);
|
|
|
|
console.info(` Name: ${response.dashboard.name}`);
|
|
|
|
console.info(` URL: ${response.dashboard.url}`);
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof z.ZodError) {
|
|
|
|
console.error('❌ Invalid options:', error.errors);
|
|
|
|
} else {
|
|
|
|
console.error('❌ Failed to create dashboard:', error.message);
|
|
|
|
}
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### Interactive Command Pattern
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import { prompt } from 'enquirer';
|
|
|
|
|
|
|
|
export const loginCommand = new Command('login')
|
|
|
|
.description('Login to Buster')
|
|
|
|
.option('-e, --email <email>', 'Email address')
|
|
|
|
.option('-p, --password <password>', 'Password')
|
|
|
|
.action(async (options) => {
|
|
|
|
// Get credentials interactively if not provided
|
|
|
|
const credentials = await getCredentials(options);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const response = await apiClient.login(credentials);
|
|
|
|
|
|
|
|
// Save token
|
|
|
|
await saveAuthToken(response.token);
|
|
|
|
|
|
|
|
console.info('✅ Successfully logged in');
|
|
|
|
console.info(` User: ${response.user.email}`);
|
|
|
|
console.info(` Organization: ${response.organization.name}`);
|
|
|
|
} catch (error) {
|
|
|
|
console.error('❌ Login failed:', error.message);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
async function getCredentials(options: any) {
|
|
|
|
const email = options.email || await prompt({
|
|
|
|
type: 'input',
|
|
|
|
name: 'email',
|
|
|
|
message: 'Email:',
|
|
|
|
validate: (value) => z.string().email().safeParse(value).success
|
|
|
|
});
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
const password = options.password || await prompt({
|
|
|
|
type: 'password',
|
|
|
|
name: 'password',
|
|
|
|
message: 'Password:'
|
2025-08-28 04:33:42 +08:00
|
|
|
});
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
return { email, password };
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
2025-09-16 05:06:41 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
## API Client
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Type-Safe API Client
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
```typescript
|
|
|
|
// lib/api-client.ts
|
|
|
|
import type {
|
|
|
|
CreateDashboardRequest,
|
|
|
|
CreateDashboardResponse,
|
|
|
|
GetDashboardsResponse
|
|
|
|
} from '@buster/server-shared';
|
|
|
|
|
|
|
|
class ApiClient {
|
|
|
|
private baseUrl: string;
|
|
|
|
private token?: string;
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
constructor(baseUrl: string) {
|
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
this.token = getStoredToken();
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
private async request<T>(
|
|
|
|
path: string,
|
|
|
|
options: RequestInit = {}
|
|
|
|
): Promise<T> {
|
|
|
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
|
|
...options,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
...(this.token && { Authorization: `Bearer ${this.token}` }),
|
|
|
|
...options.headers
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
const error = await response.json();
|
|
|
|
throw new ApiError(error.message, response.status);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.json();
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
async createDashboard(
|
|
|
|
data: CreateDashboardRequest
|
|
|
|
): Promise<CreateDashboardResponse> {
|
|
|
|
return this.request('/api/v2/dashboards', {
|
|
|
|
method: 'POST',
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
});
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
async getDashboards(): Promise<GetDashboardsResponse> {
|
|
|
|
return this.request('/api/v2/dashboards');
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export const apiClient = new ApiClient(
|
|
|
|
process.env.BUSTER_API_URL || 'http://localhost:8080'
|
|
|
|
);
|
2025-08-28 04:33:42 +08:00
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Configuration Management
|
|
|
|
|
|
|
|
### Config File Pattern
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```typescript
|
2025-09-16 05:06:41 +08:00
|
|
|
// lib/config.ts
|
2025-08-28 04:33:42 +08:00
|
|
|
import { z } from 'zod';
|
2025-09-16 05:06:41 +08:00
|
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
|
import { homedir } from 'node:os';
|
|
|
|
import { join } from 'node:path';
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
const ConfigSchema = z.object({
|
|
|
|
apiUrl: z.string().url().default('http://localhost:8080'),
|
|
|
|
token: z.string().optional(),
|
|
|
|
organization: z.string().uuid().optional(),
|
|
|
|
outputFormat: z.enum(['json', 'table', 'csv']).default('table')
|
2025-08-28 04:33:42 +08:00
|
|
|
});
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
type Config = z.infer<typeof ConfigSchema>;
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
const CONFIG_PATH = join(homedir(), '.buster', 'config.json');
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export function loadConfig(): Config {
|
|
|
|
try {
|
|
|
|
const data = readFileSync(CONFIG_PATH, 'utf-8');
|
|
|
|
return ConfigSchema.parse(JSON.parse(data));
|
|
|
|
} catch {
|
|
|
|
return ConfigSchema.parse({});
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
}
|
2025-09-16 05:06:41 +08:00
|
|
|
|
|
|
|
export function saveConfig(config: Partial<Config>): void {
|
|
|
|
const current = loadConfig();
|
|
|
|
const updated = { ...current, ...config };
|
|
|
|
const validated = ConfigSchema.parse(updated);
|
|
|
|
|
|
|
|
writeFileSync(
|
|
|
|
CONFIG_PATH,
|
|
|
|
JSON.stringify(validated, null, 2)
|
|
|
|
);
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Output Formatting
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Table Output
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
```typescript
|
|
|
|
import { table } from 'table';
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export function formatAsTable(data: any[]): string {
|
|
|
|
if (data.length === 0) {
|
|
|
|
return 'No data';
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
const headers = Object.keys(data[0]);
|
|
|
|
const rows = data.map(item =>
|
|
|
|
headers.map(h => String(item[h] ?? ''))
|
|
|
|
);
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
return table([headers, ...rows], {
|
|
|
|
border: {
|
|
|
|
topBody: '─',
|
|
|
|
topJoin: '┬',
|
|
|
|
topLeft: '┌',
|
|
|
|
topRight: '┐',
|
|
|
|
bottomBody: '─',
|
|
|
|
bottomJoin: '┴',
|
|
|
|
bottomLeft: '└',
|
|
|
|
bottomRight: '┘',
|
|
|
|
bodyLeft: '│',
|
|
|
|
bodyRight: '│',
|
|
|
|
bodyJoin: '│',
|
|
|
|
joinBody: '─',
|
|
|
|
joinLeft: '├',
|
|
|
|
joinRight: '┤',
|
|
|
|
joinJoin: '┼'
|
|
|
|
}
|
|
|
|
});
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
2025-09-16 05:06:41 +08:00
|
|
|
```
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### JSON Output
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
export function formatAsJson(data: any): string {
|
|
|
|
return JSON.stringify(data, null, 2);
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### CSV Output
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```typescript
|
2025-09-16 05:06:41 +08:00
|
|
|
export function formatAsCsv(data: any[]): string {
|
|
|
|
if (data.length === 0) {
|
|
|
|
return '';
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
const headers = Object.keys(data[0]);
|
|
|
|
const rows = data.map(item =>
|
|
|
|
headers.map(h => escapeCSV(String(item[h] ?? '')))
|
|
|
|
);
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
return [
|
|
|
|
headers.join(','),
|
|
|
|
...rows.map(r => r.join(','))
|
|
|
|
].join('\n');
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
function escapeCSV(value: string): string {
|
|
|
|
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
|
|
return `"${value.replace(/"/g, '""')}"`;
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
2025-09-16 05:06:41 +08:00
|
|
|
return value;
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Progress Indicators
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Spinner Pattern
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```typescript
|
2025-09-16 05:06:41 +08:00
|
|
|
import ora from 'ora';
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export async function withSpinner<T>(
|
|
|
|
message: string,
|
|
|
|
task: () => Promise<T>
|
|
|
|
): Promise<T> {
|
|
|
|
const spinner = ora(message).start();
|
|
|
|
|
|
|
|
try {
|
|
|
|
const result = await task();
|
|
|
|
spinner.succeed();
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
|
|
spinner.fail();
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
// Usage
|
|
|
|
const dashboards = await withSpinner(
|
|
|
|
'Fetching dashboards...',
|
|
|
|
() => apiClient.getDashboards()
|
|
|
|
);
|
2025-08-28 04:33:42 +08:00
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Progress Bar
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
```typescript
|
|
|
|
import { createProgressBar } from './utils';
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export async function importData(file: string) {
|
|
|
|
const data = await readDataFile(file);
|
|
|
|
const progress = createProgressBar(data.length);
|
|
|
|
|
|
|
|
for (const [index, record] of data.entries()) {
|
|
|
|
await processRecord(record);
|
|
|
|
progress.update(index + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
progress.stop();
|
|
|
|
console.info('✅ Import complete');
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
## Error Handling
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### User-Friendly Errors
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```typescript
|
2025-09-16 05:06:41 +08:00
|
|
|
export class CliError extends Error {
|
|
|
|
constructor(
|
|
|
|
message: string,
|
|
|
|
public readonly code: string,
|
|
|
|
public readonly details?: any
|
|
|
|
) {
|
|
|
|
super(message);
|
|
|
|
this.name = 'CliError';
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
export function handleError(error: unknown): void {
|
|
|
|
if (error instanceof CliError) {
|
|
|
|
console.error(`❌ ${error.message}`);
|
|
|
|
if (error.details && process.env.DEBUG) {
|
|
|
|
console.error('Details:', error.details);
|
|
|
|
}
|
|
|
|
} else if (error instanceof z.ZodError) {
|
|
|
|
console.error('❌ Validation error:');
|
|
|
|
error.errors.forEach(e => {
|
|
|
|
console.error(` - ${e.path.join('.')}: ${e.message}`);
|
|
|
|
});
|
|
|
|
} else if (error instanceof ApiError) {
|
|
|
|
console.error(`❌ API error: ${error.message}`);
|
|
|
|
} else {
|
|
|
|
console.error('❌ Unexpected error:', error);
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
process.exit(1);
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Main Entry Point
|
2025-08-28 04:33:42 +08:00
|
|
|
|
|
|
|
```typescript
|
2025-09-16 05:06:41 +08:00
|
|
|
// index.ts
|
|
|
|
#!/usr/bin/env bun
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
import { Command } from 'commander';
|
|
|
|
import { version } from '../package.json';
|
|
|
|
import { authCommands } from './commands/auth';
|
|
|
|
import { dataCommands } from './commands/data';
|
|
|
|
import { dashboardCommands } from './commands/dashboard';
|
|
|
|
|
|
|
|
const program = new Command();
|
|
|
|
|
|
|
|
program
|
|
|
|
.name('buster')
|
|
|
|
.description('Buster CLI - Command line interface for Buster')
|
|
|
|
.version(version);
|
|
|
|
|
|
|
|
// Add command groups
|
|
|
|
program.addCommand(authCommands);
|
|
|
|
program.addCommand(dataCommands);
|
|
|
|
program.addCommand(dashboardCommands);
|
|
|
|
|
|
|
|
// Global error handling
|
|
|
|
program.exitOverride((err) => {
|
|
|
|
handleError(err);
|
2025-08-28 04:33:42 +08:00
|
|
|
});
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
// Parse arguments
|
|
|
|
program.parse(process.argv);
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
// Show help if no command
|
|
|
|
if (!process.argv.slice(2).length) {
|
|
|
|
program.outputHelp();
|
2025-08-28 04:33:42 +08:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Testing Patterns
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### Command Testing
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
```typescript
|
|
|
|
describe('create dashboard command', () => {
|
|
|
|
it('should create dashboard with valid options', async () => {
|
|
|
|
const mockApi = jest.spyOn(apiClient, 'createDashboard')
|
|
|
|
.mockResolvedValue({
|
|
|
|
dashboard: {
|
|
|
|
id: '123',
|
|
|
|
name: 'Test Dashboard',
|
|
|
|
url: 'http://example.com/dashboard/123'
|
|
|
|
}
|
|
|
|
});
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
await createDashboardCommand.parseAsync([
|
|
|
|
'node',
|
|
|
|
'cli',
|
|
|
|
'--name',
|
|
|
|
'Test Dashboard',
|
|
|
|
'--datasource',
|
|
|
|
'abc-123'
|
|
|
|
]);
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
expect(mockApi).toHaveBeenCalledWith({
|
|
|
|
name: 'Test Dashboard',
|
|
|
|
dataSourceId: 'abc-123',
|
|
|
|
isPublic: false
|
2025-08-28 04:33:42 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Best Practices
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### DO:
|
|
|
|
- Use Commander.js for command parsing
|
|
|
|
- Validate all inputs with Zod
|
|
|
|
- Import types from server-shared
|
|
|
|
- Provide interactive prompts for missing options
|
|
|
|
- Use spinners for long operations
|
|
|
|
- Format output based on user preference
|
|
|
|
- Handle errors gracefully
|
|
|
|
- Store configuration securely
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
### DON'T:
|
|
|
|
- Define API types locally
|
|
|
|
- Use classes for command logic
|
|
|
|
- Skip input validation
|
|
|
|
- Expose sensitive information
|
|
|
|
- Block on long operations without feedback
|
|
|
|
- Hardcode API endpoints
|
|
|
|
- Store passwords in plain text
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
## Package Integration
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
The CLI can directly use packages for local operations:
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import { introspectDatabase } from '@buster/data-source';
|
|
|
|
import { validateSchema } from '@buster/database';
|
|
|
|
|
|
|
|
export const validateCommand = new Command('validate')
|
|
|
|
.description('Validate database schema')
|
|
|
|
.requiredOption('-d, --datasource <id>', 'Data source ID')
|
|
|
|
.action(async (options) => {
|
|
|
|
const spinner = ora('Validating schema...').start();
|
2025-08-28 04:33:42 +08:00
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
try {
|
|
|
|
// Direct package usage for local operation
|
|
|
|
const schema = await introspectDatabase(options.datasource);
|
|
|
|
const validation = await validateSchema(schema);
|
|
|
|
|
|
|
|
spinner.succeed('Schema validated');
|
|
|
|
|
|
|
|
if (validation.warnings.length > 0) {
|
|
|
|
console.warn('⚠️ Warnings:');
|
|
|
|
validation.warnings.forEach(w => console.warn(` - ${w}`));
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
spinner.fail('Validation failed');
|
|
|
|
throw error;
|
|
|
|
}
|
2025-08-28 04:33:42 +08:00
|
|
|
});
|
|
|
|
```
|
|
|
|
|
2025-09-16 05:06:41 +08:00
|
|
|
This app should assemble packages for CLI operations and communicate with the server using server-shared types.
|