Skip to content

Commit

Permalink
🧪
Browse files Browse the repository at this point in the history
  • Loading branch information
thibaultyou committed Oct 27, 2024
1 parent 3a6e549 commit b334ac2
Show file tree
Hide file tree
Showing 26 changed files with 1,456 additions and 1,299 deletions.
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default [
allowNamedFunctions: true
}
],
'func-style': ['error', 'declaration'],
'func-style': ['error', 'declaration', { allowArrowFunctions: true } ],
'no-multi-spaces': 'error',
'no-multiple-empty-lines': [
'error',
Expand Down
6 changes: 3 additions & 3 deletions src/app/utils/yaml-operations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as yaml from 'js-yaml';

import { PromptMetadata, Variable } from '../../shared/types';
import { PromptMetadata, PromptVariable } from '../../shared/types';
import logger from '../../shared/utils/logger';
import { appConfig } from '../config/app-config';

Expand Down Expand Up @@ -73,8 +73,8 @@ export function isValidMetadata(obj: unknown): obj is PromptMetadata {
return true;
}

function isValidVariable(obj: unknown): obj is Variable {
const variable = obj as Partial<Variable>;
function isValidVariable(obj: unknown): obj is PromptVariable {
const variable = obj as Partial<PromptVariable>;
return (
typeof variable === 'object' &&
variable !== null &&
Expand Down
47 changes: 25 additions & 22 deletions src/cli/commands/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// command.ts
import { Command } from 'commander';
import * as A from 'fp-ts/lib/Array';
import * as E from 'fp-ts/lib/Either';
Expand All @@ -21,18 +20,23 @@ export type CommandError = {
readonly context?: unknown;
};

export interface CommandResult {
readonly completed: boolean;
readonly action?: string;
}

export interface BaseCommand<A> {
readonly name: string;
readonly description: string;
readonly execute: (ctx: CommandContext) => Promise<TE.TaskEither<CommandError, A>>;
}

export type CommandResult<A> = E.Either<CommandError, A>;
// export type CommandResult<A> = E.Either<CommandError, A>;

export type CommandTask<A> = TE.TaskEither<CommandError, A>;

// Command creation helper
export const createCommand = <A>(
export const createCommand = <A extends CommandResult>(
name: string,
description: string,
execute: (ctx: CommandContext) => TE.TaskEither<CommandError, A>
Expand All @@ -54,19 +58,25 @@ export const createCommand = <A>(
throw new Error(`Failed to execute ${name} command: ${error.message}`);
})()
),
(value: A) =>
T.of(
(() => {
console.log('Command Result:', value);
return value;
})()
)
(value: A) => T.of(value)
)
)();
});
return command;
};

export const createCommandLoop = <A extends CommandResult>(
loopFn: () => TE.TaskEither<CommandError, A>
): TE.TaskEither<CommandError, A> => {
const runLoop = (previousResult?: A): TE.TaskEither<CommandError, A> => {
if (previousResult?.completed) {
return TE.right(previousResult);
}
return pipe(loopFn(), TE.chain(runLoop));
};
return runLoop();
};

// Helper for handling command results
export const handleCommandResult = <A>(
result: A,
Expand All @@ -75,11 +85,9 @@ export const handleCommandResult = <A>(
silent?: boolean;
}
): void => {
if (result !== undefined && !options?.silent) {
if (!options?.silent) {
if (options?.onSuccess) {
options.onSuccess(result);
} else {
console.log(result);
}
}
};
Expand Down Expand Up @@ -120,11 +128,7 @@ export const taskEitherFromPromise = <A>(
): TE.TaskEither<CommandError, A> =>
TE.tryCatch(async () => {
const result = await promise();

if (result === undefined) {
throw new Error('Unexpected undefined result');
}
return result;
return result as A; // Allow undefined/void returns
}, errorMapper);

// Helper for converting ApiResult to TaskEither
Expand All @@ -136,8 +140,7 @@ export const fromApiResult = <A>(promise: Promise<ApiResult<A>>): TE.TaskEither<
if (!result.success) {
throw new Error(result.error || 'Operation failed');
}
// Handle the case where data might be undefined but success is true
return result.data !== undefined ? result.data : (null as any);
return result.data as A; // Allow undefined data
},
(error) => createCommandError('API_ERROR', String(error))
);
Expand All @@ -148,10 +151,10 @@ export const fromApiFunction = <A>(fn: () => Promise<ApiResult<A>>): TE.TaskEith
async () => {
const result = await fn();

if (!result.success || result.data === undefined) {
if (!result.success) {
throw new Error(result.error || 'Operation failed');
}
return result.data;
return result.data as A; // Allow undefined data
},
(error) => createCommandError('API_ERROR', String(error))
);
Expand Down
175 changes: 122 additions & 53 deletions src/cli/commands/config-command.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,135 @@
import chalk from 'chalk';
import * as E from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import * as TE from 'fp-ts/lib/TaskEither';

import { BaseCommand } from './base-command';
import {
CommandError,
CommandResult,
createCommand,
createCommandError,
createCommandLoop,
taskEitherFromPromise
} from './base-command';
import { createInteractivePrompts, MenuChoice } from './interactive';
import { Config, getConfig, setConfig } from '../../shared/config';

