@@ -91,103 +91,150 @@ Future<CallToolResult> runCommandInRoots(
9191 ];
9292 }
9393
94- final outputs = < TextContent > [];
94+ final outputs = < Content > [];
9595 for (var rootConfig in rootConfigs) {
96- final rootUriString = rootConfig[ParameterNames .root] as String ? ;
97- if (rootUriString == null ) {
98- // This shouldn't happen based on the schema, but handle defensively.
99- return CallToolResult (
100- content: [
101- TextContent (text: 'Invalid root configuration: missing `root` key.' ),
102- ],
103- isError: true ,
104- );
105- }
96+ final result = await runCommandInRoot (
97+ request,
98+ rootConfig: rootConfig,
99+ commandForRoot: commandForRoot,
100+ arguments: arguments,
101+ commandDescription: commandDescription,
102+ fileSystem: fileSystem,
103+ processManager: processManager,
104+ knownRoots: knownRoots,
105+ defaultPaths: defaultPaths,
106+ );
107+ if (result.isError == true ) return result;
108+ outputs.addAll (result.content);
109+ }
110+ return CallToolResult (content: outputs);
111+ }
106112
107- final root = _findRoot (rootUriString, knownRoots);
108- if (root == null ) {
109- return CallToolResult (
110- content: [
111- TextContent (
112- text:
113- 'Invalid root $rootUriString , must be under one of the '
114- 'registered project roots:\n\n ${knownRoots .join ('\n ' )}' ,
115- ),
116- ],
117- isError: true ,
118- );
119- }
113+ /// Runs [commandForRoot] in a single project root specified in the
114+ /// [request] , with [arguments] .
115+ ///
116+ /// If [rootConfig] is passed, this will be used to read the root configuration,
117+ /// otherwise it is read directly off of `request.arguments` .
118+ ///
119+ /// These [commandForRoot] plus [arguments] are passed directly to
120+ /// [ProcessManager.run] .
121+ ///
122+ /// The [commandDescription] is used in the output to describe the command
123+ /// being run. For example, if the command is `['dart', 'fix', '--apply']` , the
124+ /// command description might be `dart fix` .
125+ ///
126+ /// [defaultPaths] may be specified if one or more path arguments are required
127+ /// for the command (e.g. `dart format <default paths>` ). The paths can be
128+ /// absolute or relative paths that point to the directories on which the
129+ /// command should be run. For example, the `dart format` command may pass a
130+ /// default path of '.', which indicates that every Dart file in the working
131+ /// directory should be formatted. The value of `defaultPaths` will only be used
132+ /// if the [request] 's root configuration does not contain a set value for a
133+ /// root's 'paths'.
134+ Future <CallToolResult > runCommandInRoot (
135+ CallToolRequest request, {
136+ Map <String , Object ?>? rootConfig,
137+ FutureOr <String > Function (Root , FileSystem ) commandForRoot =
138+ defaultCommandForRoot,
139+ List <String > arguments = const [],
140+ required String commandDescription,
141+ required FileSystem fileSystem,
142+ required ProcessManager processManager,
143+ required List <Root > knownRoots,
144+ List <String > defaultPaths = const < String > [],
145+ }) async {
146+ rootConfig ?? = request.arguments;
147+ final rootUriString = rootConfig? [ParameterNames .root] as String ? ;
148+ if (rootUriString == null ) {
149+ // This shouldn't happen based on the schema, but handle defensively.
150+ return CallToolResult (
151+ content: [
152+ TextContent (text: 'Invalid root configuration: missing `root` key.' ),
153+ ],
154+ isError: true ,
155+ );
156+ }
120157
121- final rootUri = Uri .parse (rootUriString);
122- if (rootUri.scheme != 'file' ) {
123- return CallToolResult (
124- content: [
125- TextContent (
126- text:
127- 'Only file scheme uris are allowed for roots, but got '
128- '$rootUri ' ,
129- ),
130- ],
131- isError: true ,
132- );
133- }
134- final projectRoot = fileSystem.directory (rootUri);
158+ final root = _findRoot (rootUriString, knownRoots);
159+ if (root == null ) {
160+ return CallToolResult (
161+ content: [
162+ TextContent (
163+ text:
164+ 'Invalid root $rootUriString , must be under one of the '
165+ 'registered project roots:\n\n ${knownRoots .join ('\n ' )}' ,
166+ ),
167+ ],
168+ isError: true ,
169+ );
170+ }
135171
136- final commandWithPaths = < String > [
137- await commandForRoot (root, fileSystem),
138- ...arguments,
139- ];
140- final paths =
141- (rootConfig[ParameterNames .paths] as List ? )? .cast <String >() ??
142- defaultPaths;
143- final invalidPaths = paths.where ((path) {
144- final resolvedPath = rootUri.resolve (path).toString ();
145- return rootUriString != resolvedPath &&
146- ! p.isWithin (rootUriString, resolvedPath);
147- });
148- if (invalidPaths.isNotEmpty) {
149- return CallToolResult (
150- content: [
151- TextContent (
152- text:
153- 'Paths are not allowed to escape their project root:\n '
154- '${invalidPaths .join ('\n ' )}' ,
155- ),
156- ],
157- isError: true ,
158- );
159- }
160- commandWithPaths.addAll (paths);
172+ final rootUri = Uri .parse (rootUriString);
173+ if (rootUri.scheme != 'file' ) {
174+ return CallToolResult (
175+ content: [
176+ TextContent (
177+ text:
178+ 'Only file scheme uris are allowed for roots, but got '
179+ '$rootUri ' ,
180+ ),
181+ ],
182+ isError: true ,
183+ );
184+ }
185+ final projectRoot = fileSystem.directory (rootUri);
161186
162- final result = await processManager.run (
163- commandWithPaths,
164- workingDirectory: projectRoot.path,
165- runInShell: true ,
187+ final commandWithPaths = < String > [
188+ await commandForRoot (root, fileSystem),
189+ ...arguments,
190+ ];
191+ final paths =
192+ (rootConfig? [ParameterNames .paths] as List ? )? .cast <String >() ??
193+ defaultPaths;
194+ final invalidPaths = paths.where ((path) {
195+ final resolvedPath = rootUri.resolve (path).toString ();
196+ return rootUriString != resolvedPath &&
197+ ! p.isWithin (rootUriString, resolvedPath);
198+ });
199+ if (invalidPaths.isNotEmpty) {
200+ return CallToolResult (
201+ content: [
202+ TextContent (
203+ text:
204+ 'Paths are not allowed to escape their project root:\n '
205+ '${invalidPaths .join ('\n ' )}' ,
206+ ),
207+ ],
208+ isError: true ,
166209 );
210+ }
211+ commandWithPaths.addAll (paths);
167212
168- final output = (result.stdout as String ).trim ();
169- final errors = (result.stderr as String ).trim ();
170- if (result.exitCode != 0 ) {
171- return CallToolResult (
172- content: [
173- TextContent (
174- text:
175- '$commandDescription failed in ${projectRoot .path }:\n '
176- '$output \n\n Errors\n $errors ' ,
177- ),
178- ],
179- isError: true ,
180- );
181- }
182- if (output.isNotEmpty) {
183- outputs.add (
213+ final result = await processManager.run (
214+ commandWithPaths,
215+ workingDirectory: projectRoot.path,
216+ runInShell: true ,
217+ );
218+
219+ final output = (result.stdout as String ).trim ();
220+ final errors = (result.stderr as String ).trim ();
221+ if (result.exitCode != 0 ) {
222+ return CallToolResult (
223+ content: [
184224 TextContent (
185- text: '$commandDescription in ${projectRoot .path }:\n $output ' ,
225+ text:
226+ '$commandDescription failed in ${projectRoot .path }:\n '
227+ '$output \n\n Errors\n $errors ' ,
186228 ),
187- );
188- }
229+ ],
230+ isError: true ,
231+ );
189232 }
190- return CallToolResult (content: outputs);
233+ return CallToolResult (
234+ content: [
235+ TextContent (text: '$commandDescription in ${projectRoot .path }:\n $output ' ),
236+ ],
237+ );
191238}
192239
193240/// Returns 'dart' or 'flutter' based on the pubspec contents.
@@ -224,12 +271,7 @@ ListSchema rootsSchema({bool supportsPaths = false}) => Schema.list(
224271 title: 'All projects roots to run this tool in.' ,
225272 items: Schema .object (
226273 properties: {
227- ParameterNames .root: Schema .string (
228- title: 'The URI of the project root to run this tool in.' ,
229- description:
230- 'This must be equal to or a subdirectory of one of the roots '
231- 'returned by a call to "listRoots".' ,
232- ),
274+ ParameterNames .root: rootSchema,
233275 if (supportsPaths)
234276 ParameterNames .paths: Schema .list (
235277 title:
@@ -242,6 +284,13 @@ ListSchema rootsSchema({bool supportsPaths = false}) => Schema.list(
242284 ),
243285);
244286
287+ final rootSchema = Schema .string (
288+ title: 'The URI of the project root to run this tool in.' ,
289+ description:
290+ 'This must be equal to or a subdirectory of one of the roots '
291+ 'returned by a call to "listRoots".' ,
292+ );
293+
245294/// Very thin extension type for a pubspec just containing what we need.
246295///
247296/// We assume a valid pubspec.
0 commit comments