commit ab56ddfff069274c8d47b8e5af8acdc72b60c367 Author: warren Date: Sun Apr 5 18:28:24 2026 -0400 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8c1259 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ctask.exe +*.exe diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..2d9e607 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var version = "0.1.0" + +var rootCmd = &cobra.Command{ + Use: "ctask", + Short: "Create and manage AI-agent task workspaces", + Long: "ctask is a local CLI that creates and manages named AI-agent task workspaces so developers can start, resume, and organize work more safely and predictably.", +} + +func Execute() error { + return rootCmd.Execute() +} + +func init() { + rootCmd.Version = version + rootCmd.SetVersionTemplate(fmt.Sprintf("ctask v%s\n", version)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2fd1cfb --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/warrenronsiek/ctask + +go 1.26.1 + +require github.com/spf13/cobra v1.10.2 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6ee3e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..46a2942 --- /dev/null +++ b/internal/config/config.go @@ -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, + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..48b2cc8 --- /dev/null +++ b/internal/config/config_test.go @@ -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]) + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ba5c36f --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/warrenronsiek/ctask/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +}