Skip to content

Commit 44cf246

Browse files
committed
Golang acknoledge subfolder project
1 parent f3dbf27 commit 44cf246

10 files changed

Lines changed: 1075 additions & 74 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
.local/
33
build/*
44
.ti/
5-
.antigravity/
5+
.antigravity/
6+
.kiro/

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ require (
2727
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
2828
golang.org/x/sys v0.36.0 // indirect
2929
golang.org/x/text v0.8.0 // indirect
30+
pgregory.net/rapid v1.2.0 // indirect
3031
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
641641
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
642642
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
643643
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
644+
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
645+
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
644646
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
645647
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
646648
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

internal/dirtracker/dirtracker.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package dirtracker
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
// CodeBlockInfo represents a code block with its language tag.
10+
type CodeBlockInfo struct {
11+
Language string // e.g., "bash", "go", "python", ""
12+
Content string // The code block content (without fence markers)
13+
}
14+
15+
// DirCommand represents a parsed directory command.
16+
type DirCommand struct {
17+
Type string // "mkdir" or "cd"
18+
Path string // The directory argument
19+
MkdirP bool // True if mkdir had -p flag
20+
}
21+
22+
// DirectoryTracker computes effective working directories from
23+
// a sequence of code blocks in an AI response.
24+
type DirectoryTracker struct {
25+
workspaceRoot string
26+
}
27+
28+
// New creates a DirectoryTracker anchored to the given workspace root.
29+
// If workspaceRoot is empty, falls back to os.Getwd().
30+
func New(workspaceRoot string) *DirectoryTracker {
31+
if workspaceRoot == "" {
32+
wd, err := os.Getwd()
33+
if err == nil {
34+
workspaceRoot = wd
35+
}
36+
}
37+
return &DirectoryTracker{workspaceRoot: workspaceRoot}
38+
}
39+
40+
// ParseBashCommands extracts mkdir and cd commands from a single bash
41+
// code block's content. Returns the sequence of directory operations.
42+
func (dt *DirectoryTracker) ParseBashCommands(content string) []DirCommand {
43+
var commands []DirCommand
44+
45+
lines := strings.Split(content, "\n")
46+
for _, line := range lines {
47+
line = strings.TrimSpace(line)
48+
49+
// Skip empty lines and comment lines
50+
if line == "" || strings.HasPrefix(line, "#") {
51+
continue
52+
}
53+
54+
// Split on && to handle compound commands
55+
parts := strings.Split(line, "&&")
56+
for _, part := range parts {
57+
part = strings.TrimSpace(part)
58+
if part == "" {
59+
continue
60+
}
61+
62+
cmd := parseCommand(part)
63+
if cmd != nil {
64+
commands = append(commands, *cmd)
65+
}
66+
}
67+
}
68+
69+
return commands
70+
}
71+
72+
// parseCommand parses a single command string into a DirCommand, or nil if not recognized.
73+
func parseCommand(cmd string) *DirCommand {
74+
fields := strings.Fields(cmd)
75+
if len(fields) == 0 {
76+
return nil
77+
}
78+
79+
switch fields[0] {
80+
case "mkdir":
81+
return parseMkdir(fields[1:])
82+
case "cd":
83+
return parseCd(fields[1:])
84+
default:
85+
return nil
86+
}
87+
}
88+
89+
// parseMkdir parses mkdir arguments into a DirCommand.
90+
func parseMkdir(args []string) *DirCommand {
91+
if len(args) == 0 {
92+
return nil
93+
}
94+
95+
mkdirP := false
96+
var path string
97+
98+
for _, arg := range args {
99+
if arg == "-p" {
100+
mkdirP = true
101+
} else if !strings.HasPrefix(arg, "-") {
102+
path = arg
103+
break
104+
}
105+
}
106+
107+
if path == "" {
108+
return nil
109+
}
110+
111+
return &DirCommand{
112+
Type: "mkdir",
113+
Path: path,
114+
MkdirP: mkdirP,
115+
}
116+
}
117+
118+
// parseCd parses cd arguments into a DirCommand.
119+
func parseCd(args []string) *DirCommand {
120+
if len(args) == 0 {
121+
return nil
122+
}
123+
124+
path := args[0]
125+
if path == "" {
126+
return nil
127+
}
128+
129+
return &DirCommand{
130+
Type: "cd",
131+
Path: path,
132+
}
133+
}
134+
135+
// ComputeMappings analyzes the ordered code blocks and returns a
136+
// slice where result[i] is the effective working directory for block i.
137+
// The returned paths are always absolute.
138+
func (dt *DirectoryTracker) ComputeMappings(blocks []CodeBlockInfo) []string {
139+
if len(blocks) == 0 {
140+
return []string{}
141+
}
142+
143+
mappings := make([]string, len(blocks))
144+
currentDir := dt.workspaceRoot
145+
146+
for i, block := range blocks {
147+
if isBashBlock(block.Language) {
148+
commands := dt.ParseBashCommands(block.Content)
149+
for _, cmd := range commands {
150+
if cmd.Type == "cd" {
151+
currentDir = dt.resolveCD(currentDir, cmd.Path)
152+
}
153+
}
154+
}
155+
mappings[i] = currentDir
156+
}
157+
158+
return mappings
159+
}
160+
161+
// isBashBlock returns true if the language tag indicates a bash/shell block.
162+
func isBashBlock(lang string) bool {
163+
l := strings.ToLower(strings.TrimSpace(lang))
164+
return l == "bash" || l == "sh" || l == "shell"
165+
}
166+
167+
// resolveCD resolves a cd target path relative to the current directory,
168+
// clamped at the workspace root. Returns the new effective directory.
169+
func (dt *DirectoryTracker) resolveCD(currentDir, target string) string {
170+
// Ignore absolute paths
171+
if strings.HasPrefix(target, "/") {
172+
return currentDir
173+
}
174+
175+
// Ignore home directory references
176+
if target == "~" || strings.HasPrefix(target, "~/") {
177+
return currentDir
178+
}
179+
180+
// Ignore $HOME
181+
if target == "$HOME" || strings.HasPrefix(target, "$HOME/") {
182+
return currentDir
183+
}
184+
185+
// Ignore shell variable expansions
186+
if strings.HasPrefix(target, "$") {
187+
return currentDir
188+
}
189+
190+
// Resolve relative path
191+
resolved := filepath.Join(currentDir, target)
192+
resolved = filepath.Clean(resolved)
193+
194+
// Clamp at workspace root: ensure resolved path is within workspace
195+
if !strings.HasPrefix(resolved, dt.workspaceRoot) {
196+
return dt.workspaceRoot
197+
}
198+
199+
return resolved
200+
}

0 commit comments

Comments
 (0)