project-compass 4.3.6 → 4.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,60 +1,21 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import path from 'path';
4
3
 
5
- const SPARKS = ['✦', '✧', '✩', '✪', '✫'];
6
-
7
- export default function Header({ projectCountLabel, rootPath, running, toggleHint, orbitHint }) {
8
- const time = new Date().toLocaleTimeString('en-US', { hour12: false });
9
- const spark = SPARKS[Math.floor(Date.now() / 1000) % SPARKS.length];
10
-
4
+ export default function Header({ projectCountLabel, rootPath, running, statusHint, toggleHint, orbitHint, artHint }) {
11
5
  return React.createElement(
12
6
  Box,
13
- { flexDirection: 'column', marginBottom: 1 },
14
- // Top bar with logo and status
15
- React.createElement(
16
- Box,
17
- { justifyContent: 'space-between', alignItems: 'center' },
18
- React.createElement(
19
- Box,
20
- { flexDirection: 'row', alignItems: 'center' },
21
- React.createElement(Text, { color: 'magenta', bold: true }, '🧭 '),
22
- React.createElement(Text, { color: 'magenta', bold: true }, 'PROJECT COMPASS'),
23
- React.createElement(Text, { color: 'cyan' }, ` ${spark}`)
24
- ),
25
- React.createElement(
26
- Box,
27
- { flexDirection: 'row', alignItems: 'center' },
28
- React.createElement(Text, {
29
- color: running ? 'yellow' : 'green',
30
- bold: true
31
- }, running ? '⚡ ACTIVE' : '✓ IDLE'),
32
- React.createElement(Text, { dimColor: true }, ` ${time}`)
33
- )
34
- ),
35
- // Info bar
7
+ { justifyContent: 'space-between' },
36
8
  React.createElement(
37
9
  Box,
38
- { justifyContent: 'space-between', marginTop: 0 },
39
- React.createElement(
40
- Box,
41
- { flexDirection: 'row' },
42
- React.createElement(Text, { dimColor: true }, `${spark} `),
43
- React.createElement(Text, { color: 'cyan' }, projectCountLabel),
44
- React.createElement(Text, { dimColor: true }, ' in '),
45
- React.createElement(Text, { color: 'white' }, path.basename(rootPath))
46
- ),
47
- React.createElement(
48
- Box,
49
- { flexDirection: 'row' },
50
- React.createElement(Text, { dimColor: true }, `${toggleHint} · ${orbitHint}`)
51
- )
10
+ { flexDirection: 'column' },
11
+ React.createElement(Text, { color: 'magenta', bold: true }, 'Project Compass'),
12
+ React.createElement(Text, { dimColor: true }, `${projectCountLabel} detected in ${rootPath}`)
52
13
  ),
53
- // Separator
54
14
  React.createElement(
55
15
  Box,
56
- { marginTop: 0 },
57
- React.createElement(Text, { dimColor: true }, '─'.repeat(50))
16
+ { flexDirection: 'column', alignItems: 'flex-end' },
17
+ React.createElement(Text, { color: running ? 'yellow' : 'green' }, statusHint),
18
+ React.createElement(Text, { dimColor: true }, `${toggleHint} · ${orbitHint} · ${artHint} · Shift+Q: Quit`)
58
19
  )
59
20
  );
60
21
  }
@@ -1,102 +1,48 @@
1
- /* global setInterval, clearInterval */
2
- import React, { useMemo, useState, useEffect } from 'react';
1
+ import React, { useMemo } from 'react';
3
2
  import { Box, Text } from 'ink';
4
3
  import path from 'path';
5
4
 
6
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
- const SELECTION_BAR = '█';
8
-
9
5
  export default function Navigator({
10
6
  projects,
11
7
  selectedIndex,
12
8
  rootPath,
13
9
  loading,
14
10
  error,
15
- maxVisibleProjects = 3
11
+ maxVisibleProjects = 3
16
12
  }) {
17
- const [tick, setTick] = useState(0);
18
-
19
- useEffect(() => {
20
- if (!loading) return;
21
- const timer = setInterval(() => setTick(t => t + 1), 100);
22
- return () => clearInterval(timer);
23
- }, [loading]);
24
-
25
13
  const page = Math.floor(selectedIndex / maxVisibleProjects);
26
14
  const start = page * maxVisibleProjects;
27
15
  const end = start + maxVisibleProjects;
28
16
  const visibleProjects = projects.slice(start, end);
29
-
17
+
30
18
  const projectRows = useMemo(() => {
31
- if (loading) {
32
- const spinner = SPINNER[tick % SPINNER.length];
33
- return [React.createElement(
34
- Box,
35
- { key: 'scanning', flexDirection: 'row', alignItems: 'center' },
36
- React.createElement(Text, { color: 'cyan' }, ` ${spinner} `),
37
- React.createElement(Text, { dimColor: true }, 'Scanning projects…'),
38
- React.createElement(Text, { color: 'cyan' }, ' ●'.repeat(3 + (tick % 3)))
39
- )];
40
- }
41
- if (error) return [React.createElement(
42
- Box,
43
- { key: 'error', flexDirection: 'row', alignItems: 'center' },
44
- React.createElement(Text, { color: 'red', bold: true }, ' ✗ '),
45
- React.createElement(Text, { color: 'red' }, `Unable to scan: ${error}`)
46
- )];
47
- if (projects.length === 0) return [React.createElement(
48
- Box,
49
- { key: 'empty', flexDirection: 'row', alignItems: 'center' },
50
- React.createElement(Text, { color: 'yellow' }, ' ⚠ '),
51
- React.createElement(Text, { dimColor: true }, 'No recognizable project manifests found.')
52
- )];
19
+ if (loading) return [React.createElement(Text, { key: 'scanning', dimColor: true }, 'Scanning projects…')];
20
+ if (error) return [React.createElement(Text, { key: 'error', color: 'red' }, `Unable to scan: ${error}`)];
21
+ if (projects.length === 0) return [React.createElement(Text, { key: 'empty', dimColor: true }, 'No recognizable project manifests found.')];
53
22
 
54
23
  return visibleProjects.map((project, index) => {
55
24
  const absoluteIndex = start + index;
56
25
  const isSelected = absoluteIndex === selectedIndex;
57
- const frameworkBadges = (project.frameworks || []).map((frame) =>
58
- React.createElement(Text, { key: frame.name, color: 'cyan', dimColor: !isSelected }, ` ${frame.icon}${frame.name}`)
59
- );
26
+ const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
60
27
  const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
61
28
 
62
29
  return React.createElement(
63
30
  Box,
64
- {
65
- key: project.id,
66
- flexDirection: 'column',
67
- marginBottom: 0,
68
- borderStyle: isSelected ? 'bold' : 'single',
69
- borderColor: isSelected ? 'cyan' : 'gray',
70
- paddingX: 1,
71
- paddingY: 0
72
- },
31
+ { key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1 },
73
32
  React.createElement(
74
33
  Box,
75
- { flexDirection: 'row', alignItems: 'center' },
76
- React.createElement(Text, {
77
- color: isSelected ? 'cyan' : 'white',
78
- bold: isSelected
79
- }, `${isSelected ? SELECTION_BAR + ' ' : ' '}${project.icon} ${project.name}`),
80
- hasMissingRuntime && React.createElement(Text, { color: 'red', bold: true }, ' ⚠ Runtime missing')
34
+ { flexDirection: 'row' },
35
+ React.createElement(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected }, `${project.icon} ${project.name}`),
36
+ hasMissingRuntime && React.createElement(Text, { color: 'red', bold: true }, ' ⚠️ Runtime missing')
81
37
  ),
82
- React.createElement(
83
- Box,
84
- { flexDirection: 'row', alignItems: 'center' },
85
- React.createElement(Text, { dimColor: true }, ` ${project.type}`),
86
- React.createElement(Text, { dimColor: true }, ` · ${path.relative(rootPath, project.path) || '.'}`)
87
- ),
88
- frameworkBadges.length > 0 && React.createElement(
89
- Box,
90
- { marginTop: 0 },
91
- React.createElement(Text, { dimColor: true }, ' '),
92
- ...frameworkBadges
93
- )
38
+ React.createElement(Text, { dimColor: true }, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
39
+ frameworkBadges && React.createElement(Text, { dimColor: true }, ` ${frameworkBadges}`)
94
40
  );
95
41
  });
96
- }, [loading, error, projects.length, visibleProjects, selectedIndex, start, rootPath, tick]);
42
+ }, [loading, error, projects.length, visibleProjects, selectedIndex, start, rootPath]);
97
43
 
