Skip to content

Commit cf27085

Browse files
committed
fix(cli): tighten proxy and setup behavior
Support explicit proxy disabling and ambient proxy fallback without leaking env state into config. Improve first-run detection, endpoint-specific error messaging, diff exclusions, and runtime helper boundaries covered by unit tests.
1 parent 7fa2384 commit cf27085

23 files changed

+532
-219
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,13 @@ If you are behind a proxy, you can set it in the config:
245245
oco config set OCO_PROXY=http://127.0.0.1:7890
246246
```
247247

248-
Or it will automatically use `HTTPS_PROXY` or `HTTP_PROXY` environment variables.
248+
If `OCO_PROXY` is unset, OpenCommit will automatically use `HTTPS_PROXY` or `HTTP_PROXY` environment variables.
249+
250+
To explicitly disable proxy use for OpenCommit, even when those environment variables are set:
251+
252+
```sh
253+
oco config set OCO_PROXY=null
254+
```
249255

250256
### Locale configuration
251257

src/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { commitlintConfigCommand } from './commands/commitlint';
88
import { configCommand, getConfig } from './commands/config';
99
import { hookCommand, isHookCalled } from './commands/githook.js';
1010
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
11-
import { setupProxy } from './utils/proxy';
11+
import { resolveProxy, setupProxy } from './utils/proxy';
1212
import {
1313
setupCommand,
1414
isFirstRun,
@@ -20,7 +20,7 @@ import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
2020
import { runMigrations } from './migrations/_run.js';
2121

2222
const config = getConfig();
23-
setupProxy(config.OCO_PROXY);
23+
setupProxy(resolveProxy(config.OCO_PROXY));
2424

2525
const OCO_FLAGS_WITH_VALUE = new Set(['-c', '--context']);
2626
const OCO_BOOLEAN_FLAGS = new Set(['-y', '--yes', '--fgm']);

src/commands/commit.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ ${chalk.grey('——————————————————')}`
249249

250250
const errorConfig = getConfig();
251251
const provider = errorConfig.OCO_AI_PROVIDER || 'openai';
252-
const formatted = formatUserFriendlyError(error, provider);
252+
const formatted = formatUserFriendlyError(error, provider, {
253+
baseURL: errorConfig.OCO_API_URL
254+
});
253255
outro(printFormattedError(formatted));
254256

255257
process.exit(1);

