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:
2026-07-24 07:02:11 -04:00
parent ec33544172
commit 6c438eab32
13 changed files with 1644 additions and 3 deletions
+72
View File
@@ -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;
}
+47
View File
@@ -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(),
};
}
+51
View File
@@ -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)}`);
}