Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { red } from 'kolorist';
import {
hasOwn,
getConfig,
getUIConfig,
setConfigs,
showConfigUI,
} from '../helpers/config.js';
Expand Down Expand Up @@ -37,7 +38,7 @@ export default command(
}

if (mode === 'get') {
const config = await getConfig();
const config = await getUIConfig();
for (const key of keyValues) {
if (hasOwn(config, key)) {
console.log(`${key}=${config[key as keyof typeof config]}`);
Expand Down
30 changes: 25 additions & 5 deletions src/helpers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ type ValidConfig = {
[Key in ConfigKeys]: ReturnType<(typeof configParsers)[Key]>;
};

type UIConfig = Omit<ValidConfig, 'OPENAI_KEY'> & {
OPENAI_KEY?: string;
};

const configPath = path.join(os.homedir(), '.ai-shell');

const fileExists = (filePath: string) =>
Expand All @@ -84,21 +88,37 @@ const readConfigFile = async (): Promise<RawConfig> => {
return ini.parse(configString);
};

export const getConfig = async (
cliConfig?: RawConfig
): Promise<ValidConfig> => {
const parseConfig = async <T extends ValidConfig | UIConfig>(
cliConfig: RawConfig | undefined,
options: { requireOpenAIKey: boolean }
): Promise<T> => {
const config = await readConfigFile();
const parsedConfig: Record<string, unknown> = {};

for (const key of Object.keys(configParsers) as ConfigKeys[]) {
const parser = configParsers[key];
const value = cliConfig?.[key] ?? config[key];

if (key === 'OPENAI_KEY' && !options.requireOpenAIKey) {
parsedConfig[key] = value;
continue;
}

parsedConfig[key] = parser(value);
}

return parsedConfig as ValidConfig;
return parsedConfig as T;
};

export const getConfig = async (
cliConfig?: RawConfig
): Promise<ValidConfig> => parseConfig<ValidConfig>(cliConfig, {
requireOpenAIKey: true,
});

export const getUIConfig = async (cliConfig?: RawConfig): Promise<UIConfig> =>
parseConfig<UIConfig>(cliConfig, { requireOpenAIKey: false });

export const setConfigs = async (keyValues: [key: string, value: string][]) => {
const config = await readConfigFile();

Expand All @@ -116,7 +136,7 @@ export const setConfigs = async (keyValues: [key: string, value: string][]) => {

export const showConfigUI = async () => {
try {
const config = await getConfig();
const config = await getUIConfig();
const choice = (await p.select({
message: i18n.t('Set config') + ':',
options: [
Expand Down
40 changes: 40 additions & 0 deletions test/config-ui-first-run.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
import jitiFactory from 'jiti';

const jiti = jitiFactory(import.meta.url, { interopDefault: true });
const { getConfig, getUIConfig } = await jiti('../src/helpers/config.ts');

test('getUIConfig works on first run without OPENAI_KEY', async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'ai-shell-home-'));
const originalHome = process.env.HOME;

process.env.HOME = home;
try {
const config = await getUIConfig();
assert.equal(config.OPENAI_KEY, undefined);
assert.equal(config.MODEL, 'gpt-4o-mini');
assert.equal(config.OPENAI_API_ENDPOINT, 'https://api.openai.com/v1');
assert.equal(config.SILENT_MODE, false);
assert.equal(config.LANGUAGE, 'en');
} finally {
process.env.HOME = originalHome;
await fs.rm(home, { recursive: true, force: true });
}
});

test('getConfig still requires OPENAI_KEY for runtime flows', async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'ai-shell-home-'));
const originalHome = process.env.HOME;

process.env.HOME = home;
try {
await assert.rejects(getConfig(), /Please set your OpenAI API key/);
} finally {
process.env.HOME = originalHome;
await fs.rm(home, { recursive: true, force: true });
}
});