feat: project init with config package and root command

Go module, cobra root command, config resolution (CTASK_ROOT, CTASK_AGENT, EnvVars) with tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 18:28:24 -04:00
commit ab56ddfff0
7 changed files with 192 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
package config
import (
"os"
"path/filepath"
"strings"
)
// ResolveRoot returns the absolute workspace root path.
// Reads CTASK_ROOT env var, falls back to ~/ai-workspaces (or %USERPROFILE%\ai-workspaces on Windows).
func ResolveRoot() string {
root := os.Getenv("CTASK_ROOT")
if root == "" {
home, _ := os.UserHomeDir()
return filepath.Join(home, "ai-workspaces")
}
// Expand leading tilde
if strings.HasPrefix(root, "~/") || root == "~" {
home, _ := os.UserHomeDir()
root = filepath.Join(home, root[2:])
}
abs, err := filepath.Abs(root)
if err != nil {
return root
}
return abs
}
// ResolveAgent returns the agent command.
// Reads CTASK_AGENT env var, falls back to "claude".
func ResolveAgent() string {
agent := os.Getenv("CTASK_AGENT")
if agent == "" {
return "claude"
}
return agent
}
// EnvVars returns the environment variables to export into child sessions.
func EnvVars(slug, mode, root, workspace, category string) map[string]string {
return map[string]string{
"CTASK_TASK": slug,
"CTASK_MODE": mode,
"CTASK_ROOT": root,
"CTASK_WORKSPACE": workspace,
"CTASK_CATEGORY": category,
}
}
+85
View File
@@ -0,0 +1,85 @@
package config
import (
"os"
"path/filepath"
"runtime"
"testing"
)
func TestDefaultRoot(t *testing.T) {
os.Unsetenv("CTASK_ROOT")
root := ResolveRoot()
home, _ := os.UserHomeDir()
expected := filepath.Join(home, "ai-workspaces")
if root != expected {
t.Errorf("expected %q, got %q", expected, root)
}
}
func TestCustomRoot(t *testing.T) {
dir := t.TempDir()
os.Setenv("CTASK_ROOT", dir)
defer os.Unsetenv("CTASK_ROOT")
root := ResolveRoot()
if root != dir {
t.Errorf("expected %q, got %q", dir, root)
}
}
func TestRootResolvesRelative(t *testing.T) {
os.Setenv("CTASK_ROOT", "relative/path")
defer os.Unsetenv("CTASK_ROOT")
root := ResolveRoot()
if !filepath.IsAbs(root) {
t.Errorf("expected absolute path, got %q", root)
}
}
func TestDefaultAgent(t *testing.T) {
os.Unsetenv("CTASK_AGENT")
agent := ResolveAgent()
if agent != "claude" {
t.Errorf("expected \"claude\", got %q", agent)
}
}
func TestCustomAgent(t *testing.T) {
os.Setenv("CTASK_AGENT", "aider")
defer os.Unsetenv("CTASK_AGENT")
agent := ResolveAgent()
if agent != "aider" {
t.Errorf("expected \"aider\", got %q", agent)
}
}
func TestRootResolvesTilde(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("tilde expansion not typical on Windows")
}
os.Setenv("CTASK_ROOT", "~/my-workspaces")
defer os.Unsetenv("CTASK_ROOT")
root := ResolveRoot()
if root[0] == '~' {
t.Errorf("tilde not expanded: %q", root)
}
if !filepath.IsAbs(root) {
t.Errorf("expected absolute path, got %q", root)
}
}
func TestEnvVars(t *testing.T) {
vars := EnvVars("my-slug", "local", "/abs/root", "/abs/root/cat/ws", "general")
expected := map[string]string{
"CTASK_TASK": "my-slug",
"CTASK_MODE": "local",
"CTASK_ROOT": "/abs/root",
"CTASK_WORKSPACE": "/abs/root/cat/ws",
"CTASK_CATEGORY": "general",
}
for k, v := range expected {
if vars[k] != v {
t.Errorf("env %s: expected %q, got %q", k, v, vars[k])
}
}
}