project-compass 1.0.10 → 2.0.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.
- package/README.md +12 -6
- package/package.json +1 -1
- package/src/cli.js +41 -767
- package/src/configPaths.js +13 -0
- package/src/projectDetection.js +627 -0
package/src/cli.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
3
3
|
import {render, Box, Text, useApp, useInput} from 'ink';
|
|
4
|
-
import fastGlob from 'fast-glob';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import fs from 'fs';
|
|
7
|
-
import os from 'os';
|
|
8
6
|
import kleur from 'kleur';
|
|
9
7
|
import {execa} from 'execa';
|
|
8
|
+
import {discoverProjects, SCHEMA_GUIDE} from './projectDetection.js';
|
|
9
|
+
import {CONFIG_PATH, PLUGIN_FILE, ensureConfigDir} from './configPaths.js';
|
|
10
10
|
|
|
11
11
|
const create = React.createElement;
|
|
12
|
-
const CONFIG_DIR = path.join(os.homedir(), '.project-compass');
|
|
13
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
14
|
-
const PLUGIN_FILE = path.join(CONFIG_DIR, 'plugins.json');
|
|
15
12
|
const DEFAULT_CONFIG = {customCommands: {}};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
const ART_CHARS = ['▁', '▃', '▄', '▅', '▇'];
|
|
14
|
+
const ART_COLORS = ['magenta', 'blue', 'cyan', 'yellow', 'red'];
|
|
15
|
+
const OUTPUT_WINDOW_SIZE = 8;
|
|
16
|
+
const OUTPUT_WINDOW_HEIGHT = OUTPUT_WINDOW_SIZE + 2;
|
|
17
|
+
const PROJECTS_MIN_WIDTH = 32;
|
|
18
|
+
const DETAILS_MIN_WIDTH = 44;
|
|
19
|
+
const HELP_CARD_MIN_WIDTH = 28;
|
|
20
|
+
const RECENT_RUN_LIMIT = 5;
|
|
21
|
+
const ACTION_MAP = {
|
|
22
|
+
b: 'build',
|
|
23
|
+
t: 'test',
|
|
24
|
+
r: 'run'
|
|
25
|
+
};
|
|
21
26
|
|
|
22
27
|
function saveConfig(config) {
|
|
23
28
|
try {
|
|
@@ -48,744 +53,6 @@ function loadConfig() {
|
|
|
48
53
|
return {...DEFAULT_CONFIG};
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
function parseCommandTokens(value) {
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
return value.map((token) => String(token));
|
|
54
|
-
}
|
|
55
|
-
if (typeof value === 'string') {
|
|
56
|
-
return value.trim().split(/\s+/).filter(Boolean);
|
|
57
|
-
}
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolveScriptCommand(project, scriptName, fallback = null) {
|
|
62
|
-
const scripts = project.metadata?.scripts || {};
|
|
63
|
-
if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
64
|
-
return ['npm', 'run', scriptName];
|
|
65
|
-
}
|
|
66
|
-
if (typeof fallback === 'function') {
|
|
67
|
-
return fallback();
|
|
68
|
-
}
|
|
69
|
-
return fallback;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function gatherNodeDependencies(pkg) {
|
|
73
|
-
const deps = new Set();
|
|
74
|
-
['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach((key) => {
|
|
75
|
-
if (pkg[key]) {
|
|
76
|
-
Object.keys(pkg[key]).forEach((name) => deps.add(name));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return Array.from(deps);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function gatherPythonDependencies(projectPath) {
|
|
83
|
-
const set = new Set();
|
|
84
|
-
const addFromFile = (filePath) => {
|
|
85
|
-
if (!fs.existsSync(filePath)) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
89
|
-
raw.split(/\r?\n/).forEach((line) => {
|
|
90
|
-
const clean = line.trim().split('#')[0].trim();
|
|
91
|
-
if (clean) {
|
|
92
|
-
const token = clean.split(/[>=<=~!]/)[0].trim().toLowerCase();
|
|
93
|
-
if (token) {
|
|
94
|
-
set.add(token);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
addFromFile(path.join(projectPath, 'requirements.txt'));
|
|
100
|
-
const pyproject = path.join(projectPath, 'pyproject.toml');
|
|
101
|
-
if (fs.existsSync(pyproject)) {
|
|
102
|
-
const content = fs.readFileSync(pyproject, 'utf-8').toLowerCase();
|
|
103
|
-
const matches = content.match(/\b[a-z0-9-_/]+\b/g);
|
|
104
|
-
(matches || []).forEach((match) => {
|
|
105
|
-
if (match) {
|
|
106
|
-
set.add(match);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
return Array.from(set);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function dependencyMatches(project, needle) {
|
|
114
|
-
const dependencies = (project.metadata?.dependencies || []).map((dep) => dep.toLowerCase());
|
|
115
|
-
const target = needle.toLowerCase();
|
|
116
|
-
return dependencies.some((value) => value === target || value.startsWith(`${target}@`) || value.includes(`/${target}`));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function hasProjectFile(project, file) {
|
|
120
|
-
return fs.existsSync(path.join(project.path, file));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const builtInFrameworks = [
|
|
125
|
-
{
|
|
126
|
-
id: 'next',
|
|
127
|
-
name: 'Next.js',
|
|
128
|
-
icon: '🧭',
|
|
129
|
-
description: 'React + Next.js (SSR/SSG) apps',
|
|
130
|
-
languages: ['Node.js'],
|
|
131
|
-
priority: 115,
|
|
132
|
-
match(project) {
|
|
133
|
-
const hasNextConfig = fs.existsSync(path.join(project.path, 'next.config.js'));
|
|
134
|
-
return dependencyMatches(project, 'next') || hasNextConfig;
|
|
135
|
-
},
|
|
136
|
-
commands(project) {
|
|
137
|
-
const commands = {};
|
|
138
|
-
const add = (key, label, fallback) => {
|
|
139
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
140
|
-
if (tokens) {
|
|
141
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
const buildFallback = () => ['npx', 'next', 'build'];
|
|
145
|
-
const startFallback = () => ['npx', 'next', 'start'];
|
|
146
|
-
const devFallback = () => ['npx', 'next', 'dev'];
|
|
147
|
-
add('run', 'Next dev', devFallback);
|
|
148
|
-
add('build', 'Next build', buildFallback);
|
|
149
|
-
add('test', 'Next test', () => ['npm', 'run', 'test']);
|
|
150
|
-
add('start', 'Next start', startFallback);
|
|
151
|
-
return commands;
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: 'react',
|
|
156
|
-
name: 'React',
|
|
157
|
-
icon: '⚛️',
|
|
158
|
-
description: 'React apps (CRA, Vite React)',
|
|
159
|
-
languages: ['Node.js'],
|
|
160
|
-
priority: 112,
|
|
161
|
-
match(project) {
|
|
162
|
-
return dependencyMatches(project, 'react') && (dependencyMatches(project, 'react-scripts') || dependencyMatches(project, 'vite') || hasProjectFile(project, 'vite.config.js'));
|
|
163
|
-
},
|
|
164
|
-
commands(project) {
|
|
165
|
-
const commands = {};
|
|
166
|
-
const add = (key, label, fallback) => {
|
|
167
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
168
|
-
if (tokens) {
|
|
169
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
add('run', 'React dev', () => ['npm', 'run', 'dev']);
|
|
173
|
-
add('build', 'React build', () => ['npm', 'run', 'build']);
|
|
174
|
-
add('test', 'React test', () => ['npm', 'run', 'test']);
|
|
175
|
-
return commands;
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
id: 'vue',
|
|
180
|
-
name: 'Vue.js',
|
|
181
|
-
icon: '🟩',
|
|
182
|
-
description: 'Vue CLI or Vite + Vue apps',
|
|
183
|
-
languages: ['Node.js'],
|
|
184
|
-
priority: 111,
|
|
185
|
-
match(project) {
|
|
186
|
-
return dependencyMatches(project, 'vue') && (hasProjectFile(project, 'vue.config.js') || dependencyMatches(project, '@vue/cli-service') || dependencyMatches(project, 'vite'));
|
|
187
|
-
},
|
|
188
|
-
commands(project) {
|
|
189
|
-
const commands = {};
|
|
190
|
-
const add = (key, label, fallback) => {
|
|
191
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
192
|
-
if (tokens) {
|
|
193
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
add('run', 'Vue dev', () => ['npm', 'run', 'dev']);
|
|
197
|
-
add('build', 'Vue build', () => ['npm', 'run', 'build']);
|
|
198
|
-
add('test', 'Vue test', () => ['npm', 'run', 'test']);
|
|
199
|
-
return commands;
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
id: 'nest',
|
|
204
|
-
name: 'NestJS',
|
|
205
|
-
icon: '🛡️',
|
|
206
|
-
description: 'NestJS backend',
|
|
207
|
-
languages: ['Node.js'],
|
|
208
|
-
priority: 110,
|
|
209
|
-
match(project) {
|
|
210
|
-
return dependencyMatches(project, '@nestjs/cli') || dependencyMatches(project, '@nestjs/core');
|
|
211
|
-
},
|
|
212
|
-
commands(project) {
|
|
213
|
-
const commands = {};
|
|
214
|
-
const add = (key, label, fallback) => {
|
|
215
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
216
|
-
if (tokens) {
|
|
217
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
add('run', 'Nest dev', () => ['npm', 'run', 'start:dev']);
|
|
221
|
-
add('build', 'Nest build', () => ['npm', 'run', 'build']);
|
|
222
|
-
add('test', 'Nest test', () => ['npm', 'run', 'test']);
|
|
223
|
-
return commands;
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
id: 'angular',
|
|
228
|
-
name: 'Angular',
|
|
229
|
-
icon: '🅰️',
|
|
230
|
-
description: 'Angular CLI projects',
|
|
231
|
-
languages: ['Node.js'],
|
|
232
|
-
priority: 109,
|
|
233
|
-
match(project) {
|
|
234
|
-
return hasProjectFile(project, 'angular.json') || dependencyMatches(project, '@angular/cli');
|
|
235
|
-
},
|
|
236
|
-
commands(project) {
|
|
237
|
-
const commands = {};
|
|
238
|
-
const add = (key, label, fallback) => {
|
|
239
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
240
|
-
if (tokens) {
|
|
241
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
add('run', 'Angular serve', () => ['npm', 'run', 'start']);
|
|
245
|
-
add('build', 'Angular build', () => ['npm', 'run', 'build']);
|
|
246
|
-
add('test', 'Angular test', () => ['npm', 'run', 'test']);
|
|
247
|
-
return commands;
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
id: 'sveltekit',
|
|
252
|
-
name: 'SvelteKit',
|
|
253
|
-
icon: '🌀',
|
|
254
|
-
description: 'SvelteKit apps',
|
|
255
|
-
languages: ['Node.js'],
|
|
256
|
-
priority: 108,
|
|
257
|
-
match(project) {
|
|
258
|
-
return hasProjectFile(project, 'svelte.config.js') || dependencyMatches(project, '@sveltejs/kit');
|
|
259
|
-
},
|
|
260
|
-
commands(project) {
|
|
261
|
-
const commands = {};
|
|
262
|
-
const add = (key, label, fallback) => {
|
|
263
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
264
|
-
if (tokens) {
|
|
265
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
add('run', 'SvelteKit dev', () => ['npm', 'run', 'dev']);
|
|
269
|
-
add('build', 'SvelteKit build', () => ['npm', 'run', 'build']);
|
|
270
|
-
add('test', 'SvelteKit test', () => ['npm', 'run', 'test']);
|
|
271
|
-
add('preview', 'SvelteKit preview', () => ['npm', 'run', 'preview']);
|
|
272
|
-
return commands;
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
id: 'nuxt',
|
|
277
|
-
name: 'Nuxt',
|
|
278
|
-
icon: '🪄',
|
|
279
|
-
description: 'Nuxt.js / Vue SSR',
|
|
280
|
-
languages: ['Node.js'],
|
|
281
|
-
priority: 107,
|
|
282
|
-
match(project) {
|
|
283
|
-
return hasProjectFile(project, 'nuxt.config.js') || dependencyMatches(project, 'nuxt');
|
|
284
|
-
},
|
|
285
|
-
commands(project) {
|
|
286
|
-
const commands = {};
|
|
287
|
-
const add = (key, label, fallback) => {
|
|
288
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
289
|
-
if (tokens) {
|
|
290
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
add('run', 'Nuxt dev', () => ['npm', 'run', 'dev']);
|
|
294
|
-
add('build', 'Nuxt build', () => ['npm', 'run', 'build']);
|
|
295
|
-
add('start', 'Nuxt start', () => ['npm', 'run', 'start']);
|
|
296
|
-
return commands;
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
id: 'astro',
|
|
301
|
-
name: 'Astro',
|
|
302
|
-
icon: '✨',
|
|
303
|
-
description: 'Astro static sites',
|
|
304
|
-
languages: ['Node.js'],
|
|
305
|
-
priority: 106,
|
|
306
|
-
match(project) {
|
|
307
|
-
const matches = ['astro.config.mjs', 'astro.config.ts'].some((file) => hasProjectFile(project, file));
|
|
308
|
-
return matches || dependencyMatches(project, 'astro');
|
|
309
|
-
},
|
|
310
|
-
commands(project) {
|
|
311
|
-
const commands = {};
|
|
312
|
-
const add = (key, label, fallback) => {
|
|
313
|
-
const tokens = resolveScriptCommand(project, key, fallback);
|
|
314
|
-
if (tokens) {
|
|
315
|
-
commands[key] = {label, command: tokens, source: 'framework'};
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
add('run', 'Astro dev', () => ['npm', 'run', 'dev']);
|
|
319
|
-
add('build', 'Astro build', () => ['npm', 'run', 'build']);
|
|
320
|
-
add('preview', 'Astro preview', () => ['npm', 'run', 'preview']);
|
|
321
|
-
return commands;
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
id: 'django',
|
|
326
|
-
name: 'Django',
|
|
327
|
-
icon: '🌿',
|
|
328
|
-
description: 'Django web application',
|
|
329
|
-
languages: ['Python'],
|
|
330
|
-
priority: 110,
|
|
331
|
-
match(project) {
|
|
332
|
-
return dependencyMatches(project, 'django') || hasProjectFile(project, 'manage.py');
|
|
333
|
-
},
|
|
334
|
-
commands(project) {
|
|
335
|
-
const managePath = path.join(project.path, 'manage.py');
|
|
336
|
-
if (!fs.existsSync(managePath)) {
|
|
337
|
-
return {};
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
run: {label: 'Django runserver', command: ['python', 'manage.py', 'runserver'], source: 'framework'},
|
|
341
|
-
test: {label: 'Django test', command: ['python', 'manage.py', 'test'], source: 'framework'},
|
|
342
|
-
migrate: {label: 'Django migrate', command: ['python', 'manage.py', 'migrate'], source: 'framework'}
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
id: 'flask',
|
|
348
|
-
name: 'Flask',
|
|
349
|
-
icon: '🍶',
|
|
350
|
-
description: 'Flask microservices',
|
|
351
|
-
languages: ['Python'],
|
|
352
|
-
priority: 105,
|
|
353
|
-
match(project) {
|
|
354
|
-
return dependencyMatches(project, 'flask') || hasProjectFile(project, 'app.py');
|
|
355
|
-
},
|
|
356
|
-
commands(project) {
|
|
357
|
-
const commands = {};
|
|
358
|
-
const entry = hasProjectFile(project, 'app.py') ? 'app.py' : 'main.py';
|
|
359
|
-
commands.run = {label: 'Flask app', command: ['python', entry], source: 'framework'};
|
|
360
|
-
commands.test = {label: 'Pytest', command: ['pytest'], source: 'framework'};
|
|
361
|
-
return commands;
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
id: 'fastapi',
|
|
366
|
-
name: 'FastAPI',
|
|
367
|
-
icon: '⚡',
|
|
368
|
-
description: 'FastAPI + Uvicorn',
|
|
369
|
-
languages: ['Python'],
|
|
370
|
-
priority: 105,
|
|
371
|
-
match(project) {
|
|
372
|
-
return dependencyMatches(project, 'fastapi');
|
|
373
|
-
},
|
|
374
|
-
commands(project) {
|
|
375
|
-
const entry = hasProjectFile(project, 'main.py') ? 'main.py' : 'app.py';
|
|
376
|
-
return {
|
|
377
|
-
run: {label: 'Uvicorn reload', command: ['uvicorn', `${entry.split('.')[0]}:app`, '--reload'], source: 'framework'},
|
|
378
|
-
test: {label: 'Pytest', command: ['pytest'], source: 'framework'}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
id: 'spring',
|
|
384
|
-
name: 'Spring Boot',
|
|
385
|
-
icon: '🌱',
|
|
386
|
-
description: 'Spring Boot apps',
|
|
387
|
-
languages: ['Java'],
|
|
388
|
-
priority: 105,
|
|
389
|
-
match(project) {
|
|
390
|
-
return dependencyMatches(project, 'spring-boot-starter') || hasProjectFile(project, 'src/main/java');
|
|
391
|
-
},
|
|
392
|
-
commands(project) {
|
|
393
|
-
const hasMvnw = fs.existsSync(path.join(project.path, 'mvnw'));
|
|
394
|
-
const base = hasMvnw ? './mvnw' : 'mvn';
|
|
395
|
-
return {
|
|
396
|
-
run: {label: 'Spring Boot run', command: [base, 'spring-boot:run'], source: 'framework'},
|
|
397
|
-
build: {label: 'Maven package', command: [base, 'package'], source: 'framework'},
|
|
398
|
-
test: {label: 'Maven test', command: [base, 'test'], source: 'framework'}
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
];
|
|
403
|
-
function loadUserFrameworks() {
|
|
404
|
-
ensureConfigDir();
|
|
405
|
-
try {
|
|
406
|
-
if (!fs.existsSync(PLUGIN_FILE)) {
|
|
407
|
-
return [];
|
|
408
|
-
}
|
|
409
|
-
const payload = JSON.parse(fs.readFileSync(PLUGIN_FILE, 'utf-8') || '{}');
|
|
410
|
-
const plugins = payload.plugins || [];
|
|
411
|
-
return plugins.map((entry) => {
|
|
412
|
-
const normalizedId = entry.id || (entry.name ? entry.name.toLowerCase().replace(/\s+/g, '-') : `plugin-${Math.random().toString(36).slice(2, 8)}`);
|
|
413
|
-
const commands = {};
|
|
414
|
-
Object.entries(entry.commands || {}).forEach(([key, value]) => {
|
|
415
|
-
const command = parseCommandTokens(typeof value === 'object' ? value.command : value);
|
|
416
|
-
if (!command.length) {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
commands[key] = {
|
|
420
|
-
label: typeof value === 'object' ? value.label || key : key,
|
|
421
|
-
command,
|
|
422
|
-
source: 'plugin'
|
|
423
|
-
};
|
|
424
|
-
});
|
|
425
|
-
return {
|
|
426
|
-
id: normalizedId,
|
|
427
|
-
name: entry.name || normalizedId,
|
|
428
|
-
icon: entry.icon || '🧩',
|
|
429
|
-
description: entry.description || '',
|
|
430
|
-
languages: entry.languages || [],
|
|
431
|
-
files: entry.files || [],
|
|
432
|
-
dependencies: entry.dependencies || [],
|
|
433
|
-
scripts: entry.scripts || [],
|
|
434
|
-
priority: Number.isFinite(entry.priority) ? entry.priority : 70,
|
|
435
|
-
commands,
|
|
436
|
-
match: entry.match
|
|
437
|
-
};
|
|
438
|
-
})
|
|
439
|
-
.filter((plugin) => plugin.name && plugin.commands && Object.keys(plugin.commands).length);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
console.error(`Failed to parse plugins.json: ${error.message}`);
|
|
442
|
-
return [];
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
let cachedFrameworkPlugins = null;
|
|
447
|
-
|
|
448
|
-
function getFrameworkPlugins() {
|
|
449
|
-
if (cachedFrameworkPlugins) {
|
|
450
|
-
return cachedFrameworkPlugins;
|
|
451
|
-
}
|
|
452
|
-
cachedFrameworkPlugins = [...builtInFrameworks, ...loadUserFrameworks()];
|
|
453
|
-
return cachedFrameworkPlugins;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function matchesPlugin(project, plugin) {
|
|
457
|
-
if (plugin.languages && plugin.languages.length > 0 && !plugin.languages.includes(project.type)) {
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
if (plugin.files && plugin.files.length > 0) {
|
|
461
|
-
const hit = plugin.files.some((file) => fs.existsSync(path.join(project.path, file)));
|
|
462
|
-
if (!hit) {
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (plugin.dependencies && plugin.dependencies.length > 0) {
|
|
467
|
-
const hit = plugin.dependencies.some((dep) => dependencyMatches(project, dep));
|
|
468
|
-
if (!hit) {
|
|
469
|
-
return false;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (plugin.scripts && plugin.scripts.length > 0) {
|
|
473
|
-
const scripts = project.metadata?.scripts || {};
|
|
474
|
-
const hit = plugin.scripts.some((name) => Object.prototype.hasOwnProperty.call(scripts, name));
|
|
475
|
-
if (!hit) {
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (typeof plugin.match === 'function') {
|
|
480
|
-
if (!plugin.match(project)) {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function applyFrameworkPlugins(project) {
|
|
488
|
-
const plugins = getFrameworkPlugins();
|
|
489
|
-
let commands = {...project.commands};
|
|
490
|
-
const frameworks = [];
|
|
491
|
-
let maxPriority = project.priority || 0;
|
|
492
|
-
for (const plugin of plugins) {
|
|
493
|
-
if (!matchesPlugin(project, plugin)) {
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
frameworks.push({id: plugin.id, name: plugin.name, icon: plugin.icon, description: plugin.description});
|
|
497
|
-
if (plugin.priority && plugin.priority > maxPriority) {
|
|
498
|
-
maxPriority = plugin.priority;
|
|
499
|
-
}
|
|
500
|
-
const pluginCommands = typeof plugin.commands === 'function' ? plugin.commands(project) : plugin.commands;
|
|
501
|
-
if (pluginCommands) {
|
|
502
|
-
Object.entries(pluginCommands).forEach(([key, command]) => {
|
|
503
|
-
if (!Array.isArray(command.command) || command.command.length === 0) {
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
commands = {
|
|
507
|
-
...commands,
|
|
508
|
-
[key]: {
|
|
509
|
-
...command,
|
|
510
|
-
source: command.source || 'framework'
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return {...project, commands, frameworks, priority: maxPriority};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const IGNORE_PATTERNS = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/target/**'];
|
|
520
|
-
|
|
521
|
-
const SCHEMAS = [
|
|
522
|
-
{
|
|
523
|
-
type: 'node',
|
|
524
|
-
label: 'Node.js',
|
|
525
|
-
icon: '🟢',
|
|
526
|
-
priority: 90,
|
|
527
|
-
files: ['package.json'],
|
|
528
|
-
async build(projectPath, manifest) {
|
|
529
|
-
const pkgPath = path.join(projectPath, 'package.json');
|
|
530
|
-
if (!fs.existsSync(pkgPath)) {
|
|
531
|
-
return null;
|
|
532
|
-
}
|
|
533
|
-
const content = await fs.promises.readFile(pkgPath, 'utf-8');
|
|
534
|
-
const pkg = JSON.parse(content);
|
|
535
|
-
const scripts = pkg.scripts || {};
|
|
536
|
-
const commands = {};
|
|
537
|
-
const preferScript = (targetKey, names, label) => {
|
|
538
|
-
for (const name of names) {
|
|
539
|
-
if (Object.prototype.hasOwnProperty.call(scripts, name)) {
|
|
540
|
-
commands[targetKey] = {label, command: ['npm', 'run', name]};
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
preferScript('build', ['build', 'compile', 'dist'], 'Build');
|
|
546
|
-
preferScript('test', ['test', 'check', 'spec'], 'Test');
|
|
547
|
-
preferScript('run', ['start', 'dev', 'serve', 'run'], 'Start');
|
|
548
|
-
|
|
549
|
-
const metadata = {
|
|
550
|
-
dependencies: gatherNodeDependencies(pkg),
|
|
551
|
-
scripts,
|
|
552
|
-
packageJson: pkg
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
return {
|
|
556
|
-
id: `${projectPath}::node`,
|
|
557
|
-
path: projectPath,
|
|
558
|
-
name: pkg.name || path.basename(projectPath),
|
|
559
|
-
type: 'Node.js',
|
|
560
|
-
icon: '🟢',
|
|
561
|
-
priority: this.priority,
|
|
562
|
-
commands,
|
|
563
|
-
metadata,
|
|
564
|
-
manifest: path.basename(manifest),
|
|
565
|
-
description: pkg.description || '',
|
|
566
|
-
extra: {
|
|
567
|
-
scripts: Object.keys(scripts)
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
type: 'python',
|
|
574
|
-
label: 'Python',
|
|
575
|
-
icon: '🐍',
|
|
576
|
-
priority: 80,
|
|
577
|
-
files: ['pyproject.toml', 'requirements.txt', 'setup.py'],
|
|
578
|
-
async build(projectPath, manifest) {
|
|
579
|
-
const commands = {};
|
|
580
|
-
if (fs.existsSync(path.join(projectPath, 'pyproject.toml'))) {
|
|
581
|
-
commands.test = {label: 'Pytest', command: ['pytest']};
|
|
582
|
-
} else {
|
|
583
|
-
commands.test = {label: 'Unittest', command: ['python', '-m', 'unittest', 'discover']};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const entry = await findPythonEntry(projectPath);
|
|
587
|
-
if (entry) {
|
|
588
|
-
commands.run = {label: 'Run', command: ['python', entry]};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const metadata = {
|
|
592
|
-
dependencies: gatherPythonDependencies(projectPath)
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
id: `${projectPath}::python`,
|
|
597
|
-
path: projectPath,
|
|
598
|
-
name: path.basename(projectPath),
|
|
599
|
-
type: 'Python',
|
|
600
|
-
icon: '🐍',
|
|
601
|
-
priority: this.priority,
|
|
602
|
-
commands,
|
|
603
|
-
metadata,
|
|
604
|
-
manifest: path.basename(manifest),
|
|
605
|
-
description: '',
|
|
606
|
-
extra: {
|
|
607
|
-
entry
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
type: 'rust',
|
|
614
|
-
label: 'Rust',
|
|
615
|
-
icon: '🦀',
|
|
616
|
-
priority: 85,
|
|
617
|
-
files: ['Cargo.toml'],
|
|
618
|
-
async build(projectPath, manifest) {
|
|
619
|
-
return {
|
|
620
|
-
id: `${projectPath}::rust`,
|
|
621
|
-
path: projectPath,
|
|
622
|
-
name: path.basename(projectPath),
|
|
623
|
-
type: 'Rust',
|
|
624
|
-
icon: '🦀',
|
|
625
|
-
priority: this.priority,
|
|
626
|
-
commands: {
|
|
627
|
-
build: {label: 'Cargo build', command: ['cargo', 'build']},
|
|
628
|
-
test: {label: 'Cargo test', command: ['cargo', 'test']},
|
|
629
|
-
run: {label: 'Cargo run', command: ['cargo', 'run']}
|
|
630
|
-
},
|
|
631
|
-
metadata: {},
|
|
632
|
-
manifest: path.basename(manifest),
|
|
633
|
-
description: '',
|
|
634
|
-
extra: {}
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
},
|
|
638
|
-
{
|
|
639
|
-
type: 'go',
|
|
640
|
-
label: 'Go',
|
|
641
|
-
icon: '🐹',
|
|
642
|
-
priority: 80,
|
|
643
|
-
files: ['go.mod'],
|
|
644
|
-
async build(projectPath, manifest) {
|
|
645
|
-
return {
|
|
646
|
-
id: `${projectPath}::go`,
|
|
647
|
-
path: projectPath,
|
|
648
|
-
name: path.basename(projectPath),
|
|
649
|
-
type: 'Go',
|
|
650
|
-
icon: '🐹',
|
|
651
|
-
priority: this.priority,
|
|
652
|
-
commands: {
|
|
653
|
-
build: {label: 'Go build', command: ['go', 'build', './...']},
|
|
654
|
-
test: {label: 'Go test', command: ['go', 'test', './...']},
|
|
655
|
-
run: {label: 'Go run', command: ['go', 'run', '.']}
|
|
656
|
-
},
|
|
657
|
-
metadata: {},
|
|
658
|
-
manifest: path.basename(manifest),
|
|
659
|
-
description: '',
|
|
660
|
-
extra: {}
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
type: 'java',
|
|
666
|
-
label: 'Java',
|
|
667
|
-
icon: '☕️',
|
|
668
|
-
priority: 75,
|
|
669
|
-
files: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
670
|
-
async build(projectPath, manifest) {
|
|
671
|
-
const hasMvnw = fs.existsSync(path.join(projectPath, 'mvnw'));
|
|
672
|
-
const hasGradlew = fs.existsSync(path.join(projectPath, 'gradlew'));
|
|
673
|
-
const commands = {};
|
|
674
|
-
if (hasGradlew) {
|
|
675
|
-
commands.build = {label: 'Gradle build', command: ['./gradlew', 'build']};
|
|
676
|
-
commands.test = {label: 'Gradle test', command: ['./gradlew', 'test']};
|
|
677
|
-
} else if (hasMvnw) {
|
|
678
|
-
commands.build = {label: 'Maven package', command: ['./mvnw', 'package']};
|
|
679
|
-
commands.test = {label: 'Maven test', command: ['./mvnw', 'test']};
|
|
680
|
-
} else {
|
|
681
|
-
commands.build = {label: 'Maven package', command: ['mvn', 'package']};
|
|
682
|
-
commands.test = {label: 'Maven test', command: ['mvn', 'test']};
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return {
|
|
686
|
-
id: `${projectPath}::java`,
|
|
687
|
-
path: projectPath,
|
|
688
|
-
name: path.basename(projectPath),
|
|
689
|
-
type: 'Java',
|
|
690
|
-
icon: '☕️',
|
|
691
|
-
priority: this.priority,
|
|
692
|
-
commands,
|
|
693
|
-
metadata: {},
|
|
694
|
-
manifest: path.basename(manifest),
|
|
695
|
-
description: '',
|
|
696
|
-
extra: {}
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
type: 'scala',
|
|
702
|
-
label: 'Scala',
|
|
703
|
-
icon: '🔵',
|
|
704
|
-
priority: 70,
|
|
705
|
-
files: ['build.sbt'],
|
|
706
|
-
async build(projectPath, manifest) {
|
|
707
|
-
return {
|
|
708
|
-
id: `${projectPath}::scala`,
|
|
709
|
-
path: projectPath,
|
|
710
|
-
name: path.basename(projectPath),
|
|
711
|
-
type: 'Scala',
|
|
712
|
-
icon: '🔵',
|
|
713
|
-
priority: this.priority,
|
|
714
|
-
commands: {
|
|
715
|
-
build: {label: 'sbt compile', command: ['sbt', 'compile']},
|
|
716
|
-
test: {label: 'sbt test', command: ['sbt', 'test']},
|
|
717
|
-
run: {label: 'sbt run', command: ['sbt', 'run']}
|
|
718
|
-
},
|
|
719
|
-
metadata: {},
|
|
720
|
-
manifest: path.basename(manifest),
|
|
721
|
-
description: '',
|
|
722
|
-
extra: {}
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
];
|
|
727
|
-
|
|
728
|
-
async function findPythonEntry(projectPath) {
|
|
729
|
-
const candidates = ['main.py', 'app.py', 'src/main.py', 'src/app.py'];
|
|
730
|
-
for (const candidate of candidates) {
|
|
731
|
-
const candidatePath = path.join(projectPath, candidate);
|
|
732
|
-
if (fs.existsSync(candidatePath)) {
|
|
733
|
-
return candidate;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
async function discoverProjects(root) {
|
|
740
|
-
const projectMap = new Map();
|
|
741
|
-
for (const schema of SCHEMAS) {
|
|
742
|
-
const patterns = schema.files.map((file) => `**/${file}`);
|
|
743
|
-
const matches = await fastGlob(patterns, {
|
|
744
|
-
cwd: root,
|
|
745
|
-
ignore: IGNORE_PATTERNS,
|
|
746
|
-
onlyFiles: true,
|
|
747
|
-
deep: 5
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
for (const match of matches) {
|
|
751
|
-
const projectDir = path.resolve(root, path.dirname(match));
|
|
752
|
-
const existing = projectMap.get(projectDir);
|
|
753
|
-
if (existing && existing.priority >= schema.priority) {
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
const entry = await schema.build(projectDir, match);
|
|
757
|
-
if (!entry) {
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
const withFrameworks = applyFrameworkPlugins(entry);
|
|
761
|
-
projectMap.set(projectDir, withFrameworks);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return Array.from(projectMap.values()).sort((a, b) => b.priority - a.priority);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const SCHEMA_GUIDE = SCHEMAS.map((schema) => ({
|
|
768
|
-
type: schema.type,
|
|
769
|
-
label: schema.label || schema.type,
|
|
770
|
-
icon: schema.icon || '⚙',
|
|
771
|
-
files: schema.files,
|
|
772
|
-
hint: schema.label || schema.type
|
|
773
|
-
}));
|
|
774
|
-
|
|
775
|
-
const ACTION_MAP = {
|
|
776
|
-
b: 'build',
|
|
777
|
-
t: 'test',
|
|
778
|
-
r: 'run'
|
|
779
|
-
};
|
|
780
|
-
const ART_CHARS = ['▁', '▃', '▄', '▅', '▇'];
|
|
781
|
-
const ART_COLORS = ['magenta', 'blue', 'cyan', 'yellow', 'red'];
|
|
782
|
-
const OUTPUT_WINDOW_SIZE = 8;
|
|
783
|
-
const OUTPUT_WINDOW_HEIGHT = OUTPUT_WINDOW_SIZE + 2;
|
|
784
|
-
const PROJECTS_MIN_WIDTH = 32;
|
|
785
|
-
const DETAILS_MIN_WIDTH = 44;
|
|
786
|
-
const HELP_CARD_MIN_WIDTH = 28;
|
|
787
|
-
const RECENT_RUN_LIMIT = 5;
|
|
788
|
-
|
|
789
56
|
function useScanner(rootPath) {
|
|
790
57
|
const [state, setState] = useState({projects: [], loading: true, error: null});
|
|
791
58
|
|
|
@@ -996,11 +263,14 @@ function Compass({rootPath}) {
|
|
|
996
263
|
}
|
|
997
264
|
|
|
998
265
|
const normalizedInput = input?.toLowerCase();
|
|
999
|
-
|
|
266
|
+
const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
|
|
267
|
+
const shiftCombo = (char) => key.shift && normalizedInput === char;
|
|
268
|
+
const toggleShortcut = (char) => ctrlCombo(char) || shiftCombo(char);
|
|
269
|
+
if (toggleShortcut('h')) {
|
|
1000
270
|
setShowHelpCards((prev) => !prev);
|
|
1001
271
|
return;
|
|
1002
272
|
}
|
|
1003
|
-
if (
|
|
273
|
+
if (toggleShortcut('s')) {
|
|
1004
274
|
setShowStructureGuide((prev) => !prev);
|
|
1005
275
|
return;
|
|
1006
276
|
}
|
|
@@ -1048,7 +318,7 @@ function Compass({rootPath}) {
|
|
|
1048
318
|
setShowHelp((prev) => !prev);
|
|
1049
319
|
return;
|
|
1050
320
|
}
|
|
1051
|
-
if (
|
|
321
|
+
if (ctrlCombo('l') && lastCommandRef.current) {
|
|
1052
322
|
runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project);
|
|
1053
323
|
return;
|
|
1054
324
|
}
|
|
@@ -1068,7 +338,7 @@ function Compass({rootPath}) {
|
|
|
1068
338
|
setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
|
|
1069
339
|
return;
|
|
1070
340
|
}
|
|
1071
|
-
if (
|
|
341
|
+
if (ctrlCombo('q')) {
|
|
1072
342
|
exit();
|
|
1073
343
|
return;
|
|
1074
344
|
}
|
|
@@ -1149,6 +419,11 @@ const projectRows = [];
|
|
|
1149
419
|
if (!detailedIndexed.length) {
|
|
1150
420
|
detailContent.push(create(Text, {dimColor: true}, 'No built-in commands yet. Add a custom command with C.'));
|
|
1151
421
|
}
|
|
422
|
+
const setupHints = selectedProject.extra?.setupHints || [];
|
|
423
|
+
if (setupHints.length) {
|
|
424
|
+
detailContent.push(create(Text, {dimColor: true, marginTop: 1}, 'Setup hints:'));
|
|
425
|
+
setupHints.forEach((hint) => detailContent.push(create(Text, {dimColor: true}, ` • ${hint}`)));
|
|
426
|
+
}
|
|
1152
427
|
detailContent.push(create(Text, {dimColor: true}, 'Press C → label|cmd to save custom actions, Enter to close detail view.'));
|
|
1153
428
|
} else {
|
|
1154
429
|
detailContent.push(create(Text, {dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
|
|
@@ -1250,19 +525,19 @@ const projectRows = [];
|
|
|
1250
525
|
color: 'magenta',
|
|
1251
526
|
body: [
|
|
1252
527
|
'↑ / ↓ move the project focus',
|
|
1253
|
-
'Enter
|
|
1254
|
-
'Shift + ↑ / ↓ scrolls the log buffer',
|
|
1255
|
-
'H toggles cards, ? opens the overlay'
|
|
528
|
+
'Enter opens or closes details',
|
|
529
|
+
'Shift + ↑ / ↓ scrolls only the log buffer',
|
|
530
|
+
'Ctrl+H toggles cards, ? opens the overlay'
|
|
1256
531
|
]
|
|
1257
532
|
},
|
|
1258
533
|
{
|
|
1259
534
|
label: 'Command flow',
|
|
1260
535
|
color: 'cyan',
|
|
1261
536
|
body: [
|
|
1262
|
-
'B / T / R
|
|
1263
|
-
'1-9 execute
|
|
1264
|
-
'L reruns the last command',
|
|
1265
|
-
'Ctrl+C aborts, typing feeds stdin (buffer mirrors
|
|
537
|
+
'B / T / R trigger build/test/run',
|
|
538
|
+
'1-9 execute detail commands in order',
|
|
539
|
+
'Ctrl+L reruns the last command you launched',
|
|
540
|
+
'Ctrl+C aborts, typing feeds stdin (buffer mirrors it)'
|
|
1266
541
|
]
|
|
1267
542
|
},
|
|
1268
543
|
{
|
|
@@ -1270,12 +545,11 @@ const projectRows = [];
|
|
|
1270
545
|
color: 'yellow',
|
|
1271
546
|
body: [
|
|
1272
547
|
recentRuns.length ? `${recentRuns.length} runs recorded` : 'No runs yet · start with B/T/R',
|
|
1273
|
-
'
|
|
548
|
+
'Ctrl+S shows the structure guide when unsure',
|
|
1274
549
|
'Save custom commands with C → label|cmd'
|
|
1275
550
|
]
|
|
1276
551
|
}
|
|
1277
552
|
];
|
|
1278
|
-
|
|
1279
553
|
const helpSection = showHelpCards
|
|
1280
554
|
? create(
|
|
1281
555
|
Box,
|
|
@@ -1333,17 +607,17 @@ const projectRows = [];
|
|
|
1333
607
|
},
|
|
1334
608
|
create(Text, {color: 'cyan', bold: true}, 'Help overlay · press ? to hide'),
|
|
1335
609
|
create(Text, null, 'Shift+arrows scroll the output buffer.'),
|
|
1336
|
-
create(Text, null, 'Run commands and type to feed stdin (Enter submits, Ctrl+C aborts)
|
|
1337
|
-
create(Text, null, 'H
|
|
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.'),
|
|
1338
612
|
create(Text, null, 'Projects + Details stay side-by-side while Output and the log buffer stay fixed in their own band.'),
|
|
1339
|
-
create(Text, null, 'Structure guide lists the manifests that trigger each language detection (press S to toggle).')
|
|
613
|
+
create(Text, null, 'Structure guide lists the manifests that trigger each language detection (press Ctrl+S to toggle).')
|
|
1340
614
|
)
|
|
1341
615
|
: null;
|
|
1342
616
|
|
|
1343
|
-
const toggleHint = showHelpCards ? 'H hides the help cards' : 'H shows the help cards';
|
|
617
|
+
const toggleHint = showHelpCards ? 'Ctrl+H hides the help cards' : 'Ctrl+H shows the help cards';
|
|
1344
618
|
const headerHint = viewMode === 'detail'
|
|
1345
|
-
? `Detail mode · 1-${Math.max(detailedIndexed.length, 1)} to execute, C: add custom commands, Enter: back to list,
|
|
1346
|
-
: `Quick run · B/T/R to build/test/run, Enter: view details,
|
|
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`;
|
|
1347
621
|
|
|
1348
622
|
return create(
|
|
1349
623
|
Box,
|