diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 552e4a23..36009399 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -2,7 +2,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 { ensureUserSettingsFile, getProjectCode, setShellIfWindows } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check"; import { AppContainer } from "./ui"; import { parseArguments } from "./cli-args"; @@ -28,6 +28,13 @@ async function main(): Promise { // If called before argument parsing, --help and --version would fail on those machines. configureWindowsShell(); + // First run: scaffold a ready-to-edit settings.json so the user only has to + // paste their API key instead of authoring the file by hand. (#202) + const scaffold = ensureUserSettingsFile(); + if (scaffold.created) { + writeStderrLine(`Created ${scaffold.path} — edit it to add your API key.\n`); + } + let initialPrompt = parsed.prompt; let resumeSessionId = parsed.resume; let forkSessionId = parsed.fork; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d389348c..3c5e0d13 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,6 +16,7 @@ export { getProjectSettingsPath, getDefaultContextWindow, getDefaultAutoCompactWindow, + ensureUserSettingsFile, DEFAULT_MODEL, DEFAULT_BASE_URL, } from "./settings"; diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index da0e2d94..48c3330d 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -683,6 +683,34 @@ export function readProjectSettings(projectRoot: string = process.cwd()): Deepco return readSettingsFile(getProjectSettingsPath(projectRoot)); } +export const DEFAULT_SETTINGS_TEMPLATE: DeepcodingSettings = { + env: { + API_KEY: "", + BASE_URL: DEFAULT_BASE_URL, + MODEL: DEFAULT_MODEL, + }, +}; + +/** + * Create the user settings file on first run with a ready-to-edit template so + * the user only has to paste their API key instead of authoring the file and + * guessing the schema by hand. Never overwrites an existing file. (#202) + */ +export function ensureUserSettingsFile(settingsPath: string = getUserSettingsPath()): { + created: boolean; + path: string; +} { + if (fs.existsSync(settingsPath)) { + return { created: false, path: settingsPath }; + } + try { + writeSettingsFile(settingsPath, DEFAULT_SETTINGS_TEMPLATE); + return { created: true, path: settingsPath }; + } catch { + return { created: false, path: settingsPath }; + } +} + function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); diff --git a/packages/core/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts index 93e8dc29..2c9620d6 100644 --- a/packages/core/src/tests/settings-and-notify.test.ts +++ b/packages/core/src/tests/settings-and-notify.test.ts @@ -8,9 +8,32 @@ import { type NotifySpawn, } from "../common/notify"; import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } from "../settings"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { DEFAULT_SETTINGS_TEMPLATE, ensureUserSettingsFile, readSettingsFile } from "../settings"; const TEST_PROCESS_ENV = {}; +test("ensureUserSettingsFile scaffolds a template and never overwrites an existing file", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-settings-test-")); + try { + const settingsPath = path.join(dir, "settings.json"); + + const first = ensureUserSettingsFile(settingsPath); + assert.equal(first.created, true); + assert.equal(first.path, settingsPath); + assert.deepEqual(readSettingsFile(settingsPath), DEFAULT_SETTINGS_TEMPLATE); + + fs.writeFileSync(settingsPath, JSON.stringify({ model: "custom-model" }), "utf8"); + const second = ensureUserSettingsFile(settingsPath); + assert.equal(second.created, false); + assert.deepEqual(readSettingsFile(settingsPath), { model: "custom-model" }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => { const resolved = resolveSettings( {