class ConfigCommand extends BaseCommand {
constructor() {
super('config', 'Manage CLI configuration');
this.action(this.execute.bind(this));
// Type definitions
type ConfigAction = 'view' | 'set' | 'back';

interface ConfigCommandResult extends CommandResult {
// readonly action: ConfigAction;
readonly key?: keyof Config;
readonly value?: string;
}

// Create interactive prompts instance
const prompts = createInteractivePrompts();
// Pure functions for config operations
const viewConfig = (): TE.TaskEither<CommandError, ConfigCommandResult> =>
pipe(
taskEitherFromPromise(
() => Promise.resolve(getConfig()),
(error) => createCommandError('CONFIG_ERROR', 'Failed to read config', error)
),
TE.map((config) => {
displayConfig(config);
return {
completed: false,
action: 'view' as const
};
})
);
const setConfigValue = (key: keyof Config, value: string): TE.TaskEither<CommandError, ConfigCommandResult> =>
pipe(
taskEitherFromPromise(
() => {
setConfig(key, value);
return Promise.resolve(getConfig());
},
(error) => createCommandError('CONFIG_ERROR', 'Failed to set config value', error)
),
TE.map(() => ({
completed: false,
action: 'set' as const,
key,
value
}))
);
// Menu choices generators
const createConfigMenuChoices = (): ReadonlyArray<MenuChoice<ConfigAction>> => [
{ name: 'View current configuration', value: 'view' },
{ name: 'Set a configuration value', value: 'set' }
];
const createConfigKeyChoices = (config: Config): ReadonlyArray<MenuChoice<keyof Config>> =>
(Object.keys(config) as Array<keyof Config>).map((key) => ({
name: key,
value: key
}));
// Helper functions for displaying config values
const formatConfigValue = (key: keyof Config, value: unknown): string =>
key === 'ANTHROPIC_API_KEY' ? '********' : String(value);
const displayConfig = (config: Config): void => {
console.log(chalk.cyan('Current configuration:'));

if (Object.keys(config).length === 0) {
console.log(chalk.yellow('The configuration is empty.'));
return;
}

async execute(): Promise<void> {
while (true) {
try {
const action = await this.showMenu<'view' | 'set' | 'back'>('Select an action:', [
{ name: 'View current configuration', value: 'view' },
{ name: 'Set a configuration value', value: 'set' }
]);
const maxKeyLength = Math.max(...Object.keys(config).map((key) => key.length));

Object.entries(config).forEach(([key, value]) => {

Check warning on line 82 in src/cli/commands/config-command.ts

View workflow job for this annotation

GitHub Actions / update_views

Unexpected blank line before this statement
const paddingLength = maxKeyLength - key.length;
const paddedSpaces = paddingLength > 0 ? ' '.repeat(paddingLength) : '';
console.log(`${key}${paddedSpaces} -> ${chalk.green(formatConfigValue(key as keyof Config, value))}`);
});
};
// Validation helpers
const validateConfigValue = (key: keyof Config, value: string): E.Either<CommandError, string> =>
value.trim() === ''
? E.left(createCommandError('INVALID_CONFIG_VALUE', 'Configuration value cannot be empty'))
: E.right(value);
// Interactive workflow functions
const handleSetConfig = (config: Config): TE.TaskEither<CommandError, ConfigCommandResult> =>
pipe(
prompts.showMenu<keyof Config | 'back'>('Select the configuration key:', createConfigKeyChoices(config)),
TE.chain((key) => {
if (key === 'back') {
return TE.right({ completed: true });
}
return pipe(
prompts.getInput(`Enter the value for ${chalk.cyan(key)}:`),
TE.chain((value) =>
pipe(
validateConfigValue(key, value),
TE.fromEither,
TE.chain(() => setConfigValue(key, value))
)
)
);
})
);
// Main command execution
const executeConfigCommand = (): TE.TaskEither<CommandError, ConfigCommandResult> => {
const loop = (): TE.TaskEither<CommandError, ConfigCommandResult> =>
pipe(
prompts.showMenu<ConfigAction>('Select an action:', createConfigMenuChoices()),
TE.chain((action) => {
switch (action) {
case 'view':
this.viewConfig();
await this.pressKeyToContinue();
break;
return viewConfig();
case 'set':
await this.setConfigValue();
break;
return handleSetConfig(getConfig());
case 'back':
return;
return TE.right({ completed: true, action });
default:
return TE.left(createCommandError('INVALID_ACTION', `Invalid action: ${action}`));
}
} catch (error) {
this.handleError(error, 'config command');
await this.pressKeyToContinue();
}
}
}

private viewConfig(): void {
const currentConfig = getConfig();
console.log(chalk.cyan('Current configuration:'));

if (Object.keys(currentConfig).length === 0) {
console.log(chalk.yellow('The configuration is empty.'));
} else {
Object.entries(currentConfig).forEach(([key, value]) => {
console.log(chalk.green(`${key}:`), chalk.yellow(key === 'ANTHROPIC_API_KEY' ? '********' : value));
});
}
}

private async setConfigValue(): Promise<void> {
const currentConfig = getConfig();
const configKeys = Object.keys(currentConfig) as Array<keyof Config>;
const key = await this.showMenu<keyof Config | 'back'>(
'Select the configuration key:',
configKeys.map((k) => ({ value: k, name: k }))
})
);
return createCommandLoop(loop);
};

if (key === 'back') return;

const value = await this.getInput(`Enter the value for ${chalk.cyan(key)}:`);
setConfig(key, value);
console.log(chalk.green(`Configuration updated: ${key} = ${value}`));
}
}

export default new ConfigCommand();
// Command export
export const configCommand = createCommand('config', 'Manage CLI configuration', executeConfigCommand);
Loading

0 comments on commit b334ac2

Please sign in to comment.