feat: add admin CLI for project management
- User management (list, create, delete, reset-password) - Data CRUD for all 32 collections - Import/export with JSON support - Worker/queue management (status, jobs, retry, trigger) - Webhook management (list, test, deliveries, retry) - Agent management (list, CRUD, tasks, trigger, activity) - Health diagnostics (check, errors, stats) - JSON output mode (--json flag) - Run via: npm run cli -- <command>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
export interface ParsedArgs {
|
||||
command: string;
|
||||
subcommand: string;
|
||||
positional: string[];
|
||||
flags: Record<string, string | boolean>;
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const args = argv.slice(2); // skip node and script path
|
||||
const flags: Record<string, string | boolean> = {};
|
||||
const positional: string[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.slice(2);
|
||||
// Check if next arg is a value (not a flag)
|
||||
const next = args[i + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
flags[key] = next;
|
||||
i++;
|
||||
} else {
|
||||
flags[key] = true;
|
||||
}
|
||||
} else if (arg.startsWith('-') && arg.length === 2) {
|
||||
// Short flags like -j
|
||||
const key = arg.slice(1);
|
||||
const next = args[i + 1];
|
||||
if (next && !next.startsWith('-')) {
|
||||
flags[key] = next;
|
||||
i++;
|
||||
} else {
|
||||
flags[key] = true;
|
||||
}
|
||||
} else {
|
||||
positional.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command: positional[0] || '',
|
||||
subcommand: positional[1] || '',
|
||||
positional: positional.slice(2),
|
||||
flags,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFlag(flags: Record<string, string | boolean>, name: string, defaultValue = ''): string {
|
||||
const val = flags[name];
|
||||
if (val === undefined || val === true) return defaultValue;
|
||||
return String(val);
|
||||
}
|
||||
|
||||
export function getFlagNumber(flags: Record<string, string | boolean>, name: string, defaultValue: number): number {
|
||||
const val = getFlag(flags, name);
|
||||
if (!val) return defaultValue;
|
||||
const num = parseInt(val, 10);
|
||||
return isNaN(num) ? defaultValue : num;
|
||||
}
|
||||
|
||||
export function hasFlag(flags: Record<string, string | boolean>, name: string): boolean {
|
||||
return flags[name] !== undefined;
|
||||
}
|
||||
|
||||
export function requirePositional(positional: string[], index: number, name: string): string {
|
||||
const val = positional[index];
|
||||
if (!val) {
|
||||
console.error(`Error: Missing required argument: ${name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
// Load .env.local from project root
|
||||
const envPaths = [
|
||||
resolve(process.cwd(), '.env.local'),
|
||||
resolve(process.cwd(), '.env'),
|
||||
];
|
||||
|
||||
for (const p of envPaths) {
|
||||
if (existsSync(p)) {
|
||||
config({ path: p });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Default DATABASE_URL for local development
|
||||
const defaultUrl = 'postgresql://project_e:development-password@localhost:5432/project_e';
|
||||
if (!process.env.DATABASE_URL) {
|
||||
process.env.DATABASE_URL = defaultUrl;
|
||||
}
|
||||
|
||||
// Import db package after env is set
|
||||
const { db, sql: drizzleSql } = await import('@project-e/db');
|
||||
const { records } = await import('@project-e/db/schema');
|
||||
|
||||
// Re-export everything
|
||||
export { db, records, and, eq };
|
||||
|
||||
// Export a raw SQL function for direct queries
|
||||
export async function rawQuery(query: string, params?: unknown[]): Promise<unknown[]> {
|
||||
const result = await drizzleSql.unsafe(query, params);
|
||||
return result;
|
||||
}
|
||||
|
||||
export type RecordData = Record<string, unknown>;
|
||||
|
||||
export function serializeRecord(record: typeof records.$inferSelect): RecordData {
|
||||
return {
|
||||
...(record.data as RecordData),
|
||||
id: record.id,
|
||||
created: record.createdAt.toISOString(),
|
||||
updated: record.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
type Column = { key: string; label: string; width?: number; align?: 'left' | 'right' };
|
||||
|
||||
export function printTable(rows: Record<string, unknown>[], columns: Column[]): void {
|
||||
if (rows.length === 0) {
|
||||
console.log('(no records)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate widths
|
||||
const widths = columns.map((col) => {
|
||||
if (col.width) return col.width;
|
||||
const headerLen = col.label.length;
|
||||
const maxDataLen = rows.reduce((max, row) => {
|
||||
const val = String(row[col.key] ?? '');
|
||||
return Math.max(max, val.length);
|
||||
}, 0);
|
||||
return Math.max(headerLen, maxDataLen);
|
||||
});
|
||||
|
||||
// Print header
|
||||
const header = columns.map((col, i) => col.label.padEnd(widths[i])).join(' ');
|
||||
console.log(header);
|
||||
console.log(widths.map((w) => '-'.repeat(w)).join(' '));
|
||||
|
||||
// Print rows
|
||||
for (const row of rows) {
|
||||
const line = columns.map((col, i) => {
|
||||
const val = String(row[col.key] ?? '');
|
||||
return col.align === 'right' ? val.padStart(widths[i]) : val.padEnd(widths[i]);
|
||||
}).join(' ');
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
export function printJson(data: unknown): void {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function printSuccess(message: string): void {
|
||||
console.log(`✓ ${message}`);
|
||||
}
|
||||
|
||||
export function printError(message: string): void {
|
||||
console.error(`✗ ${message}`);
|
||||
}
|
||||
|
||||
export function printHeader(message: string): void {
|
||||
console.log(`\n${'='.repeat(50)}`);
|
||||
console.log(` ${message}`);
|
||||
console.log(`${'='.repeat(50)}`);
|
||||
}
|
||||
Reference in New Issue
Block a user