Merge twenty-cli into twenty-sdk (#16150)
- Moves twenty-cli content into twenty-sdk - add a new twenty-sdk:0.1.0 version - this new twenty-sdk exports a cli command called 'twenty' (like twenty-cli before) - deprecates twenty-cli - simplify app init command base-project - use `twenty-sdk:0.1.0` in base project - move the "twenty-sdk/application" barrel to "twenty-sdk" - add `create-twenty-app` package <img width="1512" height="919" alt="image" src="https://github.com/user-attachments/assets/007bef45-4e71-419a-9213-cebed376adbf" /> <img width="1506" height="929" alt="image" src="https://github.com/user-attachments/assets/3de2fec6-1624-4923-ae13-f4e1cf165eb5" />
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import { join } from 'path';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { getObjectDecoratedClass } from '../utils/get-object-decorated-class';
|
||||
import { getFunctionBaseFile } from '../utils/get-function-base-file';
|
||||
import { convertToLabel } from '../utils/convert-to-label';
|
||||
|
||||
export enum SyncableEntity {
|
||||
AGENT = 'agent',
|
||||
OBJECT = 'object',
|
||||
FUNCTION = 'function',
|
||||
}
|
||||
|
||||
export const isSyncableEntity = (value: string): value is SyncableEntity => {
|
||||
return Object.values(SyncableEntity).includes(value as SyncableEntity);
|
||||
};
|
||||
|
||||
export class AppAddCommand {
|
||||
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
|
||||
try {
|
||||
const appPath = join(CURRENT_EXECUTION_DIRECTORY, path ?? '');
|
||||
|
||||
await fs.ensureDir(appPath);
|
||||
|
||||
const entity = entityType ?? (await this.getEntity());
|
||||
|
||||
if (entity === SyncableEntity.OBJECT) {
|
||||
const entityData = await this.getObjectData();
|
||||
|
||||
const name = entityData.nameSingular;
|
||||
|
||||
const objectFileName = `${camelcase(name)}.ts`;
|
||||
|
||||
const decoratedObject = getObjectDecoratedClass({
|
||||
data: entityData,
|
||||
name,
|
||||
});
|
||||
|
||||
await fs.writeFile(join(appPath, objectFileName), decoratedObject);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity === SyncableEntity.FUNCTION) {
|
||||
const entityName = await this.getEntityName(entity);
|
||||
|
||||
const objectFileName = `${camelcase(entityName)}.ts`;
|
||||
|
||||
const decoratedServerlessFunction = getFunctionBaseFile({
|
||||
name: entityName,
|
||||
});
|
||||
|
||||
await fs.writeFile(
|
||||
join(appPath, objectFileName),
|
||||
decoratedServerlessFunction,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(`Add new entity failed:`),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntity() {
|
||||
const { entity } = await inquirer.prompt<{ entity: SyncableEntity }>([
|
||||
{
|
||||
type: 'select',
|
||||
name: 'entity',
|
||||
message: `What entity do you want to create?`,
|
||||
default: '',
|
||||
choices: [SyncableEntity.FUNCTION, SyncableEntity.OBJECT],
|
||||
},
|
||||
]);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private async getEntityName(entity: SyncableEntity) {
|
||||
const { name } = await inquirer.prompt<{ name: string }>([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: `Enter a name for your new ${entity}:`,
|
||||
default: '',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) {
|
||||
return `${entity} name is required`;
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9-]+$/.test(input)) {
|
||||
return 'Name must contain only lowercase letters, numbers, and hyphens';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private async getObjectData() {
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'nameSingular',
|
||||
message: 'Enter a name singular for your object (eg: company):',
|
||||
default: '',
|
||||
validate: (input: string) => {
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'namePlural',
|
||||
message: 'Enter a name plural for your object (eg: companies):',
|
||||
default: '',
|
||||
validate: (input: string, answers?: any) => {
|
||||
if (input.trim() === answers?.nameSingular.trim()) {
|
||||
return 'Name plural must be different from name singular';
|
||||
}
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'labelSingular',
|
||||
message: 'Enter a label singular for your object:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers.nameSingular);
|
||||
},
|
||||
validate: (input: string) => {
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'labelPlural',
|
||||
message: 'Enter a label plural for your object:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers.namePlural);
|
||||
},
|
||||
validate: (input: string) => {
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import chalk from 'chalk';
|
||||
import * as chokidar from 'chokidar';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { AppSyncCommand } from './app-sync.command';
|
||||
|
||||
export class AppDevCommand {
|
||||
private syncCommand = new AppSyncCommand();
|
||||
|
||||
async execute(options: {
|
||||
appPath?: string;
|
||||
debounce: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
const debounceMs = parseInt(options.debounce, 10);
|
||||
|
||||
this.logStartupInfo(appPath, debounceMs);
|
||||
|
||||
await this.syncCommand.execute(appPath);
|
||||
|
||||
const watcher = this.setupFileWatcher(appPath, debounceMs);
|
||||
|
||||
this.setupGracefulShutdown(watcher);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Development mode failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private logStartupInfo(appPath: string, debounceMs: number): void {
|
||||
console.log(chalk.blue('🚀 Starting Twenty Application Development Mode'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log(chalk.gray(`⏱️ Debounce: ${debounceMs}ms`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private setupFileWatcher(
|
||||
appPath: string,
|
||||
debounceMs: number,
|
||||
): chokidar.FSWatcher {
|
||||
const watcher = chokidar.watch(appPath, {
|
||||
ignored: /node_modules|\.git/,
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const debouncedSync = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(async () => {
|
||||
console.log(chalk.blue('🔄 Changes detected, syncing...'));
|
||||
|
||||
await this.syncCommand.execute(appPath);
|
||||
|
||||
console.log(
|
||||
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
|
||||
);
|
||||
}, debounceMs);
|
||||
};
|
||||
|
||||
watcher.on('change', () => {
|
||||
debouncedSync();
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
|
||||
);
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
private setupGracefulShutdown(watcher: chokidar.FSWatcher): void {
|
||||
process.on('SIGINT', () => {
|
||||
console.log(chalk.yellow('\n🛑 Stopping development mode...'));
|
||||
watcher.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import chalk from 'chalk';
|
||||
import { GenerateService } from '../services/generate.service';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
|
||||
export class AppGenerateCommand {
|
||||
private generateService = new GenerateService();
|
||||
|
||||
async execute(appPath: string = CURRENT_EXECUTION_DIRECTORY) {
|
||||
try {
|
||||
await this.generateService.generateClient(appPath);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Generate Twenty client failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import chalk from 'chalk';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { GenerateService } from '../services/generate.service';
|
||||
import { type ApiResponse } from '../types/config.types';
|
||||
import { loadManifest } from '../utils/load-manifest';
|
||||
|
||||
export class AppSyncCommand {
|
||||
private apiService = new ApiService();
|
||||
private generateService = new GenerateService();
|
||||
|
||||
async execute(
|
||||
appPath: string = CURRENT_EXECUTION_DIRECTORY,
|
||||
): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log(chalk.blue('🚀 Syncing Twenty Application'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
return await this.synchronize({ appPath });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Sync failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async synchronize({ appPath }: { appPath: string }) {
|
||||
const { manifest, packageJson, yarnLock, shouldGenerate } =
|
||||
await loadManifest(appPath);
|
||||
|
||||
let serverlessSyncResult = await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
if (shouldGenerate) {
|
||||
await this.generateService.generateClient(appPath);
|
||||
|
||||
const { manifest: manifestWithClient } = await loadManifest(appPath);
|
||||
|
||||
serverlessSyncResult = await this.apiService.syncApplication({
|
||||
manifest: manifestWithClient,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
}
|
||||
|
||||
if (serverlessSyncResult.success === false) {
|
||||
console.error(
|
||||
chalk.red('❌ Serverless functions Sync failed:'),
|
||||
serverlessSyncResult.error,
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('✅ Serverless functions synced successfully'));
|
||||
}
|
||||
|
||||
return serverlessSyncResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { type ApiResponse } from '../types/config.types';
|
||||
import { loadManifest } from '../utils/load-manifest';
|
||||
|
||||
export class AppUninstallCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute({
|
||||
appPath = CURRENT_EXECUTION_DIRECTORY,
|
||||
askForConfirmation,
|
||||
}: {
|
||||
appPath?: string;
|
||||
askForConfirmation: boolean;
|
||||
}): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log(chalk.blue('🚀 Uninstall Twenty Application'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
if (askForConfirmation && !(await this.confirmationPrompt())) {
|
||||
console.error(chalk.red('⛔️ Aborting uninstall'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { manifest } = await loadManifest(appPath);
|
||||
|
||||
const result = await this.apiService.uninstallApplication(
|
||||
manifest.application.universalIdentifier,
|
||||
);
|
||||
|
||||
if (result.success === false) {
|
||||
console.error(chalk.red('❌ Uninstall failed:'), result.error);
|
||||
} else {
|
||||
console.log(chalk.green('✅ Application uninstalled successfully'));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Uninstall failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmationPrompt(): Promise<boolean> {
|
||||
const { confirmation } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmation',
|
||||
message: 'Are you sure you want to uninstall this application?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
return confirmation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
AppAddCommand,
|
||||
isSyncableEntity,
|
||||
SyncableEntity,
|
||||
} from './app-add.command';
|
||||
import { AppUninstallCommand } from './app-uninstall.command';
|
||||
import { AppDevCommand } from './app-dev.command';
|
||||
import { AppSyncCommand } from './app-sync.command';
|
||||
import { formatPath } from '../utils/format-path';
|
||||
import { AppGenerateCommand } from './app-generate.command';
|
||||
|
||||
export class AppCommand {
|
||||
private devCommand = new AppDevCommand();
|
||||
private syncCommand = new AppSyncCommand();
|
||||
private uninstallCommand = new AppUninstallCommand();
|
||||
private addCommand = new AppAddCommand();
|
||||
private generateCommand = new AppGenerateCommand();
|
||||
|
||||
getCommand(): Command {
|
||||
const appCommand = new Command('app');
|
||||
appCommand.description('Application development commands');
|
||||
|
||||
appCommand
|
||||
.command('dev [appPath]')
|
||||
.description('Watch and sync local application changes')
|
||||
.option('-d, --debounce <ms>', 'Debounce delay in milliseconds', '1000')
|
||||
.action(async (appPath, options) => {
|
||||
await this.devCommand.execute({
|
||||
...options,
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('sync [appPath]')
|
||||
.description('Sync application to Twenty')
|
||||
.action(async (appPath?: string) => {
|
||||
try {
|
||||
const result = await this.syncCommand.execute(formatPath(appPath));
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('uninstall [appPath]')
|
||||
.description('Uninstall application from Twenty')
|
||||
.action(async (appPath?: string) => {
|
||||
try {
|
||||
const result = await this.uninstallCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
askForConfirmation: true,
|
||||
});
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Keeping to avoid breaking changes
|
||||
appCommand
|
||||
.command('delete [appPath]', { hidden: true })
|
||||
.description('Delete application from Twenty')
|
||||
.action(async (appPath?: string) => {
|
||||
try {
|
||||
const result = await this.uninstallCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
askForConfirmation: true,
|
||||
});
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('add [entityType]')
|
||||
.option('--path <path>', 'Path in which the entity should be created.')
|
||||
.description(
|
||||
`Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`,
|
||||
)
|
||||
.action(async (entityType?: string, options?: { path?: string }) => {
|
||||
if (entityType && !isSyncableEntity(entityType)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Invalid entity type "${entityType}". Must be one of: ${Object.values(SyncableEntity).join('|')}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await this.addCommand.execute(
|
||||
entityType as SyncableEntity,
|
||||
options?.path,
|
||||
);
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('generate [outputPath]')
|
||||
.description('Generate Twenty client')
|
||||
.action(async (appPath?: string) => {
|
||||
await this.generateCommand.execute(formatPath(appPath));
|
||||
});
|
||||
|
||||
return appCommand;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import inquirer from 'inquirer';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { ConfigService } from '../services/config.service';
|
||||
|
||||
export class AuthCommand {
|
||||
private configService = new ConfigService();
|
||||
private apiService = new ApiService();
|
||||
|
||||
getCommand(): Command {
|
||||
const authCommand = new Command('auth');
|
||||
authCommand.description('Authentication commands');
|
||||
|
||||
authCommand
|
||||
.command('login')
|
||||
.description('Authenticate with Twenty')
|
||||
.option('--api-key <key>', 'API key for authentication')
|
||||
.option('--api-url <url>', 'Twenty API URL')
|
||||
.action(async (options) => {
|
||||
await this.login(options);
|
||||
});
|
||||
|
||||
authCommand
|
||||
.command('logout')
|
||||
.description('Remove authentication credentials')
|
||||
.action(async () => {
|
||||
await this.logout();
|
||||
});
|
||||
|
||||
authCommand
|
||||
.command('status')
|
||||
.description('Check authentication status')
|
||||
.action(async () => {
|
||||
await this.status();
|
||||
});
|
||||
|
||||
return authCommand;
|
||||
}
|
||||
|
||||
private async login(options: {
|
||||
apiKey?: string;
|
||||
apiUrl?: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
let { apiKey, apiUrl } = options;
|
||||
|
||||
// Get current config
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
// Prompt for missing values
|
||||
if (!apiUrl) {
|
||||
const urlAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'apiUrl',
|
||||
message: 'Twenty API URL:',
|
||||
default: config.apiUrl,
|
||||
validate: (input) => {
|
||||
try {
|
||||
new URL(input);
|
||||
return true;
|
||||
} catch {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
apiUrl = urlAnswer.apiUrl;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
const keyAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'apiKey',
|
||||
message: 'API Key:',
|
||||
mask: '*',
|
||||
validate: (input) => input.length > 0 || 'API key is required',
|
||||
},
|
||||
]);
|
||||
apiKey = keyAnswer.apiKey;
|
||||
}
|
||||
|
||||
// Update config
|
||||
await this.configService.setConfig({
|
||||
apiUrl,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
// Validate authentication
|
||||
const isValid = await this.apiService.validateAuth();
|
||||
|
||||
if (isValid) {
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Successfully authenticated with Twenty (workspace: ${activeWorkspace})`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.red('✗ Authentication failed. Please check your credentials.'),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Login failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async logout(): Promise<void> {
|
||||
try {
|
||||
await this.configService.clearConfig();
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Successfully logged out (workspace: ${activeWorkspace})`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Logout failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async status(): Promise<void> {
|
||||
try {
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
console.log(chalk.blue('Authentication Status:'));
|
||||
console.log(`Workspace: ${activeWorkspace}`);
|
||||
console.log(`API URL: ${config.apiUrl}`);
|
||||
console.log(
|
||||
`API Key: ${config.apiKey ? '***' + config.apiKey.slice(-4) : 'Not set'}`,
|
||||
);
|
||||
|
||||
if (config.apiKey) {
|
||||
const isValid = await this.apiService.validateAuth();
|
||||
console.log(
|
||||
`Status: ${isValid ? chalk.green('✓ Valid') : chalk.red('✗ Invalid')}`,
|
||||
);
|
||||
} else {
|
||||
console.log(`Status: ${chalk.yellow('⚠ Not authenticated')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Status check failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user