core/app/app.go
core/app/app.goBrowse 1970 files
1,501 tokens
5,504 bytes
Token encoding: o200k_base
Snapshot 21a254f
← Back to SKILL.md
1package app2 3import (4 "encoding/json"5 "errors"6 "fmt"7 "os"8 "path/filepath"9 "regexp"10 "strings"11 12 "github.com/BurntSushi/toml"13 "github.com/bmatcuk/doublestar/v4"14 "github.com/railwayapp/railpack/internal/utils"15 "gopkg.in/yaml.v2"16)17 18var ErrNoFileFound = errors.New("unable to find a matching file")19 20type App struct {21 Source string22 globCache map[string][]string23}24 25func NewApp(path string) (*App, error) {26 var source string27 28 if filepath.IsAbs(path) {29 source = path30 } else {31 currentDir, err := os.Getwd()32 if err != nil {33 return nil, err34 }35 source, err = filepath.Abs(filepath.Join(currentDir, path))36 if err != nil {37 return nil, errors.New("failed to read app source directory")38 }39 }40 41 if _, err := os.Stat(source); err != nil {42 if os.IsNotExist(err) {43 return nil, fmt.Errorf("directory %s does not exist", source)44 }45 return nil, fmt.Errorf("failed to check directory %s: %w", source, err)46 }47 48 return &App{49 Source: source,50 globCache: make(map[string][]string),51 }, nil52}53 54// findMatches returns a list of paths matching a glob pattern, filtered by isDir55func (a *App) findMatches(pattern string, isDir bool) ([]string, error) {56 matches, err := a.findGlob(pattern)57 58 if err != nil {59 return nil, err60 }61 62 var paths []string63 for _, match := range matches {64 fullPath := filepath.Join(a.Source, match)65 66 info, err := os.Stat(fullPath)67 if err != nil {68 continue69 }70 71 if info.IsDir() == isDir {72 paths = append(paths, match)73 }74 }75 return paths, nil76}77 78// returns a list of file paths matching a glob pattern79func (a *App) FindFiles(pattern string) ([]string, error) {80 return a.findMatches(pattern, false)81}82 83// FindDirectories returns a list of directory paths matching a glob pattern84func (a *App) FindDirectories(pattern string) ([]string, error) {85 return a.findMatches(pattern, true)86}87 88// findGlob finds paths matching a glob pattern, with caching89func (a *App) findGlob(pattern string) ([]string, error) {90 if cached, ok := a.globCache[pattern]; ok {91 return cached, nil92 }93 94 matches, err := doublestar.Glob(os.DirFS(a.Source), pattern)95 if err != nil {96 return nil, err97 }98 99 a.globCache[pattern] = matches100 return matches, nil101}102 103// Check if a relative file exists in the app's source directory104func (a *App) HasFile(path string) bool {105 fullPath := filepath.Join(a.Source, path)106 107 _, err := os.Stat(fullPath)108 return !os.IsNotExist(err)109}110 111// HasMatch checks if a path matching a glob exists (files or directories)112func (a *App) HasMatch(pattern string) bool {113 files, err := a.FindFiles(pattern)114 if err != nil {115 return false116 }117 118 dirs, err := a.FindDirectories(pattern)119 if err != nil {120 return false121 }122 123 return len(files) > 0 || len(dirs) > 0124}125 126func (a *App) FindFilesWithContent(pattern string, regex *regexp.Regexp) []string {127 files, err := a.FindFiles(pattern)128 if err != nil {129 return nil130 }131 132 var matches []string133 for _, file := range files {134 content, err := a.ReadFile(file)135 if err != nil {136 continue137 }138 139 if regex.MatchString(content) {140 matches = append(matches, file)141 }142 }143 144 return matches145}146 147// reads the contents of the first file that exists within the application source directory148// helpful for reading config from multiple possible locations (something.js, something.ts, etc)149func (a *App) ReadFirstFileOf(names ...string) (string, string, error) {150 for _, name := range names {151 if !a.HasFile(name) {152 continue153 }154 155 contents, err := a.ReadFile(name)156 if err != nil {157 return "", "", err158 }159 160 return name, contents, nil161 }162 163 return "", "", ErrNoFileFound164}165 166// ReadFile reads the contents of a file within the application source directory167func (a *App) ReadFile(name string) (string, error) {168 path := filepath.Join(a.Source, name)169 data, err := os.ReadFile(path)170 if err != nil {171 relativePath, _ := a.stripSourcePath(path)172 return "", fmt.Errorf("error reading %s: %w", relativePath, err)173 }174 175 return strings.ReplaceAll(string(data), "\r\n", "\n"), nil176}177 178// ReadJSON reads and parses a JSON file179func (a *App) ReadJSON(name string, v any) error {180 data, err := a.ReadFile(name)181 if err != nil {182 return err183 }184 185 jsonBytes, err := utils.StandardizeJSON([]byte(data))186 if err != nil {187 return err188 }189 190 data = string(jsonBytes)191 192 if err := json.Unmarshal([]byte(data), v); err != nil {193 relativePath, _ := a.stripSourcePath(filepath.Join(a.Source, name))194 return fmt.Errorf("error reading %s as JSON: %w", relativePath, err)195 }196 197 return nil198}199 200// ReadYAML reads and parses a YAML file201func (a *App) ReadYAML(name string, v any) error {202 data, err := a.ReadFile(name)203 if err != nil {204 return err205 }206 207 if err := yaml.Unmarshal([]byte(data), v); err != nil {208 return fmt.Errorf("error reading %s as YAML: %w", name, err)209 }210 211 return nil212}213 214func (a *App) ReadTOML(name string, v any) error {215 data, err := a.ReadFile(name)216 if err != nil {217 return err218 }219 220 return toml.Unmarshal([]byte(data), v)221}222 223// checks if a path is an executable file224func (a *App) IsFileExecutable(name string) bool {225 path := filepath.Join(a.Source, name)226 info, err := os.Stat(path)227 if err != nil {228 return false229 }230 231 if !info.Mode().IsRegular() {232 return false233 }234 235 // Check executable bit236 return info.Mode()&0111 != 0237}238 239// converts an absolute path to a path relative to the app source directory240func (a *App) stripSourcePath(absPath string) (string, error) {241 rel, err := filepath.Rel(a.Source, absPath)242 if err != nil {243 return "", errors.New("failed to parse source path")244 }245 return rel, nil246}247