@@ -9,38 +9,54 @@ const EOL = require('os').EOL;
99const prompts = require ( 'prompts' ) ;
1010const execa = require ( 'execa' ) ;
1111const updateNotifier = require ( 'update-notifier' ) ;
12+ const envPaths = require ( 'env-paths' ) ;
13+ const fs = require ( 'fs-extra' ) ;
14+ const crypto = require ( 'crypto' ) ;
1215
1316const 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 */
2024const 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 */
85100function printColumns ( heading , data ) {
86101 const columns = columnify ( data , { } ) ;
@@ -99,7 +114,7 @@ function printColumns(heading, data) {
99114 * Print a nice header
100115 */
101116function 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() {
108123function 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) {
119136function 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 */
190264function 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 */
200287async 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+
213318try {
214319 init ( ) ;
215320} catch ( error ) {
0 commit comments