Skip to content

Commit b20b3db

Browse files
committed
v0.5.5: kode init command
New subcommand to bootstrap config files: kode init Create ./kode.json (local, default) kode init --global / -g Create ~/kode/config.json (global) kode init --force / -f Overwrite existing file Creates a full template with all fields and sensible defaults. Prints field descriptions and priority chain on creation. 6 new tests covering local, global, exists, force, short flags, error paths.
1 parent 730ebd0 commit b20b3db

2 files changed

Lines changed: 227 additions & 1 deletion

File tree

cmd/kode/main.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"os/signal"
9+
"path/filepath"
910
"runtime/debug"
1011
"strings"
1112

@@ -54,6 +55,11 @@ func main() {
5455
}
5556
case "version":
5657
fmt.Println("kode", getVersion())
58+
case "init":
59+
if err := initConfig(os.Args[2:]); err != nil {
60+
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
61+
os.Exit(1)
62+
}
5763
default:
5864
fmt.Fprintf(os.Stderr, "kode: unknown command %q\n", os.Args[1])
5965
printUsage()
@@ -127,9 +133,19 @@ done:
127133
func printUsage() {
128134
fmt.Println(`Usage:
129135
kode run [flags] <task>
136+
kode init [--global | -g] [--force | -f]
130137
kode version
131138
132-
Flags:
139+
Commands:
140+
run Execute a task with the agent loop
141+
init Create a config file (default: ./kode.json)
142+
version Print version and exit
143+
144+
Init flags:
145+
--global, -g Create global config at ~/kode/config.json
146+
--force, -f Overwrite existing file without prompting
147+
148+
Run flags:
133149
--model <name> LLM model (default: deepseek-chat)
134150
Known profiles: deepseek-v4-flash, deepseek-v4-pro
135151
Profiles auto-set thinking/timeout defaults.
@@ -161,6 +177,81 @@ Environment variables:
161177
KODE_SYSTEM System prompt override`)
162178
}
163179

