Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cmd/promptext/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"

"github.com/1broseidon/promptext/internal/initializer"
"github.com/1broseidon/promptext/internal/processor"
"github.com/1broseidon/promptext/internal/update"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -77,6 +78,11 @@ UPDATE OPTIONS:
--update Update promptext to the latest version from GitHub
--check-update Check if a new version is available without updating

INITIALIZATION OPTIONS:
--init Initialize a new .promptext.yml config file with smart defaults
Detects project type and suggests framework-specific settings
--force Force overwrite of existing config (use with --init)

EXAMPLES:
# Basic usage - process current directory, copy to clipboard
prx
Expand Down Expand Up @@ -132,6 +138,10 @@ EXAMPLES:
prx --check-update # Check only
prx --update # Update to latest version

# Initialize config file with smart defaults based on project type
prx --init # Interactive mode
prx --init --force # Overwrite existing config

CONFIGURATION:
Create a .promptext.yml file in your project root for persistent settings:

Expand Down Expand Up @@ -171,6 +181,10 @@ func main() {
checkUpdate := pflag.Bool("check-update", false, "Check if a new version is available")
doUpdate := pflag.Bool("update", false, "Update to the latest version from GitHub")

// Initialization options
initConfig := pflag.Bool("init", false, "Initialize a new .promptext.yml config file with smart defaults")
forceInit := pflag.Bool("force", false, "Force overwrite of existing config (use with --init)")

// Input options
dirPath := pflag.StringP("directory", "d", ".", "Directory to process (default: current directory)")
extension := pflag.StringP("extension", "e", "", "File extensions to include (comma-separated, e.g., .go,.js,.py)")
Expand Down Expand Up @@ -235,6 +249,24 @@ func main() {
os.Exit(0)
}

// Handle initialization flag
if *initConfig {
// Get absolute path
absPath, err := filepath.Abs(*dirPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving directory path: %v\n", err)
os.Exit(1)
}

// Create and run initializer
init := initializer.NewInitializer(absPath, *forceInit, *quiet)
if err := init.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error initializing config: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}

// Automatic update check (non-blocking, silently fails on network issues)
// Only runs during normal operation, not for update/version/help commands
go update.CheckAndNotifyUpdate(version)
Expand Down
247 changes: 247 additions & 0 deletions internal/initializer/detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package initializer

import (
"os"
"path/filepath"
)

// ProjectType represents a detected project framework or language
type ProjectType struct {
Name string
Description string
Priority int // Higher priority types are listed first
}

// DetectionResult contains all detected project types
type DetectionResult struct {
ProjectTypes []ProjectType
RootPath string
}

// Detector interface for project type detection
type Detector interface {
Detect(rootPath string) ([]ProjectType, error)
}

// FileDetector detects project types based on file presence
type FileDetector struct{}

// NewFileDetector creates a new file-based detector
func NewFileDetector() *FileDetector {
return &FileDetector{}
}

// Detect scans the directory for known project indicators
func (d *FileDetector) Detect(rootPath string) ([]ProjectType, error) {
var detected []ProjectType

// Define detection rules: file -> project type
detectionRules := []struct {
files []string // Any of these files indicates this project type
projectType ProjectType
}{
// JavaScript/TypeScript frameworks
{
files: []string{"next.config.js", "next.config.mjs", "next.config.ts"},
projectType: ProjectType{
Name: "nextjs",
Description: "Next.js",
Priority: 100,
},
},
{
files: []string{"nuxt.config.js", "nuxt.config.ts"},
projectType: ProjectType{
Name: "nuxt",
Description: "Nuxt.js",
Priority: 100,
},
},
{
files: []string{"vite.config.js", "vite.config.ts"},
projectType: ProjectType{
Name: "vite",
Description: "Vite",
Priority: 90,
},
},
{
files: []string{"vue.config.js"},
projectType: ProjectType{
Name: "vue",
Description: "Vue.js",
Priority: 90,
},
},
{
files: []string{"angular.json"},
projectType: ProjectType{
Name: "angular",
Description: "Angular",
Priority: 100,
},
},
{
files: []string{"svelte.config.js"},
projectType: ProjectType{
Name: "svelte",
Description: "Svelte",
Priority: 90,
},
},

// Go
{
files: []string{"go.mod"},
projectType: ProjectType{
Name: "go",
Description: "Go",
Priority: 80,
},
},

// Python frameworks
{
files: []string{"manage.py", "django"},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: False negatives in Django project detection detection logic

Django detection includes "django" as a filename to check for, but Django projects don't have a file literally named "django". This will cause false negatives where Django projects with only manage.py (and not a file called "django") are still detected, making the second entry in the slice misleading and potentially incorrect. The detection should likely only check for "manage.py" or include other Django-specific indicators like "settings.py" or specific directory structures.

Fix in Cursor Fix in Web

projectType: ProjectType{
Name: "django",
Description: "Django",
Priority: 100,
},
},
{
files: []string{"app.py", "wsgi.py"},
projectType: ProjectType{
Name: "flask",
Description: "Flask",
Priority: 90,
},
},
{
files: []string{"pyproject.toml", "setup.py", "requirements.txt"},
projectType: ProjectType{
Name: "python",
Description: "Python",
Priority: 70,
},
},

// Rust
{
files: []string{"Cargo.toml"},
projectType: ProjectType{
Name: "rust",
Description: "Rust",
Priority: 80,
},
},

// Java/Kotlin
{
files: []string{"pom.xml"},
projectType: ProjectType{
Name: "maven",
Description: "Maven (Java)",
Priority: 80,
},
},
{
files: []string{"build.gradle", "build.gradle.kts"},
projectType: ProjectType{
Name: "gradle",
Description: "Gradle (Java/Kotlin)",
Priority: 80,
},
},

// Ruby
{
files: []string{"Gemfile", "config.ru"},
projectType: ProjectType{
Name: "ruby",
Description: "Ruby/Rails",
Priority: 80,
},
},

// PHP
{
files: []string{"composer.json"},
projectType: ProjectType{
Name: "php",
Description: "PHP",
Priority: 70,
},
},
{
files: []string{"artisan"},
projectType: ProjectType{
Name: "laravel",
Description: "Laravel",
Priority: 90,
},
},

// .NET
{
files: []string{"*.csproj", "*.fsproj", "*.vbproj"},
projectType: ProjectType{
Name: "dotnet",
Description: ".NET",
Priority: 80,
},
},

// Node.js (generic - lowest priority)
{
files: []string{"package.json"},
projectType: ProjectType{
Name: "node",
Description: "Node.js",
Priority: 60,
},
},
}

// Check each detection rule
for _, rule := range detectionRules {
for _, file := range rule.files {
// Handle wildcards
if filepath.Base(file) != file && (file[0] == '*' || file[len(file)-1] == '*') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wildcard detection logic breaks project file matching

Wildcard detection logic is incorrect. The condition filepath.Base(file) != file && (file[0] == '*' || file[len(file)-1] == '*') will always be false for simple wildcard patterns like ".csproj", ".fsproj", and ".vbproj" because filepath.Base("*.csproj") returns ".csproj" (making the first part of the AND condition false). This means .NET projects will never be detected, as the code falls through to the regular os.Stat() check which tries to find a literal file named "*.csproj" instead of using glob matching.

Fix in Cursor Fix in Web

matches, err := filepath.Glob(filepath.Join(rootPath, file))
if err == nil && len(matches) > 0 {
detected = append(detected, rule.projectType)
break
}
} else {
// Regular file check
filePath := filepath.Join(rootPath, file)
if _, err := os.Stat(filePath); err == nil {
detected = append(detected, rule.projectType)
break
}
}
}
}

// Sort by priority (highest first)
for i := 0; i < len(detected); i++ {
for j := i + 1; j < len(detected); j++ {
if detected[j].Priority > detected[i].Priority {
detected[i], detected[j] = detected[j], detected[i]
}
}
}

// Deduplicate
seen := make(map[string]bool)
var unique []ProjectType
for _, pt := range detected {
if !seen[pt.Name] {
seen[pt.Name] = true
unique = append(unique, pt)
}
}

return unique, nil
}
Loading