project-compass 4.2.0 โ†’ 4.3.0

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,5 +1,72 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
- import { checkBinary } from './utils.js';
3
+ import { checkBinary, hasProjectFile } from './utils.js';
4
+
5
+ function parseGoMod(content) {
6
+ const metadata = {
7
+ module: '',
8
+ goVersion: '',
9
+ dependencies: []
10
+ };
11
+
12
+ const lines = content.split('\n');
13
+ let inRequire = false;
14
+
15
+ for (const line of lines) {
16
+ const trimmed = line.trim();
17
+
18
+ if (trimmed.startsWith('module ')) {
19
+ metadata.module = trimmed.split(/\s+/)[1]?.replace(/"/g, '') || '';
20
+ }
21
+ if (trimmed.startsWith('go ')) {
22
+ metadata.goVersion = trimmed.split(/\s+/)[1] || '';
23
+ }
24
+
25
+ if (trimmed === 'require (') {
26
+ inRequire = true;
27
+ continue;
28
+ }
29
+ if (trimmed === ')') {
30
+ inRequire = false;
31
+ continue;
32
+ }
33
+
34
+ if (inRequire || (trimmed.includes(' ') && !trimmed.startsWith('//'))) {
35
+ const parts = trimmed.split(/\s+/);
36
+ if (parts[0] && !parts[0].startsWith('//')) {
37
+ metadata.dependencies.push(parts[0].replace(/"/g, ''));
38
+ }
39
+ }
40
+ }
41
+
42
+ return metadata;
43
+ }
44
+
45
+ function detectGoFrameworks(deps) {
46
+ const frameworks = [];
47
+ const depStr = deps.join(' ').toLowerCase();
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: '๐Ÿค' });
53
+ if (depStr.includes('gorilla')) frameworks.push({ name: 'Gorilla', icon: '๐Ÿฆ' });
54
+ if (depStr.includes('iris')) frameworks.push({ name: 'Iris', icon: '๐ŸŒบ' });
55
+ if (depStr.includes('beego')) frameworks.push({ name: 'Beego', icon: '๐Ÿ' });
56
+ if (depStr.includes('revel')) frameworks.push({ name: 'Revel', icon: '๐ŸŽ‰' });
57
+ if (depStr.includes('gqlgen')) frameworks.push({ name: 'GQLGen', icon: 'โ—ผ๏ธ' });
58
+ if (depStr.includes('grpc')) frameworks.push({ name: 'gRPC', icon: '๐Ÿ”Œ' });
59
+
60
+ return frameworks;
61
+ }
62
+
63
+ function findGoEntry(projectPath) {
64
+ const possibleEntries = ['main.go', 'cmd/main.go', 'app.go', 'server.go'];
65
+ for (const entry of possibleEntries) {
66
+ if (hasProjectFile(projectPath, entry)) return entry;
67
+ }
68
+ return 'main.go';
69
+ }
3
70
 
4
71
  export default {
5
72
  type: 'go',
@@ -10,25 +77,59 @@ export default {
10
77
  binaries: ['go'],
11
78
  async build(projectPath, manifest) {
12
79
  const missingBinaries = this.binaries.filter(b => !checkBinary(b));
80
+ let metadata = { module: '', goVersion: '', dependencies: [] };
81
+ let frameworks = [];
82
+ let entryPoint = 'main.go';
83
+
84
+ const goModPath = path.join(projectPath, 'go.mod');
85
+ if (fs.existsSync(goModPath)) {
86
+ const content = fs.readFileSync(goModPath, 'utf-8');
87
+ metadata = parseGoMod(content);
88
+ frameworks = detectGoFrameworks(metadata.dependencies);
89
+ }
90
+
91
+ entryPoint = findGoEntry(projectPath);
92
+
93
+ const commands = {
94
+ install: { label: 'Go tidy', command: ['go', 'mod', 'tidy'], source: 'builtin' },
95
+ build: { label: 'Go build', command: ['go', 'build', '-o', 'app', '.'], source: 'builtin' },
96
+ test: { label: 'Go test', command: ['go', 'test', './...'], source: 'builtin' },
97
+ run: { label: 'Go run', command: ['go', 'run', entryPoint], source: 'builtin' },
98
+ fmt: { label: 'Go fmt', command: ['go', 'fmt', './...'], source: 'builtin' },
99
+ vet: { label: 'Go vet', command: ['go', 'vet', './...'], source: 'builtin' }
100
+ };
101
+
102
+ const setupHints = [];
103
+ if (missingBinaries.length > 0) {
104
+ setupHints.push('Install Go from https://go.dev/dl/');
105
+ }
106
+ if (metadata.dependencies.length > 0) {
107
+ setupHints.push('Run go mod tidy to ensure dependencies');
108
+ }
109
+ if (metadata.goVersion) {
110
+ setupHints.push(`Requires Go ${metadata.goVersion}+`);
111
+ }
112
+
13
113
  return {
14
114
  id: `${projectPath}::go`,
15
115
  path: projectPath,
16
- name: path.basename(projectPath),
116
+ name: metadata.module || path.basename(projectPath),
17
117
  type: 'Go',
18
118
  icon: '๐Ÿน',
19
119
  priority: this.priority,
20
- commands: {
21
- install: { label: 'Go tidy', command: ['go', 'mod', 'tidy'] },
22
- build: { label: 'Go build', command: ['go', 'build', './...'] },
23
- test: { label: 'Go test', command: ['go', 'test', './...'] },
24
- run: { label: 'Go run', command: ['go', 'run', '.'] }
120
+ commands,
121
+ metadata: {
122
+ ...metadata,
123
+ packageManager: 'go',
124
+ entryPoint
25
125
  },
26
- metadata: {},
27
126
  manifest: path.basename(manifest),
28
- description: '',
127
+ description: frameworks.map(f => f.name).join(', ') || `Go ${metadata.goVersion || ''}`,
29
128
  missingBinaries,
129
+ frameworks,
30
130
  extra: {
31
- setupHints: ['go mod tidy', 'Ensure Go toolchain is installed']
131
+ setupHints,
132
+ entryPoint
32
133
  }
33
134
  };
34
135
  }
@@ -1,6 +1,79 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
3
  import { checkBinary, hasProjectFile } from './utils.js';
3
4
 
5
+ function parsePomXml(content) {
6
+ const metadata = {
7
+ groupId: '',
8
+ artifactId: '',
9
+ version: '',
10
+ name: '',
11
+ description: '',
12
+ dependencies: []
13
+ };
14
+
15
+ const groupIdMatch = content.match(/<groupId>([^<]+)<\/groupId>/);
16
+ if (groupIdMatch) metadata.groupId = groupIdMatch[1];
17
+
18
+ const artifactIdMatch = content.match(/<artifactId>([^<]+)<\/artifactId>/);
19
+ if (artifactIdMatch) metadata.artifactId = artifactIdMatch[1];
20
+
21
+ const versionMatch = content.match(/<version>([^<]+)<\/version>/);
22
+ if (versionMatch) metadata.version = versionMatch[1];
23
+
24
+ const nameMatch = content.match(/<name>([^<]+)<\/name>/);
25
+ if (nameMatch) metadata.name = nameMatch[1];
26
+
27
+ const descMatch = content.match(/<description>([^<]+)<\/description>/);
28
+ if (descMatch) metadata.description = descMatch[1];
29
+
30
+ const depMatches = content.matchAll(/<dependency>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?<\/dependency>/g);
31
+ for (const match of depMatches) {
32
+ if (match[1]) metadata.dependencies.push(match[1]);
33
+ }
34
+
35
+ return metadata;
36
+ }
37
+
38
+ function parseGradleBuild(content) {
39
+ const metadata = {
40
+ name: '',
41
+ version: '',
42
+ description: '',
43
+ dependencies: []
44
+ };
45
+
46
+ const nameMatch = content.match(/rootProject\.name\s*=\s*['"]([^'"]+)['"]/);
47
+ if (nameMatch) metadata.name = nameMatch[1];
48
+
49
+ const versionMatch = content.match(/version\s*=\s*['"]([^'"]+)['"]/);
50
+ if (versionMatch) metadata.version = versionMatch[1];
51
+
52
+ const depMatches = content.matchAll(/implementation\s+['"]([^'"]+)['"]/g);
53
+ for (const match of depMatches) {
54
+ if (match[1]) metadata.dependencies.push(match[1].split(':')[1] || match[1]);
55
+ }
56
+
57
+ return metadata;
58
+ }
59
+
60
+ function detectJavaFrameworks(deps) {
61
+ const frameworks = [];
62
+ const depStr = deps.join(' ').toLowerCase();
63
+
64
+ if (depStr.includes('spring-boot') || depStr.includes('springframework')) frameworks.push({ name: 'Spring Boot', icon: '๐Ÿƒ' });
65
+ if (depStr.includes('quarkus')) frameworks.push({ name: 'Quarkus', icon: 'โšก' });
66
+ if (depStr.includes('micronaut')) frameworks.push({ name: 'Micronaut', icon: '๐Ÿš€' });
67
+ if (depStr.includes('play')) frameworks.push({ name: 'Play Framework', icon: '๐ŸŽญ' });
68
+ if (depStr.includes('vertx')) frameworks.push({ name: 'Vert.x', icon: '๐Ÿ”‹' });
69
+ if (depStr.includes('dropwizard')) frameworks.push({ name: 'Dropwizard', icon: '๐Ÿ“Š' });
70
+ if (depStr.includes(('hibernate'))) frameworks.push({ name: 'Hibernate', icon: '๐Ÿ—„๏ธ' });
71
+ if (depStr.includes('junit')) frameworks.push({ name: 'JUnit', icon: 'โœ…' });
72
+ if (depStr.includes('lombok')) frameworks.push({ name: 'Lombok', icon: '๐Ÿ”ง' });
73
+
74
+ return frameworks;
75
+ }
76
+
4
77
  export default {
5
78
  type: 'java',
6
79
  label: 'Java',
@@ -12,34 +85,76 @@ export default {
12
85
  const missingBinaries = this.binaries.filter(b => !checkBinary(b));
13
86
  const hasMvnw = hasProjectFile(projectPath, 'mvnw');
14
87
  const hasGradlew = hasProjectFile(projectPath, 'gradlew');
88
+ const isMaven = hasProjectFile(projectPath, 'pom.xml');
89
+ const isGradle = hasProjectFile(projectPath, 'build.gradle') || hasProjectFile(projectPath, 'build.gradle.kts');
90
+
91
+ let metadata = { name: '', version: '', description: '', dependencies: [] };
92
+ let frameworks = [];
93
+ let buildTool = isGradle ? 'gradle' : 'maven';
94
+
95
+ if (isMaven) {
96
+ const pomPath = path.join(projectPath, 'pom.xml');
97
+ if (fs.existsSync(pomPath)) {
98
+ const content = fs.readFileSync(pomPath, 'utf-8');
99
+ metadata = parsePomXml(content);
100
+ frameworks = detectJavaFrameworks(metadata.dependencies);
101
+ }
102
+ } else if (isGradle) {
103
+ const gradleFile = hasProjectFile(projectPath, 'build.gradle.kts') ? 'build.gradle.kts' : 'build.gradle';
104
+ const gradlePath = path.join(projectPath, gradleFile);
105
+ if (fs.existsSync(gradlePath)) {
106
+ const content = fs.readFileSync(gradlePath, 'utf-8');
107
+ metadata = parseGradleBuild(content);
108
+ frameworks = detectJavaFrameworks(metadata.dependencies);
109
+ }
110
+ }
111
+
15
112
  const commands = {};
16
- if (hasGradlew) {
17
- commands.install = { label: 'Gradle resolve', command: ['./gradlew', 'dependencies'] };
18
- commands.build = { label: 'Gradle build', command: ['./gradlew', 'build'] };
19
- commands.test = { label: 'Gradle test', command: ['./gradlew', 'test'] };
20
- } else if (hasMvnw) {
21
- commands.install = { label: 'Maven install', command: ['./mvnw', 'install'] };
22
- commands.build = { label: 'Maven package', command: ['./mvnw', 'package'] };
23
- commands.test = { label: 'Maven test', command: ['./mvnw', 'test'] };
113
+ if (isGradle) {
114
+ const gradleCmd = hasGradlew ? ['./gradlew'] : ['gradle'];
115
+ commands.install = { label: 'Gradle dependencies', command: [...gradleCmd, 'dependencies'], source: 'builtin' };
116
+ commands.build = { label: 'Gradle build', command: [...gradleCmd, 'build'], source: 'builtin' };
117
+ commands.test = { label: 'Gradle test', command: [...gradleCmd, 'test'], source: 'builtin' };
118
+ commands.run = { label: 'Gradle run', command: [...gradleCmd, 'run'], source: 'builtin' };
119
+ commands.clean = { label: 'Gradle clean', command: [...gradleCmd, 'clean'], source: 'builtin' };
24
120
  } else {
25
- commands.build = { label: 'Maven package', command: ['mvn', 'package'] };
26
- commands.test = { label: 'Maven test', command: ['mvn', 'test'] };
121
+ const mvnCmd = hasMvnw ? ['./mvnw'] : ['mvn'];
122
+ commands.install = { label: 'Maven install', command: [...mvnCmd, 'install'], source: 'builtin' };
123
+ commands.build = { label: 'Maven package', command: [...mvnCmd, 'package'], source: 'builtin' };
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' };
126
+ commands.clean = { label: 'Maven clean', command: [...mvnCmd, 'clean'], source: 'builtin' };
127
+ }
128
+
129
+ const setupHints = [];
130
+ if (missingBinaries.length > 0) {
131
+ setupHints.push('Install JDK 17+ from https://adoptium.net/');
132
+ }
133
+ if (isMaven) {
134
+ setupHints.push('Run ' + (hasMvnw ? './mvnw install' : 'mvn install') + ' to build');
135
+ } else if (isGradle) {
136
+ setupHints.push('Run ' + (hasGradlew ? './gradlew build' : 'gradle build') + ' to build');
27
137
  }
28
138
 
29
139
  return {
30
140
  id: `${projectPath}::java`,
31
141
  path: projectPath,
32
- name: path.basename(projectPath),
142
+ name: metadata.name || metadata.artifactId || path.basename(projectPath),
33
143
  type: 'Java',
34
144
  icon: 'โ˜•',
35
145
  priority: this.priority,
36
146
  commands,
37
- metadata: {},
147
+ metadata: {
148
+ ...metadata,
149
+ packageManager: buildTool
150
+ },
38
151
  manifest: path.basename(manifest),
39
- description: '',
152
+ description: metadata.description || frameworks.map(f => f.name).join(', '),
40
153
  missingBinaries,
154
+ frameworks,
41
155
  extra: {
42
- setupHints: ['Install JDK 17+ and run ./mvnw install or ./gradlew build']
156
+ setupHints,
157
+ buildTool
43
158
  }
44
159
  };
45
160
  }
@@ -9,7 +9,43 @@ function gatherNodeDependencies(pkg) {
9
9
  Object.keys(pkg[key]).forEach((name) => deps.add(name));
10
10
  }
11
11
  });
12
- return Array.from(deps);
12
+ return Array.from(deps).map(dep => ({ name: dep, version: pkg.dependencies?.[dep] || pkg.devDependencies?.[dep] || 'latest' }));
13
+ }
14
+
15
+ function detectNodeProjectType(pkg) {
16
+ const deps = pkg.dependencies || {};
17
+ const devDeps = pkg.devDependencies || {};
18
+ const allDeps = { ...deps, ...devDeps };
19
+
20
+ if (allDeps['next']) return { type: 'Next.js', icon: '๐Ÿงญ' };
21
+ if (allDeps['react'] && (allDeps['react-scripts'] || allDeps['vite'])) return { type: 'React', icon: 'โš›๏ธ' };
22
+ if (allDeps['vue']) return { type: 'Vue.js', icon: '๐ŸŸฉ' };
23
+ if (allDeps['@nestjs/core']) return { type: 'NestJS', icon: '๐Ÿ›ก๏ธ' };
24
+ if (allDeps['express']) return { type: 'Express', icon: '๐Ÿš‚' };
25
+ if (allDeps['fastify']) return { type: 'Fastify', icon: 'โšก' };
26
+ if (allDeps['koa']) return { type: 'Koa', icon: '๐ŸŽ‹' };
27
+ if (allDeps['@sveltejs/kit'] || allDeps['svelte']) return { type: 'Svelte', icon: '๐Ÿงก' };
28
+ if (allDeps['astro']) return { type: 'Astro', icon: '๐Ÿš€' };
29
+ if (allDeps['nuxt']) return { type: 'Nuxt', icon: '๐ŸŸข' };
30
+ if (allDeps['vite']) return { type: 'Vite', icon: 'โšก' };
31
+ if (allDeps['electron']) return { type: 'Electron', icon: 'โš›๏ธ' };
32
+ if (allDeps['typescript']) return { type: 'TypeScript', icon: '๐Ÿ”ท' };
33
+ return { type: 'Node.js', icon: '๐ŸŸข' };
34
+ }
35
+
36
+ function findEntryPoint(projectPath, pkg) {
37
+ const possibleEntries = [
38
+ 'src/index.js', 'src/index.ts', 'index.js', 'index.ts',
39
+ 'src/main.js', 'src/main.ts', 'main.js', 'main.ts',
40
+ 'app.js', 'app.ts', 'server.js', 'server.ts'
41
+ ];
42
+
43
+ for (const entry of possibleEntries) {
44
+ if (hasProjectFile(projectPath, entry)) return entry;
45
+ }
46
+
47
+ if (pkg.main) return pkg.main;
48
+ return null;
13
49
  }
14
50
 
15
51
  export default {
@@ -29,53 +65,75 @@ export default {
29
65
  const pkg = JSON.parse(content);
30
66
  const pm = getPackageManager(projectPath);
31
67
  const scripts = pkg.scripts || {};
68
+ const projectType = detectNodeProjectType(pkg);
69
+ const entryPoint = findEntryPoint(projectPath, pkg);
70
+
32
71
  const commands = {};
33
72
  const preferScript = (targetKey, names, labelText) => {
34
73
  for (const name of names) {
35
74
  if (Object.prototype.hasOwnProperty.call(scripts, name)) {
36
- commands[targetKey] = { label: labelText, command: [pm, 'run', name] };
75
+ commands[targetKey] = { label: labelText, command: [pm, 'run', name], source: 'builtin' };
37
76
  break;
38
77
  }
39
78
  }
40
79
  };
41
- commands.install = { label: 'Install', command: [pm, 'install'] };
80
+
81
+ commands.install = { label: `${pm} install`, command: [pm, 'install'], source: 'builtin' };
42
82
  preferScript('build', ['build', 'compile', 'dist'], 'Build');
43
83
  preferScript('test', ['test', 'check', 'spec'], 'Test');
44
- preferScript('run', ['start', 'dev', 'serve', 'run'], 'Start');
84
+ preferScript('dev', ['dev', 'start:dev'], 'Dev');
85
+ preferScript('run', ['start', 'serve', 'run'], 'Start');
86
+
87
+ if (entryPoint && !commands.run && !commands.dev) {
88
+ commands.run = { label: 'Run', command: ['node', entryPoint], source: 'builtin' };
89
+ }
90
+
45
91
  if (Object.prototype.hasOwnProperty.call(scripts, 'lint')) {
46
- commands.lint = { label: 'Lint', command: [pm, 'run', 'lint'] };
92
+ commands.lint = { label: 'Lint', command: [pm, 'run', 'lint'], source: 'builtin' };
93
+ }
94
+ if (Object.prototype.hasOwnProperty.call(scripts, 'format')) {
95
+ commands.format = { label: 'Format', command: [pm, 'run', 'format'], source: 'builtin' };
47
96
  }
48
97
 
49
98
  const metadata = {
50
99
  dependencies: gatherNodeDependencies(pkg),
51
100
  scripts,
52
101
  packageJson: pkg,
53
- packageManager: getPackageManager(projectPath)
102
+ packageManager: pm,
103
+ projectType: projectType.type,
104
+ entryPoint
54
105
  };
55
106
 
56
107
  const setupHints = [];
57
108
  if (metadata.dependencies.length) {
58
- setupHints.push('Run npm install to fetch dependencies.');
59
- if (hasProjectFile(projectPath, 'yarn.lock')) {
109
+ setupHints.push(`Run ${pm} install to fetch dependencies.`);
110
+ if (pm === 'npm' && hasProjectFile(projectPath, 'yarn.lock')) {
60
111
  setupHints.push('Or run yarn install if you prefer Yarn.');
61
112
  }
62
113
  }
114
+
115
+ const workspaces = pkg.workspaces || [];
116
+ if (workspaces.length > 0) {
117
+ setupHints.push('This is a monorepo with workspaces: ' + workspaces.join(', '));
118
+ }
63
119
 
64
120
  return {
65
121
  id: `${projectPath}::node`,
66
122
  path: projectPath,
67
123
  name: pkg.name || path.basename(projectPath),
68
124
  type: 'Node.js',
69
- icon: '๐ŸŸข',
125
+ icon: projectType.icon,
70
126
  priority: this.priority,
71
127
  commands,
72
128
  metadata,
73
129
  manifest: path.basename(manifest),
74
- description: pkg.description || '',
130
+ description: pkg.description || projectType.type,
75
131
  missingBinaries,
132
+ frameworks: [{ name: projectType.type, icon: projectType.icon }],
76
133
  extra: {
77
134
  scripts: Object.keys(scripts),
78
- setupHints
135
+ setupHints,
136
+ workspaces
79
137
  }
80
138
  };
81
139
  }
@@ -1,13 +1,106 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
- import { checkBinary } from './utils.js';
3
+ import { checkBinary, hasProjectFile } from './utils.js';
4
+
5
+ function parseComposerJson(content) {
6
+ try {
7
+ const pkg = JSON.parse(content);
8
+ return {
9
+ name: pkg.name || '',
10
+ description: pkg.description || '',
11
+ dependencies: [
12
+ ...Object.keys(pkg.require || {}),
13
+ ...Object.keys(pkg['require-dev'] || {})
14
+ ],
15
+ scripts: pkg.scripts || {}
16
+ };
17
+ } catch {
18
+ return { name: '', description: '', dependencies: [], scripts: {} };
19
+ }
20
+ }
21
+
22
+ function detectPhpFrameworks(deps) {
23
+ const frameworks = [];
24
+ const depStr = deps.join(' ').toLowerCase();
25
+
26
+ if (depStr.includes('laravel/framework')) frameworks.push({ name: 'Laravel', icon: '๐Ÿงก' });
27
+ if (depStr.includes('symfony/symfony') || depStr.includes('symfony/framework-bundle')) frameworks.push({ name: 'Symfony', icon: '๐ŸŽต' });
28
+ if (depStr.includes('codeigniter4/framework')) frameworks.push({ name: 'CodeIgniter', icon: '๐Ÿ”ฅ' });
29
+ if (depStr.includes('cakephp/cakephp')) frameworks.push({ name: 'CakePHP', icon: '๐Ÿฐ' });
30
+ if (depStr.includes('slim/slim')) frameworks.push({ name: 'Slim', icon: '๐Ÿƒ' });
31
+ if (depStr.includes('lumen')) frameworks.push({ name: 'Lumen', icon: '๐Ÿ’ก' });
32
+ if (depStr.includes('phpunit/phpunit')) frameworks.push({ name: 'PHPUnit', icon: 'โœ…' });
33
+ if (depStr.includes('laravel/octane')) frameworks.push({ name: 'Laravel Octane', icon: '๐Ÿš€' });
34
+ return frameworks;
35
+ }
36
+
3
37
  export default {
4
- type: 'php', label: 'PHP', icon: '๐Ÿ˜', priority: 65, files: ['composer.json'], binaries: ['php', 'composer'],
38
+ type: 'php',
39
+ label: 'PHP',
40
+ icon: '๐Ÿ˜',
41
+ priority: 65,
42
+ files: ['composer.json'],
43
+ binaries: ['php', 'composer'],
5
44
  async build(projectPath, manifest) {
6
45
  const missingBinaries = this.binaries.filter(b => !checkBinary(b));
46
+ let metadata = { name: '', description: '', dependencies: [], scripts: {} };
47
+ let frameworks = [];
48
+
49
+ const composerPath = path.join(projectPath, 'composer.json');
50
+ if (fs.existsSync(composerPath)) {
51
+ const content = fs.readFileSync(composerPath, 'utf-8');
52
+ metadata = parseComposerJson(content);
53
+ frameworks = detectPhpFrameworks(metadata.dependencies);
54
+ }
55
+
56
+ const commands = {
57
+ install: { label: 'Composer install', command: ['composer', 'install'], source: 'builtin' },
58
+ update: { label: 'Composer update', command: ['composer', 'update'], source: 'builtin' }
59
+ };
60
+
61
+ if (hasProjectFile(projectPath, 'artisan')) {
62
+ commands.run = { label: 'Artisan serve', command: ['php', 'artisan', 'serve'], source: 'builtin' };
63
+ commands.test = { label: 'Artisan test', command: ['php', 'artisan', 'test'], source: 'builtin' };
64
+ commands.migrate = { label: 'Artisan migrate', command: ['php', 'artisan', 'migrate'], source: 'builtin' };
65
+ }
66
+
67
+ if (hasProjectFile(projectPath, 'bin/phpunit') || metadata.dependencies.includes('phpunit/phpunit')) {
68
+ commands.test = { label: 'PHPUnit', command: ['php', 'bin/phpunit'], source: 'builtin' };
69
+ }
70
+
71
+ if (hasProjectFile(projectPath, 'symfony.lock')) {
72
+ commands.run = { label: 'Symfony server', command: ['symfony', 'server:start'], source: 'builtin' };
73
+ commands.test = { label: 'Symfony test', command: ['php', 'bin/phpunit'], source: 'builtin' };
74
+ }
75
+
76
+ const setupHints = [];
77
+ if (missingBinaries.includes('composer')) {
78
+ setupHints.push('Install Composer: https://getcomposer.org/');
79
+ }
80
+ if (metadata.dependencies.length > 0) {
81
+ setupHints.push('Run composer install to fetch dependencies');
82
+ }
83
+
7
84
  return {
8
- id: `${projectPath}::php`, path: projectPath, name: path.basename(projectPath), type: 'PHP', icon: '๐Ÿ˜',
9
- priority: this.priority, commands: { install: { label: 'Composer install', command: ['composer', 'install'] } },
10
- metadata: {}, manifest: path.basename(manifest), description: '', missingBinaries, extra: {}
85
+ id: `${projectPath}::php`,
86
+ path: projectPath,
87
+ name: metadata.name || path.basename(projectPath),
88
+ type: 'PHP',
89
+ icon: '๐Ÿ˜',
90
+ priority: this.priority,
91
+ commands,
92
+ metadata: {
93
+ ...metadata,
94
+ packageManager: 'composer'
95
+ },
96
+ manifest: path.basename(manifest),
97
+ description: metadata.description || frameworks.map(f => f.name).join(', '),
98
+ missingBinaries,
99
+ frameworks,
100
+ extra: {
101
+ setupHints,
102
+ scripts: Object.keys(metadata.scripts)
103
+ }
11
104
  };
12
105
  }
13
106
  };