src/commands/config.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,8 @@ export const configValidators = {
723723
[CONFIG_KEYS.OCO_API_URL](value: any) {
724724
validateConfig(
725725
CONFIG_KEYS.OCO_API_URL,
726-
typeof value === 'string',
726+
typeof value === 'string' &&
727+
/^(https?:\/\/)/.test(value),
727728
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
728729
);
729730
return value;
@@ -732,7 +733,8 @@ export const configValidators = {
732733
[CONFIG_KEYS.OCO_PROXY](value: any) {
733734
validateConfig(
734735
CONFIG_KEYS.OCO_PROXY,
735-
typeof value === 'string',
736+
value === null ||
737+
(typeof value === 'string' && /^(https?:\/\/)/.test(value)),
736738
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
737739
);
738740
return value;
@@ -900,7 +902,7 @@ export type ConfigType = {
900902
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
901903
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
902904
[CONFIG_KEYS.OCO_API_URL]?: string;
903-
[CONFIG_KEYS.OCO_PROXY]?: string;
905+
[CONFIG_KEYS.OCO_PROXY]?: string | null;
904906
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS]?: string;
905907
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
906908
[CONFIG_KEYS.OCO_EMOJI]: boolean;
@@ -986,10 +988,7 @@ const getEnvConfig = (envPath: string) => {
986988
return {
987989
OCO_MODEL: process.env.OCO_MODEL,
988990
OCO_API_URL: process.env.OCO_API_URL,
989-
OCO_PROXY:
990-
process.env.OCO_PROXY ||
991-
process.env.HTTPS_PROXY ||
992-
process.env.HTTP_PROXY,
991+
OCO_PROXY: process.env.OCO_PROXY,
993992
OCO_API_KEY: process.env.OCO_API_KEY,
994993
OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS,
995994
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
@@ -1027,16 +1026,13 @@ export const getIsGlobalConfigFileExist = (
10271026
};
10281027

10291028
export const getGlobalConfig = (configPath: string = defaultConfigPath) => {
1030-
let globalConfig: ConfigType;
1031-
10321029
const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath);
1033-
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath);
1034-
else {
1035-
const configFile = readFileSync(configPath, 'utf8');
1036-
globalConfig = iniParse(configFile) as ConfigType;
1030+
if (!isGlobalConfigFileExist) {
1031+
return { ...DEFAULT_CONFIG };
10371032
}
10381033

1039-
return globalConfig;
1034+
const configFile = readFileSync(configPath, 'utf8');
1035+
return iniParse(configFile) as ConfigType;
10401036
};
10411037

10421038
/**
@@ -1049,7 +1045,10 @@ export const getGlobalConfig = (configPath: string = defaultConfigPath) => {
10491045
const mergeConfigs = (main: Partial<ConfigType>, fallback: ConfigType) => {
10501046
const allKeys = new Set([...Object.keys(main), ...Object.keys(fallback)]);
10511047
return Array.from(allKeys).reduce((acc, key) => {
1052-
acc[key] = parseConfigVarValue(main[key] ?? fallback[key]);
1048+
const mainValue = main[key];
1049+
acc[key] = parseConfigVarValue(
1050+
mainValue !== undefined ? mainValue : fallback[key]
1051+
);
10531052
return acc;
10541053
}, {} as ConfigType);
10551054
};
@@ -1218,7 +1217,10 @@ function getConfigKeyDetails(key) {
12181217
case CONFIG_KEYS.OCO_PROXY:
12191218
return {
12201219
description: 'HTTP/HTTPS Proxy URL',
1221-
values: ["URL string (must start with 'http://' or 'https://')"]
1220+
values: [
1221+
"URL string (must start with 'http://' or 'https://')",
1222+
'null (disable proxy even when HTTP_PROXY/HTTPS_PROXY are set)'
1223+
]
12221224
};
12231225
case CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
12241226
return {

src/commands/setup.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -427,22 +427,23 @@ export async function runSetup(): Promise<boolean> {
427427
}
428428

429429
export function isFirstRun(): boolean {
430+
const hasGlobalConfig = getIsGlobalConfigFileExist();
430431
const config = getConfig();
431432

432-
// Check if API key is missing for providers that need it
433433
const provider = config.OCO_AI_PROVIDER || OCO_AI_PROVIDER_ENUM.OPENAI;
434434

435-
if (MODEL_REQUIRED_PROVIDERS.includes(provider as OCO_AI_PROVIDER_ENUM)) {
436-
// For Ollama/MLX, check if model is set
437-
return !config.OCO_MODEL;
438-
}
439-
440435
if (provider === OCO_AI_PROVIDER_ENUM.TEST) {
441436
return false;
442437
}
443438

444-
// For other providers, check if API key is set
445-
return !config.OCO_API_KEY;
439+
const hasRequiredConfig = MODEL_REQUIRED_PROVIDERS.includes(
440+
provider as OCO_AI_PROVIDER_ENUM
441+
)
442+
? Boolean(config.OCO_MODEL)
443+
: Boolean(config.OCO_API_KEY);
444+
445+
// Trigger the full setup wizard only when nothing usable was configured yet.
446+
return !hasGlobalConfig && !hasRequiredConfig;
446447
}
447448

448449
export async function promptForMissingApiKey(): Promise<boolean> {

src/engine/Engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface AiEngineConfig {
1111
maxTokensOutput: number;
1212
maxTokensInput: number;
1313
baseURL?: string;
14-
proxy?: string;
14+
proxy?: string | null;
1515
customHeaders?: Record<string, string>;
1616
ollamaThink?: boolean;
1717
}

src/engine/anthropic.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
MessageParam
66
} from '@anthropic-ai/sdk/resources/messages.mjs';
77
import { OpenAI } from 'openai';
8-
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
98
import { normalizeEngineError } from '../utils/engineErrorHandler';
9+
import { GenerateCommitMessageErrorEnum } from '../utils/generateCommitMessageErrors';
1010
import { removeContentTags } from '../utils/removeContentTags';
1111
import { tokenCount } from '../utils/tokenCount';
1212
import { AiEngine, AiEngineConfig } from './Engine';
@@ -21,8 +21,7 @@ export class AnthropicEngine implements AiEngine {
2121
this.config = config;
2222
const clientOptions: any = { apiKey: this.config.apiKey };
2323

24-
const proxy =
25-
config.proxy || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
24+
const proxy = config.proxy;
2625
if (proxy) {
2726
clientOptions.httpAgent = new HttpsProxyAgent(proxy);
2827
}

src/engine/azure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
OpenAIClient as AzureOpenAIClient
44
} from '@azure/openai';
55
import { OpenAI } from 'openai';
6-
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
76
import { normalizeEngineError } from '../utils/engineErrorHandler';
7+
import { GenerateCommitMessageErrorEnum } from '../utils/generateCommitMessageErrors';
88
import { removeContentTags } from '../utils/removeContentTags';
99
import { tokenCount } from '../utils/tokenCount';
1010
import { AiEngine, AiEngineConfig } from './Engine';

src/engine/deepseek.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OpenAI } from 'openai';
2-
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
32
import { normalizeEngineError } from '../utils/engineErrorHandler';
3+
import { GenerateCommitMessageErrorEnum } from '../utils/generateCommitMessageErrors';
44
import { removeContentTags } from '../utils/removeContentTags';
55
import { tokenCount } from '../utils/tokenCount';
66
import { OpenAiEngine, OpenAiConfig } from './openAi';

src/engine/mistral.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { OpenAI } from 'openai';
22
import { HttpsProxyAgent } from 'https-proxy-agent';
3-
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
3+
import { createRequire } from 'module';
44
import { normalizeEngineError } from '../utils/engineErrorHandler';
5+
import { GenerateCommitMessageErrorEnum } from '../utils/generateCommitMessageErrors';
56
import { removeContentTags } from '../utils/removeContentTags';
67
import { tokenCount } from '../utils/tokenCount';
78
import { AiEngine, AiEngineConfig } from './Engine';
@@ -10,8 +11,14 @@ import { AiEngine, AiEngineConfig } from './Engine';
1011
export interface MistralAiConfig extends AiEngineConfig {}
1112
export type MistralCompletionMessageParam = Array<any>;
1213

13-
// Import Mistral dynamically to avoid TS errors
14-
// eslint-disable-next-line @typescript-eslint/no-var-requires
14+
let require: NodeRequire;
15+
16+
try {
17+
require = createRequire(__filename);
18+
} catch {
19+
require = createRequire(import.meta.url);
20+
}
21+
1522
const Mistral = require('@mistralai/mistralai').Mistral;
1623

1724
export class MistralAiEngine implements AiEngine {

0 commit comments

Comments
 (0)