@@ -34,6 +34,9 @@ func DetectProject(dir string) DetectionResult {
3434 result := DetectionResult {
3535 AppName : filepath .Base (dir ),
3636 }
37+ if name := readProjectName (dir ); name != "" {
38+ result .AppName = name
39+ }
3740
3841 // ── 1. angular.json ─────────────────────────────────────────────────────
3942 if fileExists (dir , "angular.json" ) {
@@ -325,6 +328,143 @@ func readPackageJSONDeps(dir string) map[string]bool {
325328 return deps
326329}
327330
331+ // readPackageJSONName reads the "name" field from package.json in dir.
332+ // Returns empty string if not found or on any error.
333+ func readPackageJSONName (dir string ) string {
334+ data , err := os .ReadFile (filepath .Join (dir , "package.json" ))
335+ if err != nil {
336+ return ""
337+ }
338+ var pkg struct {
339+ Name string `json:"name"`
340+ }
341+ if err := json .Unmarshal (data , & pkg ); err != nil {
342+ return ""
343+ }
344+ return pkg .Name
345+ }
346+
347+ // readProjectName tries to extract a meaningful project name from language-specific
348+ // manifest files. It falls back to empty string if none are found; the caller then
349+ // uses filepath.Base(dir).
350+ func readProjectName (dir string ) string {
351+ if name := readPackageJSONName (dir ); name != "" {
352+ return name
353+ }
354+ if name := readGoModuleName (dir ); name != "" {
355+ return name
356+ }
357+ if name := readPyprojectName (dir ); name != "" {
358+ return name
359+ }
360+ if name := readPubspecName (dir ); name != "" {
361+ return name
362+ }
363+ if name := readComposerName (dir ); name != "" {
364+ return name
365+ }
366+ if name := readPomArtifactID (dir ); name != "" {
367+ return name
368+ }
369+ return ""
370+ }
371+
372+ // readGoModuleName reads the module path from go.mod and returns its last path segment.
373+ func readGoModuleName (dir string ) string {
374+ data , ok := readFileContent (dir , "go.mod" )
375+ if ! ok {
376+ return ""
377+ }
378+ for _ , line := range strings .SplitN (data , "\n " , 20 ) {
379+ line = strings .TrimSpace (line )
380+ if strings .HasPrefix (line , "module " ) {
381+ modulePath := strings .TrimSpace (strings .TrimPrefix (line , "module " ))
382+ return filepath .Base (modulePath )
383+ }
384+ }
385+ return ""
386+ }
387+
388+ // readPyprojectName reads the project name from pyproject.toml ([project] or [tool.poetry] section).
389+ func readPyprojectName (dir string ) string {
390+ data , ok := readFileContent (dir , "pyproject.toml" )
391+ if ! ok {
392+ return ""
393+ }
394+ for _ , line := range strings .Split (data , "\n " ) {
395+ line = strings .TrimSpace (line )
396+ if ! strings .HasPrefix (line , "name " ) && ! strings .HasPrefix (line , "name=" ) {
397+ continue
398+ }
399+ parts := strings .SplitN (line , "=" , 2 )
400+ if len (parts ) != 2 {
401+ continue
402+ }
403+ val := strings .TrimSpace (parts [1 ])
404+ val = strings .Trim (val , `"'` )
405+ if val != "" {
406+ return val
407+ }
408+ }
409+ return ""
410+ }
411+
412+ // readPubspecName reads the name field from pubspec.yaml.
413+ func readPubspecName (dir string ) string {
414+ data , ok := readFileContent (dir , "pubspec.yaml" )
415+ if ! ok {
416+ return ""
417+ }
418+ for _ , line := range strings .Split (data , "\n " ) {
419+ trimmed := strings .TrimSpace (line )
420+ if strings .HasPrefix (trimmed , "name:" ) {
421+ val := strings .TrimSpace (strings .TrimPrefix (trimmed , "name:" ))
422+ if val != "" {
423+ return val
424+ }
425+ }
426+ }
427+ return ""
428+ }
429+
430+ // readComposerName reads the package name from composer.json and returns the part after "/".
431+ func readComposerName (dir string ) string {
432+ data , err := os .ReadFile (filepath .Join (dir , "composer.json" ))
433+ if err != nil {
434+ return ""
435+ }
436+ var pkg struct {
437+ Name string `json:"name"`
438+ }
439+ if err := json .Unmarshal (data , & pkg ); err != nil || pkg .Name == "" {
440+ return ""
441+ }
442+ if idx := strings .LastIndex (pkg .Name , "/" ); idx >= 0 {
443+ return pkg .Name [idx + 1 :]
444+ }
445+ return pkg .Name
446+ }
447+
448+ // readPomArtifactID reads the first <artifactId> value from pom.xml.
449+ func readPomArtifactID (dir string ) string {
450+ data , ok := readFileContent (dir , "pom.xml" )
451+ if ! ok {
452+ return ""
453+ }
454+ const open = "<artifactId>"
455+ const close = "</artifactId>"
456+ start := strings .Index (data , open )
457+ if start == - 1 {
458+ return ""
459+ }
460+ start += len (open )
461+ end := strings .Index (data [start :], close )
462+ if end == - 1 {
463+ return ""
464+ }
465+ return strings .TrimSpace (data [start : start + end ])
466+ }
467+
328468// hasDep returns true if the named dependency is in the deps set.
329469func hasDep (deps map [string ]bool , name string ) bool {
330470 return deps [name ]
@@ -360,13 +500,13 @@ func findJavaBuildContent(dir string) (content, buildTool string, ok bool) {
360500 return "" , "" , false
361501}
362502
363- // friendlyAppType returns the human-readable label for an app type key .
364- func friendlyAppType (qsType string ) string {
503+ // detectionFriendlyAppType returns a concise label for the detection summary display .
504+ func detectionFriendlyAppType (qsType string ) string {
365505 switch qsType {
366506 case "spa" :
367- return "Single Page App (SPA) "
507+ return "Single Page App"
368508 case "regular" :
369- return "Regular Web App / API / Backend "
509+ return "Regular Web App"
370510 case "native" :
371511 return "Native / Mobile"
372512 case "m2m" :
0 commit comments