project-compass 2.0.0 → 2.0.1

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
@@ -81,10 +81,12 @@ Projects and details now occupy the same row, while the output panel takes its o
81
81
 
82
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.
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.1",
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
@@ -5,6 +5,21 @@ import {ensureConfigDir, PLUGIN_FILE} from './configPaths.js';
5
5
 
6
6
  const IGNORE_PATTERNS = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/target/**'];
7
7
 
8
+ function hasProjectFile(projectPath, file) {
9
+ return fs.existsSync(path.join(projectPath, file));
10
+ }
11
+
12
+ function resolveScriptCommand(project, scriptName, fallback = null) {
13
+ const scripts = project.metadata?.scripts || {};
14
+ if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
15
+ return ['npm', 'run', scriptName];
16
+ }
17
+ if (typeof fallback === 'function') {
18
+ return fallback();
19
+ }
20
+ return fallback;
21
+ }
22
+
8
23
  function gatherNodeDependencies(pkg) {
9
24
  const deps = new Set();
10
25
  ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach((key) => {
@@ -71,6 +86,286 @@ function parseCommandTokens(value) {
71
86
  return [];
72
87
  }
73
88
 
89
+ const builtInFrameworks = [
90
+ {
91
+ id: 'next',
92
+ name: 'Next.js',
93
+ icon: '🧭',
94
+ description: 'React + Next.js (SSR/SSG) apps',
95
+ languages: ['Node.js'],
96
+ priority: 115,
97
+ match(project) {
98
+ const hasNextConfig = hasProjectFile(project.path, 'next.config.js');
99
+ return dependencyMatches(project, 'next') || hasNextConfig;
100
+ },
101
+ commands(project) {
102
+ const commands = {};
103
+ const add = (key, label, fallback) => {
104
+ const tokens = resolveScriptCommand(project, key, fallback);
105
+ if (tokens) {
106
+ commands[key] = {label, command: tokens, source: 'framework'};
107
+ }
108
+ };
109
+ const buildFallback = () => ['npx', 'next', 'build'];
110
+ const startFallback = () => ['npx', 'next', 'start'];
111
+ const devFallback = () => ['npx', 'next', 'dev'];
112
+ add('run', 'Next dev', devFallback);
113
+ add('build', 'Next build', buildFallback);
114
+ add('test', 'Next test', () => ['npm', 'run', 'test']);
115
+ add('start', 'Next start', startFallback);
116
+ return commands;
117
+ }
118
+ },
119
+ {
120
+ id: 'react',
121
+ name: 'React',
122
+ icon: '⚛️',
123
+ description: 'React apps (CRA, Vite React)',
124
+ languages: ['Node.js'],
125
+ priority: 112,
126
+ match(project) {
127
+ return dependencyMatches(project, 'react') && (dependencyMatches(project, 'react-scripts') || dependencyMatches(project, 'vite') || hasProjectFile(project.path, 'vite.config.js'));
128
+ },
129
+ commands(project) {
130
+ const commands = {};
131
+ const add = (key, label, fallback) => {
132
+ const tokens = resolveScriptCommand(project, key, fallback);
133
+ if (tokens) {
134
+ commands[key] = {label, command: tokens, source: 'framework'};
135
+ }
136
+ };
137
+ add('run', 'React dev', () => ['npm', 'run', 'dev']);
138
+ add('build', 'React build', () => ['npm', 'run', 'build']);
139
+ add('test', 'React test', () => ['npm', 'run', 'test']);
140
+ return commands;
141
+ }
142
+ },
143
+ {
144
+ id: 'vue',
145
+ name: 'Vue.js',
146
+ icon: '🟩',
147
+ description: 'Vue CLI or Vite + Vue apps',
148
+ languages: ['Node.js'],
149
+ priority: 111,
150
+ match(project) {
151
+ return dependencyMatches(project, 'vue') && (hasProjectFile(project.path, 'vue.config.js') || dependencyMatches(project, '@vue/cli-service') || dependencyMatches(project, 'vite'));
152
+ },
153
+ commands(project) {
154
+ const commands = {};
155
+ const add = (key, label, fallback) => {
156
+ const tokens = resolveScriptCommand(project, key, fallback);
157
+ if (tokens) {
158
+ commands[key] = {label, command: tokens, source: 'framework'};
159
+ }
160
+ };
161
+ add('run', 'Vue dev', () => ['npm', 'run', 'dev']);
162
+ add('build', 'Vue build', () => ['npm', 'run', 'build']);
163
+ add('test', 'Vue test', () => ['npm', 'run', 'test']);
164
+ return commands;
165
+ }
166
+ },
167
+ {
168
+ id: 'nest',
169
+ name: 'NestJS',
170
+ icon: '🛡️',
171
+ description: 'NestJS backend',
172
+ languages: ['Node.js'],
173
+ priority: 110,
174
+ match(project) {
175
+ return dependencyMatches(project, '@nestjs/cli') || dependencyMatches(project, '@nestjs/core');
176
+ },
177
+ commands(project) {
178
+ const commands = {};
179
+ const add = (key, label, fallback) => {
180
+ const tokens = resolveScriptCommand(project, key, fallback);
181
+ if (tokens) {
182
+ commands[key] = {label, command: tokens, source: 'framework'};
183
+ }
184
+ };
185
+ add('run', 'Nest dev', () => ['npm', 'run', 'start:dev']);
186
+ add('build', 'Nest build', () => ['npm', 'run', 'build']);
187
+ add('test', 'Nest test', () => ['npm', 'run', 'test']);
188
+ return commands;
189
+ }
190
+ },
191
+ {
192
+ id: 'angular',
193
+ name: 'Angular',
194
+ icon: '🅰️',
195
+ description: 'Angular CLI projects',
196
+ languages: ['Node.js'],
197
+ priority: 109,
198
+ match(project) {
199
+ return hasProjectFile(project.path, 'angular.json') || dependencyMatches(project, '@angular/cli');
200
+ },
201
+ commands(project) {
202
+ const commands = {};
203
+ const add = (key, label, fallback) => {
204
+ const tokens = resolveScriptCommand(project, key, fallback);
205
+ if (tokens) {
206
+ commands[key] = {label, command: tokens, source: 'framework'};
207
+ }
208
+ };
209
+ add('run', 'Angular serve', () => ['npm', 'run', 'start']);
210
+ add('build', 'Angular build', () => ['npm', 'run', 'build']);
211
+ add('test', 'Angular test', () => ['npm', 'run', 'test']);
212
+ return commands;
213
+ }
214
+ },
215
+ {
216
+ id: 'sveltekit',
217
+ name: 'SvelteKit',
218
+ icon: '🌀',
219
+ description: 'SvelteKit apps',
220
+ languages: ['Node.js'],
221
+ priority: 108,
222
+ match(project) {
223
+ return hasProjectFile(project.path, 'svelte.config.js') || dependencyMatches(project, '@sveltejs/kit');
224
+ },
225
+ commands(project) {
226
+ const commands = {};
227
+ const add = (key, label, fallback) => {
228
+ const tokens = resolveScriptCommand(project, key, fallback);
229
+ if (tokens) {
230
+ commands[key] = {label, command: tokens, source: 'framework'};
231
+ }
232
+ };
233
+ add('run', 'SvelteKit dev', () => ['npm', 'run', 'dev']);
234
+ add('build', 'SvelteKit build', () => ['npm', 'run', 'build']);
235
+ add('test', 'SvelteKit test', () => ['npm', 'run', 'test']);
236
+ add('preview', 'SvelteKit preview', () => ['npm', 'run', 'preview']);
237
+ return commands;
238
+ }
239
+ },
240
+ {
241
+ id: 'nuxt',
242
+ name: 'Nuxt',
243
+ icon: '🪄',
244
+ description: 'Nuxt.js / Vue SSR',
245
+ languages: ['Node.js'],
246
+ priority: 107,
247
+ match(project) {
248
+ return hasProjectFile(project.path, 'nuxt.config.js') || dependencyMatches(project, 'nuxt');
249
+ },
250
+ commands(project) {
251
+ const commands = {};
252
+ const add = (key, label, fallback) => {
253
+ const tokens = resolveScriptCommand(project, key, fallback);
254
+ if (tokens) {
255
+ commands[key] = {label, command: tokens, source: 'framework'};
256
+ }
257
+ };
258
+ add('run', 'Nuxt dev', () => ['npm', 'run', 'dev']);
259
+ add('build', 'Nuxt build', () => ['npm', 'run', 'build']);
260
+ add('start', 'Nuxt start', () => ['npm', 'run', 'start']);
261
+ return commands;
262
+ }
263
+ },
264
+ {
265
+ id: 'astro',
266
+ name: 'Astro',
267
+ icon: '✨',
268
+ description: 'Astro static sites',
269
+ languages: ['Node.js'],
270
+ priority: 106,
271
+ match(project) {
272
+ const matches = ['astro.config.mjs', 'astro.config.ts'].some((file) => hasProjectFile(project.path, file));
273
+ return matches || dependencyMatches(project, 'astro');
274
+ },
275
+ commands(project) {
276
+ const commands = {};
277
+ const add = (key, label, fallback) => {
278
+ const tokens = resolveScriptCommand(project, key, fallback);
279
+ if (tokens) {
280
+ commands[key] = {label, command: tokens, source: 'framework'};
281
+ }
282
+ };
283
+ add('run', 'Astro dev', () => ['npm', 'run', 'dev']);
284
+ add('build', 'Astro build', () => ['npm', 'run', 'build']);
285
+ add('preview', 'Astro preview', () => ['npm', 'run', 'preview']);
286
+ return commands;
287
+ }
288
+ },
289
+ {
290
+ id: 'django',
291
+ name: 'Django',
292
+ icon: '🌿',
293
+ description: 'Django web application',
294
+ languages: ['Python'],
295
+ priority: 110,
296
+ match(project) {
297
+ return dependencyMatches(project, 'django') || hasProjectFile(project.path, 'manage.py');
298
+ },
299
+ commands(project) {
300
+ const managePath = path.join(project.path, 'manage.py');
301
+ if (!fs.existsSync(managePath)) {
302
+ return {};
303
+ }
304
+ return {
305
+ run: {label: 'Django runserver', command: ['python', 'manage.py', 'runserver'], source: 'framework'},
306
+ test: {label: 'Django test', command: ['python', 'manage.py', 'test'], source: 'framework'},
307
+ migrate: {label: 'Django migrate', command: ['python', 'manage.py', 'migrate'], source: 'framework'}
308
+ };
309
+ }
310
+ },
311
+ {
312
+ id: 'flask',
313
+ name: 'Flask',
314
+ icon: '🍶',
315
+ description: 'Flask microservices',
316
+ languages: ['Python'],
317
+ priority: 105,
318
+ match(project) {
319
+ return dependencyMatches(project, 'flask') || hasProjectFile(project.path, 'app.py');
320
+ },
321
+ commands(project) {
322
+ const entry = hasProjectFile(project.path, 'app.py') ? 'app.py' : 'main.py';
323
+ return {
324
+ run: {label: 'Flask app', command: ['python', entry], source: 'framework'},
325
+ test: {label: 'Pytest', command: ['pytest'], source: 'framework'}
326
+ };
327
+ }
328
+ },
329
+ {
330
+ id: 'fastapi',
331
+ name: 'FastAPI',
332
+ icon: '⚡',
333
+ description: 'FastAPI + Uvicorn',
334
+ languages: ['Python'],
335
+ priority: 105,
336
+ match(project) {
337
+ return dependencyMatches(project, 'fastapi');
338
+ },
339
+ commands(project) {
340
+ const entry = hasProjectFile(project.path, 'main.py') ? 'main.py' : 'app.py';
341
+ return {
342
+ run: {label: 'Uvicorn reload', command: ['uvicorn', `${entry.split('.')[0]}:app`, '--reload'], source: 'framework'},
343
+ test: {label: 'Pytest', command: ['pytest'], source: 'framework'}
344
+ };
345
+ }
346
+ },
347
+ {
348
+ id: 'spring',
349
+ name: 'Spring Boot',
350
+ icon: '🌱',
351
+ description: 'Spring Boot apps',
352
+ languages: ['Java'],
353
+ priority: 105,
354
+ match(project) {
355
+ return dependencyMatches(project, 'spring-boot-starter') || hasProjectFile(project.path, 'src/main/java');
356
+ },
357
+ commands(project) {
358
+ const hasMvnw = hasProjectFile(project.path, 'mvnw');
359
+ const base = hasMvnw ? './mvnw' : 'mvn';
360
+ return {
361
+ run: {label: 'Spring Boot run', command: [base, 'spring-boot:run'], source: 'framework'},
362
+ build: {label: 'Maven package', command: [base, 'package'], source: 'framework'},
363
+ test: {label: 'Maven test', command: [base, 'test'], source: 'framework'}
364
+ };
365
+ }
366
+ }
367
+ ];
368
+
74
369
  class SchemaRegistry {
75
370
  constructor() {
76
371
  this.cache = null;
@@ -102,10 +397,10 @@ class SchemaRegistry {
102
397
  const pkg = JSON.parse(content);
103
398
  const scripts = pkg.scripts || {};
104
399
  const commands = {};
105
- const preferScript = (targetKey, names, label) => {
400
+ const preferScript = (targetKey, names, labelText) => {
106
401
  for (const name of names) {
107
402
  if (Object.prototype.hasOwnProperty.call(scripts, name)) {
108
- commands[targetKey] = {label, command: ['npm', 'run', name]};
403
+ commands[targetKey] = {label: labelText, command: ['npm', 'run', name]};
109
404
  break;
110
405
  }
111
406
  }
@@ -126,7 +421,7 @@ class SchemaRegistry {
126
421
  const setupHints = [];
127
422
  if (metadata.dependencies.length) {
128
423
  setupHints.push('Run npm install to fetch dependencies.');
129
- if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) {
424
+ if (hasProjectFile(projectPath, 'yarn.lock')) {
130
425
  setupHints.push('Or run yarn install if you prefer Yarn.');
131
426
  }
132
427
  }
@@ -157,7 +452,7 @@ class SchemaRegistry {
157
452
  files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'],
158
453
  async build(projectPath, manifest) {
159
454
  const commands = {};
160
- if (fs.existsSync(path.join(projectPath, 'pyproject.toml'))) {
455
+ if (hasProjectFile(projectPath, 'pyproject.toml')) {
161
456
  commands.test = {label: 'Pytest', command: ['pytest']};
162
457
  } else {
163
458
  commands.test = {label: 'Unittest', command: ['python', '-m', 'unittest', 'discover']};
@@ -173,12 +468,11 @@ class SchemaRegistry {
173
468
  };
174
469
 
175
470
  const setupHints = [];
176
- const reqPath = path.join(projectPath, 'requirements.txt');
177
- if (fs.existsSync(reqPath)) {
471
+ if (hasProjectFile(projectPath, 'requirements.txt')) {
178
472
  setupHints.push('pip install -r requirements.txt');
179
473
  }
180
- if (fs.existsSync(path.join(projectPath, 'Pipfile'))) {
181
- setupHints.push('pipenv install --dev or poetry install');
474
+ if (hasProjectFile(projectPath, 'Pipfile')) {
475
+ setupHints.push('Use pipenv install --dev or poetry install');
182
476
  }
183
477
 
184
478
  return {
@@ -201,8 +495,7 @@ class SchemaRegistry {
201
495
  findPythonEntry(projectPath) {
202
496
  const candidates = ['main.py', 'app.py', 'src/main.py', 'src/app.py'];
203
497
  for (const candidate of candidates) {
204
- const candidatePath = path.join(projectPath, candidate);
205
- if (fs.existsSync(candidatePath)) {
498
+ if (hasProjectFile(projectPath, candidate)) {
206
499
  return candidate;
207
500
  }
208
501
  }
@@ -272,8 +565,8 @@ class SchemaRegistry {
272
565
  priority: 80,
273
566
  files: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
274
567
  async build(projectPath, manifest) {
275
- const hasMvnw = fs.existsSync(path.join(projectPath, 'mvnw'));
276
- const hasGradlew = fs.existsSync(path.join(projectPath, 'gradlew'));
568
+ const hasMvnw = hasProjectFile(projectPath, 'mvnw');
569
+ const hasGradlew = hasProjectFile(projectPath, 'gradlew');
277
570
  const commands = {};
278
571
  if (hasGradlew) {
279
572
  commands.build = {label: 'Gradle build', command: ['./gradlew', 'build']};
@@ -285,6 +578,7 @@ class SchemaRegistry {
285
578
  commands.build = {label: 'Maven package', command: ['mvn', 'package']};
286
579
  commands.test = {label: 'Maven test', command: ['mvn', 'test']};
287
580
  }
581
+
288
582
  return {
289
583
  id: `${projectPath}::java`,
290
584
  path: projectPath,
@@ -297,7 +591,7 @@ class SchemaRegistry {
297
591
  manifest: path.basename(manifest),
298
592
  description: '',
299
593
  extra: {
300
- setupHints: ['Install JDK 17+ and run ./mvnw install / ./gradlew build']
594
+ setupHints: ['Install JDK 17+ and run ./mvnw install or ./gradlew build']
301
595
  }
302
596
  };
303
597
  }
@@ -345,7 +639,7 @@ class SchemaRegistry {
345
639
  icon: '🐘',
346
640
  priority: this.priority,
347
641
  commands: {
348
- test: {label: 'PHPStorm', command: ['php', '-v']}
642
+ test: {label: 'PHP -v', command: ['php', '-v']}
349
643
  },
350
644
  metadata: {},
351
645
  manifest: path.basename(manifest),
@@ -469,9 +763,6 @@ class SchemaRegistry {
469
763
 
470
764
  const schemaRegistry = new SchemaRegistry();
471
765
 
472
- const builtInFrameworks = [];
473
-
474
-
475
766
  function loadUserFrameworks() {
476
767
  ensureConfigDir();
477
768
  if (!fs.existsSync(PLUGIN_FILE)) {
@@ -484,7 +775,7 @@ function loadUserFrameworks() {
484
775
  const normalizedId = entry.id || (entry.name ? entry.name.toLowerCase().replace(/\s+/g, '-') : `plugin-${Math.random().toString(36).slice(2, 8)}`);
485
776
  const commands = {};
486
777
  Object.entries(entry.commands || {}).forEach(([key, value]) => {
487
- const tokens = parseCommandTokens(value);
778
+ const tokens = parseCommandTokens(typeof value === 'object' ? value.command : value);
488
779
  if (!tokens.length) {
489
780
  return;
490
781
  }
@@ -507,8 +798,7 @@ function loadUserFrameworks() {
507
798
  commands,
508
799
  match: entry.match
509
800
  };
510
- })
511
- .filter((plugin) => plugin.name && plugin.commands && Object.keys(plugin.commands).length);
801
+ }).filter((plugin) => plugin.name && plugin.commands && Object.keys(plugin.commands).length);
512
802
  } catch (error) {
513
803
  console.error(`Failed to parse plugins.json: ${error.message}`);
514
804
  return [];
@@ -530,7 +820,7 @@ function matchesPlugin(project, plugin) {
530
820
  return false;
531
821
  }
532
822
  if (plugin.files && plugin.files.length > 0) {
533
- const hit = plugin.files.some((file) => fs.existsSync(path.join(project.path, file)));
823
+ const hit = plugin.files.some((file) => hasProjectFile(project.path, file));
534
824
  if (!hit) {
535
825
  return false;
536
826
  }