diff --git a/CHANGELOG.md b/CHANGELOG.md index d89fdec..f8495fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [1.1.0](https://github.com/niledatabase/cli/compare/v1.0.11...v1.1.0) (2025-03-24) + + +### Features + +* new features for local dev ([f542d3d](https://github.com/niledatabase/cli/commit/f542d3dfeccfaef051f2f9652ef8c986794c8356)) +* new features for local dev ([5758a5f](https://github.com/niledatabase/cli/commit/5758a5f2fcbbd49dcfd9bd3a0778ce223729d0bf)) + # [1.1.0-alpha.1](https://github.com/niledatabase/cli/compare/v1.0.11-alpha.1...v1.1.0-alpha.1) (2025-02-22) diff --git a/src/__tests__/commands/db.test.ts b/src/__tests__/commands/db.test.ts index 6c217d7..181046e 100644 --- a/src/__tests__/commands/db.test.ts +++ b/src/__tests__/commands/db.test.ts @@ -118,16 +118,10 @@ describe('DB Command', () => { it('should handle API errors', async () => { const error = new Error('API Error'); mockNileAPI.listDatabases.mockRejectedValueOnce(error); - try { - await program.parseAsync(['node', 'test', 'db', 'list']); - fail('Should have thrown an error'); - } catch (e) { - expect(e).toBeInstanceOf(ProcessExitError); - expect(console.error).toHaveBeenCalledWith( - theme.error('Failed to list databases:'), - 'API Error' - ); - } + + await expect( + program.parseAsync(['node', 'test', 'db', 'list']) + ).rejects.toThrow('API Error'); }); it('should output in JSON format', async () => { @@ -171,7 +165,9 @@ describe('DB Command', () => { expect(mockNileAPI.createDatabase).toHaveBeenCalledWith('test-workspace', 'test-db', 'AWS_US_WEST_2'); const calls = (console.log as jest.Mock).mock.calls; const output = calls.map(call => call[0]).join('\n'); - expect(output).toContain(`Database '${theme.bold('test-db')}' created successfully`); + expect(output).toContain('test-db'); + expect(output).toContain('AWS_US_WEST_2'); + expect(output).toContain('CREATING'); }); it('should require name option', async () => { @@ -183,7 +179,7 @@ describe('DB Command', () => { const stderrCalls = mockStderrWrite.mock.calls; const stderrOutput = stderrCalls.map(call => call[0]).join(''); - expect(stderrOutput).toContain("error: required option '--name ' not specified"); + expect(stderrOutput).toContain("error: required option '--name ' not specified"); }); it('should require region option', async () => { @@ -195,19 +191,15 @@ describe('DB Command', () => { const stderrCalls = mockStderrWrite.mock.calls; const stderrOutput = stderrCalls.map(call => call[0]).join(''); - expect(stderrOutput).toContain("error: required option '--region ' not specified"); + expect(stderrOutput).toContain("error: required option '--region ' not specified"); }); it('should handle API errors', async () => { mockNileAPI.createDatabase.mockRejectedValue(new Error('API error')); - try { - await program.parseAsync(['node', 'test', 'db', 'create', '--name', 'test-db', '--region', 'AWS_US_WEST_2']); - } catch (error) { - expectProcessExit(error); - } - - expect(console.error).toHaveBeenCalledWith(theme.error('Failed to create database:'), 'API error'); + await expect( + program.parseAsync(['node', 'test', 'db', 'create', '--name', 'test-db', '--region', 'AWS_US_WEST_2']) + ).rejects.toThrow('API error'); }); }); @@ -222,7 +214,11 @@ describe('DB Command', () => { await program.parseAsync(['node', 'test', 'db', 'show', 'test-db']); expect(mockNileAPI.getDatabase).toHaveBeenCalledWith('test-workspace', 'test-db'); - expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Database details:')); + const calls = (console.log as jest.Mock).mock.calls; + const output = calls.map(call => call[0]).join('\n'); + expect(output).toContain('test-db'); + expect(output).toContain('AWS_US_WEST_2'); + expect(output).toContain('ACTIVE'); }); it('should output in JSON format', async () => { @@ -261,39 +257,24 @@ describe('DB Command', () => { it('should delete database with provided name', async () => { mockNileAPI.deleteDatabase.mockResolvedValue(undefined); - await program.parseAsync(['node', 'test', 'db', 'delete', 'test-db', '--force']); + await program.parseAsync(['node', 'test', 'db', 'delete', 'test-db']); expect(mockNileAPI.deleteDatabase).toHaveBeenCalledWith('test-workspace', 'test-db'); expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Database deleted successfully')); }); it('should require database name', async () => { - mockConfigManager.getDatabase.mockReturnValue(undefined); - - try { - await program.parseAsync(['node', 'test', 'db', 'delete']); - } catch (error) { - expectProcessExit(error); - } - - const actualError = (console.error as jest.Mock).mock.calls[0][1]; - expect(console.error).toHaveBeenCalledWith( - theme.error('Failed to delete database:'), - expect.stringContaining('No database specified') - ); - expect(actualError).toContain('No database specified'); + await expect( + program.parseAsync(['node', 'test', 'db', 'delete']) + ).rejects.toThrow(); }); it('should handle API errors', async () => { mockNileAPI.deleteDatabase.mockRejectedValue(new Error('API error')); - try { - await program.parseAsync(['node', 'test', 'db', 'delete', 'test-db', '--force']); - } catch (error) { - expectProcessExit(error); - } - - expect(console.error).toHaveBeenCalledWith(theme.error('Failed to delete database:'), 'API error'); + await expect( + program.parseAsync(['node', 'test', 'db', 'delete', 'test-db']) + ).rejects.toThrow('API error'); }); }); @@ -304,9 +285,11 @@ describe('DB Command', () => { await program.parseAsync(['node', 'test', 'db', 'regions']); expect(mockNileAPI.listRegions).toHaveBeenCalledWith('test-workspace'); - expect(console.log).toHaveBeenCalledWith(theme.primary('\nAvailable regions:')); - expect(console.log).toHaveBeenCalledWith(expect.stringContaining('AWS_US_WEST_2')); - expect(console.log).toHaveBeenCalledWith(expect.stringContaining('AWS_US_EAST_1')); + const calls = (console.log as jest.Mock).mock.calls; + const output = calls.map(call => call[0]).join('\n'); + expect(output).toContain('NAME'); + expect(output).toContain('AWS_US_WEST_2'); + expect(output).toContain('AWS_US_EAST_1'); }); it('should output in JSON format', async () => { @@ -324,7 +307,7 @@ describe('DB Command', () => { await program.parseAsync(['node', 'test', 'db', 'regions']); - expect(console.log).toHaveBeenCalledWith('REGION'); + expect(console.log).toHaveBeenCalledWith('NAME'); expect(console.log).toHaveBeenCalledWith('AWS_US_WEST_2'); expect(console.log).toHaveBeenCalledWith('AWS_US_EAST_1'); }); @@ -332,13 +315,9 @@ describe('DB Command', () => { it('should handle API errors', async () => { mockNileAPI.listRegions.mockRejectedValue(new Error('API error')); - try { - await program.parseAsync(['node', 'test', 'db', 'regions']); - } catch (error) { - expectProcessExit(error); - } - - expect(console.error).toHaveBeenCalledWith(theme.error('Failed to list regions:'), 'API error'); + await expect( + program.parseAsync(['node', 'test', 'db', 'regions']) + ).rejects.toThrow('API error'); }); }); @@ -381,67 +360,25 @@ describe('DB Command', () => { }); it('should require database name', async () => { - mockConfigManager.getDatabase.mockReturnValue(undefined); - mockNileAPI.getDatabaseConnection.mockImplementation(() => { - throw new Error('No database specified. Use one of:\n1. --db flag\n2. nile config --db \n3. NILE_DB environment variable'); - }); - - try { - await program.parseAsync(['node', 'test', 'db', 'connectionstring', '--psql']); - } catch (error) { - expectProcessExit(error); - } - - const actualError = (console.error as jest.Mock).mock.calls[0][1]; - expect(console.error).toHaveBeenCalledWith( - theme.error('Failed to get connection string:'), - expect.stringContaining('No database specified') - ); - expect(actualError).toContain('No database specified'); - }); - - it('should require --psql flag', async () => { - const stderrWrite = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); - - try { - await program.parseAsync(['node', 'test', 'db', 'connectionstring', '--name', 'test-db']); - } catch (error) { - expectProcessExit(error); - } - - const stderrOutput = stderrWrite.mock.calls.map(call => call[0]).join(''); - expect(stderrOutput).toContain("error: required option '--psql' not specified"); - stderrWrite.mockRestore(); + await expect( + program.parseAsync(['node', 'test', 'db', 'connectionstring']) + ).rejects.toThrow(); }); it('should require workspace', async () => { mockConfigManager.getWorkspace.mockReturnValue(undefined); - mockConfigManager.getDatabase.mockReturnValue('test-db'); - try { - await program.parseAsync(['node', 'test', 'db', 'connectionstring', '--name', 'test-db', '--psql']); - } catch (error) { - expectProcessExit(error); - } - - const actualError = (console.error as jest.Mock).mock.calls[0][1]; - expect(console.error).toHaveBeenCalledWith( - theme.error('Failed to get connection string:'), - expect.stringContaining('No workspace specified') - ); - expect(actualError).toContain('No workspace specified'); + await expect( + program.parseAsync(['node', 'test', 'db', 'connectionstring', 'test-db']) + ).rejects.toThrow(); }); it('should handle API errors', async () => { mockNileAPI.getDatabaseConnection.mockRejectedValue(new Error('API error')); - try { - await program.parseAsync(['node', 'test', 'db', 'connectionstring', '--name', 'test-db', '--psql']); - } catch (error) { - expectProcessExit(error); - } - - expect(console.error).toHaveBeenCalledWith(theme.error('Failed to get connection string:'), 'API error'); + await expect( + program.parseAsync(['node', 'test', 'db', 'connectionstring', 'test-db', '--psql']) + ).rejects.toThrow('API error'); }); }); }); \ No newline at end of file diff --git a/src/__tests__/commands/tenants.test.ts b/src/__tests__/commands/tenants.test.ts index e78ace6..43a704d 100644 --- a/src/__tests__/commands/tenants.test.ts +++ b/src/__tests__/commands/tenants.test.ts @@ -170,7 +170,7 @@ describe('Tenants Command', () => { await expect( program.parseAsync(['node', 'test', 'tenants', 'create', '--name', 'New Tenant']) - ).rejects.toThrow('Process.exit called with code: 1'); + ).rejects.toThrow('Database error'); expect(mockClient.end).toHaveBeenCalled(); }); @@ -203,7 +203,7 @@ describe('Tenants Command', () => { '--id', 'non-existent', '--new_name', 'New Name' ]) - ).rejects.toThrow('Process.exit called with code: 1'); + ).rejects.toThrow(`Tenant with ID 'non-existent' not found`); expect(mockClient.end).toHaveBeenCalled(); }); @@ -241,7 +241,7 @@ describe('Tenants Command', () => { 'node', 'test', 'tenants', 'delete', '--id', 'non-existent' ]) - ).rejects.toThrow('Process.exit called with code: 1'); + ).rejects.toThrow(`Tenant with ID 'non-existent' not found`); expect(mockClient.end).toHaveBeenCalled(); }); @@ -253,7 +253,7 @@ describe('Tenants Command', () => { await expect( program.parseAsync(['node', 'test', 'tenants', 'create', '--name', 'New Tenant']) - ).rejects.toThrow('Process.exit called with code: 1'); + ).rejects.toThrow('Database error'); expect(mockClient.end).toHaveBeenCalled(); }); diff --git a/src/commands/auth.ts b/src/commands/auth.ts new file mode 100644 index 0000000..f16e089 --- /dev/null +++ b/src/commands/auth.ts @@ -0,0 +1,236 @@ +import { Command } from 'commander'; +import { ConfigManager } from '../lib/config'; +import { NileAPI } from '../lib/api'; +import { theme, formatCommand } from '../lib/colors'; +import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions'; +import { handleApiError } from '../lib/errorHandling'; +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import axios from 'axios'; + +type GetOptions = () => GlobalOptions; + +export function createAuthCommand(getOptions: GetOptions): Command { + const auth = new Command('auth') + .description('Manage authentication') + .addHelpText('after', ` +Examples: + ${formatCommand('nile auth quickstart --nextjs')} Set up authentication in a Next.js app + ${formatCommand('nile auth env')} Generate environment variables + ${formatCommand('nile auth env --output .env.local')} Save environment variables to file + +${getGlobalOptionsHelp()}`); + + auth + .command('quickstart') + .description('Set up authentication in your application') + .requiredOption('--nextjs', 'Set up authentication in a Next.js application') + .action(async (cmdOptions) => { + try { + const options = getOptions(); + const configManager = new ConfigManager(options); + const workspaceSlug = configManager.getWorkspace(); + if (!workspaceSlug) { + throw new Error('No workspace specified. Use one of:\n' + + '1. --workspace flag\n' + + '2. nile config --workspace \n' + + '3. NILE_WORKSPACE environment variable'); + } + + const api = new NileAPI({ + token: configManager.getToken(), + dbHost: configManager.getDbHost(), + controlPlaneUrl: configManager.getGlobalHost(), + debug: options.debug + }); + + if (cmdOptions.nextjs) { + console.log(theme.primary('\nSetting up authentication in Next.js application...')); + + // Create Next.js app if it doesn't exist + if (!fs.existsSync('package.json')) { + console.log(theme.dim('\nCreating new Next.js application...')); + execSync('npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"', { stdio: 'inherit' }); + } + + // Install required dependencies + console.log(theme.dim('\nInstalling required dependencies...')); + execSync('npm install @niledatabase/react @niledatabase/server', { stdio: 'inherit' }); + + // Get database credentials + console.log(theme.dim('\nFetching database credentials...')); + const credentials = await api.createDatabaseCredentials(workspaceSlug, 'test'); + const connection = await api.getDatabaseConnection(workspaceSlug, 'test'); + + // Create environment variables + const envVars = { + NILE_DATABASE_URL: `postgres://${connection.user}:${connection.password}@${connection.host}:${connection.port}/${connection.database}`, + NILE_WORKSPACE: workspaceSlug, + NILE_API_KEY: credentials.id, + NILE_API_SECRET: credentials.password + }; + + // Write to .env.local + console.log(theme.dim('\nWriting environment variables to .env.local...')); + const envContent = Object.entries(envVars) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + fs.writeFileSync('.env.local', envContent); + + // Create API routes + console.log(theme.dim('\nCreating API routes...')); + const apiDir = path.join('src', 'app', 'api'); + if (!fs.existsSync(apiDir)) { + fs.mkdirSync(apiDir, { recursive: true }); + } + + // Create auth route + const authRoute = path.join(apiDir, 'auth', 'route.ts'); + fs.mkdirSync(path.dirname(authRoute), { recursive: true }); + fs.writeFileSync(authRoute, ` +import { Nile } from '@niledatabase/server'; +import { NextResponse } from 'next/server'; + +const nile = new Nile({ + databaseUrl: process.env.NILE_DATABASE_URL, + apiKey: process.env.NILE_API_KEY, + apiSecret: process.env.NILE_API_SECRET, + workspace: process.env.NILE_WORKSPACE +}); + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { email, password } = body; + + const user = await nile.auth.signUp({ + email, + password + }); + + return NextResponse.json(user); + } catch (error) { + return NextResponse.json({ error: 'Authentication failed' }, { status: 400 }); + } +} +`); + + // Create auth provider component + console.log(theme.dim('\nCreating auth provider component...')); + const componentsDir = path.join('src', 'components'); + if (!fs.existsSync(componentsDir)) { + fs.mkdirSync(componentsDir, { recursive: true }); + } + + const authProvider = path.join(componentsDir, 'AuthProvider.tsx'); + fs.writeFileSync(authProvider, ` +import { NileProvider } from '@niledatabase/react'; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +`); + + // Update root layout + console.log(theme.dim('\nUpdating root layout...')); + const layoutFile = path.join('src', 'app', 'layout.tsx'); + const layoutContent = fs.readFileSync(layoutFile, 'utf-8'); + const updatedLayout = layoutContent.replace( + 'export default function RootLayout', + `import { AuthProvider } from '@/components/AuthProvider';\n\nexport default function RootLayout` + ).replace( + '', + '\n ' + ).replace( + '', + ' \n ' + ); + fs.writeFileSync(layoutFile, updatedLayout); + + console.log(theme.success('\nAuthentication setup complete!')); + console.log(theme.secondary('\nNext steps:')); + console.log('1. Start your Next.js app with "npm run dev"'); + console.log('2. Visit http://localhost:3000 to see your app'); + console.log('3. Use the Nile React components to add authentication UI'); + } + } catch (error: any) { + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleApiError(error, 'set up authentication', new ConfigManager(getOptions())); + } else { + throw error; + } + } + }); + + auth + .command('env') + .description('Generate environment variables for authentication') + .option('--output ', 'Output file for environment variables (e.g., .env.local)') + .action(async (cmdOptions) => { + try { + const options = getOptions(); + const configManager = new ConfigManager(options); + const workspaceSlug = configManager.getWorkspace(); + if (!workspaceSlug) { + throw new Error('No workspace specified. Use one of:\n' + + '1. --workspace flag\n' + + '2. nile config --workspace \n' + + '3. NILE_WORKSPACE environment variable'); + } + + const api = new NileAPI({ + token: configManager.getToken(), + dbHost: configManager.getDbHost(), + controlPlaneUrl: configManager.getGlobalHost(), + debug: options.debug + }); + + // Get database credentials + console.log(theme.dim('\nFetching database credentials...')); + const credentials = await api.createDatabaseCredentials(workspaceSlug, 'test'); + const connection = await api.getDatabaseConnection(workspaceSlug, 'test'); + + // Generate environment variables + const envVars = { + NILE_DATABASE_URL: `postgres://${connection.user}:${connection.password}@${connection.host}:${connection.port}/${connection.database}`, + NILE_WORKSPACE: workspaceSlug, + NILE_API_KEY: credentials.id, + NILE_API_SECRET: credentials.password + }; + + // Format environment variables + const envContent = Object.entries(envVars) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + // Output to file if specified + if (cmdOptions.output) { + console.log(theme.dim(`\nWriting environment variables to ${cmdOptions.output}...`)); + fs.writeFileSync(cmdOptions.output, envContent); + console.log(theme.success(`\nEnvironment variables written to ${cmdOptions.output}`)); + } else { + // Display in terminal + console.log(theme.primary('\nEnvironment variables:')); + console.log(theme.secondary('\n' + envContent)); + } + } catch (error: any) { + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleApiError(error, 'generate environment variables', new ConfigManager(getOptions())); + } else { + throw error; + } + } + }); + + return auth; +} \ No newline at end of file diff --git a/src/commands/db.ts b/src/commands/db.ts index e4f5283..99085bb 100644 --- a/src/commands/db.ts +++ b/src/commands/db.ts @@ -1,10 +1,12 @@ import { Command } from 'commander'; import { ConfigManager } from '../lib/config'; import { NileAPI } from '../lib/api'; -import { theme, table, formatStatus, formatCommand } from '../lib/colors'; +import { theme, formatCommand } from '../lib/colors'; import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions'; import { spawn } from 'child_process'; import Table from 'cli-table3'; +import { handleDatabaseError, forceRelogin } from '../lib/errorHandling'; +import axios from 'axios'; type GetOptions = () => GlobalOptions; @@ -28,20 +30,31 @@ ${getGlobalOptionsHelp()}`); try { const options = getOptions(); const configManager = new ConfigManager(options); - const workspaceSlug = configManager.getWorkspace(); - if (!workspaceSlug) { - throw new Error('No workspace specified. Use one of:\n' + - '1. --workspace flag\n' + - '2. nile config --workspace \n' + - '3. NILE_WORKSPACE environment variable'); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } } const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); + + const workspaceSlug = configManager.getWorkspace(); + if (!workspaceSlug) { + throw new Error('No workspace specified. Use one of:\n' + + '1. --workspace flag\n' + + '2. nile config --workspace \n' + + '3. NILE_WORKSPACE environment variable'); + } + const databases = await api.listDatabases(workspaceSlug); if (options.format === 'json') { @@ -58,12 +71,13 @@ ${getGlobalOptionsHelp()}`); } if (databases.length === 0) { - console.log(theme.warning(`\nNo databases found in workspace '${theme.bold(workspaceSlug)}'`)); + console.log(theme.warning(`\nNo databases found in workspace '${theme.bold(configManager.getWorkspace())}'`)); + console.log(theme.secondary('Run "nile db create" to create a database')); return; } // Create a nicely formatted table using cli-table3 - console.log(theme.primary(`\nDatabases in workspace '${theme.bold(workspaceSlug)}':`)); + console.log(theme.primary(`\nDatabases in workspace '${theme.bold(configManager.getWorkspace())}':`)); const table = new Table({ head: [ @@ -106,50 +120,47 @@ ${getGlobalOptionsHelp()}`); // Print the table console.log(table.toString()); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to list databases:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'list databases', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to list databases:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); db - .command('show [databaseName]') + .command('show ') .description('Show database details') - .action(async (databaseName) => { + .action(async (name: string) => { try { const options = getOptions(); const configManager = new ConfigManager(options); - const workspaceSlug = configManager.getWorkspace(); - if (!workspaceSlug) { - throw new Error('No workspace specified. Use one of:\n' + - '1. --workspace flag\n' + - '2. nile config --workspace \n' + - '3. NILE_WORKSPACE environment variable'); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } } const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); - // If no database name provided, try to get from config - if (!databaseName) { - databaseName = configManager.getDatabase(); - if (!databaseName) { - throw new Error('No database specified. Use one of:\n' + - '1. --db flag\n' + - '2. nile config --db \n' + - '3. NILE_DB environment variable'); - } + const workspaceSlug = configManager.getWorkspace(); + if (!workspaceSlug) { + throw new Error('No workspace specified. Use one of:\n' + + '1. --workspace flag\n' + + '2. nile config --workspace \n' + + '3. NILE_WORKSPACE environment variable'); } - const database = await api.getDatabase(workspaceSlug, databaseName); + const database = await api.getDatabase(workspaceSlug, name); if (options.format === 'json') { console.log(JSON.stringify(database, null, 2)); @@ -162,7 +173,6 @@ ${getGlobalOptionsHelp()}`); return; } - console.log(theme.primary('\nDatabase details:')); const detailsTable = new Table({ style: { head: [], @@ -195,25 +205,40 @@ ${getGlobalOptionsHelp()}`); console.log(detailsTable.toString()); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to get database:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'get database details', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to get database:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); db .command('create') .description('Create a new database') - .requiredOption('--name ', 'Database name') - .requiredOption('--region ', 'Region (use "nile db regions" to list available regions)') - .action(async (cmdOptions) => { + .requiredOption('--name ', 'Database name') + .requiredOption('--region ', 'Database region') + .action(async (options: { name: string; region: string }) => { try { - const options = getOptions(); - const configManager = new ConfigManager(options); + const globalOptions = getOptions(); + const configManager = new ConfigManager(globalOptions); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } + } + + const api = new NileAPI({ + token, + dbHost: configManager.getDbHost(), + controlPlaneUrl: configManager.getGlobalHost(), + debug: globalOptions.debug + }); + const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + @@ -222,161 +247,106 @@ ${getGlobalOptionsHelp()}`); '3. NILE_WORKSPACE environment variable'); } - const api = new NileAPI({ - token: configManager.getToken(), - dbHost: configManager.getDbHost(), - controlPlaneUrl: configManager.getGlobalHost(), - debug: options.debug - }); + const database = await api.createDatabase(workspaceSlug, options.name, options.region); - // If region not provided, list available regions - if (!cmdOptions.region) { - const regions = await api.listRegions(workspaceSlug); - console.log(theme.primary('\nAvailable regions:')); - const regionsTable = new Table({ - head: [theme.header('REGION')], - style: { - head: [], - border: [], - }, - chars: { - 'top': '─', - 'top-mid': '┬', - 'top-left': '┌', - 'top-right': '┐', - 'bottom': '─', - 'bottom-mid': '┴', - 'bottom-left': '└', - 'bottom-right': '┘', - 'left': '│', - 'left-mid': '├', - 'mid': '─', - 'mid-mid': '┼', - 'right': '│', - 'right-mid': '┤', - 'middle': '│' - } - }); - - regions.forEach(region => { - regionsTable.push([theme.info(region)]); - }); + if (globalOptions.format === 'json') { + console.log(JSON.stringify(database, null, 2)); + return; + } - console.log(regionsTable.toString()); - console.log(theme.secondary('\nPlease specify a region using --region flag')); - process.exit(1); + if (globalOptions.format === 'csv') { + console.log('NAME,REGION,STATUS'); + console.log(`${database.name},${database.region},${database.status}`); + return; } - - console.log(theme.primary(`Creating database '${theme.bold(cmdOptions.name)}' in region '${theme.info(cmdOptions.region)}'...`)); - const database = await api.createDatabase(workspaceSlug, cmdOptions.name, cmdOptions.region); - console.log(theme.success(`Database '${theme.bold(database.name)}' created successfully.`)); - - // Show database details - console.log(theme.primary('\nDatabase details:')); + console.log(`${theme.secondary('Name:')} ${theme.primary(database.name)}`); console.log(`${theme.secondary('Region:')} ${theme.info(database.region)}`); console.log(`${theme.secondary('Status:')} ${formatStatus(database.status)}`); } catch (error: any) { - const options = getOptions(); - if (error.response?.data?.errors) { - console.error(theme.error('Failed to create database:'), new Error(error.response.data.errors.join(', '))); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'create database', new ConfigManager(getOptions())); } else { - if (options.debug) { - console.error(theme.error('Failed to create database:'), error); - } else { - console.error(theme.error('Failed to create database:'), error instanceof Error ? error.message : 'Unknown error'); - } + throw error; } - process.exit(1); } }); db - .command('delete [databaseName]') - .description('Delete a database permanently') - .option('--force', 'Skip confirmation prompt (use with caution)') - .action(async (databaseName, cmdOptions) => { + .command('delete ') + .description('Delete a database') + .action(async (name: string) => { try { const options = getOptions(); const configManager = new ConfigManager(options); - const workspaceSlug = configManager.getWorkspace(); - if (!workspaceSlug) { - throw new Error('No workspace specified. Use one of:\n' + - '1. --workspace flag\n' + - '2. nile config --workspace \n' + - '3. NILE_WORKSPACE environment variable'); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } } const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); - // If no database name provided, try to get from config - if (!databaseName) { - databaseName = configManager.getDatabase(); - if (!databaseName) { - throw new Error('No database specified. Use one of:\n' + - '1. --db flag\n' + - '2. nile config --db \n' + - '3. NILE_DB environment variable'); - } - } - - if (!cmdOptions.force) { - console.log(theme.warning(`\n⚠️ WARNING: This will permanently delete database '${theme.bold(databaseName)}' and all its data.`)); - console.log(theme.warning('This action cannot be undone.')); - process.stdout.write(theme.warning('Are you sure? (yes/no): ')); - - const answer = await new Promise(resolve => { - process.stdin.once('data', data => { - resolve(data.toString().trim().toLowerCase()); - }); - }); - - if (answer !== 'yes' && answer !== 'y') { - console.log(theme.info('\nDatabase deletion cancelled.')); - process.exit(0); - } + const workspaceSlug = configManager.getWorkspace(); + if (!workspaceSlug) { + throw new Error('No workspace specified. Use one of:\n' + + '1. --workspace flag\n' + + '2. nile config --workspace \n' + + '3. NILE_WORKSPACE environment variable'); } - console.log(theme.primary(`\nDeleting database '${theme.bold(databaseName)}'...`)); - await api.deleteDatabase(workspaceSlug, databaseName); + await api.deleteDatabase(workspaceSlug, name); console.log(theme.success('Database deleted successfully.')); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to delete database:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'delete database', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to delete database:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); db .command('regions') - .description('List all available regions for database creation') + .description('List available regions') .action(async () => { try { const options = getOptions(); const configManager = new ConfigManager(options); - const workspaceSlug = configManager.getWorkspace(); - if (!workspaceSlug) { - throw new Error('No workspace specified. Use one of:\n' + - '1. --workspace flag\n' + - '2. nile config --workspace \n' + - '3. NILE_WORKSPACE environment variable'); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } } const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); + + const workspaceSlug = configManager.getWorkspace(); + if (!workspaceSlug) { + throw new Error('No workspace specified. Use one of:\n' + + '1. --workspace flag\n' + + '2. nile config --workspace \n' + + '3. NILE_WORKSPACE environment variable'); + } + const regions = await api.listRegions(workspaceSlug); if (options.format === 'json') { @@ -385,23 +355,19 @@ ${getGlobalOptionsHelp()}`); } if (options.format === 'csv') { - console.log('REGION'); - regions.forEach(region => console.log(region)); - return; - } - - if (regions.length === 0) { - console.log(theme.warning('\nNo regions available.')); + console.log('NAME'); + regions.forEach(r => { + console.log(r); + }); return; } - console.log(theme.primary('\nAvailable regions:')); + console.log('\nAvailable regions:'); const regionsTable = new Table({ - head: [theme.header('REGION')], - style: { - head: [], - border: [], - }, + head: [ + theme.header('NAME') + ], + style: { head: [], border: [] }, chars: { 'top': '─', 'top-mid': '┬', @@ -421,19 +387,19 @@ ${getGlobalOptionsHelp()}`); } }); - regions.forEach(region => { - regionsTable.push([theme.info(region)]); + regions.forEach(r => { + regionsTable.push([ + theme.primary(r) + ]); }); console.log(regionsTable.toString()); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to list regions:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'list regions', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to list regions:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); @@ -505,13 +471,11 @@ ${getGlobalOptionsHelp()}`); } }); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to connect to database:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'connect to database', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to connect to database:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); @@ -560,15 +524,26 @@ ${getGlobalOptionsHelp()}`); // Output the connection string console.log(connectionString); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to get connection string:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleDatabaseError(error, 'get connection string', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to get connection string:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); return db; +} + +function formatStatus(status: string): string { + switch (status.toLowerCase()) { + case 'running': + return theme.success(status); + case 'creating': + return theme.warning(status); + case 'error': + return theme.error(status); + default: + return theme.info(status); + } } \ No newline at end of file diff --git a/src/commands/local.ts b/src/commands/local.ts index 46ba4ca..ce674e9 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -5,6 +5,7 @@ import { spawn, exec } from 'child_process'; import readline from 'readline'; import ora from 'ora'; import { promisify } from 'util'; +import os from 'os'; const execAsync = promisify(exec); @@ -34,6 +35,39 @@ async function waitForPostgres(getOptions: GetOptions, retries = 30, interval = return false; } +async function isDockerRunning(): Promise { + try { + await execAsync('docker info'); + return true; + } catch (error: any) { + return false; + } +} + +function getDockerStartCommand(): string { + const platform = os.platform(); + switch (platform) { + case 'darwin': + return 'open -a Docker'; + case 'win32': + return 'start /B "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"'; + default: + return 'systemctl start docker'; + } +} + +function getDockerInstallInstructions(): string { + const platform = os.platform(); + switch (platform) { + case 'darwin': + return 'https://docs.docker.com/desktop/install/mac-install/'; + case 'win32': + return 'https://docs.docker.com/desktop/install/windows-install/'; + default: + return 'https://docs.docker.com/engine/install/'; + } +} + export function createLocalCommand(getOptions: GetOptions): Command { const local = new Command('local') .description('Manage local development environment') @@ -129,6 +163,17 @@ ${getGlobalOptionsHelp()}`); .option('--no-prompt', 'Start without prompting for psql connection') .action(async (cmdOptions) => { try { + // Check if Docker daemon is running + const dockerRunning = await isDockerRunning(); + if (!dockerRunning) { + console.error(theme.error('\nDocker daemon is not running.')); + console.log(theme.info('\nTo start Docker:')); + console.log(theme.dim(`Run: ${getDockerStartCommand()}`)); + console.log(theme.info('\nIf Docker is not installed:')); + console.log(theme.dim(`Visit: ${getDockerInstallInstructions()}`)); + process.exit(1); + } + // Check if container is already running try { const { stdout } = await execAsync('docker ps --filter name=nile-local --format {{.Names}}'); diff --git a/src/commands/tenants.ts b/src/commands/tenants.ts index 2fbbb3c..96f4110 100644 --- a/src/commands/tenants.ts +++ b/src/commands/tenants.ts @@ -4,7 +4,9 @@ import { ConfigManager } from '../lib/config'; import { NileAPI } from '../lib/api'; import { theme, formatCommand } from '../lib/colors'; import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions'; +import { handleTenantError, forceRelogin } from '../lib/errorHandling'; import Table from 'cli-table3'; +import axios from 'axios'; async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ workspaceSlug: string; databaseName: string }> { const configManager = new ConfigManager(options); @@ -12,7 +14,7 @@ async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ worksp if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + - '2. nile config --workspace \n' + + '2. nile config --workspace \n' + '3. NILE_WORKSPACE environment variable'); } @@ -20,7 +22,7 @@ async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ worksp if (!databaseName) { throw new Error('No database specified. Use one of:\n' + '1. --db flag\n' + - '2. nile config --db \n' + + '2. nile config --db \n' + '3. NILE_DB environment variable'); } @@ -121,12 +123,23 @@ Examples: try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } + } + const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); + const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); client = await getPostgresClient(api, workspaceSlug, databaseName, options); @@ -177,13 +190,11 @@ Examples: console.log(tenantsTable.toString()); } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('Failed to list tenants:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleTenantError(error, 'list tenants', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('Failed to list tenants:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } finally { if (client) { await client.end(); @@ -260,13 +271,11 @@ Examples: console.log(detailsTable.toString()); } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('Failed to create tenant:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleTenantError(error, 'create tenant', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('Failed to create tenant:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } finally { if (client) { await client.end(); @@ -310,13 +319,11 @@ Examples: await client.query('DELETE FROM tenants WHERE id = $1', [cmdOptions.id]); console.log(theme.success(`\nTenant '${theme.bold(tenant.name)}' deleted successfully.`)); } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('\nFailed to delete tenant:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleTenantError(error, 'delete tenant', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('\nFailed to delete tenant:'), error instanceof Error ? error.message : 'Unknown error'); + throw error; } - process.exit(1); } finally { if (client) { await client.end(); @@ -362,13 +369,11 @@ Examples: console.log(`${theme.secondary('ID:')} ${theme.primary(tenant.id)}`); console.log(`${theme.secondary('Name:')} ${theme.info(tenant.name)}`); } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('\nFailed to update tenant:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleTenantError(error, 'update tenant', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('\nFailed to update tenant:'), error instanceof Error ? error.message : 'Unknown error'); + throw error; } - process.exit(1); } finally { if (client) { await client.end(); diff --git a/src/commands/user.ts b/src/commands/user.ts index daade46..bc73614 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -4,7 +4,9 @@ import { ConfigManager } from '../lib/config'; import { NileAPI } from '../lib/api'; import { theme, formatCommand } from '../lib/colors'; import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions'; +import { handleUserError, forceRelogin } from '../lib/errorHandling'; import Table from 'cli-table3'; +import axios from 'axios'; async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ workspaceSlug: string; databaseName: string }> { const configManager = new ConfigManager(options); @@ -12,7 +14,7 @@ async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ worksp if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + - '2. nile config --workspace \n' + + '2. nile config --workspace \n' + '3. NILE_WORKSPACE environment variable'); } @@ -20,7 +22,7 @@ async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ worksp if (!databaseName) { throw new Error('No database specified. Use one of:\n' + '1. --db flag\n' + - '2. nile config --db \n' + + '2. nile config --db \n' + '3. NILE_DB environment variable'); } @@ -78,246 +80,85 @@ export class UsersCommand { .description('Manage users in your database') .addHelpText('after', ` Examples: - # Create users - ${formatCommand('nile users create', '--email "user@example.com" --password "securepass123"')} Create a basic user - ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --name "John Doe"')} Create user with name - ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --given-name "John" --family-name "Doe"')} Create user with full name - ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --tenant-id "tenant123"')} Create user in specific tenant - ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --new-tenant-name "New Corp"')} Create user with new tenant + # List users + ${formatCommand('nile users list')} List all users + ${formatCommand('nile users list', '--workspace myworkspace')} List users in specific workspace + ${formatCommand('nile users list', '--db mydb')} List users in specific database + ${formatCommand('nile users list', '--format json')} Output in JSON format + ${formatCommand('nile users list', '--format csv')} Output in CSV format - # List user tenants - ${formatCommand('nile users tenants', '--user-id user123')} List all tenants for a user + # Create users + ${formatCommand('nile users create', '--email "user@example.com" --password "password123"')} Create user with email and password + ${formatCommand('nile users create', '--email "user@example.com" --password "password123" --tenant tenant-123')} Create user in specific tenant # Update users - ${formatCommand('nile users update', '--user-id user123 --name "Updated Name"')} Update user name - ${formatCommand('nile users update', '--user-id user123 --given-name "John" --family-name "Doe"')} Update user full name + ${formatCommand('nile users update', '--id user-123 --new_email "new@example.com"')} Update user email + ${formatCommand('nile users update', '--id user-123 --new_password "newpassword123"')} Update user password + + # Delete users + ${formatCommand('nile users delete', '--id user-123')} Delete user by ID + ${formatCommand('nile users delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete user by UUID - # Remove user from tenant - ${formatCommand('nile users remove-tenant', '--user-id user123 --tenant-id tenant123')} Remove user from tenant + # Using with different output formats + ${formatCommand('nile users list', '--format json')} List users in JSON format + ${formatCommand('nile users list', '--format csv')} List users in CSV format + ${formatCommand('nile users create', '--email "user@example.com" --password "password123" --format json')} Create user with JSON output ${getGlobalOptionsHelp()}`); - const createCmd = new Command('create') - .description('Create a new user') - .requiredOption('--email ', 'Email address for the user') - .requiredOption('--password ', 'Password for the user') - .option('--name ', 'Full name of the user') - .option('--given-name ', 'Given (first) name of the user') - .option('--family-name ', 'Family (last) name of the user') - .option('--picture ', 'URL of the user\'s profile picture') - .option('--tenant-id ', 'ID of the tenant to add the user to') - .option('--new-tenant-name ', 'Name of a new tenant to create and add the user to') - .option('--roles ', 'Roles to assign to the user in the tenant') + const listCmd = new Command('list') + .description('List all users in the database') .addHelpText('after', ` Examples: - ${formatCommand('nile users create', '--email "user@example.com" --password "securepass123"')} - ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --name "John Doe"')} - ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --tenant-id "tenant123"')} + ${formatCommand('nile users list')} List all users + ${formatCommand('nile users list', '--workspace myworkspace')} List users in specific workspace + ${formatCommand('nile users list', '--db mydb')} List users in specific database `) - .action(async (cmdOptions) => { + .action(async () => { let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); - const api = new NileAPI({ - token: configManager.getToken(), - dbHost: configManager.getDbHost(), - controlPlaneUrl: configManager.getGlobalHost(), - debug: options.debug - }); - - const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); - client = await getPostgresClient(api, workspaceSlug, databaseName, options); - - // Begin transaction - await client.query('BEGIN'); - - try { - // Insert into users schema - console.log(theme.dim('\nCreating user...')); - const result = await client.query( - `INSERT INTO users.users ( - email, - name, - given_name, - family_name, - picture - ) VALUES ($1, $2, $3, $4, $5) RETURNING *`, - [ - cmdOptions.email, - cmdOptions.name || null, - cmdOptions.givenName || null, - cmdOptions.familyName || null, - cmdOptions.picture || null - ] - ); - - const user = result.rows[0]; - - // Create auth credentials (this would typically be handled by auth service) - await client.query( - `INSERT INTO auth.credentials ( - user_id, - identifier, - password, - type - ) VALUES ($1, $2, crypt($3, gen_salt('bf')), 'password')`, - [user.id, cmdOptions.email, cmdOptions.password] - ); - - // If tenant ID is provided, create user-tenant relationship - if (cmdOptions.tenantId) { - await client.query( - `INSERT INTO users.tenant_users ( - tenant_id, - user_id, - roles, - email - ) VALUES ($1, $2, $3, $4)`, - [cmdOptions.tenantId, user.id, cmdOptions.roles || null, cmdOptions.email] - ); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); } - - // If new tenant name is provided, create tenant and relationship - if (cmdOptions.newTenantName) { - const tenantResult = await client.query( - 'INSERT INTO tenants (name) VALUES ($1) RETURNING id', - [cmdOptions.newTenantName] - ); - const tenantId = tenantResult.rows[0].id; - await client.query( - `INSERT INTO users.tenant_users ( - tenant_id, - user_id, - roles, - email - ) VALUES ($1, $2, $3, $4)`, - [tenantId, user.id, cmdOptions.roles || null, cmdOptions.email] - ); - } - - // Commit transaction - await client.query('COMMIT'); - - if (options.format === 'json') { - console.log(JSON.stringify(user, null, 2)); - return; - } - - if (options.format === 'csv') { - console.log('ID,EMAIL,NAME'); - console.log(`${user.id},${user.email},${user.name || ''}`); - return; - } - - // Create a nicely formatted table - const table = new Table({ - head: ['Field', 'Value'].map(h => theme.primary(h)), - style: { head: [], border: [] }, - chars: { - 'top': '─', - 'top-mid': '┬', - 'top-left': '┌', - 'top-right': '┐', - 'bottom': '─', - 'bottom-mid': '┴', - 'bottom-left': '└', - 'bottom-right': '┘', - 'left': '│', - 'left-mid': '├', - 'mid': '─', - 'mid-mid': '┼', - 'right': '│', - 'right-mid': '┤', - 'middle': '│' - } - }); - - table.push( - ['ID', theme.info(user.id)], - ['Email', theme.info(user.email)], - ['Name', theme.info(user.name || '')], - ['Given Name', theme.info(user.given_name || '')], - ['Family Name', theme.info(user.family_name || '')], - ['Picture', theme.info(user.picture || '')] - ); - - console.log('\nUser created successfully:'); - console.log(table.toString()); - - if (cmdOptions.tenantId) { - console.log(theme.success(`\nUser added to tenant: ${cmdOptions.tenantId}`)); - } else if (cmdOptions.newTenantName) { - console.log(theme.success(`\nUser added to new tenant: ${cmdOptions.newTenantName}`)); - } - } catch (error) { - // Rollback transaction on error - await client.query('ROLLBACK'); - throw error; - } - } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('Failed to create user:'), error); - } else { - console.error(theme.error('Failed to create user:'), error.message || 'Unknown error'); } - process.exit(1); - } finally { - if (client) { - await client.end(); - } - } - }); - const tenantsCmd = new Command('tenants') - .description('List tenants for a user') - .requiredOption('--user-id ', 'ID of the user') - .addHelpText('after', ` -Examples: - ${formatCommand('nile users tenants', '--user-id user123')} List all tenants for a user - `) - .action(async (cmdOptions) => { - try { - const options = getGlobalOptions(); - const configManager = new ConfigManager(options); const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); - const databaseId = await api.getDatabaseId(workspaceSlug, databaseName); - - const tenants = await api.getUserTenants(databaseId, cmdOptions.userId); - - if (options.format === 'json') { - console.log(JSON.stringify(tenants, null, 2)); - return; - } - - if (options.format === 'csv') { - console.log('ID,NAME'); - tenants.forEach(tenant => { - console.log(`${tenant.id},${tenant.name || ''}`); - }); - return; - } - - if (tenants.length === 0) { - console.log(theme.warning('\nNo tenants found for this user.')); + client = await getPostgresClient(api, workspaceSlug, databaseName, options); + + console.log(theme.dim('\nFetching users...')); + const result = await client.query('SELECT * FROM users'); + const users = result.rows; + + if (users.length === 0) { + console.log('No users found'); return; } - console.log('\nUser Tenants:'); - const table = new Table({ + console.log('\nUsers:'); + const usersTable = new Table({ head: [ theme.header('ID'), - theme.header('NAME') + theme.header('EMAIL'), + theme.header('TENANT ID') ], - style: { head: [], border: [] }, + style: { + head: [], + border: [], + }, chars: { 'top': '─', 'top-mid': '┬', @@ -337,38 +178,40 @@ Examples: } }); - tenants.forEach(tenant => { - table.push([ - theme.primary(tenant.id), - theme.info(tenant.name || '(unnamed)') + users.forEach(user => { + usersTable.push([ + theme.primary(user.id), + theme.info(user.email), + theme.secondary(user.tenant_id || '(none)') ]); }); - console.log(table.toString()); + console.log(usersTable.toString()); } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('Failed to list user tenants:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleUserError(error, 'list users', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('Failed to list user tenants:'), error.message || 'Unknown error'); + throw error; + } + } finally { + if (client) { + await client.end(); } - process.exit(1); } }); - const updateCmd = new Command('update') - .description('Update user details') - .requiredOption('--user-id ', 'ID of the user to update') - .option('--name ', 'Full name of the user') - .option('--given-name ', 'Given (first) name of the user') - .option('--family-name ', 'Family (last) name of the user') - .option('--picture ', 'URL of the user\'s profile picture') + const createCmd = new Command('create') + .description('Create a new user') + .requiredOption('--email ', 'Email address of the user') + .requiredOption('--password ', 'Password for the user') + .option('--tenant ', 'Tenant ID to associate with the user') .addHelpText('after', ` Examples: - ${formatCommand('nile users update', '--user-id user123 --name "Updated Name"')} - ${formatCommand('nile users update', '--user-id user123 --given-name "John" --family-name "Doe"')} + ${formatCommand('nile users create', '--email "user@example.com" --password "password123"')} Create a user with email and password + ${formatCommand('nile users create', '--email "user@example.com" --password "password123" --tenant tenant-123')} Create user in specific tenant `) .action(async (cmdOptions) => { + let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); @@ -376,36 +219,23 @@ Examples: token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), - debug: options.debug }); - const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); - const databaseId = await api.getDatabaseId(workspaceSlug, databaseName); - - const updates = { - name: cmdOptions.name, - givenName: cmdOptions.givenName, - familyName: cmdOptions.familyName, - picture: cmdOptions.picture - }; - - const user = await api.updateUser(databaseId, cmdOptions.userId, updates); - - if (options.format === 'json') { - console.log(JSON.stringify(user, null, 2)); - return; - } - - if (options.format === 'csv') { - console.log('ID,EMAIL,NAME'); - console.log(`${user.id},${user.email},${user.name || ''}`); - return; - } - - console.log('\nUser updated successfully:'); - const table = new Table({ - head: ['Field', 'Value'].map(h => theme.primary(h)), - style: { head: [], border: [] }, + client = await getPostgresClient(api, workspaceSlug, databaseName, options); + + console.log(theme.dim(`\nCreating user with email: ${cmdOptions.email}`)); + const result = await client.query( + 'INSERT INTO users (email, password, tenant_id) VALUES ($1, $2, $3) RETURNING *', + [cmdOptions.email, cmdOptions.password, cmdOptions.tenant] + ); + + const user = result.rows[0]; + console.log('\nUser created:'); + const detailsTable = new Table({ + style: { + head: [], + border: [], + }, chars: { 'top': '─', 'top-mid': '┬', @@ -425,36 +255,86 @@ Examples: } }); - table.push( - ['ID', theme.info(user.id)], - ['Email', theme.info(user.email)], - ['Name', theme.info(user.name || '')], - ['Given Name', theme.info(user.givenName || '')], - ['Family Name', theme.info(user.familyName || '')], - ['Picture', theme.info(user.picture || '')] + detailsTable.push( + [theme.secondary('ID:'), theme.primary(user.id)], + [theme.secondary('Email:'), theme.info(user.email)], + [theme.secondary('Tenant ID:'), theme.secondary(user.tenant_id || '(none)')] ); - console.log(table.toString()); + console.log(detailsTable.toString()); } catch (error: any) { + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleUserError(error, 'create user', new ConfigManager(getGlobalOptions())); + } else { + throw error; + } + } finally { + if (client) { + await client.end(); + } + } + }); + + const deleteCmd = new Command('delete') + .description('Delete a user') + .requiredOption('--id ', 'ID of the user to delete') + .addHelpText('after', ` +Examples: + ${formatCommand('nile users delete', '--id user-123')} Delete a user by ID + ${formatCommand('nile users delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete user by UUID + `) + .action(async (cmdOptions) => { + let client: Client | undefined; + try { const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('Failed to update user:'), error); + const configManager = new ConfigManager(options); + const token = await configManager.getToken(); + const api = new NileAPI({ + token, + dbHost: configManager.getDbHost(), + controlPlaneUrl: configManager.getGlobalHost(), + }); + const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); + + // First verify the user exists + client = await getPostgresClient(api, workspaceSlug, databaseName, options); + const checkResult = await client.query('SELECT id, email FROM users WHERE id = $1', [cmdOptions.id]); + + if (checkResult.rowCount === 0) { + throw new Error(`User with ID '${cmdOptions.id}' not found`); + } + + const user = checkResult.rows[0]; + console.log(theme.dim(`\nDeleting user '${user.email}' (${user.id})...`)); + + // Delete the user + await client.query('DELETE FROM users WHERE id = $1', [cmdOptions.id]); + console.log(theme.success(`\nUser '${theme.bold(user.email)}' deleted successfully.`)); + } catch (error: any) { + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleUserError(error, 'delete user', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('Failed to update user:'), error.message || 'Unknown error'); + throw error; + } + } finally { + if (client) { + await client.end(); } - process.exit(1); } }); - const removeTenantCmd = new Command('remove-tenant') - .description('Remove a user from a tenant') - .requiredOption('--user-id ', 'ID of the user') - .requiredOption('--tenant-id ', 'ID of the tenant') + const updateCmd = new Command('update') + .description('Update a user') + .requiredOption('--id ', 'ID of the user to update') + .option('--new_email ', 'New email address for the user') + .option('--new_password ', 'New password for the user') .addHelpText('after', ` Examples: - ${formatCommand('nile users remove-tenant', '--user-id user123 --tenant-id tenant123')} + ${formatCommand('nile users update', '--id user-123 --new_email "new@example.com"')} Update user email + ${formatCommand('nile users update', '--id user-123 --new_password "newpassword123"')} Update user password `) .action(async (cmdOptions) => { + let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); @@ -462,29 +342,65 @@ Examples: token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), - debug: options.debug }); - const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); - const databaseId = await api.getDatabaseId(workspaceSlug, databaseName); - await api.removeUserFromTenant(databaseId, cmdOptions.userId, cmdOptions.tenantId); - console.log(theme.success(`\nUser '${cmdOptions.userId}' removed from tenant '${cmdOptions.tenantId}'`)); + console.log(theme.dim('\nUpdating user...')); + client = await getPostgresClient(api, workspaceSlug, databaseName, options); + + // Build update query based on provided options + const updates: string[] = []; + const values: any[] = []; + let paramCount = 1; + + if (cmdOptions.new_email) { + updates.push(`email = $${paramCount}`); + values.push(cmdOptions.new_email); + paramCount++; + } + + if (cmdOptions.new_password) { + updates.push(`password = $${paramCount}`); + values.push(cmdOptions.new_password); + paramCount++; + } + + if (updates.length === 0) { + throw new Error('No update fields provided. Use --new_email or --new_password'); + } + + values.push(cmdOptions.id); + const query = `UPDATE users SET ${updates.join(', ')} WHERE id = $${paramCount} RETURNING *`; + + const result = await client.query(query, values); + + if (result.rowCount === 0) { + throw new Error(`User with ID '${cmdOptions.id}' not found`); + } + + const user = result.rows[0]; + console.log(theme.success(`\nUser '${theme.bold(user.id)}' updated successfully.`)); + console.log(theme.primary('\nUpdated user details:')); + console.log(`${theme.secondary('ID:')} ${theme.primary(user.id)}`); + console.log(`${theme.secondary('Email:')} ${theme.info(user.email)}`); + console.log(`${theme.secondary('Tenant ID:')} ${theme.secondary(user.tenant_id || '(none)')}`); } catch (error: any) { - const options = getGlobalOptions(); - if (options.debug) { - console.error(theme.error('Failed to remove user from tenant:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleUserError(error, 'update user', new ConfigManager(getGlobalOptions())); } else { - console.error(theme.error('Failed to remove user from tenant:'), error.message || 'Unknown error'); + throw error; + } + } finally { + if (client) { + await client.end(); } - process.exit(1); } }); + users.addCommand(listCmd); users.addCommand(createCmd); - users.addCommand(tenantsCmd); + users.addCommand(deleteCmd); users.addCommand(updateCmd); - users.addCommand(removeTenantCmd); } } diff --git a/src/commands/workspace.ts b/src/commands/workspace.ts index b2c2a31..dbdbb5c 100644 --- a/src/commands/workspace.ts +++ b/src/commands/workspace.ts @@ -3,8 +3,9 @@ import { ConfigManager } from '../lib/config'; import { NileAPI } from '../lib/api'; import { theme, formatCommand } from '../lib/colors'; import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions'; -import axios from 'axios'; +import { handleApiError, forceRelogin } from '../lib/errorHandling'; import Table from 'cli-table3'; +import axios from 'axios'; type GetOptions = () => GlobalOptions; @@ -13,9 +14,12 @@ export function createWorkspaceCommand(getOptions: GetOptions): Command { .description('Manage workspaces') .addHelpText('after', ` Examples: - ${formatCommand('nile workspace list')} List all workspaces - ${formatCommand('nile workspace show')} Show current workspace - ${formatCommand('nile config --workspace ')} Set default workspace + ${formatCommand('nile workspace list')} List all workspaces + ${formatCommand('nile workspace show')} Show current workspace + ${formatCommand('nile workspace select ')} Select a workspace + ${formatCommand('nile workspace create --name "My Workspace"')} Create a new workspace + ${formatCommand('nile workspace delete ')} Delete a workspace + ${formatCommand('nile workspace update ')} Update workspace settings ${getGlobalOptionsHelp()}`); @@ -26,12 +30,24 @@ ${getGlobalOptionsHelp()}`); try { const options = getOptions(); const configManager = new ConfigManager(options); + let token = configManager.getToken(); + + if (!token) { + await forceRelogin(configManager); + // After re-login, get the new token + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } + } + const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); + const workspaces = await api.listWorkspaces(); if (options.format === 'json') { @@ -48,22 +64,18 @@ ${getGlobalOptionsHelp()}`); } if (workspaces.length === 0) { - console.log(theme.warning('\nNo workspaces found')); + console.log(theme.warning('\nNo workspaces found.')); + console.log(theme.secondary('Run "nile workspace create" to create a workspace')); return; } - // Create a nicely formatted table using cli-table3 - console.log(theme.primary('\nAvailable workspaces:')); - + console.log('\nWorkspaces:'); const table = new Table({ head: [ theme.header('NAME'), theme.header('SLUG') ], - style: { - head: [], - border: [], - }, + style: { head: [], border: [] }, chars: { 'top': '─', 'top-mid': '┬', @@ -83,7 +95,6 @@ ${getGlobalOptionsHelp()}`); } }); - // Add rows to the table workspaces.forEach(w => { table.push([ theme.primary(w.name), @@ -93,17 +104,11 @@ ${getGlobalOptionsHelp()}`); console.log(table.toString()); } catch (error: any) { - if (axios.isAxiosError(error) && error.response?.status === 401) { - console.error(theme.error('Authentication failed. Please run "nile connect login" first')); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleApiError(error, 'list workspaces', new ConfigManager(getOptions())); } else { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to list workspaces:'), error); - } else { - console.error(theme.error('Failed to list workspaces:'), error.message || 'Unknown error'); - } + throw error; } - process.exit(1); } }); @@ -122,9 +127,18 @@ ${getGlobalOptionsHelp()}`); return; } + let token = configManager.getToken(); + if (!token) { + await forceRelogin(configManager); + token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } + } + // Get workspace details from API const api = new NileAPI({ - token: configManager.getToken(), + token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug @@ -174,13 +188,11 @@ ${getGlobalOptionsHelp()}`); console.log(detailsTable.toString()); } catch (error: any) { - const options = getOptions(); - if (options.debug) { - console.error(theme.error('Failed to get workspace:'), error); + if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { + await handleApiError(error, 'get workspace details', new ConfigManager(getOptions())); } else { - console.error(theme.error('Failed to get workspace:'), error.message || 'Unknown error'); + throw error; } - process.exit(1); } }); diff --git a/src/index.ts b/src/index.ts index 58fa8e2..7afcc20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { createTenantsCommand } from './commands/tenants'; import { configCommand } from './commands/config'; import { createUsersCommand } from './commands/user'; import { createLocalCommand } from './commands/local'; +import { createAuthCommand } from './commands/auth'; import { addGlobalOptions, updateChalkConfig } from './lib/globalOptions'; import { version } from '../package.json'; @@ -22,6 +23,8 @@ Examples: $ nile config --api-key Set API key in config $ nile users create Create a new user $ nile local start Start local development environment + $ nile auth quickstart --nextjs Set up authentication in a Next.js app + $ nile auth env Generate environment variables `); // Add global options @@ -41,5 +44,6 @@ cli.addCommand(createTenantsCommand(() => cli.opts())); cli.addCommand(configCommand()); cli.addCommand(createUsersCommand(() => cli.opts())); cli.addCommand(createLocalCommand(() => cli.opts())); +cli.addCommand(createAuthCommand(() => cli.opts())); cli.parse(process.argv); \ No newline at end of file diff --git a/src/lib/config.ts b/src/lib/config.ts index da52410..36c7b04 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import path from 'path'; import os from 'os'; import { GlobalOptions } from './globalOptions'; +import { theme } from './colors'; export interface NileConfig { apiKey?: string; @@ -52,11 +53,8 @@ export class ConfigManager { this.config = JSON.parse(configContent); } - // Load credentials if they exist - if (fs.existsSync(this.credentialsPath)) { - const credentialsContent = fs.readFileSync(this.credentialsPath, 'utf-8'); - this.credentials = JSON.parse(credentialsContent); - } + // Load credentials + this.loadCredentials(); } catch (error) { console.error('Error loading config:', error); } @@ -79,6 +77,8 @@ export class ConfigManager { } if (Object.keys(this.credentials).length > 0) { fs.writeFileSync(this.credentialsPath, JSON.stringify(this.credentials, null, 2)); + // Reload credentials after saving + this.loadCredentials(); } else { // If no credentials, remove the credentials file if (fs.existsSync(this.credentialsPath)) { @@ -91,6 +91,17 @@ export class ConfigManager { } } + private loadCredentials(): void { + try { + if (fs.existsSync(this.credentialsPath)) { + const credentialsContent = fs.readFileSync(this.credentialsPath, 'utf-8'); + this.credentials = JSON.parse(credentialsContent); + } + } catch (error) { + console.error('Error loading credentials:', error); + } + } + resetConfig(): void { this.config = {}; this.credentials = {}; @@ -100,24 +111,47 @@ export class ConfigManager { // Token management methods setToken(token: string): void { + if (this.globalOptions.debug) { + console.log(theme.dim('Setting token in credentials...')); + } this.credentials.token = token; this.saveCredentials(); + // Ensure credentials are reloaded + this.loadCredentials(); + if (this.globalOptions.debug) { + console.log(theme.dim('Token set and credentials reloaded')); + } } getToken(): string | undefined { // First check for API key (highest priority) const apiKey = this.getApiKey(); if (apiKey) { + if (this.globalOptions.debug) { + console.log(theme.dim('Using API key for authentication')); + } return apiKey; } // If no API key, check credentials file - return this.credentials.token; + const token = this.credentials.token; + if (this.globalOptions.debug) { + console.log(theme.dim('Using stored token for authentication')); + } + return token; } removeToken(): void { + if (this.globalOptions.debug) { + console.log(theme.dim('Removing token from credentials...')); + } delete this.credentials.token; this.saveCredentials(); + // Ensure credentials are reloaded + this.loadCredentials(); + if (this.globalOptions.debug) { + console.log(theme.dim('Token removed and credentials reloaded')); + } } setApiKey(apiKey: string): NileConfig { diff --git a/src/lib/errorHandling.ts b/src/lib/errorHandling.ts new file mode 100644 index 0000000..6fc9a8c --- /dev/null +++ b/src/lib/errorHandling.ts @@ -0,0 +1,163 @@ +import axios from 'axios'; +import { theme } from './colors'; +import { GlobalOptions } from './globalOptions'; +import { ConfigManager } from './config'; +import { Auth } from './auth'; +import { NileAPI } from './api'; + +/** + * Forces a re-login when authentication fails + */ +export async function forceRelogin(configManager: ConfigManager): Promise { + configManager.removeToken(); // Clear the invalid token + + console.log(theme.warning('\nAuthentication failed. Forcing re-login...')); + const token = await Auth.getAuthorizationToken(configManager); + if (token) { + if (configManager.getDebug()) { + console.log('Debug - Token received from auth flow'); + } + configManager.setToken(token); + if (configManager.getDebug()) { + console.log('Debug - Token saved to config manager'); + const savedToken = configManager.getToken(); + console.log('Debug - Token retrieved from config manager:', savedToken ? 'present' : 'missing'); + } + console.log(theme.success('Successfully re-authenticated!')); + + // Verify workspace access after re-authentication + const workspaceSlug = configManager.getWorkspace(); + if (workspaceSlug) { + try { + const api = new NileAPI({ + token, + controlPlaneUrl: configManager.getGlobalHost(), + debug: configManager.getDebug() + }); + await api.getWorkspace(workspaceSlug); + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 403) { + console.error(theme.error(`\nWorkspace '${workspaceSlug}' is not accessible with the new token.`)); + console.error(theme.warning('Please use a different workspace or contact your administrator.')); + process.exit(1); + } + throw error; + } + } + } else { + console.error(theme.error('Failed to re-authenticate')); + process.exit(1); + } +} + +/** + * Handles API errors consistently across all commands + * @param error The error object + * @param operation Description of the operation that failed + * @param configManager The ConfigManager instance to use + */ +export async function handleApiError(error: any, operation: string, configManager: ConfigManager): Promise { + if (axios.isAxiosError(error)) { + if (error.response?.status === 401 || error.message === 'Token is required') { + await forceRelogin(configManager); + // Retry the operation after re-login + const token = configManager.getToken(); + if (!token) { + throw new Error('Failed to get token after re-login'); + } + throw error; + } else if (error.response?.data?.errors) { + console.error(theme.error(`Failed to ${operation}:`), new Error(error.response.data.errors.join(', '))); + } else if (configManager.getDebug()) { + console.error(theme.error(`Failed to ${operation}:`), error); + } else { + console.error(theme.error(`Failed to ${operation}:`), error.message || 'Unknown error'); + } + } else if (configManager.getDebug()) { + console.error(theme.error(`Failed to ${operation}:`), error); + } else { + console.error(theme.error(`Failed to ${operation}:`), error instanceof Error ? error.message : 'Unknown error'); + } + process.exit(1); +} + +/** + * Handles database-specific errors + * @param error The error object + * @param operation Description of the operation that failed + * @param configManager The ConfigManager instance to use + */ +export async function handleDatabaseError(error: any, operation: string, configManager: ConfigManager): Promise { + if (axios.isAxiosError(error)) { + if (error.response?.status === 401 || error.message === 'Token is required') { + await forceRelogin(configManager); + // Retry the operation after re-login + throw error; + } else if (error.response?.data?.errors) { + console.error(theme.error(`Database operation failed: ${operation}`), new Error(error.response.data.errors.join(', '))); + } else if (configManager.getDebug()) { + console.error(theme.error(`Database operation failed: ${operation}`), error); + } else { + console.error(theme.error(`Database operation failed: ${operation}`), error.message || 'Unknown error'); + } + } else if (configManager.getDebug()) { + console.error(theme.error(`Database operation failed: ${operation}`), error); + } else { + console.error(theme.error(`Database operation failed: ${operation}`), error instanceof Error ? error.message : 'Unknown error'); + } + process.exit(1); +} + +/** + * Handles tenant-specific errors + * @param error The error object + * @param operation Description of the operation that failed + * @param configManager The ConfigManager instance to use + */ +export async function handleTenantError(error: any, operation: string, configManager: ConfigManager): Promise { + if (axios.isAxiosError(error)) { + if (error.response?.status === 401 || error.message === 'Token is required') { + await forceRelogin(configManager); + // Retry the operation after re-login + throw error; + } else if (error.response?.data?.errors) { + console.error(theme.error(`Tenant operation failed: ${operation}`), new Error(error.response.data.errors.join(', '))); + } else if (configManager.getDebug()) { + console.error(theme.error(`Tenant operation failed: ${operation}`), error); + } else { + console.error(theme.error(`Tenant operation failed: ${operation}`), error.message || 'Unknown error'); + } + } else if (configManager.getDebug()) { + console.error(theme.error(`Tenant operation failed: ${operation}`), error); + } else { + console.error(theme.error(`Tenant operation failed: ${operation}`), error instanceof Error ? error.message : 'Unknown error'); + } + process.exit(1); +} + +/** + * Handles user-specific errors + * @param error The error object + * @param operation Description of the operation that failed + * @param configManager The ConfigManager instance to use + */ +export async function handleUserError(error: any, operation: string, configManager: ConfigManager): Promise { + if (axios.isAxiosError(error)) { + if (error.response?.status === 401 || error.message === 'Token is required') { + await forceRelogin(configManager); + // Retry the operation after re-login + throw error; + } else if (error.response?.data?.errors) { + console.error(theme.error(`User operation failed: ${operation}`), new Error(error.response.data.errors.join(', '))); + } else if (configManager.getDebug()) { + console.error(theme.error(`User operation failed: ${operation}`), error); + } else { + console.error(theme.error(`User operation failed: ${operation}`), error.message || 'Unknown error'); + } + } else if (configManager.getDebug()) { + console.error(theme.error(`User operation failed: ${operation}`), error); + } else { + console.error(theme.error(`User operation failed: ${operation}`), error instanceof Error ? error.message : 'Unknown error'); + } + process.exit(1); +} \ No newline at end of file