Skip to content

Commit 18f2ce0

Browse files
committed
feat: enhance ScriptRunner TUI with execution history, monorepo support, and favorites functionality
1 parent 3b2fdab commit 18f2ce0

File tree

7 files changed

+585
-249
lines changed

7 files changed

+585
-249
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ A terminal UI for running package.json scripts interactively.
2424
- **Background process management** - View logs, monitor and kill processes
2525
- **Copy command to clipboard** - Quick access to the underlying npm command
2626
- **Script descriptions** - Shows `scriptsDescriptions` from package.json
27+
- **Execution history** - Recent scripts appear at the top of the menu
28+
- **Monorepo support** - Auto-detects npm/yarn/pnpm workspaces and Lerna
29+
- **Favorites** - Star frequently used scripts for quick access
2730

2831
## Installation
2932

index.js

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env node
22

33
import { parsePackageJson } from './src/parser.js';
4-
import { showHeader, showScriptMenu, showExecutionOptions, showBackgroundProcessOptions, showError, showSuccess, showInfo, showLogs, clearScreen } from './src/ui.js';
4+
import { showHeader, showScriptMenu, showScriptMenuWithHistory, showExecutionOptions, showBackgroundProcessOptions, showWorkspaceMenu, showMonorepoHeader, showError, showSuccess, showInfo, showLogs, clearScreen } from './src/ui.js';
55
import { runScript, runScriptBackground, getScriptCommand, copyToClipboard, killProcess, isProcessRunning } from './src/runner.js';
6+
import { addToHistory, getRecentScripts } from './src/history.js';
7+
import { detectMonorepo, findWorkspaces, isMonorepo } from './src/monorepo.js';
8+
import { toggleFavorite, isFavorite, getFavorites } from './src/favorites.js';
69

710
const backgroundProcesses = [];
811

@@ -70,14 +73,54 @@ async function main() {
7073
}
7174

