Skip to content

Commit acc6dfe

Browse files
committed
feat: implement initial project structure and Ollama-based codebase documentation engine
0 parents  commit acc6dfe

16 files changed

Lines changed: 1618 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.code-reducer.lock

‎cmd/init.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
var initCmd = &cobra.Command{
8+
Use: "init [message]",
9+
Short: "Initialize project documentation",
10+
Long: `Scan the repository and build the initial set of wiki markdown pages.`,
11+
Args: cobra.MaximumNArgs(1),
12+
Run: func(cmd *cobra.Command, args []string) {
13+
userMessage := ""
14+
if len(args) > 0 {
15+
userMessage = args[0]
16+
}
17+
executeCommand("init", userMessage)
18+
},
19+
}
20+
21+
func init() {
22+
RootCmd.AddCommand(initCmd)
23+
}

‎cmd/root.go‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/signal"
9+
"strconv"
10+
"strings"
11+
"syscall"
12+
13+
"github.com/mattn/go-isatty"
14+
"github.com/spf13/cobra"
15+
16+
"github.com/arrase/code-reducer/internal/config"
17+
"github.com/arrase/code-reducer/internal/engine"
18+
"github.com/arrase/code-reducer/internal/security"
19+
)
20+
21+
var (
22+
modelIdFlag string
23+
numCtxFlag string
24+
printFlag bool
25+
initFlag bool
26+
updateFlag bool
27+
dryRunFlag bool
28+
)
29+
30+
var RootCmd = &cobra.Command{
31+
Use: "code-reducer [message]",
32+
Short: "Code-Reducer is a documentation agent that writes and maintains a project wiki.",
33+
Long: `A pure Go port of Code-Reducer CLI, optimized for performance and local LLM execution.`,
34+
Args: cobra.MaximumNArgs(1),
35+
Run: func(cmd *cobra.Command, args []string) {
36+
// Load existing environment variables
37+
_, _ = config.LoadEnv()
38+
39+
userMessage := ""
40+
if len(args) > 0 {
41+
userMessage = args[0]
42+
}
43+
44+
// Determine command mode
45+
commandMode := "update"
46+
if initFlag {
47+
commandMode = "init"
48+
}
49+
50+
executeCommand(commandMode, userMessage)
51+
},
52+
}
53+
54+
func init() {
55+
RootCmd.PersistentFlags().StringVar(&modelIdFlag, "model-id", "", "Specify LLM model ID")
56+
RootCmd.PersistentFlags().StringVar(&modelIdFlag, "modelId", "", "Specify LLM model ID (alias)")
57+
RootCmd.PersistentFlags().StringVar(&numCtxFlag, "num-ctx", "", "Specify Ollama context window size")
58+
RootCmd.PersistentFlags().BoolVarP(&printFlag, "print", "p", false, "Run once and print the final assistant output (non-interactive)")
59+
RootCmd.PersistentFlags().BoolVar(&initFlag, "init", false, "Initialize project documentation")
60+
RootCmd.PersistentFlags().BoolVar(&updateFlag, "update", false, "Update existing project documentation")
61+
RootCmd.PersistentFlags().BoolVar(&dryRunFlag, "dry-run", false, "Show plan without invoking agent")
62+
}
63+
64+
func executeCommand(command string, userMessage string) {
65+
repoRoot, err := os.Getwd()
66+
if err != nil {
67+
fmt.Printf("Error getting current working directory: %v\n", err)
68+
os.Exit(1)
69+
}
70+
71+
// Apply flag overrides to environment variables
72+
if modelIdFlag != "" {
73+
_ = os.Setenv(config.CodeReducerModelIdEnvKey, modelIdFlag)
74+
}
75+
if numCtxFlag != "" {
76+
_ = os.Setenv(config.OllamaNumCtxEnvKey, numCtxFlag)
77+
}
78+
79+
needsSetup := NeedsCredentialSetup()
80+
isTTY := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
81+
82+
if needsSetup {
83+
if !isTTY {
84+
fmt.Println("Error: Credentials/Model ID not configured. Please set the model ID using --model-id or the CODE_REDUCER_MODEL_ID environment variable.")
85+
os.Exit(1)
86+
}
87+
88+
// Interactive CLI onboarding
89+
reader := bufio.NewReader(os.Stdin)
90+
fmt.Println("Welcome to Code-Reducer CLI Setup")
91+
fmt.Println("---------------------------------")
92+
93+
fmt.Print("Enter LLM Model ID (e.g. gemma4:26b-a4b-it-qat): ")
94+
modelInput, _ := reader.ReadString('\n')
95+
modelInput = strings.TrimSpace(modelInput)
96+
if modelInput == "" {
97+
modelInput = "gemma4:26b-a4b-it-qat"
98+
}
99+
100+
fmt.Printf("Enter Ollama Base URL [%s]: ", config.OllamaDefaultBaseURL)
101+
urlInput, _ := reader.ReadString('\n')
102+
urlInput = strings.TrimSpace(urlInput)
103+
if urlInput == "" {
104+
urlInput = config.OllamaDefaultBaseURL
105+
}
106+
107+
fmt.Printf("Enter Ollama Context Size [%d]: ", config.OllamaDefaultNumCtx)
108+
ctxInput, _ := reader.ReadString('\n')
109+
ctxInput = strings.TrimSpace(ctxInput)
110+
if ctxInput == "" {
111+
ctxInput = strconv.Itoa(config.OllamaDefaultNumCtx)
112+
}
113+
114+
updates := map[string]string{
115+
config.CodeReducerModelIdEnvKey: modelInput,
116+
config.OllamaBaseUrlEnvKey: urlInput,
117+
config.OllamaNumCtxEnvKey: ctxInput,
118+
}
119+
120+
err = config.SaveEnv(updates)
121+
if err != nil {
122+
fmt.Printf("Error saving configuration: %v\n", err)
123+
os.Exit(1)
124+
}
125+
fmt.Println("Configuration successfully saved to local .env file.")
126+
fmt.Println()
127+
}
128+
129+
if dryRunFlag {
130+
fmt.Println("Dry-run mode: configuration is verified, but agent was not executed.")
131+
return
132+
}
133+
134+
// Acquire exclusive repository lock
135+
lock, err := security.AcquireLock(repoRoot, true)
136+
if err != nil {
137+
fmt.Printf("Error acquiring repository lock: %v\n", err)
138+
os.Exit(1)
139+
}
140+
defer lock.Unlock()
141+
142+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
143+
defer stop()
144+
145+
modelId := os.Getenv(config.CodeReducerModelIdEnvKey)
146+
baseURL := os.Getenv(config.OllamaBaseUrlEnvKey)
147+
numCtx := config.GetOllamaContextSize(numCtxFlag)
148+
149+
client := engine.NewLLMClient(modelId, baseURL, numCtx)
150+
151+
err = client.RunInitOrUpdate(ctx, command, repoRoot, userMessage, func(ev engine.Event) {
152+
if ev.Type == "status" {
153+
fmt.Println(ev.Message)
154+
} else if ev.Type == "error" {
155+
fmt.Fprintf(os.Stderr, "Error: %s\n", ev.Message)
156+
} else {
157+
fmt.Println(ev.Message)
158+
}
159+
})
160+
161+
if err != nil {
162+
fmt.Printf("Documentation Run Failed: %v\n", err)
163+
os.Exit(1)
164+
}
165+
}
166+
167+
// NeedsCredentialSetup checks if critical configuration is missing
168+
func NeedsCredentialSetup() bool {
169+
return os.Getenv(config.CodeReducerModelIdEnvKey) == ""
170+
}
171+

‎cmd/update.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
var updateCmd = &cobra.Command{
8+
Use: "update [message]",
9+
Short: "Update existing project documentation",
10+
Long: `Scan git changes and selectively update stale markdown pages.`,
11+
Args: cobra.MaximumNArgs(1),
12+
Run: func(cmd *cobra.Command, args []string) {
13+
userMessage := ""
14+
if len(args) > 0 {
15+
userMessage = args[0]
16+
}
17+
executeCommand("update", userMessage)
18+
},
19+
}
20+
21+
func init() {
22+
RootCmd.AddCommand(updateCmd)
23+
}

‎go.mod‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/arrase/code-reducer
2+
3+
go 1.26
4+
5+
require (
6+
github.com/gofrs/flock v0.13.0
7+
github.com/mattn/go-isatty v0.0.22
8+
github.com/spf13/cobra v1.10.2
9+
)
10+
11+
require (
12+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
13+
github.com/spf13/pflag v1.0.9 // indirect
14+
golang.org/x/sys v0.44.0 // indirect
15+
)

‎go.sum‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
5+
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
6+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
7+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
8+
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
9+
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
10+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
13+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
14+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
15+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
16+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
17+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
18+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
19+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
20+
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
21+
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
22+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)