Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
414 changes: 259 additions & 155 deletions out/cli.cjs

Large diffs are not rendered by default.

20 changes: 3 additions & 17 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { join as pathJoin, resolve as pathResolve } from 'path';
import { COMMANDS } from './ENUMS';
import { TEST_MOCK_TYPES } from '../engine/testAi';
import { getI18nLocal, i18n } from '../i18n';
import { OCO_AI_PROVIDER_ENUM } from '../utils/provider';

export { OCO_AI_PROVIDER_ENUM } from '../utils/provider';

export enum CONFIG_KEYS {
OCO_API_KEY = 'OCO_API_KEY',
Expand Down Expand Up @@ -855,23 +858,6 @@ export const configValidators = {
}
};

export enum OCO_AI_PROVIDER_ENUM {
OLLAMA = 'ollama',
LLAMACPP = 'llamacpp',
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
GEMINI = 'gemini',
AZURE = 'azure',
TEST = 'test',
FLOWISE = 'flowise',
GROQ = 'groq',
MISTRAL = 'mistral',
MLX = 'mlx',
DEEPSEEK = 'deepseek',
AIMLAPI = 'aimlapi',
OPENROUTER = 'openrouter'
}

export const PROVIDER_API_KEY_URLS: Record<string, string | null> = {
[OCO_AI_PROVIDER_ENUM.OPENAI]: 'https://platform.openai.com/api-keys',
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]:
Expand Down
40 changes: 23 additions & 17 deletions src/commands/githook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,54 @@ import { command } from 'cleye';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
import { assertGitRepo, getGitHooksPath } from '../utils/git.js';
import { COMMANDS } from './ENUMS';

const HOOK_NAME = 'prepare-commit-msg';
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);

const getHooksPath = async (): Promise<string> => {
try {
const hooksPath = await getCoreHooksPath();
return path.join(hooksPath, HOOK_NAME);
} catch (error) {
return DEFAULT_SYMLINK_URL;
}
return path.join(await getGitHooksPath(), HOOK_NAME);
};

export const isHookCalled = async (): Promise<boolean> => {
const hooksPath = await getHooksPath();
return process.argv[1].endsWith(hooksPath);
const normalizeHookPath = async (hookPath: string): Promise<string> => {
const absolutePath = path.resolve(hookPath);
const realDirectory = await fs.realpath(path.dirname(absolutePath));
return path.join(realDirectory, path.basename(absolutePath));
};

const isHookExists = async (): Promise<boolean> => {
const hooksPath = await getHooksPath();
return existsSync(hooksPath);
export const isHookCalled = async (): Promise<boolean> => {
try {
const invokedPath = process.argv[1];
if (!invokedPath) return false;

return (
(await normalizeHookPath(invokedPath)) ===
(await normalizeHookPath(await getHooksPath()))
);
} catch {
return false;
}
};

const isHookExists = (hooksPath: string): boolean => existsSync(hooksPath);

export const hookCommand = command(
{
name: COMMANDS.hook,
parameters: ['<set/unset>']
},
async (argv) => {
const HOOK_URL = __filename;
const SYMLINK_URL = await getHooksPath();
try {
await assertGitRepo();
const SYMLINK_URL = await getHooksPath();

const { setUnset: mode } = argv._;

if (mode === 'set') {
intro(`setting opencommit as '${HOOK_NAME}' hook at ${SYMLINK_URL}`);

if (await isHookExists()) {
if (isHookExists(SYMLINK_URL)) {
let realPath;
try {
realPath = await fs.realpath(SYMLINK_URL);
Expand Down Expand Up @@ -74,7 +80,7 @@ export const hookCommand = command(
`unsetting opencommit as '${HOOK_NAME}' hook from ${SYMLINK_URL}`
);

if (!(await isHookExists())) {
if (!isHookExists(SYMLINK_URL)) {
return outro(
`OpenCommit wasn't previously set as '${HOOK_NAME}' hook, nothing to remove`
);
Expand Down
6 changes: 5 additions & 1 deletion src/commands/prepare-commit-msg-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { intro, outro, spinner } from '@clack/prompts';

import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
import { getProviderConfigRequirement } from '../utils/provider';
import { getConfig } from './config';

const [messageFilePath, commitSource] = process.argv.slice(2);
Expand Down Expand Up @@ -39,7 +40,10 @@ export const prepareCommitMessageHook = async (

const config = getConfig();

if (!config.OCO_API_KEY) {
if (
getProviderConfigRequirement(config.OCO_AI_PROVIDER) === 'apiKey' &&
!config.OCO_API_KEY
) {
outro(
'No OCO_API_KEY is set. Set your key via `oco config set OCO_API_KEY=<value>. For more info see https://github.com/di-sukharev/opencommit'
);
Expand Down
55 changes: 18 additions & 37 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,101 +19,86 @@ import {
fetchOllamaModels,
getCacheInfo
} from '../utils/modelCache';
import { getProviderConfigRequirement } from '../utils/provider';

type ProviderSelectionGroup = 'primary' | 'other' | 'hidden';
type FirstRunRequirement = 'apiKey' | 'model' | 'none';

interface SetupProviderDefinition {
provider: OCO_AI_PROVIDER_ENUM;
displayName: string;
selectionGroup: ProviderSelectionGroup;
firstRunRequirement: FirstRunRequirement;
}

const SETUP_PROVIDERS: SetupProviderDefinition[] = [
{
provider: OCO_AI_PROVIDER_ENUM.OPENAI,
displayName: 'OpenAI (GPT)',
selectionGroup: 'primary',
firstRunRequirement: 'apiKey'
selectionGroup: 'primary'
},
{
provider: OCO_AI_PROVIDER_ENUM.ANTHROPIC,
displayName: 'Anthropic (Claude Sonnet, Opus)',
selectionGroup: 'primary',
firstRunRequirement: 'apiKey'
selectionGroup: 'primary'
},
{
provider: OCO_AI_PROVIDER_ENUM.OLLAMA,
displayName: 'Ollama (Free, runs locally)',
selectionGroup: 'primary',
firstRunRequirement: 'model'
selectionGroup: 'primary'
},
{
provider: OCO_AI_PROVIDER_ENUM.LLAMACPP,
displayName: 'llama.cpp (Free, runs locally)',
selectionGroup: 'primary',
firstRunRequirement: 'model'
selectionGroup: 'primary'
},
{
provider: OCO_AI_PROVIDER_ENUM.GEMINI,
displayName: 'Google Gemini',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.GROQ,
displayName: 'Groq (Fast inference, free tier)',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.MISTRAL,
displayName: 'Mistral AI',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.DEEPSEEK,
displayName: 'DeepSeek',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.OPENROUTER,
displayName: 'OpenRouter (Multiple providers)',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.AIMLAPI,
displayName: 'AI/ML API',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.AZURE,
displayName: 'Azure OpenAI',
selectionGroup: 'other',
firstRunRequirement: 'apiKey'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.MLX,
displayName: 'MLX (Apple Silicon, local)',
selectionGroup: 'other',
firstRunRequirement: 'model'
selectionGroup: 'other'
},
{
provider: OCO_AI_PROVIDER_ENUM.FLOWISE,
displayName: OCO_AI_PROVIDER_ENUM.FLOWISE,
selectionGroup: 'hidden',
firstRunRequirement: 'apiKey'
selectionGroup: 'hidden'
},
{
provider: OCO_AI_PROVIDER_ENUM.TEST,
displayName: OCO_AI_PROVIDER_ENUM.TEST,
selectionGroup: 'hidden',
firstRunRequirement: 'none'
selectionGroup: 'hidden'
}
];

Expand All @@ -138,10 +123,6 @@ function getProviderDisplayName(provider: string): string {
return getProviderDefinition(provider)?.displayName || provider;
}

function getFirstRunRequirement(provider: string): FirstRunRequirement {
return getProviderDefinition(provider)?.firstRunRequirement || 'apiKey';
}

async function selectProvider(): Promise<string | symbol> {
const primaryOptions = getProviderOptions('primary');

Expand Down Expand Up @@ -239,7 +220,7 @@ async function selectModel(

if (models.length === 0) {
// Providers without API keys can accept a local model name directly.
if (getFirstRunRequirement(provider) !== 'apiKey') {
if (getProviderConfigRequirement(provider) !== 'apiKey') {
return await text({
message: 'Enter model name (e.g., llama3:8b, mistral):',
placeholder: 'llama3:8b',
Expand Down Expand Up @@ -548,7 +529,7 @@ export function isFirstRun(): boolean {

const provider = config.OCO_AI_PROVIDER || OCO_AI_PROVIDER_ENUM.OPENAI;

const requirement = getFirstRunRequirement(provider);
const requirement = getProviderConfigRequirement(provider);
const hasRequiredConfig =
requirement === 'model'
? Boolean(config.OCO_MODEL)
Expand All @@ -564,7 +545,7 @@ export async function promptForMissingApiKey(): Promise<boolean> {
const config = getConfig();
const provider = config.OCO_AI_PROVIDER || OCO_AI_PROVIDER_ENUM.OPENAI;

if (getFirstRunRequirement(provider) !== 'apiKey') {
if (getProviderConfigRequirement(provider) !== 'apiKey') {
return true; // No API key needed
}

Expand Down
8 changes: 4 additions & 4 deletions src/utils/git.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execa } from 'execa';
import { readFileSync, existsSync } from 'fs';
import ignore, { Ignore } from 'ignore';
import { join } from 'path';
import { join, resolve as pathResolve } from 'path';
import { homedir } from 'os';
import { outro, spinner } from '@clack/prompts';

Expand Down Expand Up @@ -44,14 +44,14 @@ export const getOpenCommitIgnore = async (): Promise<Ignore> => {
return ig;
};

export const getCoreHooksPath = async (): Promise<string> => {
export const getGitHooksPath = async (): Promise<string> => {
const gitDir = await getGitDir();

const { stdout } = await execa('git', ['config', 'core.hooksPath'], {
const { stdout } = await execa('git', ['rev-parse', '--git-path', 'hooks'], {
cwd: gitDir
});

return stdout;
return pathResolve(gitDir, stdout);
};

export const getStagedFiles = async (): Promise<string[]> => {
Expand Down
43 changes: 43 additions & 0 deletions src/utils/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export enum OCO_AI_PROVIDER_ENUM {
OLLAMA = 'ollama',
LLAMACPP = 'llamacpp',
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
GEMINI = 'gemini',
AZURE = 'azure',
TEST = 'test',
FLOWISE = 'flowise',
GROQ = 'groq',
MISTRAL = 'mistral',
MLX = 'mlx',
DEEPSEEK = 'deepseek',
AIMLAPI = 'aimlapi',
OPENROUTER = 'openrouter'
}

export type ProviderConfigRequirement = 'apiKey' | 'model' | 'none';

const PROVIDER_CONFIG_REQUIREMENTS: Record<
OCO_AI_PROVIDER_ENUM,
ProviderConfigRequirement
> = {
[OCO_AI_PROVIDER_ENUM.OPENAI]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.OLLAMA]: 'model',
[OCO_AI_PROVIDER_ENUM.LLAMACPP]: 'model',
[OCO_AI_PROVIDER_ENUM.GEMINI]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.GROQ]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.MISTRAL]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.DEEPSEEK]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.OPENROUTER]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.AIMLAPI]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.AZURE]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.MLX]: 'model',
[OCO_AI_PROVIDER_ENUM.FLOWISE]: 'apiKey',
[OCO_AI_PROVIDER_ENUM.TEST]: 'none'
};

export const getProviderConfigRequirement = (
provider: string = OCO_AI_PROVIDER_ENUM.OPENAI
): ProviderConfigRequirement =>
PROVIDER_CONFIG_REQUIREMENTS[provider as OCO_AI_PROVIDER_ENUM] || 'apiKey';
Loading
Loading