project-compass 2.0.0 → 2.0.2

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.
package/README.md CHANGED
@@ -79,12 +79,14 @@ Projects and details now occupy the same row, while the output panel takes its o
79
79
 
80
80
  ## Structure guide
81
81
 
82
- Press `S` to reveal the structure guide that lists which manifest files trigger each language detection (Node.js looks for `package.json`, Python looks for `pyproject.toml` or `requirements.txt`, Rust needs `Cargo.toml`, etc.). Use `H` to hide the help cards if you need every pixel for your output, then bring them back when you want a refresher.
82
+ Press `Ctrl+S` to reveal the structure guide that lists which manifest files trigger each language detection (Node.js looks for `package.json`, Python looks for `pyproject.toml` or `requirements.txt`, Rust needs `Cargo.toml`, etc.). Use `Ctrl+H` to hide the help cards if you need every pixel for your output, then bring them back when you want a refresher.
83
83
 
84
+ ## Detection & setup hints
84
85
 
85
- ## Project detection & setup hints
86
+ Project Compass now has a modular detection engine (`src/projectDetection.js`) that looks at manifests, frameworks, and optional plugins to identify what kind of project you are standing in. Every schema fills `extra.setupHints` (npm install, pip install, go mod tidy, etc.) and those hints show up under the detail view whenever a project needs bootstrapping.
87
+
88
+ Try the sample Python project at `/mnt/ramdisk/daily_builds/test_python_proj` (includes `pyproject.toml`, `requirements.txt`, and `app.py`) so you can run it via Project Compass and confirm stdin/input behavior.
86
89
 
87
- Project Compass now points at a dedicated detection module (`src/projectDetection.js`). It runs a set of SCHEMAS that look for Node.js, Python, Rust, Go, Java, Scala, PHP, Ruby, .NET, and shell/Makefile projects (plus a generic fallback), and it discovers any plugins you drop under `~/.project-compass/plugins.json`. Each detected project carries `setupHints` (e.g., `npm install`, `pip install -r requirements.txt`, `cargo fetch`) so you know what to do before running the commands listed in the detail view.
88
90
 
89
91
 
90
92
  ## Developer notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Ink-based project explorer that detects local repos and lets you build/test/run them without memorizing commands.",
5
5
  "main": "src/cli.js",
6
6
  "type": "module",
Binary file
Binary file
Binary file
package/src/cli.js CHANGED
@@ -524,9 +524,8 @@ const projectRows = [];
524
524
  label: 'Navigation',
525
525
  color: 'magenta',
526
526
  body: [
527
- '↑ / ↓ move the project focus',
528
- 'Enter opens or closes details',
529
- 'Shift + ↑ / ↓ scrolls only the log buffer',
527
+ '↑ / ↓ move the project focus, Enter toggles details',
528
+ 'Shift+↑ / Shift+↓ scroll only the output buffer',
530
529
  'Ctrl+H toggles cards, ? opens the overlay'
531
530
  ]
532
531
  },
@@ -534,10 +533,10 @@ const projectRows = [];
534
533
  label: 'Command flow',
535
534
  color: 'cyan',
536
535
  body: [
537
- 'B / T / R trigger build/test/run',
536
+ 'B / T / R trigger build/test/run shortcuts',
538
537
  '1-9 execute detail commands in order',
539
538
  'Ctrl+L reruns the last command you launched',
540
- 'Ctrl+C aborts, typing feeds stdin (buffer mirrors it)'
539
+ 'Ctrl+C aborts; typing feeds stdin (buffer mirrors it)'
541
540
  ]
542
541
  },
543
542
  {
@@ -545,8 +544,8 @@ const projectRows = [];
545
544
  color: 'yellow',
546
545
  body: [
547
546
  recentRuns.length ? `${recentRuns.length} runs recorded` : 'No runs yet · start with B/T/R',
548
- 'Ctrl+S shows the structure guide when unsure',
549
- 'Save custom commands with C → label|cmd'
547
+ 'Ctrl+S toggles the structure guide when unsure',
548
+ 'C → label|cmd saves a custom action'
550
549
  ]
551
550
  }
552
551
  ];
