11import { mkdtemp , readdir , stat , rm } from "fs/promises" ;
2- import { spawn } from "child_process" ;
3- import { CliFlags , RepoOwner } from "./types" ;
4- import { getRepoAndOwner , waitOnChildProcessToExit } from "./utils" ;
2+ import { ChildProcess , spawn as actualSpawn } from "child_process" ;
53import ArrayStringMap from "array-string-map" ;
64import filterAsync from "node-filter-async" ;
75import { tmpdir } from "os" ;
6+ import { getRepoAndOwner , waitOnChildProcessToExit } from "./utils" ;
7+ import { CliFlags , RepoOwner } from "./types" ;
8+
9+ // We need to make a spawn cache. If an exception is thrown,
10+ // we need to be able to comply with the --terminate flag..
11+ const spawnedProcesses :Map < ChildProcess , null > = new Map ( ) ;
12+
13+ async function spawn ( command :string , args ?:string [ ] , options ?:object ) {
14+ const childProcess = await actualSpawn ( command , args , options ) ;
15+ spawnedProcesses . set ( childProcess , null ) ;
16+ childProcess . on ( "exit" , ( ) => spawnedProcesses . delete ( childProcess ) ) ;
17+ return childProcess ;
18+ }
19+
20+ // eslint-disable-next-line no-undef -- NodeJS is a fake namespace by TypeScript.
21+ function terminateSpawnCache ( signal :NodeJS . Signals = "SIGTERM" ) {
22+ for ( const childProcess of spawnedProcesses . keys ( ) ) {
23+ childProcess . kill ( signal ) ;
24+ }
25+ }
826
927export default async function handler ( script :string , flags :CliFlags ) {
10- console . log ( script , flags ) ;
1128const { owner :defaultOwner , _ :repoNames } = flags ;
29+ let { search} = flags ;
1230const repos :RepoOwner [ ] = [ ] ;
1331for ( const repoName of repoNames ) {
1432try {
@@ -20,35 +38,39 @@ export default async function handler(script: string, flags: CliFlags) {
2038process . exit ( 1 ) ;
2139}
2240}
41+
2342const tempDir = await mkdtemp ( `${ tmpdir ( ) } /github-run-script-` ) ;
2443try {
2544const directoryMapping :ArrayStringMap < RepoOwner , string > =
2645new ArrayStringMap ( ) ;
2746const scanDirectories :Map < string , string [ ] > = new Map ( ) ;
28- if ( flags . search ) {
47+ if ( search ) {
2948// If it's one path, it won't be in an array. This converts it into an array.
30- if ( typeof flags . search === "string" ) {
31- flags . search = [ flags . search ] ;
49+ if ( typeof search === "string" ) {
50+ search = [ search ] ;
3251}
52+
3353// What this code block does is simple. It stores in `scanDirectories`
3454// a mapping of source path name to an array of directories in that directory.
3555await Promise . all (
36- flags . search . map ( async ( path ) => {
56+ search . map ( async ( path ) => {
3757scanDirectories . set (
3858path ,
39- await filterAsync ( await readdir ( path ) , async ( item ) => {
40- return ( await stat ( `${ path } /${ item } ` ) ) . isDirectory ( ) ;
41- } )
59+ await filterAsync ( await readdir ( path ) , async ( item ) =>
60+ ( await stat ( `${ path } /${ item } ` ) ) . isDirectory ( )
61+ )
4262) ;
4363} )
4464) ;
4565}
66+
4667await Promise . all (
4768repos . map ( async ( [ owner , repo ] ) => {
4869// First, we need to check if the repository exists in `scanDirectories`.
49- // TODO: Handle cases where the same repo is present multiple times in
50- // TODO: different directories, or if two repos with the same name but
51- // TODO: different owners is provided. (Maybe we can check `.git`.)
70+ // TODO:
71+ // Handle cases where the same repo is present multiple times in
72+ // different directories, or if two repos with the same name but
73+ // different owners is provided. (Maybe we can check `.git`.)
5274for ( const [ path , directories ] of scanDirectories ) {
5375for ( const directory of directories ) {
5476if ( repo === directory ) {
@@ -57,12 +79,14 @@ export default async function handler(script: string, flags: CliFlags) {
5779break ;
5880}
5981}
82+
6083// If we already found a match earlier, no need to re-iterate over the other
6184// directories.
6285if ( directoryMapping . has ( [ owner , repo ] ) ) {
6386break ;
6487}
6588}
89+
6690// Deal wit the special case where we did not find a match. Time to clone.
6791if ( ! directoryMapping . has ( [ owner , repo ] ) ) {
6892const destPath = `${ tempDir } /${ repo } ` ;
@@ -75,6 +99,7 @@ export default async function handler(script: string, flags: CliFlags) {
7599await waitOnChildProcessToExit ( childProc ) ;
76100directoryMapping . set ( [ owner , repo ] , destPath ) ;
77101}
102+
78103// Time to execute the script!
79104const path = directoryMapping . get ( [ owner , repo ] ) ;
80105const childProc = await spawn ( script , [ ] , {
@@ -92,6 +117,11 @@ export default async function handler(script: string, flags: CliFlags) {
92117} )
93118) ;
94119} finally {
120+ if ( flags . terminate ?? true ) {
121+ // eslint-disable-next-line no-undef -- NodeJS is a fake namespace by TypeScript.
122+ terminateSpawnCache ( flags . signal as NodeJS . Signals ) ;
123+ }
124+
95125// We need to clean up the temporary directory.
96126await rm ( tempDir , { recursive :true , force :true } ) ;
97127}