98
44
  const totalPages = Math.ceil(projects.length / maxVisibleProjects);
99
-
45
+
100
46
  return React.createElement(
101
47
  Box,
102
48
  { flexDirection: 'column' },
@@ -104,7 +50,7 @@ export default function Navigator({
104
50
  projects.length > maxVisibleProjects && React.createElement(
105
51
  Box,
106
52
  { marginTop: 1, justifyContent: 'center' },
107
- React.createElement(Text, { dimColor: true }, `${'-'.repeat(3)} Page ${page + 1}/${totalPages} (${projects.length} projects) ${'-'.repeat(3)}`)
53
+ React.createElement(Text, { dimColor: true }, `Page ${page + 1} of ${totalPages} (Total: ${projects.length})`)
108
54
  )
109
55
  );
110
56
  }
@@ -88,11 +88,12 @@ const PackageRegistry = memo(({selectedProject, projects = [], onRunCommand, Cur
88
88
  return;
89
89
  }
90
90
 
91
- if (inputStr.toLowerCase() === 'a') { setMode('add'); setInput(''); setCursor(0); }
92
- if (inputStr.toLowerCase() === 'r') { setMode('remove'); setInput(''); setCursor(0); }
93
- if (inputStr.toLowerCase() === 's') { setView('select'); }
91
+ if (inputStr.toLowerCase() === 'a') { setMode('add'); setInput(''); setCursor(0); return; }
92
+ if (inputStr.toLowerCase() === 'r') { setMode('remove'); setInput(''); setCursor(0); return; }
93
+ if (inputStr.toLowerCase() === 's') { setView('select'); return; }
94
94
  if (inputStr.toLowerCase() === 'v' && projectType === 'Python') {
95
95
  onRunCommand({label: 'Create venv', command: ['python3', '-m', 'venv', '.venv']}, activeProject);
96
+ return;
96
97
  }
97
98
  });
98
99
 
@@ -20,7 +20,12 @@ const ProjectArchitect = memo(({rootPath, onRunCommand, CursorText, onReturn}) =
20
20
  {name: 'Rust (Binary)', cmd: (p, n) => ['cargo', 'new', path.join(p, n)]},
21
21
  {name: 'Django Project', cmd: (p, n) => ['django-admin', 'startproject', n, path.join(p, n)]},
22
22
  {name: 'Python (Basic)', cmd: (p, n) => ['mkdir', '-p', path.join(p, n)]},
23
- {name: 'Go Module', cmd: (p, n) => ['mkdir', '-p', path.join(p, n), '&&', 'cd', path.join(p, n), '&&', 'go', 'mod', 'init', n]}
23
+ {name: 'Go Module', cmd: (p, n) => {
24
+ const dir = path.join(p, n);
25
+ return process.platform === 'win32'
26
+ ? ['cmd', '/c', `mkdir "${dir}" && cd /d "${dir}" && go mod init ${n}`]
27
+ : ['sh', '-c', `mkdir -p "${dir}" && cd "${dir}" && go mod init ${n}`];
28
+ }}
24
29
  ];
25
30
 
26
31
  useInput((inputStr, key) => {
@@ -2,80 +2,22 @@ import React, {memo} from 'react';
2
2
  import {Box, Text} from 'ink';
3
3
 
4
4
  const create = React.createElement;
5
- const STATUC_COLORS = { running: 'green', finished: 'cyan', failed: 'red', killed: 'yellow' };
6
5
 
7
6
  const TaskManager = memo(({tasks, activeTaskId, renameMode, renameInput, renameCursor, CursorText}) => {
8
- const activeTask = tasks.find(t => t.id === activeTaskId);
9
- const logLines = activeTask?.logs || [];
10
- const visibleLogs = logLines.slice(-5);
11
-
12
7
  return create(
13
8
  Box,
14
- {flexDirection: 'column', borderStyle: 'double', borderColor: 'yellow', padding: 1, height: '100%'},
15
- // Header
16
- create(Box, {flexDirection: 'row', justifyContent: 'space-between', marginBottom: 1},
17
- create(Box, {flexDirection: 'row', alignItems: 'center'},
18
- create(Text, {bold: true, color: 'yellow'}, '🛰️ '),
19
- create(Text, {bold: true, color: 'yellow'}, 'Orbit Task Manager'),
20
- create(Text, {dimColor: true}, ` | ${tasks.length} task${tasks.length !== 1 ? 's' : ''}`)
21
- ),
22
- tasks.length > 0 && create(Text, {color: 'cyan', bold: true},
23
- `[${tasks.filter(t => t.status === 'running').length} ACTIVE]`)
24
- ),
25
-
26
- // Task List
27
- create(Box, {flexDirection: 'column', flexGrow: 1},
28
- ...tasks.map((t, idx) => {
29
- const isActive = t.id === activeTaskId;
30
- const color = STATUC_COLORS[t.status] || 'white';
31
- return create(
32
- Box,
33
- {key: t.id, marginBottom: 0,
34
- borderStyle: isActive ? 'bold' : 'single',
35
- borderColor: isActive ? color : 'gray',
36
- paddingX: 1, paddingY: 0},
37
- create(Box, {flexDirection: 'column'},
38
- // Task header
39
- isActive && renameMode
40
- ? create(Box, {flexDirection: 'row'},
41
- create(Text, {color: 'cyan'}, '→ Rename: '),
42
- create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
43
- : create(Box, {flexDirection: 'row', justifyContent: 'space-between'},
44
- create(Box, {flexDirection: 'row', alignItems: 'center'},
45
- create(Text, {color: isActive ? color : 'white', bold: isActive},
46
- `${isActive ? '→ ' : ' '}[${t.status.toUpperCase()}] ${t.name}`),
47
- t.status === 'running' && create(Text, {color: 'green'}, ' ⚡'),
48
- t.status === 'failed' && create(Text, {color: 'red'}, ' ✗'),
49
- t.status === 'killed' && create(Text, {color: 'yellow'}, ' ⚠')
50
- ),
51
- create(Text, {dimColor: true}, `#${idx + 1}`)
52
- ),
53
- // Mini log preview for active task
54
- isActive && logLines.length > 0 && create(
55
- Box, {marginTop: 0, paddingLeft: 2},
56
- ...visibleLogs.map((line, i) =>
57
- create(Text, {key: i, dimColor: true},
58
- `${i === visibleLogs.length - 1 ? '>' : '·'} ${line.slice(0, 60)}`)
59
- )
60
- )
61
- )
62
- );
63
- }),
64
- !tasks.length && create(
65
- Box, {alignItems: 'center', justifyContent: 'center', flexGrow: 1},
66
- create(Text, {dimColor: true, bold: true}, 'No active tasks'),
67
- create(Text, {dimColor: true}, 'Run a command to see it here!')
68
- )
69
- ),
70
-
71
- // Footer
72
- create(Box, {marginTop: 1, flexDirection: 'column'},
73
- create(Box, {flexDirection: 'row', justifyContent: 'space-between'},
74
- create(Text, {dimColor: true}, '↑/↓: Navigate | Enter: Select'),
75
- create(Text, {dimColor: true}, 'Shift+K: Kill | Shift+R: Rename')
76
- ),
77
- create(Text, {dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
78
- )
9
+ {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
10
+ create(Text, {bold: true, color: 'yellow'}, '🛰️ Orbit Task Manager | Background Processes'),
11
+ create(Text, {dimColor: true, marginBottom: 1}, 'Up/Down: focus, Shift+K: Force Kill, Shift+R: Rename'),
12
+ ...tasks.map(t => create(
13
+ Box,
14
+ {key: t.id, marginBottom: 0, flexDirection: 'column'},
15
+ t.id === activeTaskId && renameMode
16
+ ? create(Box, {flexDirection: 'row'}, create(Text, {color: 'cyan'}, '→ Rename to: '), create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
17
+ : create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? '→' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
18
+ )),
19
+ !tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
20
+ create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
79
21
  );
80
22
  });
81
23
 
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { checkBinary, hasProjectFile } from './utils.js';
3
+ import { checkBinary } from './utils.js';
4
4
 
5
5
  function parseCsProj(content) {
6
6
  const metadata = {
@@ -78,7 +78,8 @@ export default {
78
78
  publish: { label: 'Dotnet publish', command: ['dotnet', 'publish'], source: 'builtin' }
79
79
  };
80
80
 
81
- if (hasProjectFile(projectPath, '*.sln')) {
81
+ const hasSln = fs.readdirSync(projectPath).some(f => f.endsWith('.sln'));
82
+ if (hasSln) {
82
83
  commands['restore-sl'] = { label: 'Restore Solution', command: ['dotnet', 'restore'], source: 'builtin' };
83
84
  }
84
85
 
@@ -73,7 +73,7 @@ export const builtInFrameworks = [
73
73
  languages: ['Node.js'],
74
74
  priority: 106,
75
75
  match(project) {
76
- return dependencyMatches(project, 'koa') && !dependencyMatches(project, 'koa-router');
76
+ return dependencyMatches(project, 'koa');
77
77
  },
78
78
  commands(project) {
79
79
  const pm = project.metadata?.packageManager || 'npm';
@@ -324,9 +324,9 @@ export const builtInFrameworks = [
324
324
  description: 'Modern fast web framework for Python',
325
325
  languages: ['Python'],
326
326
  priority: 112,
327
- match(project) {
328
- return dependencyMatches(project, 'fastapi') || hasProjectFile(project.path, 'main.py');
329
- },
327
+ match(project) {
328
+ return dependencyMatches(project, 'fastapi');
329
+ },
330
330
  commands(project) {
331
331
  const pm = project.metadata?.packageManager || 'pip';
332
332
  const isUV = pm === 'uv';
@@ -366,9 +366,9 @@ export const builtInFrameworks = [
366
366
  description: 'Django web application',
367
367
  languages: ['Python'],
368
368
  priority: 110,
369
- match(project) {
370
- return dependencyMatches(project, 'django') || hasProjectFile(project.path, 'manage.py');
371
- },
369
+ match(project) {
370
+ return dependencyMatches(project, 'django');
371
+ },
372
372
  commands(project) {
373
373
  const pm = project.metadata?.packageManager || 'pip';
374
374
  const isUV = pm === 'uv';
@@ -636,9 +636,9 @@ export const builtInFrameworks = [
636
636
  description: 'Spring Boot Java framework',
637
637
  languages: ['Java'],
638
638
  priority: 115,
639
- match(project) {
640
- return dependencyMatches(project, 'spring-boot') || dependencyMatches(project, 'org.springframework.boot') || hasProjectFile(project.path, 'pom.xml') || hasProjectFile(project.path, 'build.gradle');
641
- },
639
+ match(project) {
640
+ return dependencyMatches(project, 'spring-boot') || dependencyMatches(project, 'org.springframework.boot');
641
+ },
642
642
  commands(project) {
643
643
  if (hasProjectFile(project.path, 'pom.xml')) {
644
644
  return {
@@ -665,9 +665,9 @@ export const builtInFrameworks = [
665
665
  description: 'Kubernetes Native Java stack',
666
666
  languages: ['Java'],
667
667
  priority: 108,
668
- match(project) {
669
- return dependencyMatches(project, 'quarkus') || hasProjectFile(project.path, 'pom.xml');
670
- },
668
+ match(project) {
669
+ return dependencyMatches(project, 'quarkus') || dependencyMatches(project, 'io.quarkus');
670
+ },
671
671
  commands() {
672
672
  return {
673
673
  run: { label: 'Quarkus Dev', command: ['mvn', 'quarkus:dev'], source: 'framework' },
@@ -683,9 +683,9 @@ export const builtInFrameworks = [
683
683
  description: 'Modern JVM-based framework',
684
684
  languages: ['Java'],
685
685
  priority: 106,
686
- match(project) {
687
- return dependencyMatches(project, 'micronaut') || hasProjectFile(project.path, 'pom.xml');
688
- },
686
+ match(project) {
687
+ return dependencyMatches(project, 'micronaut') || dependencyMatches(project, 'io.micronaut');
688
+ },
689
689
  commands() {
690
690
  return {
691
691
  run: { label: 'Micronaut Run', command: ['./mvnw', 'run'], source: 'framework' },
@@ -757,9 +757,9 @@ export const builtInFrameworks = [
757
757
  description: 'Ruby on Rails framework',
758
758
  languages: ['Ruby'],
759
759
  priority: 110,
760
- match(project) {
761
- return hasProjectFile(project.path, 'bin/rails') || hasProjectFile(project.path, 'config/application.rb') || hasProjectFile(project.path, 'Gemfile.lock');
762
- },
760
+ match(project) {
761
+ return dependencyMatches(project, 'rails') || hasProjectFile(project.path, 'bin/rails');
762
+ },
763
763
  commands() {
764
764
  return {
765
765
  install: { label: 'Bundle install', command: ['bundle', 'install'], source: 'framework' },
@@ -776,9 +776,9 @@ export const builtInFrameworks = [
776
776
  description: 'Sinatra DSL for quickly creating web applications',
777
777
  languages: ['Ruby'],
778
778
  priority: 105,
779
- match(project) {
780
- return dependencyMatches(project, 'sinatra') || hasProjectFile(project.path, 'config.ru');
781
- },
779
+ match(project) {
780
+ return dependencyMatches(project, 'sinatra');
781
+ },
782
782
  commands() {
783
783
  return {
784
784
  install: { label: 'Bundle install', command: ['bundle', 'install'], source: 'framework' },
@@ -795,9 +795,9 @@ export const builtInFrameworks = [
795
795
  description: 'ASP.NET Core web framework',
796
796
  languages: ['.NET'],
797
797
  priority: 110,
798
- match(project) {
799
- return hasProjectFile(project.path, 'Program.cs') && (hasProjectFile(project.path, '*.csproj') || hasProjectFile(project.path, '*.fsproj'));
800
- },
798
+ match(project) {
799
+ return dependencyMatches(project, 'Microsoft.AspNetCore') || dependencyMatches(project, 'Microsoft.AspNetCore.App');
800
+ },
801
801
  commands() {
802
802
  return {
803
803
  install: { label: 'Dotnet restore', command: ['dotnet', 'restore'], source: 'framework' },
@@ -814,9 +814,9 @@ export const builtInFrameworks = [
814
814
  description: 'Blazor interactive web UI with .NET',
815
815
  languages: ['.NET'],
816
816
  priority: 105,
817
- match(project) {
818
- return dependencyMatches(project, 'Microsoft.AspNetCore.Components.Web') || hasProjectFile(project.path, '*.csproj');
819
- },
817
+ match(project) {
818
+ return dependencyMatches(project, 'Microsoft.AspNetCore.Components.Web') || dependencyMatches(project, 'Blazor');
819
+ },
820
820
  commands() {
821
821
  return {
822
822
  run: { label: 'Blazor Run', command: ['dotnet', 'run'], source: 'framework' },
@@ -46,16 +46,16 @@ function detectGoFrameworks(deps) {
46
46
  const frameworks = [];
47
47
  const depStr = deps.join(' ').toLowerCase();
48
48
 
49
- if (depStr.includes('gin') || depStr.includes('gin-gonic')) frameworks.push({ name: 'Gin', icon: '🍸' });
50
- if (depStr.includes('echo') || depStr.includes('labstack/echo')) frameworks.push({ name: 'Echo', icon: '🔊' });
51
- if (depStr.includes('fiber') || depStr.includes('gofiber')) frameworks.push({ name: 'Fiber', icon: '🔥' });
52
- if (depStr.includes('chi')) frameworks.push({ name: 'Chi', icon: '🤝' });
49
+ if (/\bgin\b/.test(depStr) || depStr.includes('gin-gonic')) frameworks.push({ name: 'Gin', icon: '🍸' });
50
+ if (depStr.includes('labstack/echo')) frameworks.push({ name: 'Echo', icon: '🔊' });
51
+ if (depStr.includes('gofiber')) frameworks.push({ name: 'Fiber', icon: '🔥' });
52
+ if (depStr.includes('go-chi/chi')) frameworks.push({ name: 'Chi', icon: '🤝' });
53
53
  if (depStr.includes('gorilla')) frameworks.push({ name: 'Gorilla', icon: '🦍' });
54
- if (depStr.includes('iris')) frameworks.push({ name: 'Iris', icon: '🌺' });
54
+ if (depStr.includes('iris-go')) frameworks.push({ name: 'Iris', icon: '🌺' });
55
55
  if (depStr.includes('beego')) frameworks.push({ name: 'Beego', icon: '🐝' });
56
56
  if (depStr.includes('revel')) frameworks.push({ name: 'Revel', icon: '🎉' });
57
57
  if (depStr.includes('gqlgen')) frameworks.push({ name: 'GQLGen', icon: '◼️' });
58
- if (depStr.includes('grpc')) frameworks.push({ name: 'gRPC', icon: '🔌' });
58
+ if (depStr.includes('/grpc')) frameworks.push({ name: 'gRPC', icon: '🔌' });
59
59
 
60
60
  return frameworks;
61
61
  }
@@ -122,7 +122,8 @@ export default {
122
122
  commands.install = { label: 'Maven install', command: [...mvnCmd, 'install'], source: 'builtin' };
123
123
  commands.build = { label: 'Maven package', command: [...mvnCmd, 'package'], source: 'builtin' };
124
124
  commands.test = { label: 'Maven test', command: [...mvnCmd, 'test'], source: 'builtin' };
125
- commands.run = { label: 'Maven spring-boot:run', command: [...mvnCmd, 'spring-boot:run'], source: 'builtin' };
125
+ const isSpring = frameworks.some(f => f.name === 'Spring Boot');
126
+ commands.run = { label: isSpring ? 'Maven spring-boot:run' : 'Maven compile', command: isSpring ? [...mvnCmd, 'spring-boot:run'] : [...mvnCmd, 'compile'], source: 'builtin' };
126
127
  commands.clean = { label: 'Maven clean', command: [...mvnCmd, 'clean'], source: 'builtin' };
127
128
  }
128
129
 
@@ -29,7 +29,6 @@ function detectNodeProjectType(pkg) {
29
29
  if (allDeps['nuxt']) return { type: 'Nuxt', icon: '🟢' };
30
30
  if (allDeps['vite']) return { type: 'Vite', icon: '⚡' };
31
31
  if (allDeps['electron']) return { type: 'Electron', icon: '⚛️' };
32
- if (allDeps['typescript']) return { type: 'TypeScript', icon: '🔷' };
33
32
  return { type: 'Node.js', icon: '🟢' };
34
33
  }
35
34
 
@@ -117,6 +116,8 @@ export default {
117
116
  setupHints.push('This is a monorepo with workspaces: ' + workspaces.join(', '));
118
117
  }
119
118
 
119
+ const detectedFrameworks = projectType.type !== 'Node.js' ? [{ name: projectType.type, icon: projectType.icon }] : [];
120
+
120
121
  return {
121
122
  id: `${projectPath}::node`,
122
123
  path: projectPath,
@@ -129,7 +130,7 @@ export default {
129
130
  manifest: path.basename(manifest),
130
131
  description: pkg.description || projectType.type,
131
132
  missingBinaries,
132
- frameworks: [{ name: projectType.type, icon: projectType.icon }],
133
+ frameworks: detectedFrameworks,
133
134
  extra: {
134
135
  scripts: Object.keys(scripts),
135
136
  setupHints,
@@ -55,7 +55,7 @@ export default {
55
55
 
56
56
  const commands = {
57
57
  install: { label: 'Composer install', command: ['composer', 'install'], source: 'builtin' },
58
- update: { label: 'Composer update', command: ['composer', 'update'], source: 'builtin' }
58
+ update: { label: 'Composer update all', command: ['composer', 'update'], source: 'builtin' }
59
59
  };
60
60
 
61
61
  if (hasProjectFile(projectPath, 'artisan')) {
@@ -123,10 +123,17 @@ export default {
123
123
  icon: '🐍',
124
124
  priority: 95,
125
125
  files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile', 'manage.py'],
126
- binaries: ['python3', 'python', 'uv'].filter(Boolean),
127
- async build(projectPath, manifest) {
128
- const missingBinaries = this.binaries.filter(b => !checkBinary(b));
129
- const pkgManager = getPythonPackageManager(projectPath);
126
+ binaries: ['python3', 'python', 'uv'],
127
+ async build(projectPath, manifest) {
128
+ const hasPython3 = checkBinary('python3');
129
+ const hasPython = checkBinary('python');
130
+ const hasUv = checkBinary('uv');
131
+ const hasRuntime = hasPython3 || hasPython || hasUv;
132
+ const missingBinaries = [];
133
+ if (!hasRuntime) {
134
+ missingBinaries.push('python');
135
+ }
136
+ const pkgManager = getPythonPackageManager(projectPath);
130
137
  const isUV = pkgManager === 'uv';
131
138
  const isPoetry = pkgManager === 'poetry';
132
139
  const isPipenv = pkgManager === 'pipenv';
@@ -58,7 +58,7 @@ export default {
58
58
  const commands = {
59
59
  install: { label: 'Bundle install', command: ['bundle', 'install'], source: 'builtin' },
60
60
  update: { label: 'Bundle update', command: ['bundle', 'update'], source: 'builtin' },
61
- console: { label: 'Ruby console', command: ['ruby', '-e', 'puts "IRB"'], source: 'builtin' }
61
+ console: { label: 'Ruby console', command: ['irb'], source: 'builtin' }
62
62
  };
63
63
 
64
64
  if (hasProjectFile(projectPath, 'bin/rails')) {
@@ -40,8 +40,8 @@ function parseCargoToml(content) {
40
40
  }
41
41
 
42
42
  if (inDependencies && trimmed && !trimmed.startsWith('#')) {
43
- const depName = trimmed.split('=')[0]?.trim() || trimmed.split('{')[0]?.trim();
44
- if (depName) metadata.dependencies.push(depName);
43
+ const depName = trimmed.split(/[={]/)[0]?.trim();
44
+ if (depName && !depName.startsWith('#')) metadata.dependencies.push(depName);
45
45
  }
46
46
 
47
47
  if (inBin && trimmed.startsWith('name')) {
@@ -98,13 +98,12 @@ export function dependencyMatches(project, needle) {
98
98
  return '';
99
99
  }).filter(Boolean);
100
100
  const target = needle.toLowerCase();
101
- return dependencies.some((value) =>
102
- value === target ||
103
- value.startsWith(`${target}@`) ||
104
- value.includes(`/${target}`) ||
105
- value.startsWith(`${target}/`) ||
106
- value.endsWith(`/${target}`)
107
- );
101
+ return dependencies.some((value) => {
102
+ if (value === target) return true;
103
+ if (value.startsWith(`${target}@`) || value.startsWith(`${target}==`) || value.startsWith(`${target}>=`) || value.startsWith(`${target}~=`)) return true;
104
+ if (value.startsWith(`${target}/`) || value.endsWith(`/${target}`)) return true;
105
+ return false;
106
+ });
108
107
  }
109
108
 
110
109
  export function parseCommandTokens(value) {