180+
// ── Init ──────────────────────────────────────────────────────────────
181+
182+
const defaultConfigTemplate = `{
183+
"model": "deepseek-v4-flash",
184+
"base_url": "https://api.deepseek.com/v1",
185+
"api_key": "${DEEPSEEK_API_KEY}",
186+
"thinking": "",
187+
"max_iterations": 90,
188+
"sandbox": false,
189+
"no_color": false,
190+
"no_agents": false,
191+
"system": ""
192+
}`
193+
194+
func initConfig(args []string) error {
195+
global := false
196+
force := false
197+
198+
for i := 0; i < len(args); i++ {
199+
switch args[i] {
200+
case "--global", "-g":
201+
global = true
202+
case "--force", "-f":
203+
force = true
204+
default:
205+
return fmt.Errorf("unknown flag %q for init", args[i])
206+
}
207+
}
208+
209+
var configPath string
210+
var scope string
211+
if global {
212+
configPath = config.GlobalConfigPath()
213+
scope = "global"
214+
} else {
215+
configPath = config.ProjectConfigPath()
216+
scope = "local"
217+
}
218+
219+
// Check if file already exists
220+
if _, err := os.Stat(configPath); err == nil && !force {
221+
fmt.Fprintf(os.Stderr, "kode: %s config already exists at %s\n", scope, configPath)
222+
fmt.Fprintf(os.Stderr, " Use --force to overwrite.\n")
223+
return nil
224+
}
225+
226+
// Create parent directory (os.MkdirAll on "." is a no-op — fine for local)
227+
dir := filepath.Dir(configPath)
228+
if err := os.MkdirAll(dir, 0755); err != nil {
229+
return fmt.Errorf("create directory %s: %w", dir, err)
230+
}
231+
232+
if err := os.WriteFile(configPath, []byte(defaultConfigTemplate+"\n"), 0644); err != nil {
233+
return fmt.Errorf("write config: %w", err)
234+
}
235+
236+
fmt.Printf("✓ Created %s config: %s\n", scope, configPath)
237+
fmt.Println()
238+
fmt.Println(" Edit this file to set your preferences. Available fields:")
239+
fmt.Println(" model LLM model name")
240+
fmt.Println(" base_url API endpoint URL")
241+
fmt.Println(" api_key API key (supports ${VAR} substitution)")
242+
fmt.Println(" thinking Reasoning depth (enabled/disabled/low/medium/high)")
243+
fmt.Println(" max_iterations Max think->act cycles")
244+
fmt.Println(" sandbox Run in Docker sandbox (true/false)")
245+
fmt.Println(" no_color Disable colored output (true/false)")
246+
fmt.Println(" no_agents Skip AGENTS.md (true/false)")
247+
fmt.Println(" system System prompt override")
248+
fmt.Println()
249+
fmt.Println(" Priority: config file < KODE_* env < CLI flags")
250+
return nil
251+
}
252+
253+
// ── Run ───────────────────────────────────────────────────────────────
254+
164255
// run executes the `kode run` command and returns an error on failure.
165256
// The caller is responsible for printing the error and calling os.Exit.
166257
func run(args []string) error {

cmd/kode/main_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ func TestPrintUsage(t *testing.T) {
196196

197197
required := []string{
198198
"kode run",
199+
"kode init",
199200
"kode version",
201+
"Commands:",
200202
"--model",
201203
"Known profiles",
202204
"deepseek-v4-flash",
@@ -209,6 +211,8 @@ func TestPrintUsage(t *testing.T) {
209211
"--no-color",
210212
"--no-agents",
211213
"--system",
214+
"--global",
215+
"--force",
212216
"~/kode/config.json",
213217
"KODE_MODEL",
214218
"KODE_API_KEY",
@@ -621,3 +625,134 @@ func dockerAvailable() bool {
621625
_, err := exec.LookPath("docker")
622626
return err == nil
623627
}
628+
629+
// ── Init Config Tests ─────────────────────────────────────────────────
630+
631+
func TestInitConfig_Local(t *testing.T) {
632+
dir := t.TempDir()
633+
cwd, _ := os.Getwd()
634+
os.Chdir(dir)
635+
defer os.Chdir(cwd)
636+
637+
origHome := os.Getenv("HOME")
638+
os.Setenv("HOME", t.TempDir()) // isolate from any global config
639+
defer os.Setenv("HOME", origHome)
640+
641+
if err := initConfig([]string{}); err != nil {
642+
t.Fatalf("initConfig() error: %v", err)
643+
}
644+
645+
// Verify the file was created
646+
data, err := os.ReadFile("kode.json")
647+
if err != nil {
648+
t.Fatalf("kode.json not created: %v", err)
649+
}
650+
content := string(data)
651+
if !strings.Contains(content, "deepseek-v4-flash") {
652+
t.Errorf("config should contain deepseek-v4-flash, got: %s", content)
653+
}
654+
if !strings.Contains(content, "api_key") {
655+
t.Errorf("config should contain api_key field, got: %s", content)
656+
}
657+
}
658+
659+
func TestInitConfig_Global(t *testing.T) {
660+
dir := t.TempDir()
661+
origHome := os.Getenv("HOME")
662+
os.Setenv("HOME", dir)
663+
defer os.Setenv("HOME", origHome)
664+
665+
if err := initConfig([]string{"--global"}); err != nil {
666+
t.Fatalf("initConfig() --global error: %v", err)
667+
}
668+
669+
// Verify the file was created
670+
data, err := os.ReadFile(dir + "/kode/config.json")
671+
if err != nil {
672+
t.Fatalf("global config not created: %v", err)
673+
}
674+
content := string(data)
675+
if !strings.Contains(content, "deepseek-v4-flash") {
676+
t.Errorf("config should contain deepseek-v4-flash, got: %s", content)
677+
}
678+
}
679+
680+
func TestInitConfig_LocalExists(t *testing.T) {
681+
dir := t.TempDir()
682+
cwd, _ := os.Getwd()
683+
os.Chdir(dir)
684+
defer os.Chdir(cwd)
685+
686+
origHome := os.Getenv("HOME")
687+
os.Setenv("HOME", t.TempDir())
688+
defer os.Setenv("HOME", origHome)
689+
690+
// Create existing config
691+
os.WriteFile("kode.json", []byte(`{"model": "existing"}`), 0644)
692+
693+
// Should warn, not overwrite
694+
if err := initConfig([]string{}); err != nil {
695+
t.Fatalf("initConfig() error: %v", err)
696+
}
697+
698+
// Content should be unchanged
699+
data, _ := os.ReadFile("kode.json")
700+
if !strings.Contains(string(data), "existing") {
701+
t.Error("config should not have been overwritten")
702+
}
703+
}
704+
705+
func TestInitConfig_LocalForce(t *testing.T) {
706+
dir := t.TempDir()
707+
cwd, _ := os.Getwd()
708+
os.Chdir(dir)
709+
defer os.Chdir(cwd)
710+
711+
origHome := os.Getenv("HOME")
712+
os.Setenv("HOME", t.TempDir())
713+
defer os.Setenv("HOME", origHome)
714+
715+
// Create existing config
716+
os.WriteFile("kode.json", []byte(`{"model": "old"}`), 0644)
717+
718+
// Force overwrite
719+
if err := initConfig([]string{"--force"}); err != nil {
720+
t.Fatalf("initConfig() --force error: %v", err)
721+
}
722+
723+
// Content should be the template
724+
data, _ := os.ReadFile("kode.json")
725+
if strings.Contains(string(data), "old") {
726+
t.Error("config should have been overwritten")
727+
}
728+
if !strings.Contains(string(data), "deepseek-v4-flash") {
729+
t.Errorf("config should contain template, got: %s", string(data))
730+
}
731+
}
732+
733+
func TestInitConfig_UnknownFlag(t *testing.T) {
734+
err := initConfig([]string{"--unknown"})
735+
if err == nil {
736+
t.Fatal("expected error for unknown flag")
737+
}
738+
if !strings.Contains(err.Error(), "unknown flag") {
739+
t.Errorf("error should mention unknown flag, got: %v", err)
740+
}
741+
}
742+
743+
func TestInitConfig_ShortFlags(t *testing.T) {
744+
// Test -g and -f short flags
745+
dir := t.TempDir()
746+
origHome := os.Getenv("HOME")
747+
os.Setenv("HOME", dir)
748+
defer os.Setenv("HOME", origHome)
749+
750+
if err := initConfig([]string{"-g", "-f"}); err != nil {
751+
t.Fatalf("initConfig() -g -f error: %v", err)
752+
}
753+
754+
// Verify
755+
if _, err := os.Stat(dir + "/kode/config.json"); err != nil {
756+
t.Errorf("global config should exist after -g -f: %v", err)
757+
}
758+
}

0 commit comments

Comments
 (0)