From 5f1c5f1012d96fe16bd4e07408a6538dcc599d82 Mon Sep 17 00:00:00 2001 From: cocoon <1569339843@qq.com> Date: Fri, 26 Jun 2026 19:31:37 +0800 Subject: [PATCH] feat(settings): scaffold default settings.json on first run A fresh install has no ~/.deepcode/settings.json, so the first task fails with "API key not found" and the user must create the file by hand while guessing the schema. Add ensureUserSettingsFile(), which writes a template settings file with an empty API_KEY and default BASE_URL/MODEL when none exists, and never overwrites an existing file. The CLI calls it on startup (after the TTY check) and tells the user where to set their key. Covered by unit tests for both the create and the never-overwrite paths. --- packages/cli/src/cli.tsx | 18 ++++- packages/core/src/index.ts | 2 + packages/core/src/settings.ts | 30 ++++++++ .../src/tests/settings-and-notify.test.ts | 69 ++++++++++++++++++- 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 80b11f08..4fa94e25 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -3,7 +3,7 @@ import { render } from "ink"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; +import { setShellIfWindows, getProjectCode, ensureUserSettingsFile } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check"; import { AppContainer } from "./ui"; import { parseArguments } from "./cli-args"; @@ -37,6 +37,22 @@ async function main(): Promise { process.exit(1); } + // Scaffold the user settings file on first run so a fresh install has a + // config file to edit instead of failing with "API key not found" and + // forcing the user to create ~/.deepcode/settings.json by hand. + try { + const { path: settingsPath, created } = ensureUserSettingsFile(); + if (created) { + writeStdoutLine( + `Created a default settings file at ${settingsPath}.\n` + + "Set your API_KEY there (and adjust BASE_URL / MODEL if needed) before running tasks.\n" + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + writeStderrLine(`deepcode: could not initialize settings file: ${message}\n`); + } + // Validate --resume before entering TUI if (typeof resumeSessionId === "string") { const projectCode = getProjectCode(projectRoot); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 832d2444..acb6e573 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,6 +14,8 @@ export { modelConfigKey, getUserSettingsPath, getProjectSettingsPath, + buildDefaultSettings, + ensureUserSettingsFile, DEFAULT_MODEL, DEFAULT_BASE_URL, } from "./settings"; diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index 5dab3b5a..7e185f83 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -637,6 +637,36 @@ export function writeSettings(settings: DeepcodingSettings): void { writeSettingsFile(settingsPath, settings); } +/** + * Default scaffold written to the user settings file on first run so that + * a freshly installed deepcode has a config file to edit instead of forcing + * the user to create one by hand (and guess the schema). + */ +export function buildDefaultSettings(): DeepcodingSettings { + return { + env: { + API_KEY: "", + BASE_URL: DEFAULT_BASE_URL, + MODEL: DEFAULT_MODEL, + }, + }; +} + +/** + * Ensure the user settings file exists, creating a template with placeholder + * values when it is missing. Never overwrites an existing file. + * + * @returns the settings path and whether a new file was created. + */ +export function ensureUserSettingsFile(): { path: string; created: boolean } { + const settingsPath = getUserSettingsPath(); + if (fs.existsSync(settingsPath)) { + return { path: settingsPath, created: false }; + } + writeSettingsFile(settingsPath, buildDefaultSettings()); + return { path: settingsPath, created: true }; +} + export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { const settingsPath = getProjectSettingsPath(projectRoot); writeSettingsFile(settingsPath, settings); diff --git a/packages/core/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts index ceddc43e..7552a18e 100644 --- a/packages/core/src/tests/settings-and-notify.test.ts +++ b/packages/core/src/tests/settings-and-notify.test.ts @@ -1,5 +1,8 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; import { buildNotifyEnv, formatDurationSeconds, @@ -7,10 +10,74 @@ import { type NotifyContext, type NotifySpawn, } from "../common/notify"; -import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } from "../settings"; +import { + applyModelConfigSelection, + buildDefaultSettings, + ensureUserSettingsFile, + getUserSettingsPath, + resolveSettings, + resolveSettingsSources, +} from "../settings"; const TEST_PROCESS_ENV = {}; +function withTempHome(fn: () => T): T { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-home-")); + const prevHome = process.env.HOME; + const prevUserProfile = process.env.USERPROFILE; + process.env.HOME = tempHome; + process.env.USERPROFILE = tempHome; + try { + return fn(); + } finally { + if (prevHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = prevHome; + } + if (prevUserProfile === undefined) { + delete process.env.USERPROFILE; + } else { + process.env.USERPROFILE = prevUserProfile; + } + fs.rmSync(tempHome, { recursive: true, force: true }); + } +} + +test("ensureUserSettingsFile creates a template when settings file is missing", () => { + withTempHome(() => { + const settingsPath = getUserSettingsPath(); + assert.equal(fs.existsSync(settingsPath), false); + + const result = ensureUserSettingsFile(); + + assert.equal(result.created, true); + assert.equal(result.path, settingsPath); + assert.equal(fs.existsSync(settingsPath), true); + + const written = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + const expected = buildDefaultSettings(); + assert.deepEqual(written, expected); + assert.equal(expected.env?.API_KEY, ""); + assert.equal(typeof expected.env?.BASE_URL, "string"); + assert.equal(typeof expected.env?.MODEL, "string"); + }); +}); + +test("ensureUserSettingsFile never overwrites an existing settings file", () => { + withTempHome(() => { + const settingsPath = getUserSettingsPath(); + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + const existing = { env: { API_KEY: "sk-existing", BASE_URL: "https://custom.example.com" } }; + fs.writeFileSync(settingsPath, JSON.stringify(existing), "utf8"); + + const result = ensureUserSettingsFile(); + + assert.equal(result.created, false); + assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf8")), existing); + }); +}); + test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => { const resolved = resolveSettings( {