@@ -575,7 +574,7 @@ const projectRows = [];
575
574
  )
576
575
  )
577
576
  )
578
- : create(Text, {dimColor: true, marginTop: 1}, 'Help cards hidden · press H to show navigation, command flow, and recent runs.');
577
+ : create(Text, {dimColor: true, marginTop: 1}, 'Help cards hidden · press Ctrl+H to show navigation, command flow, and recent runs.');
579
578
 
580
579
  const structureGuide = showStructureGuide
581
580
  ? create(
@@ -587,7 +586,7 @@ const projectRows = [];
587
586
  marginTop: 1,
588
587
  padding: 1
589
588
  },
590
- create(Text, {color: 'cyan', bold: true}, 'Project structure guide · press S to hide'),
589
+ create(Text, {color: 'cyan', bold: true}, 'Project structure guide · press Ctrl+S to hide'),
591
590
  ...SCHEMA_GUIDE.map((entry) =>
592
591
  create(Text, {key: entry.type, dimColor: true}, `• ${entry.icon} ${entry.label}: ${entry.files.join(', ')}`)
593
592
  ),
@@ -606,18 +605,18 @@ const projectRows = [];
606
605
  padding: 1
607
606
  },
608
607
  create(Text, {color: 'cyan', bold: true}, 'Help overlay · press ? to hide'),
609
- create(Text, null, 'Shift+arrows scroll the output buffer.'),
610
- create(Text, null, 'Run commands and type to feed stdin (buffer mirrors your keystrokes, Enter submits, Ctrl+C aborts).'),
611
- create(Text, null, 'Ctrl+H toggles the navigation cards, Ctrl+S shows structure tips, Ctrl+L reruns the previous command, Ctrl+Q quits.'),
612
- create(Text, null, 'Projects + Details stay side-by-side while Output and the log buffer stay fixed in their own band.'),
613
- create(Text, null, 'Structure guide lists the manifests that trigger each language detection (press Ctrl+S to toggle).')
608
+ create(Text, null, 'Shift+↑/↓ scrolls the log buffer while commands stream; type to feed stdin (Enter submits, Ctrl+C aborts).'),
609
+ create(Text, null, 'B/T/R run build/test/run; 1-9 executes detail commands; Ctrl+L reruns the previous command.'),
610
+ create(Text, null, 'Ctrl+H toggles these help cards, Ctrl+S toggles the structure guide, ? toggles this overlay, Ctrl+Q quits.'),
611
+ create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
612
+ create(Text, null, 'Structure guide lists the manifests that trigger each language detection (Ctrl+S to toggle).')
614
613
  )
615
614
  : null;
616
615
 
617
616
  const toggleHint = showHelpCards ? 'Ctrl+H hides the help cards' : 'Ctrl+H shows the help cards';
618
617
  const headerHint = viewMode === 'detail'
619
- ? `Detail mode · 1-${Math.max(detailedIndexed.length, 1)} to execute, C: add custom commands, Enter: back to list, Ctrl+Q: quit · ${toggleHint}, Ctrl+S shows structure guide`
620
- : `Quick run · B/T/R to build/test/run, Enter: view details, Ctrl+Q: quit · ${toggleHint}, Ctrl+S shows structure guide`;
618
+ ? `Detail mode · 1-${Math.max(detailedIndexed.length, 1)} to execute, C: add custom commands, Enter: back to list, Ctrl+Q: quit · ${toggleHint}, Ctrl+S toggles structure guide`
619
+ : `Quick run · B/T/R to build/test/run, Enter: view details, Ctrl+Q: quit · ${toggleHint}, Ctrl+S toggles structure guide`;
621
620
 
