11import { spawn , SpawnOptions , exec } from "child_process" ;
2+ import { promises } from "fs" ;
23import fs from "fs" ;
4+ import path from "path" ;
35import { z } from "zod" ;
4- import { promisify } from ' util' ;
6+ import { promisify } from " util" ;
57
68const execAsync = promisify ( exec ) ;
79
@@ -11,14 +13,23 @@ const COMMAND = "docker";
1113
1214type CommandResult = Promise < string > ;
1315
14- const PS_PATTERN = "\"{psID: {{.ID}}, psName: {{.Names}}, workspaceFolder: {{.Label \"devcontainer.local_folder\"}}, container: {{.Label \"dev.containers.id\"}}}\""
16+ const PS_PATTERN =
17+ '{psID: {{.ID}}, psName: {{.Names}}, workspaceFolder: {{.Label "devcontainer.local_folder"}}, container: {{.Label "dev.containers.id"}}}' ;
18+
19+ const WS_FOLDER_DESC =
20+ "A path used to search its subdirectories for all workspace folders containing a devcontainer configuration." ;
1521
1622export const DevCleanupSchema = z . object ( { } ) ;
1723
1824export const DevListSchema = z . object ( { } ) ;
1925
26+ export const DevWsFolderSchema = z . object ( {
27+ rootPath : z . string ( ) . describe ( WS_FOLDER_DESC ) . optional ( ) ,
28+ } ) ;
29+
2030type DevCleanupArgs = z . infer < typeof DevCleanupSchema > ;
2131type DevListArgs = z . infer < typeof DevListSchema > ;
32+ type DevWsFolderArgs = z . infer < typeof DevWsFolderSchema > ;
2233
2334function createOutputStream (
2435 stdioFilePath : string = NULL_DEVICE
@@ -99,11 +110,15 @@ export async function devCleanup(options: DevCleanupArgs): CommandResult {
99110 let ids : string [ ] ;
100111
101112 try {
102- const { stdout } = await execAsync ( "docker ps -aq -f label=dev.containers.id" )
113+ const { stdout } = await execAsync (
114+ "docker ps -aq -f label=dev.containers.id"
115+ ) ;
103116 const raw = stdout . toString ( ) . trim ( ) ;
104117 ids = raw ? raw . split ( "\n" ) : [ ] ;
105118 } catch ( error ) {
106- return Promise . reject ( `Cannot list all docker ps: ${ ( error as Error ) . message } ` ) ;
119+ return Promise . reject (
120+ `Cannot list all docker ps: ${ ( error as Error ) . message } `
121+ ) ;
107122 }
108123
109124 if ( ids . length === 0 ) {
@@ -124,3 +139,67 @@ export async function devList(options: DevListArgs): CommandResult {
124139 stream
125140 ) ;
126141}
142+
143+ // Optionally skip well-known heavy directories to improve performance
144+ function shouldSkipDirectory ( name : string ) : boolean {
145+ const skipDirs = [ "node_modules" , ".git" , "dist" , "build" , ".next" ] ;
146+ return skipDirs . includes ( name ) ;
147+ }
148+
149+ export async function devWsFolder (
150+ options : DevWsFolderArgs
151+ ) : Promise < CommandResult > {
152+ const rootPath = options . rootPath ;
153+ const searchRoot = rootPath ? path . resolve ( rootPath ) : process . cwd ( ) ;
154+ const results : string [ ] = [ ] ;
155+
156+ // Use concurrency to speed up directory traversal
157+ async function scanDirectory ( dir : string ) : Promise < void > {
158+ let entries : fs . Dirent [ ] ;
159+
160+ try {
161+ entries = await promises . readdir ( dir , { withFileTypes : true } ) ;
162+ } catch {
163+ // Skip directories that cannot be read (permissions, transient I/O errors, etc.)
164+ return ;
165+ }
166+
167+ // Collect subdirectories and schedule checks separately to maximize parallelism
168+ const subDirs : string [ ] = [ ] ;
169+ const checkTasks : Promise < void > [ ] = [ ] ;
170+
171+ for ( const entry of entries ) {
172+ const fullPath = path . join ( dir , entry . name ) ;
173+
174+ if ( entry . isDirectory ( ) ) {
175+ if ( entry . name === ".devcontainer" ) {
176+ checkTasks . push (
177+ promises
178+ . access ( path . join ( fullPath , "devcontainer.json" ) )
179+ . then ( ( ) => void results . push ( fullPath ) )
180+ . catch ( ( ) => {
181+ /* file not found; ignore */
182+ } )
183+ ) ;
184+ }
185+ else if ( ! shouldSkipDirectory ( entry . name ) ) {
186+ subDirs . push ( fullPath ) ;
187+ }
188+ }
189+ }
190+
191+ // Wait for all devcontainer.json checks to complete
192+ await Promise . all ( checkTasks ) ;
193+
194+ // Recursively scan subdirectories in parallel
195+ await Promise . all ( subDirs . map ( scanDirectory ) ) ;
196+ }
197+
198+ await scanDirectory ( searchRoot ) ;
199+
200+ if ( results . length === 0 ) {
201+ throw new Error ( `No Workspace Folders found in ${ searchRoot } ` ) ;
202+ }
203+
204+ return "\n" + results . join ( "\n" ) ;
205+ }
0 commit comments