f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
291 lines
7.0 KiB
Go
291 lines
7.0 KiB
Go
package server
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// resolveCursorProjectDirFromSessionFile derives the real workspace
|
|
// directory for a Cursor session from the stored transcript path.
|
|
// The bool reports whether multiple matching paths exist on disk.
|
|
func resolveCursorProjectDirFromSessionFile(
|
|
filePath string,
|
|
) (string, bool) {
|
|
projectDir := cursorProjectDirNameFromTranscriptPath(filePath)
|
|
if projectDir == "" {
|
|
return "", false
|
|
}
|
|
return resolveCursorProjectDirName(projectDir)
|
|
}
|
|
|
|
// resolveCursorProjectDirFromSessionFileHint derives the real workspace
|
|
// directory for a Cursor session from the stored transcript path,
|
|
// preferring candidates that contain the provided hint.
|
|
func resolveCursorProjectDirFromSessionFileHint(
|
|
filePath, hint string,
|
|
) string {
|
|
projectDir := cursorProjectDirNameFromTranscriptPath(filePath)
|
|
if projectDir == "" {
|
|
return ""
|
|
}
|
|
return resolveCursorProjectDirNameHint(projectDir, hint)
|
|
}
|
|
|
|
// cursorProjectDirNameFromTranscriptPath extracts the encoded Cursor
|
|
// project directory name from either flat or nested transcript paths.
|
|
func cursorProjectDirNameFromTranscriptPath(path string) string {
|
|
path = filepath.Clean(path)
|
|
dir := filepath.Dir(path)
|
|
for {
|
|
base := filepath.Base(dir)
|
|
if base == "." || base == string(filepath.Separator) {
|
|
return ""
|
|
}
|
|
if base == "agent-transcripts" {
|
|
parent := filepath.Dir(dir)
|
|
name := filepath.Base(parent)
|
|
if name == "." || name == string(filepath.Separator) {
|
|
return ""
|
|
}
|
|
return name
|
|
}
|
|
next := filepath.Dir(dir)
|
|
if next == dir {
|
|
return ""
|
|
}
|
|
dir = next
|
|
}
|
|
}
|
|
|
|
// resolveCursorProjectDirName derives a real workspace path from a
|
|
// Cursor-encoded directory name. The bool reports whether more than
|
|
// one matching path exists on disk.
|
|
func resolveCursorProjectDirName(dirName string) (string, bool) {
|
|
matches := resolveCursorProjectDirNameMatches(dirName, "", 2)
|
|
switch len(matches) {
|
|
case 0:
|
|
return "", false
|
|
case 1:
|
|
return matches[0], false
|
|
default:
|
|
return matches[0], true
|
|
}
|
|
}
|
|
|
|
// resolveCursorProjectDirNameHint derives a real workspace path from a
|
|
// Cursor-encoded directory name, preferring candidates that contain the
|
|
// provided hint.
|
|
func resolveCursorProjectDirNameHint(dirName, hint string) string {
|
|
matches := resolveCursorProjectDirNameMatches(dirName, hint, 1)
|
|
if len(matches) == 0 {
|
|
return ""
|
|
}
|
|
nh := normalizeCursorDir(hint)
|
|
if nh != "" && !cursorPathContainsHint(
|
|
normalizeCursorDir(matches[0]), nh,
|
|
) {
|
|
return ""
|
|
}
|
|
return matches[0]
|
|
}
|
|
|
|
func resolveCursorProjectDirNameMatches(
|
|
dirName, hint string, limit int,
|
|
) []string {
|
|
dirName = strings.TrimSpace(dirName)
|
|
if dirName == "" {
|
|
return nil
|
|
}
|
|
hint = normalizeCursorDir(hint)
|
|
|
|
if runtime.GOOS == "windows" {
|
|
return resolveCursorProjectDirNameFromRootMatches(
|
|
"", dirName, hint, limit,
|
|
)
|
|
}
|
|
return resolveCursorProjectDirNameFromRootMatches(
|
|
string(filepath.Separator), dirName, hint, limit,
|
|
)
|
|
}
|
|
|
|
// resolveCursorProjectDirNameFromRoot reconstructs a real path from a
|
|
// Cursor-encoded project directory name by walking an existing
|
|
// filesystem tree and matching each component against the encoded token
|
|
// stream. The root parameter is mainly for tests; empty means use the
|
|
// OS default root.
|
|
func resolveCursorProjectDirNameFromRoot(
|
|
root, dirName string,
|
|
) string {
|
|
return resolveCursorProjectDirNameFromRootHint(root, dirName, "")
|
|
}
|
|
|
|
// resolveCursorProjectDirNameFromRootHint reconstructs a real path from
|
|
// a Cursor-encoded project directory name. It backtracks across matching
|
|
// path components instead of committing to the first greedy match, and
|
|
// prefers candidates that contain the latest transcript cwd when one is
|
|
// available.
|
|
func resolveCursorProjectDirNameFromRootHint(
|
|
root, dirName, hint string,
|
|
) string {
|
|
matches := resolveCursorProjectDirNameFromRootMatches(
|
|
root, dirName, hint, 1,
|
|
)
|
|
if len(matches) == 0 {
|
|
return ""
|
|
}
|
|
nh := normalizeCursorDir(hint)
|
|
if nh != "" && !cursorPathContainsHint(
|
|
normalizeCursorDir(matches[0]), nh,
|
|
) {
|
|
return ""
|
|
}
|
|
return matches[0]
|
|
}
|
|
|
|
func resolveCursorProjectDirNameFromRootMatches(
|
|
root, dirName, hint string, limit int,
|
|
) []string {
|
|
tokens := cursorEncodedTokens(dirName)
|
|
if len(tokens) == 0 {
|
|
return nil
|
|
}
|
|
|
|
current := root
|
|
if runtime.GOOS == "windows" {
|
|
if len(tokens[0]) == 1 {
|
|
drive := tokens[0][0]
|
|
if (drive >= 'A' && drive <= 'Z') ||
|
|
(drive >= 'a' && drive <= 'z') {
|
|
current = strings.ToUpper(tokens[0]) + ":" +
|
|
string(filepath.Separator)
|
|
tokens = tokens[1:]
|
|
}
|
|
}
|
|
}
|
|
if current == "" {
|
|
current = string(filepath.Separator)
|
|
}
|
|
if !isDir(current) {
|
|
return nil
|
|
}
|
|
if len(tokens) == 0 {
|
|
return []string{current}
|
|
}
|
|
|
|
var matches []string
|
|
collectCursorPathMatches(
|
|
current, tokens, hint, limit, &matches,
|
|
)
|
|
return matches
|
|
}
|
|
|
|
type cursorPathMatch struct {
|
|
name string
|
|
path string
|
|
consumed int
|
|
hinted bool
|
|
}
|
|
|
|
func collectCursorPathMatches(
|
|
dir string, tokens []string, hint string, limit int,
|
|
matches *[]string,
|
|
) {
|
|
if limit > 0 && len(*matches) >= limit {
|
|
return
|
|
}
|
|
if len(tokens) == 0 {
|
|
if isDir(dir) {
|
|
*matches = append(*matches, dir)
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, match := range matchCursorPathComponents(dir, tokens, hint) {
|
|
collectCursorPathMatches(
|
|
match.path, tokens[match.consumed:], hint,
|
|
limit, matches,
|
|
)
|
|
if limit > 0 && len(*matches) >= limit {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func matchCursorPathComponents(
|
|
dir string, tokens []string, hint string,
|
|
) []cursorPathMatch {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
matches := make([]cursorPathMatch, 0, len(entries))
|
|
for _, entry := range entries {
|
|
fullPath := filepath.Join(dir, entry.Name())
|
|
if !isDir(fullPath) {
|
|
continue
|
|
}
|
|
candidate := cursorComponentTokens(entry.Name())
|
|
if len(candidate) == 0 || len(candidate) > len(tokens) {
|
|
continue
|
|
}
|
|
if !cursorTokenPrefixMatch(tokens, candidate) {
|
|
continue
|
|
}
|
|
matches = append(matches, cursorPathMatch{
|
|
name: entry.Name(),
|
|
path: fullPath,
|
|
consumed: len(candidate),
|
|
hinted: cursorPathContainsHint(fullPath, hint),
|
|
})
|
|
}
|
|
|
|
sort.Slice(matches, func(i, j int) bool {
|
|
if matches[i].hinted != matches[j].hinted {
|
|
return matches[i].hinted
|
|
}
|
|
if matches[i].consumed != matches[j].consumed {
|
|
return matches[i].consumed > matches[j].consumed
|
|
}
|
|
return matches[i].name < matches[j].name
|
|
})
|
|
return matches
|
|
}
|
|
|
|
func cursorPathContainsHint(path, hint string) bool {
|
|
if path == "" || hint == "" {
|
|
return false
|
|
}
|
|
rel, err := filepath.Rel(path, hint)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return rel == "." ||
|
|
(rel != ".." &&
|
|
!strings.HasPrefix(rel, ".."+string(filepath.Separator)))
|
|
}
|
|
|
|
func cursorTokenPrefixMatch(tokens, candidate []string) bool {
|
|
for i := range candidate {
|
|
if tokens[i] != candidate[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func cursorEncodedTokens(s string) []string {
|
|
return strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == '-'
|
|
})
|
|
}
|
|
|
|
func cursorComponentTokens(s string) []string {
|
|
return strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == '-' || r == '.' || r == '_'
|
|
})
|
|
}
|