Skip to content

Commit 5f1c5f1

Browse files
committed
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.
1 parent 47d1f03 commit 5f1c5f1

4 files changed

Lines changed: 117 additions & 2 deletions

File tree

packages/cli/src/cli.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render } from "ink";
33
import { readFileSync } from "node:fs";
44
import { join } from "node:path";
55
import { homedir } from "node:os";
6-
import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core";
6+
import { setShellIfWindows, getProjectCode, ensureUserSettingsFile } from "@vegamo/deepcode-core";
77
import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check";
88
import { AppContainer } from "./ui";
99
import { parseArguments } from "./cli-args";
@@ -37,6 +37,22 @@ async function main(): Promise<void> {
3737
process.exit(1);
3838
}
3939

40+
// Scaffold the user settings file on first run so a fresh install has a
41+
// config file to edit instead of failing with "API key not found" and
42+
// forcing the user to create ~/.deepcode/settings.json by hand.
43+
try {
44+
const { path: settingsPath, created } = ensureUserSettingsFile();
45+
if (created) {
46+
writeStdoutLine(
47+
`Created a default settings file at ${settingsPath}.\n` +
48+
"Set your API_KEY there (and adjust BASE_URL / MODEL if needed) before running tasks.\n"
49+
);
50+
}
51+
} catch (error) {
52+
const message = error instanceof Error ? error.message : String(error);
53+
writeStderrLine(`deepcode: could not initialize settings file: ${message}\n`);
54+
}
55+
4056
// Validate --resume <sessionId> before entering TUI
4157
if (typeof resumeSessionId === "string") {
4258
const projectCode = getProjectCode(projectRoot);

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export {
1414
modelConfigKey,
1515
getUserSettingsPath,
1616
getProjectSettingsPath,
17+
buildDefaultSettings,
18+
ensureUserSettingsFile,
1719
DEFAULT_MODEL,
1820
DEFAULT_BASE_URL,
1921
} from "./settings";

packages/core/src/settings.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,36 @@ export function writeSettings(settings: DeepcodingSettings): void {
637637
writeSettingsFile(settingsPath, settings);
638638
}
639639

640+
/**
641+
* Default scaffold written to the user settings file on first run so that
642+
* a freshly installed deepcode has a config file to edit instead of forcing
643+
* the user to create one by hand (and guess the schema).
644+
*/
645+
export function buildDefaultSettings(): DeepcodingSettings {
646+
return {
647+
env: {
648+
API_KEY: "",
649+
BASE_URL: DEFAULT_BASE_URL,
650+
MODEL: DEFAULT_MODEL,
651+
},
652+
};
653+
}
654+
655+
/**
656+
* Ensure the user settings file exists, creating a template with placeholder
657+
* values when it is missing. Never overwrites an existing file.
658+
*
659+
* @returns the settings path and whether a new file was created.
660+
*/
661+
export function ensureUserSettingsFile(): { path: string; created: boolean } {
662+
const settingsPath = getUserSettingsPath();
663+
if (fs.existsSync(settingsPath)) {
664+
return { path: settingsPath, created: false };
665+
}
666+
writeSettingsFile(settingsPath, buildDefaultSettings());
667+
return { path: settingsPath, created: true };
668+
}
669+
640670
export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void {
641671
const settingsPath = getProjectSettingsPath(projectRoot);
642672
writeSettingsFile(settingsPath, settings);

packages/core/src/tests/settings-and-notify.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,83 @@
11
import { test } from "node:test";
22
import assert from "node:assert/strict";
3+
import * as fs from "node:fs";
4+
import * as os from "node:os";
5+
import * as path from "node:path";
36
import {
47
buildNotifyEnv,
58
formatDurationSeconds,
69
launchNotifyScript,
710
type NotifyContext,
811
type NotifySpawn,
912
} from "../common/notify";
10-
import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } from "../settings";
13+
import {
14+
applyModelConfigSelection,
15+
buildDefaultSettings,
16+
ensureUserSettingsFile,
17+
getUserSettingsPath,
18+
resolveSettings,
19+
resolveSettingsSources,
20+
} from "../settings";
1121

1222
const TEST_PROCESS_ENV = {};
1323

24+
function withTempHome<T>(fn: () => T): T {
25+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-home-"));
26+
const prevHome = process.env.HOME;
27+
const prevUserProfile = process.env.USERPROFILE;
28+
process.env.HOME = tempHome;
29+
process.env.USERPROFILE = tempHome;
30+
try {
31+
return fn();
32+
} finally {
33+
if (prevHome === undefined) {
34+
delete process.env.HOME;
35+
} else {
36+
process.env.HOME = prevHome;
37+
}
38+
if (prevUserProfile === undefined) {
39+
delete process.env.USERPROFILE;
40+
} else {
41+
process.env.USERPROFILE = prevUserProfile;
42+
}
43+
fs.rmSync(tempHome, { recursive: true, force: true });
44+
}
45+
}
46+
47+
test("ensureUserSettingsFile creates a template when settings file is missing", () => {
48+
withTempHome(() => {
49+
const settingsPath = getUserSettingsPath();
50+
assert.equal(fs.existsSync(settingsPath), false);
51+
52+
const result = ensureUserSettingsFile();
53+
54+
assert.equal(result.created, true);
55+
assert.equal(result.path, settingsPath);
56+
assert.equal(fs.existsSync(settingsPath), true);
57+
58+
const written = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
59+
const expected = buildDefaultSettings();
60+
assert.deepEqual(written, expected);
61+
assert.equal(expected.env?.API_KEY, "");
62+
assert.equal(typeof expected.env?.BASE_URL, "string");
63+
assert.equal(typeof expected.env?.MODEL, "string");
64+
});
65+
});
66+
67+
test("ensureUserSettingsFile never overwrites an existing settings file", () => {
68+
withTempHome(() => {
69+
const settingsPath = getUserSettingsPath();
70+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
71+
const existing = { env: { API_KEY: "sk-existing", BASE_URL: "https://custom.example.com" } };
72+
fs.writeFileSync(settingsPath, JSON.stringify(existing), "utf8");
73+
74+
const result = ensureUserSettingsFile();
75+
76+
assert.equal(result.created, false);
77+
assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf8")), existing);
78+
});
79+
});
80+
1481
test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => {
1582
const resolved = resolveSettings(
1683
{

0 commit comments

Comments
 (0)