Skip to content

Commit d4d9c65

Browse files
authored
Merge pull request #13 from alexilyaev/add-last-command
[WIP]: Rerun last executed script
2 parents 750335b + c9556e9 commit d4d9c65

File tree

5 files changed

+193
-40
lines changed

5 files changed

+193
-40
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,26 @@ Try it out with `npx`:
1919
npx runrun-cli
2020
```
2121

22-
Install globally (enables to execute `rr` anywhere in the command line):
22+
Install globally (enables to execute `rr` and `rrr` anywhere):
2323

2424
```shell
2525
npm install -g runrun-cli
2626
```
2727

2828
## Usage
2929

30-
`rr` looks for a `package.json` with `scripts` defined and lets you
31-
interactively choose which script to run:
30+
Interactively choose which script to run from `package.json`:
3231

3332
```shell
3433
rr
3534
```
3635

36+
Re-run last chosen script (same as `rr -r`):
37+
38+
```shell
39+
rrr
40+
```
41+
3742
For CLI options, use the `-h` (or `--help`) argument:
3843

3944
```shell

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"main": "index.js",
66
"bin": {
77
"runrun": "src/bin/runrun.js",
8-
"rr": "src/bin/runrun.js"
8+
"rr": "src/bin/runrun.js",
9+
"runrunrun": "src/bin/runrunrun.js",
10+
"rrr": "src/bin/runrunrun.js"
911
},
1012
"scripts": {
1113
"start": "node index.js",
@@ -40,7 +42,9 @@
4042
"lodash": "4.17.15",
4143
"prompts": "2.3.0",
4244
"update-notifier": "4.0.0",
43-
"yargs": "14.0.0"
45+
"yargs": "14.0.0",
46+
"env-paths": "2.2.0",
47+
"fs-extra": "8.1.0"
4448
},
4549
"devDependencies": {
4650
"babel-eslint": "10.0.3",

src/bin/runrunrun.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
// Override CLI options with custom args (emulates passing `-r` option).
5+
// https://github.com/yargs/yargs/blob/master/docs/api.md#envprefix
6+
process.env.RUNRUN_CLI_RERUN = true;
7+
8+
require('../lib/cli');

src/lib/cli.js

Lines changed: 140 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,54 @@ const EOL = require('os').EOL;
99
const prompts = require('prompts');
1010
const execa = require('execa');
1111
const updateNotifier = require('update-notifier');
12+
const envPaths = require('env-paths');
13+
const fs = require('fs-extra');
14+
const crypto = require('crypto');
1215

1316
const pkg = require('../../package.json');
1417

15-
const defaultResultsLimit = 20;
18+
const DEFAULT_RESULTS_LIMIT = 20;
19+
const currentWorkingDir = process.cwd();
1620

1721
/**
1822
* Define command line arguments
1923
*/
2024
const args = yargs
21-
.usage('rr [options]')
22-
.example('rr', '')
23-
.example('rr -a', '')
24-
.example('rr -c path/to/package.custom.json', '')
25+
.example('rr [options]', 'Run interactive scripts runner')
26+
.example('rrr', 'Rerun last executed script (same as `rr -r`)')
27+
.option('r', {
28+
type: 'boolean',
29+
alias: 'rerun',
30+
describe: `Rerun the last executed script`,
31+
})
32+
.option('a', {
33+
type: 'boolean',
34+
alias: 'all',
35+
describe: `Show all available scripts instead of just ${DEFAULT_RESULTS_LIMIT}`,
36+
})
2537
.option('c', {
2638
type: 'string',
2739
alias: 'config',
28-
describe: `Path to custom package.json`,
40+
describe: `Path to custom package.json, relative to current working dir`,
2941
})
30-
.option('a', {
42+
// For debugging purposes
43+
.option('cacheFile', {
3144
type: 'boolean',
32-
alias: 'all',
33-
describe: `Show all available scripts instead of just ${defaultResultsLimit}`,
45+
alias: 'cacheFile',
46+
describe: `Show the cache file path for the current project`,
47+
hidden: true,
3448
})
3549
.help('h')
3650
.alias('h', 'help')
3751
.group(['help'], 'General:')
52+
// Allow setting CLI options with environment variables:
53+
// https://github.com/yargs/yargs/blob/master/docs/api.md#envprefix
54+
.env('RUNRUN_CLI')
3855
.wrap(100).argv;
3956

40-
const targetConfigPath = args.config
41-
? path.resolve(args.config)
42-
: path.resolve(process.cwd(), 'package.json');
43-
const configJson = require(targetConfigPath);
57+
const userConfigPath = args.config || 'package.json';
58+
const userConfigFullPath = path.resolve(currentWorkingDir, userConfigPath);
59+
const userConfig = require(userConfigFullPath);
4460

4561
/**
4662
* High resolution timing API
@@ -66,8 +82,7 @@ function handleError(err) {
6682
printColumns(chalk.red('Error: ' + errMsg));
6783
printColumns(
6884
chalk.white(
69-
"If you can't settle this, please open an issue at:" +
70-
EOL +
85+
`If you can't settle this, please open an issue at:${EOL}` +
7186
chalk.cyan(pkg.bugs.url)
7287
)
7388
);
@@ -77,10 +92,10 @@ function handleError(err) {
7792
/**
7893
* Print to stdout
7994
*
95+
* @see [columnify](https://github.com/timoxley/columnify)
96+
*
8097
* @param {string} heading
8198
* @param {Array} [data]
82-
*
83-
* @see [columnify](https://github.com/timoxley/columnify)
8499
*/
85100
function printColumns(heading, data) {
86101
const columns = columnify(data, {});
@@ -99,7 +114,7 @@ function printColumns(heading, data) {
99114
* Print a nice header
100115
*/
101116
function printBegin() {
102-
printColumns(chalk.whiteBright.bold(`runrun v${pkg.version}`));
117+
printColumns(chalk.whiteBright.dim(`runrun-cli: v${pkg.version}`));
103118
}
104119

105120
/**
@@ -108,7 +123,9 @@ function printBegin() {
108123
function printTimingAndExit(startTime) {
109124
const execTime = time() - startTime;
110125

111-
printColumns(chalk.green(`Finished in: ${execTime.toFixed()}ms`));
126+
printColumns(
127+
chalk.green(`${EOL}runrun-cli: Finished in ${execTime.toFixed()}ms`)
128+
);
112129
process.exit(0);
113130
}
114131

@@ -119,8 +136,8 @@ function printTimingAndExit(startTime) {
119136
function notifyOnUpdate() {
120137
const notifier = updateNotifier({
121138
pkg: {
122-
name: configJson.name,
123-
version: configJson.version,
139+
name: pkg.name,
140+
version: pkg.version,
124141
},
125142
// How often to check for updates (1 day)
126143
updateCheckInterval: 1000 * 60 * 60 * 24,
@@ -148,18 +165,73 @@ function suggestByTitle(input, choices) {
148165
}
149166

150167
/**
151-
* @see [prompts](https://github.com/terkelg/prompts)
168+
* Cache the executed script name so we could re-run it using `rrr`.
152169
*/
153-
async function promptUser() {
154-
const { scripts } = configJson;
170+
async function cacheTargetScript(cacheFilePath, targetScript) {
171+
await fs.outputJson(cacheFilePath, {
172+
cwd: currentWorkingDir,
173+
lastTargetScript: targetScript,
174+
});
175+
}
155176

156-
if (!scripts) {
177+
/**
178+
* Get the path to the cache file for the current working dir.
179+
*
180+
* @see
181+
* https://medium.com/@chris_72272/what-is-the-fastest-node-js-hashing-algorithm-c15c1a0e164e
182+
*/
183+
function getCacheFilePath() {
184+
// e.g. On macOS: `/Users/user_name/Library/Preferences/runrun-cli-nodejs`
185+
const osConfigDirPath = envPaths(pkg.name).config;
186+
// e.g. `CZNTqAaVkXSOJ9ywDrnElP1E1Iw=`
187+
const cwdHash = crypto
188+
.createHash('sha1')
189+
.update(currentWorkingDir)
190+
.digest('base64');
191+
// e.g. `my-project.CZNTqAaVkXSOJ9ywDrnElP1E1Iw=.json`
192+
const filename = `${path.basename(currentWorkingDir)}.${cwdHash}.json`;
193+
194+
return path.join(osConfigDirPath, filename);
195+
}
196+
197+
function getLastTargetScriptName(cacheFilePath, scripts) {
198+
try {
199+
const lastTargetScript = require(cacheFilePath).lastTargetScript;
200+
201+
if (!scripts[lastTargetScript]) {
202+
printColumns(
203+
chalk.yellow(
204+
`The script '${lastTargetScript}' no longer exists in:${EOL}` +
205+
chalk.cyan(userConfigFullPath) +
206+
`${EOL}Please choose a script to run...`
207+
)
208+
);
209+
210+
return null;
211+
}
212+
213+
return lastTargetScript;
214+
} catch (error) {
157215
printColumns(
158-
chalk.red('There are no npm scripts found in the target package.json')
216+
chalk.yellow(
217+
`No cache found for this project.${EOL}` +
218+
`Please choose a script to run...`
219+
)
159220
);
160-
process.exit(0);
221+
222+
return null;
161223
}
224+
}
162225

226+
/**
227+
* Run an interactive autocomplete for the user to choose a script to execute.
228+
*
229+
* @see
230+
* [prompts](https://github.com/terkelg/prompts)
231+
*
232+
* @param {Array} scripts List of `scripts` from a `package.json` file
233+
*/
234+
async function promptUser(scripts) {
163235
const responses = await prompts([
164236
{
165237
type: 'autocomplete',
@@ -170,7 +242,7 @@ async function promptUser() {
170242
value: scriptName,
171243
})),
172244
suggest: suggestByTitle,
173-
limit: args.all ? _.keys(scripts).length : defaultResultsLimit,
245+
limit: args.all ? _.keys(scripts).length : DEFAULT_RESULTS_LIMIT,
174246
},
175247
]);
176248

@@ -184,32 +256,65 @@ async function promptUser() {
184256
}
185257

186258
/**
259+
* @see
260+
* [execa options](https://github.com/sindresorhus/execa#options)
261+
*
187262
* @param {string} targetScriptName The npm script to run
188-
* @see [execa options](https://github.com/sindresorhus/execa#options)
189263
*/
190264
function runNpmScript(targetScriptName) {
191265
return execa.command(`npm run ${targetScriptName}`, {
192-
cwd: path.dirname(targetConfigPath),
266+
// Needed as the user can provide a custom config path
267+
cwd: path.dirname(userConfigFullPath),
193268
stdio: 'inherit',
194269
});
195270
}
196271

272+
function validateScripts(scripts) {
273+
if (!scripts) {
274+
printColumns(
275+
chalk.red(
276+
`There are no npm scripts found in:${EOL}` +
277+
chalk.cyan(userConfigFullPath)
278+
)
279+
);
280+
process.exit(0);
281+
}
282+
}
283+
197284
/**
198285
* Hit it
199286
*/
200287
async function init() {
201-
const startTime = time();
202-
203-
process.on('unhandledRejection', handleError);
204-
205288
printBegin();
206289
notifyOnUpdate();
207-
const targetScript = await promptUser();
290+
291+
const { scripts } = userConfig;
292+
293+
validateScripts(scripts);
294+
295+
let targetScript;
296+
const cacheFilePath = getCacheFilePath();
297+
298+
// For debugging purposes
299+
if (args.cacheFile) {
300+
printColumns(chalk.cyan(cacheFilePath));
301+
}
302+
303+
if (args.rerun) {
304+
targetScript = getLastTargetScriptName(cacheFilePath, scripts);
305+
}
306+
307+
targetScript = targetScript || (await promptUser(scripts));
308+
309+
const startTime = time();
208310

209311
await runNpmScript(targetScript);
312+
await cacheTargetScript(cacheFilePath, targetScript);
210313
printTimingAndExit(startTime);
211314
}
212315

316+
process.on('unhandledRejection', handleError);
317+
213318
try {
214319
init();
215320
} catch (error) {

0 commit comments

Comments
 (0)