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
18 changes: 17 additions & 1 deletion packages/cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -37,6 +37,22 @@ async function main(): Promise<void> {
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 <sessionId> before entering TUI
if (typeof resumeSessionId === "string") {
const projectCode = getProjectCode(projectRoot);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export {
modelConfigKey,
getUserSettingsPath,
getProjectSettingsPath,
buildDefaultSettings,
ensureUserSettingsFile,
DEFAULT_MODEL,
DEFAULT_BASE_URL,
} from "./settings";
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
69 changes: 68 additions & 1 deletion packages/core/src/tests/settings-and-notify.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,83 @@
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,
launchNotifyScript,
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<T>(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(
{
Expand Down