diff --git a/package-lock.json b/package-lock.json
index cd5f755..5840f2d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"cli-spinner": "0.2.10",
"commander": "12.1.0",
"dotenv": "16.4.5",
+ "fp-ts": "^2.16.9",
"fs-extra": "11.2.0",
"js-yaml": "4.1.0",
"nunjucks": "3.2.4",
@@ -4836,6 +4837,12 @@
"node": ">= 12.20"
}
},
+ "node_modules/fp-ts": {
+ "version": "2.16.9",
+ "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz",
+ "integrity": "sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==",
+ "license": "MIT"
+ },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
diff --git a/package.json b/package.json
index 558bc77..e720ee0 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"cli-spinner": "0.2.10",
"commander": "12.1.0",
"dotenv": "16.4.5",
+ "fp-ts": "2.16.9",
"fs-extra": "11.2.0",
"js-yaml": "4.1.0",
"nunjucks": "3.2.4",
diff --git a/src/cli/commands/base-command.ts b/src/cli/commands/base-command.ts
index 9ee9d50..97a3448 100644
--- a/src/cli/commands/base-command.ts
+++ b/src/cli/commands/base-command.ts
@@ -1,12 +1,13 @@
// command.ts
-import { pipe } from 'fp-ts/lib/function';
+import { Command } from 'commander';
+import * as A from 'fp-ts/lib/Array';
import * as E from 'fp-ts/lib/Either';
-import * as TE from 'fp-ts/lib/TaskEither';
+import { pipe } from 'fp-ts/lib/function';
+import * as O from 'fp-ts/lib/Option';
import * as T from 'fp-ts/lib/Task';
-import { Command } from 'commander';
+import * as TE from 'fp-ts/lib/TaskEither';
+
import { ApiResult } from '../../shared/types';
-import * as O from 'fp-ts/lib/Option';
-import * as A from 'fp-ts/lib/Array';
// Core types
export type CommandContext = {
@@ -27,6 +28,7 @@ export interface BaseCommand {
}
export type CommandResult = E.Either;
+
export type CommandTask = TE.TaskEither;
// Command creation helper
@@ -43,7 +45,6 @@ export const createCommand = (
args: args.slice(0, -1),
options: args[args.length - 1] || {}
};
-
await pipe(
execute(ctx),
TE.fold(
@@ -63,7 +64,6 @@ export const createCommand = (
)
)();
});
-
return command;
};
@@ -120,6 +120,7 @@ export const taskEitherFromPromise = (
): TE.TaskEither =>
TE.tryCatch(async () => {
const result = await promise();
+
if (result === undefined) {
throw new Error('Unexpected undefined result');
}
@@ -131,6 +132,7 @@ export const fromApiResult = (promise: Promise>): TE.TaskEither<
TE.tryCatch(
async () => {
const result = await promise;
+
if (!result.success) {
throw new Error(result.error || 'Operation failed');
}
@@ -145,6 +147,7 @@ export const fromApiFunction = (fn: () => Promise>): TE.TaskEith
TE.tryCatch(
async () => {
const result = await fn();
+
if (!result.success || result.data === undefined) {
throw new Error(result.error || 'Operation failed');
}
diff --git a/src/cli/commands/env-command.ts b/src/cli/commands/env-command.ts
index 8107ccc..f46f959 100644
--- a/src/cli/commands/env-command.ts
+++ b/src/cli/commands/env-command.ts
@@ -1,28 +1,20 @@
// env-command.ts
-import { pipe } from 'fp-ts/lib/function';
-import * as TE from 'fp-ts/lib/TaskEither';
+import chalk from 'chalk';
import * as A from 'fp-ts/lib/Array';
-import * as O from 'fp-ts/lib/Option';
import * as Eq from 'fp-ts/lib/Eq';
+import { pipe } from 'fp-ts/lib/function';
+import * as O from 'fp-ts/lib/Option';
import * as Ord from 'fp-ts/lib/Ord';
-import chalk from 'chalk';
+import * as TE from 'fp-ts/lib/TaskEither';
-import {
- CommandContext,
- CommandError,
- createCommand,
- createCommandError,
- fromApiResult,
- fromApiFunction,
- traverseArray
-} from './base-command';
+import { CommandError, createCommand, fromApiResult, fromApiFunction, traverseArray } from './base-command';
import { EnvVar, Fragment, Variable } from '../../shared/types';
import { formatTitleCase, formatSnakeCase } from '../../shared/utils/string-formatter';
import { FRAGMENT_PREFIX } from '../constants';
+import { createInteractivePrompts, MenuChoice } from './interactive';
import { createEnvVar, readEnvVars, updateEnvVar, deleteEnvVar } from '../utils/env-vars';
import { listFragments, viewFragmentContent } from '../utils/fragments';
import { listPrompts, getPromptFiles } from '../utils/prompts';
-import { createInteractivePrompts, MenuChoice } from './interactive';
// Types
type EnvAction = 'enter' | 'fragment' | 'unset' | 'back';
@@ -38,34 +30,33 @@ interface EnvCommandResult {
// Create interactive prompts instance
const prompts = createInteractivePrompts();
-
// Pure functions
const formatVariableChoices = (
allVariables: ReadonlyArray,
envVars: ReadonlyArray
): ReadonlyArray> => {
const maxNameLength = Math.max(...allVariables.map((v) => formatSnakeCase(v.name).length));
-
return allVariables.map((variable) => {
const formattedName = formatSnakeCase(variable.name);
- const paddedName = formattedName.padEnd(maxNameLength);
+ const coloredName = chalk.cyan(formattedName);
+ const paddingLength = maxNameLength - formattedName.length;
+ const paddedSpaces = paddingLength > 0 ? ' '.repeat(paddingLength) : '';
const envVar = envVars.find((v) => formatSnakeCase(v.name) === formattedName);
const status = getVariableStatus(envVar);
-
return {
- name: `${chalk.cyan(paddedName)}: ${status}`,
+ name: `${coloredName}${paddedSpaces}: ${status}`,
value: variable
};
});
};
-
const getVariableStatus = (envVar: EnvVar | undefined): string => {
if (!envVar) return chalk.yellow('Not Set');
- if (envVar.value.startsWith(FRAGMENT_PREFIX)) return chalk.blue(envVar.value);
- return chalk.green(`Set: ${envVar.value.substring(0, 20)}${envVar.value.length > 20 ? '...' : ''}`);
-};
+ const trimmedValue = envVar.value.trim();
+ if (trimmedValue.startsWith(FRAGMENT_PREFIX)) return chalk.blue(trimmedValue);
+ return chalk.green(`Set: ${trimmedValue.substring(0, 20)}${trimmedValue.length > 20 ? '...' : ''}`);
+};
// Effects
const getAllUniqueVariables = (): TE.TaskEither> =>
pipe(
@@ -89,7 +80,6 @@ const getAllUniqueVariables = (): TE.TaskEither): TE.TaskEither =>
pipe(
prompts.getMultilineInput(
@@ -103,6 +93,12 @@ const enterValueForVariable = (variable: EnvVariable, envVar: O.Option):
)
),
TE.chain((value) => {
+ // Check if the user canceled the input (e.g., empty string)
+ if (value.trim() === '') {
+ console.log(chalk.yellow('Input canceled. Returning to actions menu.'));
+ return TE.right(undefined);
+ }
+
const operation = pipe(
envVar,
O.fold(
@@ -133,24 +129,23 @@ const enterValueForVariable = (variable: EnvVariable, envVar: O.Option):
return operation;
})
);
-
const assignFragmentToVariable = (variable: EnvVariable): TE.TaskEither =>
pipe(
fromApiResult(listFragments()),
TE.chain((fragments) =>
- prompts.showMenu(
- 'Select a fragment:',
- fragments.map((f) => ({
+ prompts.showMenu('Select a fragment:', [
+ ...fragments.map((f) => ({
name: `${formatTitleCase(f.category)} / ${chalk.blue(f.name)}`,
value: f
}))
- )
+ ])
),
TE.chain((fragment) => {
if (fragment === 'back') {
console.log(chalk.yellow('Fragment assignment cancelled.'));
return TE.right(undefined);
}
+
const fragmentRef = `${FRAGMENT_PREFIX}${fragment.category}/${fragment.name}`;
return pipe(
fromApiResult(readEnvVars()),
@@ -194,7 +189,6 @@ const assignFragmentToVariable = (variable: EnvVariable): TE.TaskEither): TE.TaskEither =>
pipe(
envVar,
@@ -209,8 +203,7 @@ const unsetVariable = (variable: EnvVariable, envVar: O.Option): TE.Task
)
)
);
-
-const executeEnvCommand = (ctx: CommandContext): TE.TaskEither => {
+const executeEnvCommand = (): TE.TaskEither => {
const loop = (): TE.TaskEither =>
pipe(
TE.Do,
@@ -220,28 +213,24 @@ const executeEnvCommand = (ctx: CommandContext): TE.TaskEither(
'Select a variable to manage:',
- [
- ...formatVariableChoices(variables, envVars),
- ],
+ formatVariableChoices(variables, envVars)
),
TE.chain((selectedVariable): TE.TaskEither => {
if (selectedVariable === 'back') {
console.log(chalk.yellow('Returning to main menu.'));
return TE.right({ completed: true });
}
-
return pipe(
prompts.showMenu(
`Choose action for ${formatSnakeCase(selectedVariable.name)}:`,
[
{ name: 'Enter value', value: 'enter' },
{ name: 'Use fragment', value: 'fragment' },
- { name: 'Unset', value: 'unset' },
- ],
+ { name: 'Unset', value: 'unset' }
+ ]
),
TE.chain((action) => {
const envVar = O.fromNullable(envVars.find((v) => v.name === selectedVariable.name));
-
switch (action) {
case 'enter':
return pipe(
@@ -271,7 +260,6 @@ const executeEnvCommand = (ctx: CommandContext): TE.TaskEither {
diff --git a/src/cli/commands/interactive.ts b/src/cli/commands/interactive.ts
index 821b53e..a81af19 100644
--- a/src/cli/commands/interactive.ts
+++ b/src/cli/commands/interactive.ts
@@ -1,13 +1,15 @@
+import os from 'os';
+import path from 'path';
+
import { editor, input, select } from '@inquirer/prompts';
+import chalk from 'chalk';
+import { pipe } from 'fp-ts/lib/function';
import * as TE from 'fp-ts/lib/TaskEither';
+import fs from 'fs-extra';
+
import { CommandError, createCommandError } from '../commands/base-command';
import { cliConfig } from '../config/cli-config';
-import chalk from 'chalk';
-import { pipe } from 'fp-ts/lib/function';
import { ENV_PREFIX, FRAGMENT_PREFIX } from '../constants';
-import path from 'path';
-import os from 'os';
-import fs from 'fs-extra';
export interface MenuChoice {
readonly name: string;
@@ -46,7 +48,6 @@ export const createInteractivePrompts = (): InteractivePrompts => ({
clearConsole = true,
pageSize = cliConfig.MENU_PAGE_SIZE
} = options;
-
return TE.tryCatch(
async () => {
if (clearConsole) {
@@ -61,7 +62,6 @@ export const createInteractivePrompts = (): InteractivePrompts => ({
value: goBackValue
});
}
-
return select({
message,
choices: menuChoices,
@@ -94,9 +94,7 @@ export const createInteractivePrompts = (): InteractivePrompts => ({
initialValue.startsWith(FRAGMENT_PREFIX) || initialValue.startsWith(ENV_PREFIX)
? ''
: initialValue;
-
await fs.writeFile(tempFilePath, cleanedInitialValue);
-
return editor({
message: 'Edit your input',
default: cleanedInitialValue,
diff --git a/src/cli/commands/menu-command.ts b/src/cli/commands/menu-command.ts
index f605b3a..b53e0a0 100644
--- a/src/cli/commands/menu-command.ts
+++ b/src/cli/commands/menu-command.ts
@@ -1,14 +1,14 @@
// menu-command.ts
import chalk from 'chalk';
import { Command } from 'commander';
-import * as TE from 'fp-ts/lib/TaskEither';
-import * as T from 'fp-ts/lib/Task';
import { pipe } from 'fp-ts/lib/function';
+import * as T from 'fp-ts/lib/Task';
+import * as TE from 'fp-ts/lib/TaskEither';
-import { getConfig } from '../../shared/config';
-import { hasFragments, hasPrompts } from '../utils/file-system';
import { CommandError, taskEitherFromPromise, createCommandError, CommandContext, createCommand } from './base-command';
import { createInteractivePrompts, MenuChoice } from './interactive';
+import { getConfig } from '../../shared/config';
+import { hasFragments, hasPrompts } from '../utils/file-system';
// Types
type MenuAction = 'sync' | 'prompts' | 'fragments' | 'settings' | 'env' | 'back';
@@ -20,7 +20,6 @@ interface MenuCommandResult {
// Create interactive prompts instance
const prompts = createInteractivePrompts();
-
// Pure functions
const createMenuChoices = (
hasRemoteRepo: boolean,
@@ -41,10 +40,8 @@ const createMenuChoices = (
{ name: 'Manage environment variables', value: 'env' },
{ name: 'Settings', value: 'settings' }
);
-
return choices;
};
-
// Effects
const checkContentStatus = (): TE.TaskEither =>
pipe(
@@ -54,15 +51,16 @@ const checkContentStatus = (): TE.TaskEither =>
),
TE.map(([promptsExist, fragmentsExist]) => promptsExist || fragmentsExist)
);
-
const executeMenuAction = (program: Command, action: MenuAction): TE.TaskEither =>
pipe(
TE.tryCatch(
async () => {
const command = program.commands.find((cmd) => cmd.name() === action);
+
if (!command) {
throw new Error(`Command '${action}' not found`);
}
+
await command.parseAsync([], { from: 'user' });
},
(error) =>
@@ -73,7 +71,6 @@ const executeMenuAction = (program: Command, action: MenuAction): TE.TaskEither<
)
)
);
-
const showMenuPrompt = (hasRemoteRepo: boolean, hasExistingContent: boolean): TE.TaskEither =>
prompts.showMenu(
`${chalk.reset(chalk.italic(chalk.cyan('Want to manage AI prompts with ease ?')))}
@@ -84,13 +81,11 @@ Select an action:`
createMenuChoices(hasRemoteRepo, hasExistingContent),
{ goBackLabel: 'Exit' }
);
-
// Helper function to create menu results
const createMenuResult = (action: MenuAction, completed: boolean): MenuCommandResult => ({
action,
completed
});
-
// Main menu loop
const handleExit = (): TE.TaskEither =>
TE.tryCatch(
@@ -103,7 +98,6 @@ const handleExit = (): TE.TaskEither =>
},
(error) => createCommandError('MENU_ERROR', 'Failed to handle exit', error)
);
-
const handleMenuAction = (
program: Command,
action: Exclude
@@ -115,18 +109,15 @@ const handleMenuAction = (
completed: false
}))
);
-
const runMenuLoop = (program: Command): TE.TaskEither => {
const config = getConfig();
const hasRemoteRepo = Boolean(config.REMOTE_REPOSITORY);
-
return pipe(
checkContentStatus(),
TE.chain((hasExistingContent) => showMenuPrompt(hasRemoteRepo, hasExistingContent)),
TE.chain((action) => (action === 'back' ? handleExit() : handleMenuAction(program, action)))
);
};
-
// Command execution
const executeMenuCommand = (ctx: CommandContext & { program: Command }): TE.TaskEither => {
const loop = (): TE.TaskEither =>
@@ -139,7 +130,6 @@ const executeMenuCommand = (ctx: CommandContext & { program: Command }): TE.Task
return loop();
})
);
-
return loop();
};
diff --git a/src/cli/index.ts b/src/cli/index.ts
index bdda1a5..1e4120b 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -1,20 +1,20 @@
#!/usr/bin/env node
import { input } from '@inquirer/prompts';
+import chalk from 'chalk';
import { Command } from 'commander';
import dotenv from 'dotenv';
-import * as TE from 'fp-ts/lib/TaskEither';
-import * as T from 'fp-ts/lib/Task';
-import { pipe } from 'fp-ts/lib/function';
import * as E from 'fp-ts/lib/Either';
+import { pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/lib/Option';
-import chalk from 'chalk';
+import * as T from 'fp-ts/lib/Task';
+import * as TE from 'fp-ts/lib/TaskEither';
import { getConfigValue, setConfig } from '../shared/config';
-import { initDatabase } from './utils/database';
import { CommandError } from './commands/base-command';
-import { showMainMenu } from './commands/menu-command';
-import { envCommand } from './commands/env-command';
import { configCommand } from './commands/config-command';
+import { envCommand } from './commands/env-command';
+import { showMainMenu } from './commands/menu-command';
+import { initDatabase } from './utils/database';
// Set environment
process.env.CLI_ENV = 'cli';
@@ -36,16 +36,13 @@ const createCliError = (code: string, message: string): CliError => ({
code,
message
});
-
const validateApiKey = (apiKey: string): E.Either =>
apiKey.trim() === '' ? E.left(createCliError('INVALID_API_KEY', 'API key cannot be empty')) : E.right(apiKey);
-
const getStoredApiKey = (): O.Option =>
pipe(
O.fromNullable(getConfigValue('ANTHROPIC_API_KEY')),
O.filter((key) => key.trim() !== '')
);
-
// Effects
const promptForApiKey = (): TE.TaskEither =>
pipe(
@@ -55,7 +52,6 @@ const promptForApiKey = (): TE.TaskEither =>
),
TE.chain((key) => pipe(validateApiKey(key), TE.fromEither))
);
-
const setApiKey = (apiKey: string): TE.TaskEither =>
pipe(
TE.tryCatch(
@@ -66,7 +62,6 @@ const setApiKey = (apiKey: string): TE.TaskEither =>
(error) => createCliError('API_KEY_SAVE_ERROR', `Failed to save API key: ${error}`)
)
);
-
const ensureApiKey = (): TE.TaskEither =>
pipe(
getStoredApiKey(),
@@ -75,7 +70,6 @@ const ensureApiKey = (): TE.TaskEither =>
(apiKey) => TE.right(apiKey)
)
);
-
// Program setup
const createProgram = (): Command => {
const program = new Command();
@@ -84,13 +78,11 @@ const createProgram = (): Command => {
.description('CLI tool for managing and executing AI prompts')
.version('1.0.0');
};
-
const registerCommands = (program: Command): Command => {
const commands = [configCommand, envCommand];
commands.forEach((cmd) => program.addCommand(cmd));
return program;
};
-
const initializeCli = (): TE.TaskEither =>
pipe(
TE.tryCatch(
@@ -103,7 +95,6 @@ const initializeCli = (): TE.TaskEither =>
apiKey
}))
);
-
// Main program flow
const main = async (): Promise => {
await pipe(
@@ -130,7 +121,6 @@ const main = async (): Promise => {
)();
};
-// Execute program
if (require.main === module) {
main().catch((error) => {
console.error(chalk.red('Fatal error:'), error);