7275
try {
73-
const { scripts, scriptsDescriptions, projectName } = await parsePackageJson(options.directory);
76+
const rootPkg = await parsePackageJson(options.directory);
77+
const monorepoConfig = await detectMonorepo(options.directory);
78+
79+
// Handle monorepo workspace selection
80+
let currentDirectory = options.directory;
81+
let currentProject = rootPkg;
82+
let workspaceName = null;
83+
84+
if (isMonorepo(monorepoConfig) && !options.scriptName && !options.list) {
85+
const workspaces = await findWorkspaces(monorepoConfig);
86+
87+
if (workspaces.length > 0) {
88+
showHeader(rootPkg.projectName);
89+
showInfo(`Monorepo detectado (${monorepoConfig.type}) - ${workspaces.length} workspace(s)`);
90+
91+
const selectedWorkspace = await showWorkspaceMenu(workspaces, rootPkg.projectName);
92+
93+
if (selectedWorkspace === 'exit') {
94+
showSuccess('Até mais!');
95+
process.exit(0);
96+
}
97+
98+
if (selectedWorkspace !== 'root') {
99+
currentDirectory = selectedWorkspace.path;
100+
currentProject = {
101+
scripts: selectedWorkspace.scripts,
102+
scriptsDescriptions: selectedWorkspace.scriptsDescriptions,
103+
projectName: selectedWorkspace.name,
104+
};
105+
workspaceName = selectedWorkspace.name;
106+
}
107+
108+
clearScreen();
109+
}
110+
}
111+
112+
const { scripts, scriptsDescriptions, projectName } = currentProject;
74113

75114
if (Object.keys(scripts).length === 0) {
76115
showError('Nenhum script encontrado no package.json');
77116
process.exit(1);
78117
}
79118

80-
showHeader(projectName);
119+
if (workspaceName) {
120+
showMonorepoHeader(rootPkg.projectName, workspaceName);
121+
} else {
122+
showHeader(projectName);
123+
}
81124

82125
if (options.list) {
83126
console.log('\nScripts disponíveis:\n');
@@ -94,7 +137,8 @@ async function main() {
94137
showError(`Script "${options.scriptName}" não encontrado`);
95138
process.exit(1);
96139
}
97-
await runScript(options.scriptName, options.directory);
140+
await addToHistory({ script: options.scriptName, directory: currentDirectory, projectName });
141+
await runScript(options.scriptName, currentDirectory);
98142
process.exit(0);
99143
}
100144

@@ -110,11 +154,19 @@ async function main() {
110154
// Refresh screen if needed
111155
if (needsRefresh) {
112156
clearScreen();
113-
showHeader(projectName);
157+
if (workspaceName) {
158+
showMonorepoHeader(rootPkg.projectName, workspaceName);
159+
} else {
160+
showHeader(projectName);
161+
}
114162
needsRefresh = false;
115163
}
116164

117-
const selected = await showScriptMenu(scripts, scriptsDescriptions, backgroundProcesses);
165+
// Get recent scripts and favorites for this directory
166+
const recentScripts = await getRecentScripts(currentDirectory, 3);
167+
const favorites = await getFavorites(currentDirectory);
168+
169+
const selected = await showScriptMenuWithHistory(scripts, scriptsDescriptions, backgroundProcesses, recentScripts, favorites);
118170

119171
if (selected === 'exit') {
120172
if (backgroundProcesses.length > 0) {
@@ -130,13 +182,21 @@ async function main() {
130182

131183
while (true) {
132184
clearScreen();
133-
showHeader(projectName);
185+
if (workspaceName) {
186+
showMonorepoHeader(rootPkg.projectName, workspaceName);
187+
} else {
188+
showHeader(projectName);
189+
}
134190

135191
const procOption = await showBackgroundProcessOptions(proc || selected);
136192

137193
if (procOption === 'logs' && proc) {
138194
clearScreen();
139-
showHeader(projectName);
195+
if (workspaceName) {
196+
showMonorepoHeader(rootPkg.projectName, workspaceName);
197+
} else {
198+
showHeader(projectName);
199+
}
140200
await showLogs(proc);
141201
} else if (procOption === 'kill') {
142202
const killed = killProcess(selected.pid);
@@ -154,22 +214,33 @@ async function main() {
154214
continue;
155215
}
156216

157-
const execOption = await showExecutionOptions(selected);
217+
const isScriptFavorite = await isFavorite(selected, currentDirectory);
218+
const execOption = await showExecutionOptions(selected, isScriptFavorite);
158219

159220
if (execOption === 'back') {
160221
needsRefresh = true;
161222
continue;
162223
}
163224

164-
if (execOption === 'interactive') {
165-
await runScript(selected, options.directory);
225+
if (execOption === 'favorite') {
226+
const nowFavorite = await toggleFavorite({ script: selected, directory: currentDirectory, projectName });
227+
if (nowFavorite) {
228+
showSuccess(`"${selected}" adicionado aos favoritos`);
229+
} else {
230+
showInfo(`"${selected}" removido dos favoritos`);
231+
}
232+
needsRefresh = true;
233+
} else if (execOption === 'interactive') {
234+
await addToHistory({ script: selected, directory: currentDirectory, projectName });
235+
await runScript(selected, currentDirectory);
166236
needsRefresh = true;
167237
} else if (execOption === 'background') {
168-
const result = runScriptBackground(selected, options.directory);
238+
await addToHistory({ script: selected, directory: currentDirectory, projectName });
239+
const result = runScriptBackground(selected, currentDirectory);
169240
backgroundProcesses.push({ name: selected, pid: result.pid, logs: result.logs });
170241
needsRefresh = true;
171242
} else if (execOption === 'copy') {
172-
const command = getScriptCommand(selected, options.directory);
243+
const command = getScriptCommand(selected, currentDirectory);
173244
try {
174245
await copyToClipboard(command);
175246
showSuccess(`Comando copiado: ${command}`);

0 commit comments

Comments
 (0)