622
621
  return create(
623
622
  Box,
@@ -702,7 +701,7 @@ const projectRows = [];
702
701
  Box,
703
702
  {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
704
703
  create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter submits, Ctrl+C aborts.' : 'Run a command or press ? for extra help.'),
705
- create(Text, {dimColor: true}, `${toggleHint}, S toggles the structure guide`)
704
+ create(Text, {dimColor: true}, `${toggleHint}, Ctrl+S toggles the structure guide`)
706
705
  ),
707
706
  create(
708
707
  Box,
@@ -5,6 +5,27 @@ import {ensureConfigDir, PLUGIN_FILE} from './configPaths.js';
5
5
 
6
6
  const IGNORE_PATTERNS = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/target/**'];
7
7
 
8
+ const PYTHON_ENTRY_FILES = ['main.py', 'app.py', 'src/main.py', 'src/app.py'];
9
+
10
+ function findPythonEntry(projectPath) {
11
+ return PYTHON_ENTRY_FILES.find((file) => hasProjectFile(projectPath, file)) || null;
12
+ }
13
+
14
+ function hasProjectFile(projectPath, file) {
15
+ return fs.existsSync(path.join(projectPath, file));
16
+ }
17
+
18
+ function resolveScriptCommand(project, scriptName, fallback = null) {
19
+ const scripts = project.metadata?.scripts || {};
20
+ if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
21
+ return ['npm', 'run', scriptName];
22
+ }
23
+ if (typeof fallback === 'function') {
24
+ return fallback();
25
+ }
26
+ return fallback;
27
+ }
28
+
8
29
  function gatherNodeDependencies(pkg) {
9
30
  const deps = new Set();
10
31
  ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach((key) => {
@@ -71,6 +92,295 @@ function parseCommandTokens(value) {
71
92
  return [];
72
93
  }
73
94
 
95
+ const builtInFrameworks = [
96
+ {
97
+ id: 'next',
98
+ name: 'Next.js',
99
+ icon: '🧭',
100
+ description: 'React + Next.js (SSR/SSG) apps',
101
+ languages: ['Node.js'],
102
+ priority: 115,
103
+ match(project) {
104
+ const hasNextConfig = hasProjectFile(project.path, 'next.config.js');
105
+ return dependencyMatches(project, 'next') || hasNextConfig;
106
+ },
107
+ commands(project) {
108
+ const commands = {};
109
+ const add = (key, label, fallback) => {
110
+ const tokens = resolveScriptCommand(project, key, fallback);
111
+ if (tokens) {
112
+ commands[key] = {label, command: tokens, source: 'framework'};
113
+ }
114
+ };
115
+ const buildFallback = () => ['npx', 'next', 'build'];
116
+ const startFallback = () => ['npx', 'next', 'start'];
117
+ const devFallback = () => ['npx', 'next', 'dev'];
118
+ add('run', 'Next dev', devFallback);
119
+ add('build', 'Next build', buildFallback);
120
+ add('test', 'Next test', () => ['npm', 'run', 'test']);
121
+ add('start', 'Next start', startFallback);
122
+ return commands;
123
+ }
124
+ },
125
+ {
126
+ id: 'react',
127
+ name: 'React',
128
+ icon: '⚛️',
129
+ description: 'React apps (CRA, Vite React)',
130
+ languages: ['Node.js'],
131
+ priority: 112,
132
+ match(project) {
133
+ return dependencyMatches(project, 'react') && (dependencyMatches(project, 'react-scripts') || dependencyMatches(project, 'vite') || hasProjectFile(project.path, 'vite.config.js'));
134
+ },
135
+ commands(project) {
136
+ const commands = {};
137
+ const add = (key, label, fallback) => {
138
+ const tokens = resolveScriptCommand(project, key, fallback);
139
+ if (tokens) {
140
+ commands[key] = {label, command: tokens, source: 'framework'};
141
+ }
142
+ };
143
+ add('run', 'React dev', () => ['npm', 'run', 'dev']);
144
+ add('build', 'React build', () => ['npm', 'run', 'build']);
145
+ add('test', 'React test', () => ['npm', 'run', 'test']);
146
+ return commands;
147
+ }
148
+ },
149
+ {
150
+ id: 'vue',
151
+ name: 'Vue.js',
152
+ icon: '🟩',
153
+ description: 'Vue CLI or Vite + Vue apps',
154
+ languages: ['Node.js'],
155
+ priority: 111,
156
+ match(project) {
157
+ return dependencyMatches(project, 'vue') && (hasProjectFile(project.path, 'vue.config.js') || dependencyMatches(project, '@vue/cli-service') || dependencyMatches(project, 'vite'));
158
+ },
159
+ commands(project) {
160
+ const commands = {};
161
+ const add = (key, label, fallback) => {
162
+ const tokens = resolveScriptCommand(project, key, fallback);
163
+ if (tokens) {
164
+ commands[key] = {label, command: tokens, source: 'framework'};
165
+ }
166
+ };
167
+ add('run', 'Vue dev', () => ['npm', 'run', 'dev']);
168
+ add('build', 'Vue build', () => ['npm', 'run', 'build']);
169
+ add('test', 'Vue test', () => ['npm', 'run', 'test']);
170
+ return commands;
171
+ }
172
+ },
173
+ {
174
+ id: 'nest',
175
+ name: 'NestJS',
176
+ icon: '🛡️',
177
+ description: 'NestJS backend',
178
+ languages: ['Node.js'],
179
+ priority: 110,
180
+ match(project) {
181
+ return dependencyMatches(project, '@nestjs/cli') || dependencyMatches(project, '@nestjs/core');
182
+ },
183
+ commands(project) {
184
+ const commands = {};
185
+ const add = (key, label, fallback) => {
186
+ const tokens = resolveScriptCommand(project, key, fallback);
187
+ if (tokens) {
188
+ commands[key] = {label, command: tokens, source: 'framework'};
189
+ }
190
+ };
191
+ add('run', 'Nest dev', () => ['npm', 'run', 'start:dev']);
192
+ add('build', 'Nest build', () => ['npm', 'run', 'build']);
193
+ add('test', 'Nest test', () => ['npm', 'run', 'test']);
194
+ return commands;
195
+ }
196
+ },
197
+ {
198
+ id: 'angular',
199
+ name: 'Angular',
200
+ icon: '🅰️',
201
+ description: 'Angular CLI projects',
202
+ languages: ['Node.js'],
203
+ priority: 109,
204
+ match(project) {
205
+ return hasProjectFile(project.path, 'angular.json') || dependencyMatches(project, '@angular/cli');
206
+ },
207
+ commands(project) {
208
+ const commands = {};
209
+ const add = (key, label, fallback) => {
210
+ const tokens = resolveScriptCommand(project, key, fallback);
211
+ if (tokens) {
212
+ commands[key] = {label, command: tokens, source: 'framework'};
213
+ }
214
+ };
215
+ add('run', 'Angular serve', () => ['npm', 'run', 'start']);
216
+ add('build', 'Angular build', () => ['npm', 'run', 'build']);
217
+ add('test', 'Angular test', () => ['npm', 'run', 'test']);
218
+ return commands;
219
+ }
220
+ },
221
+ {
222
+ id: 'sveltekit',
223
+ name: 'SvelteKit',
224
+ icon: '🌀',
225
+ description: 'SvelteKit apps',
226
+ languages: ['Node.js'],
227
+ priority: 108,
228
+ match(project) {
229
+ return hasProjectFile(project.path, 'svelte.config.js') || dependencyMatches(project, '@sveltejs/kit');
230
+ },
231
+ commands(project) {
232
+ const commands = {};
233
+ const add = (key, label, fallback) => {
234
+ const tokens = resolveScriptCommand(project, key, fallback);
235
+ if (tokens) {
236
+ commands[key] = {label, command: tokens, source: 'framework'};
237
+ }
238
+ };
239
+ add('run', 'SvelteKit dev', () => ['npm', 'run', 'dev']);
240
+ add('build', 'SvelteKit build', () => ['npm', 'run', 'build']);
241
+ add('test', 'SvelteKit test', () => ['npm', 'run', 'test']);
242
+ add('preview', 'SvelteKit preview', () => ['npm', 'run', 'preview']);
243
+ return commands;
244
+ }
245
+ },
246
+ {
247
+ id: 'nuxt',
248
+ name: 'Nuxt',
249
+ icon: '🪄',
250
+ description: 'Nuxt.js / Vue SSR',
251
+ languages: ['Node.js'],
252
+ priority: 107,
253
+ match(project) {
254
+ return hasProjectFile(project.path, 'nuxt.config.js') || dependencyMatches(project, 'nuxt');
255
+ },
256
+ commands(project) {
257
+ const commands = {};
258
+ const add = (key, label, fallback) => {
259
+ const tokens = resolveScriptCommand(project, key, fallback);
260
+ if (tokens) {
261
+ commands[key] = {label, command: tokens, source: 'framework'};
262
+ }
263
+ };
264
+ add('run', 'Nuxt dev', () => ['npm', 'run', 'dev']);
265
+ add('build', 'Nuxt build', () => ['npm', 'run', 'build']);
266
+ add('start', 'Nuxt start', () => ['npm', 'run', 'start']);
267
+ return commands;
268
+ }
269
+ },
270
+ {
271
+ id: 'astro',
272
+ name: 'Astro',
273
+ icon: '✨',
274
+ description: 'Astro static sites',
275
+ languages: ['Node.js'],
276
+ priority: 106,
277
+ match(project) {
278
+ const matches = ['astro.config.mjs', 'astro.config.ts'].some((file) => hasProjectFile(project.path, file));
279
+ return matches || dependencyMatches(project, 'astro');
280
+ },
281
+ commands(project) {
282
+ const commands = {};
283
+ const add = (key, label, fallback) => {
284
+ const tokens = resolveScriptCommand(project, key, fallback);
285
+ if (tokens) {
286
+ commands[key] = {label, command: tokens, source: 'framework'};
287
+ }
288
+ };
289
+ add('run', 'Astro dev', () => ['npm', 'run', 'dev']);
290
+ add('build', 'Astro build', () => ['npm', 'run', 'build']);
291
+ add('preview', 'Astro preview', () => ['npm', 'run', 'preview']);
292
+ return commands;
293
+ }
294
+ },
295
+ {
296
+ id: 'django',
297
+ name: 'Django',
298
+ icon: '🌿',
299
+ description: 'Django web application',
300
+ languages: ['Python'],
301
+ priority: 110,
302
+ match(project) {
303
+ return dependencyMatches(project, 'django') || hasProjectFile(project.path, 'manage.py');
304
+ },
305
+ commands(project) {
306
+ const managePath = path.join(project.path, 'manage.py');
307
+ if (!fs.existsSync(managePath)) {
308
+ return {};
309
+ }
310
+ return {
311
+ run: {label: 'Django runserver', command: ['python', 'manage.py', 'runserver'], source: 'framework'},
312
+ test: {label: 'Django test', command: ['python', 'manage.py', 'test'], source: 'framework'},
313
+ migrate: {label: 'Django migrate', command: ['python', 'manage.py', 'migrate'], source: 'framework'}
314
+ };
315
+ }
316
+ },
317
+ {
318
+ id: 'flask',
319
+ name: 'Flask',
320
+ icon: '🍶',
321
+ description: 'Flask microservices',
322
+ languages: ['Python'],
323
+ priority: 105,
324
+ match(project) {
325
+ const entry = findPythonEntry(project.path);
326
+ return Boolean(entry && dependencyMatches(project, 'flask'));
327
+ },
328
+ commands(project) {
329
+ const entry = findPythonEntry(project.path);
330
+ if (!entry) {
331
+ return {};
332
+ }
333
+ return {
334
+ run: {label: 'Flask app', command: ['python', entry], source: 'framework'},
335
+ test: {label: 'Pytest', command: ['pytest'], source: 'framework'}
336
+ };
337
+ }
338
+ },
339
+ {
340
+ id: 'fastapi',
341
+ name: 'FastAPI',
342
+ icon: '⚡',
343
+ description: 'FastAPI + Uvicorn',
344
+ languages: ['Python'],
345
+ priority: 105,
346
+ match(project) {
347
+ const entry = findPythonEntry(project.path);
348
+ return Boolean(entry && dependencyMatches(project, 'fastapi'));
349
+ },
350
+ commands(project) {
351
+ const entry = findPythonEntry(project.path);
352
+ if (!entry) {
353
+ return {};
354
+ }
355
+ const moduleName = entry.split('.').slice(0, -1).join('.') || entry;
356
+ return {
357
+ run: {label: 'Uvicorn reload', command: ['uvicorn', `${moduleName}:app`, '--reload'], source: 'framework'},
358
+ test: {label: 'Pytest', command: ['pytest'], source: 'framework'}
359
+ };
360
+ }
361
+ },
362
+ {
363
+ id: 'spring',
364
+ name: 'Spring Boot',
365
+ icon: '🌱',
366
+ description: 'Spring Boot apps',
367
+ languages: ['Java'],
368
+ priority: 105,
369
+ match(project) {
370
+ return dependencyMatches(project, 'spring-boot-starter') || hasProjectFile(project.path, 'src/main/java');
371
+ },
372
+ commands(project) {
373
+ const hasMvnw = hasProjectFile(project.path, 'mvnw');
374
+ const base = hasMvnw ? './mvnw' : 'mvn';
375
+ return {
376
+ run: {label: 'Spring Boot run', command: [base, 'spring-boot:run'], source: 'framework'},
377
+ build: {label: 'Maven package', command: [base, 'package'], source: 'framework'},
378
+ test: {label: 'Maven test', command: [base, 'test'], source: 'framework'}
379
+ };
380
+ }
381
+ }
382
+ ];
383
+
74
384
  class SchemaRegistry {
75
385
  constructor() {
76
386
  this.cache = null;
@@ -102,10 +412,10 @@ class SchemaRegistry {
102
412
  const pkg = JSON.parse(content);
103
413
  const scripts = pkg.scripts || {};
104
414
  const commands = {};
105
- const preferScript = (targetKey, names, label) => {
415
+ const preferScript = (targetKey, names, labelText) => {
106
416
  for (const name of names) {
107
417
  if (Object.prototype.hasOwnProperty.call(scripts, name)) {
108
- commands[targetKey] = {label, command: ['npm', 'run', name]};
418
+ commands[targetKey] = {label: labelText, command: ['npm', 'run', name]};
109
419
  break;
110
420
  }
111
421
  }
@@ -126,7 +436,7 @@ class SchemaRegistry {
126
436
  const setupHints = [];
127
437
  if (metadata.dependencies.length) {
128
438
  setupHints.push('Run npm install to fetch dependencies.');
129
- if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) {
439
+ if (hasProjectFile(projectPath, 'yarn.lock')) {
130
440
  setupHints.push('Or run yarn install if you prefer Yarn.');
131
441
  }
132
442
  }
@@ -157,13 +467,13 @@ class SchemaRegistry {
157
467
  files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'],
158
468
  async build(projectPath, manifest) {
159
469
  const commands = {};
160
- if (fs.existsSync(path.join(projectPath, 'pyproject.toml'))) {
470
+ if (hasProjectFile(projectPath, 'pyproject.toml')) {
161
471
  commands.test = {label: 'Pytest', command: ['pytest']};
162
472
  } else {
163
473
  commands.test = {label: 'Unittest', command: ['python', '-m', 'unittest', 'discover']};
164
474
  }
165
475
 
166
- const entry = this.findPythonEntry(projectPath);
476
+ const entry = findPythonEntry(projectPath);
167
477
  if (entry) {
168
478
  commands.run = {label: 'Run', command: ['python', entry]};
169
479
  }
@@ -173,12 +483,11 @@ class SchemaRegistry {
173
483
  };
174
484
 
175
485
  const setupHints = [];
176
- const reqPath = path.join(projectPath, 'requirements.txt');
177
- if (fs.existsSync(reqPath)) {
486
+ if (hasProjectFile(projectPath, 'requirements.txt')) {
178
487
  setupHints.push('pip install -r requirements.txt');
179
488
  }
180
- if (fs.existsSync(path.join(projectPath, 'Pipfile'))) {
181
- setupHints.push('pipenv install --dev or poetry install');
489
+ if (hasProjectFile(projectPath, 'Pipfile')) {
490
+ setupHints.push('Use pipenv install --dev or poetry install');
182
491
  }
183
492
 
184
493
  return {
@@ -198,16 +507,6 @@ class SchemaRegistry {
198
507
  }
199
508
  };
200
509
  },
201
- findPythonEntry(projectPath) {
202
- const candidates = ['main.py', 'app.py', 'src/main.py', 'src/app.py'];
203
- for (const candidate of candidates) {
204
- const candidatePath = path.join(projectPath, candidate);
205
- if (fs.existsSync(candidatePath)) {
206
- return candidate;
207
- }
208
- }
209
- return null;
210
- }
211
510
  },
212
511
  {
213
512
  type: 'rust',
@@ -272,8 +571,8 @@ class SchemaRegistry {
272
571
  priority: 80,
273
572
  files: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
274
573
  async build(projectPath, manifest) {
275
- const hasMvnw = fs.existsSync(path.join(projectPath, 'mvnw'));
276
- const hasGradlew = fs.existsSync(path.join(projectPath, 'gradlew'));
574
+ const hasMvnw = hasProjectFile(projectPath, 'mvnw');
575
+ const hasGradlew = hasProjectFile(projectPath, 'gradlew');
277
576
  const commands = {};
278
577
  if (hasGradlew) {
279
578
  commands.build = {label: 'Gradle build', command: ['./gradlew', 'build']};
@@ -285,6 +584,7 @@ class SchemaRegistry {
285
584
  commands.build = {label: 'Maven package', command: ['mvn', 'package']};
286
585
  commands.test = {label: 'Maven test', command: ['mvn', 'test']};
287
586
  }
587
+
288
588
  return {
289
589
  id: `${projectPath}::java`,
290
590
  path: projectPath,
@@ -297,7 +597,7 @@ class SchemaRegistry {
297
597
  manifest: path.basename(manifest),
298
598
  description: '',
299
599
  extra: {
300
- setupHints: ['Install JDK 17+ and run ./mvnw install / ./gradlew build']
600
+ setupHints: ['Install JDK 17+ and run ./mvnw install or ./gradlew build']
301
601
  }
302
602
  };
303
603
  }
@@ -345,7 +645,7 @@ class SchemaRegistry {
345
645
  icon: '🐘',
346
646
  priority: this.priority,
347
647
  commands: {
348
- test: {label: 'PHPStorm', command: ['php', '-v']}
648
+ test: {label: 'PHP -v', command: ['php', '-v']}
349
649
  },
350
650
  metadata: {},
351
651
  manifest: path.basename(manifest),
@@ -469,9 +769,6 @@ class SchemaRegistry {
469
769
 
470
770
  const schemaRegistry = new SchemaRegistry();
471
771
 
472
- const builtInFrameworks = [];
473
-
474
-
475
772
  function loadUserFrameworks() {
476
773
  ensureConfigDir();
477
774
  if (!fs.existsSync(PLUGIN_FILE)) {
@@ -484,7 +781,7 @@ function loadUserFrameworks() {
484
781
  const normalizedId = entry.id || (entry.name ? entry.name.toLowerCase().replace(/\s+/g, '-') : `plugin-${Math.random().toString(36).slice(2, 8)}`);
485
782
  const commands = {};
486
783
  Object.entries(entry.commands || {}).forEach(([key, value]) => {
487
- const tokens = parseCommandTokens(value);
784
+ const tokens = parseCommandTokens(typeof value === 'object' ? value.command : value);
488
785
  if (!tokens.length) {
489
786
  return;
490
787
  }
@@ -507,8 +804,7 @@ function loadUserFrameworks() {
507
804
  commands,
508
805
  match: entry.match
509
806
  };
510
- })
511
- .filter((plugin) => plugin.name && plugin.commands && Object.keys(plugin.commands).length);
807
+ }).filter((plugin) => plugin.name && plugin.commands && Object.keys(plugin.commands).length);
512
808
  } catch (error) {
513
809
  console.error(`Failed to parse plugins.json: ${error.message}`);
514
810
  return [];
@@ -530,7 +826,7 @@ function matchesPlugin(project, plugin) {
530
826
  return false;
531
827
  }
532
828
  if (plugin.files && plugin.files.length > 0) {
533
- const hit = plugin.files.some((file) => fs.existsSync(path.join(project.path, file)));
829
+ const hit = plugin.files.some((file) => hasProjectFile(project.path, file));
534
830
  if (!hit) {
535
831
  return false;
536
832
  }