From 0dcbe9ff26c7a48c1af16bed05f63d9625af4842 Mon Sep 17 00:00:00 2001 From: duyua9 Date: Thu, 23 Apr 2026 02:26:30 +0800 Subject: [PATCH] fix: allow config UI before OPENAI_KEY is set --- src/commands/config.ts | 3 ++- src/helpers/config.ts | 30 +++++++++++++++++++---- test/config-ui-first-run.test.mjs | 40 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 test/config-ui-first-run.test.mjs diff --git a/src/commands/config.ts b/src/commands/config.ts index 2ae38151..a13a0ceb 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -3,6 +3,7 @@ import { red } from 'kolorist'; import { hasOwn, getConfig, + getUIConfig, setConfigs, showConfigUI, } from '../helpers/config.js'; @@ -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]}`); diff --git a/src/helpers/config.ts b/src/helpers/config.ts index cf3549af..3c66dc63 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -66,6 +66,10 @@ type ValidConfig = { [Key in ConfigKeys]: ReturnType<(typeof configParsers)[Key]>; }; +type UIConfig = Omit & { + OPENAI_KEY?: string; +}; + const configPath = path.join(os.homedir(), '.ai-shell'); const fileExists = (filePath: string) => @@ -84,21 +88,37 @@ const readConfigFile = async (): Promise => { return ini.parse(configString); }; -export const getConfig = async ( - cliConfig?: RawConfig -): Promise => { +const parseConfig = async ( + cliConfig: RawConfig | undefined, + options: { requireOpenAIKey: boolean } +): Promise => { const config = await readConfigFile(); const parsedConfig: Record = {}; 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 => parseConfig(cliConfig, { + requireOpenAIKey: true, +}); + +export const getUIConfig = async (cliConfig?: RawConfig): Promise => + parseConfig(cliConfig, { requireOpenAIKey: false }); + export const setConfigs = async (keyValues: [key: string, value: string][]) => { const config = await readConfigFile(); @@ -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: [ diff --git a/test/config-ui-first-run.test.mjs b/test/config-ui-first-run.test.mjs new file mode 100644 index 00000000..f6073950 --- /dev/null +++ b/test/config-ui-first-run.test.mjs @@ -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 }); + } +});