flutter-skill-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/bin/cli.js +73 -0
- package/dart/.dart_tool/package_config.json +196 -0
- package/dart/.dart_tool/package_graph.json +258 -0
- package/dart/.dart_tool/version +1 -0
- package/dart/bin/server.dart +3 -0
- package/dart/lib/flutter_skill.dart +1167 -0
- package/dart/lib/src/cli/act.dart +118 -0
- package/dart/lib/src/cli/inspect.dart +65 -0
- package/dart/lib/src/cli/launch.dart +89 -0
- package/dart/lib/src/cli/server.dart +594 -0
- package/dart/lib/src/cli/setup.dart +76 -0
- package/dart/lib/src/flutter_skill_client.dart +286 -0
- package/dart/pubspec.lock +237 -0
- package/dart/pubspec.yaml +26 -0
- package/index.js +7 -0
- package/package.json +41 -0
- package/scripts/build.js +67 -0
- package/scripts/check-dart.js +39 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import '../flutter_skill_client.dart';
|
|
3
|
+
|
|
4
|
+
Future<void> runAct(List<String> args) async {
|
|
5
|
+
String uri;
|
|
6
|
+
|
|
7
|
+
// Logic from bin/act.dart to separate URI from args
|
|
8
|
+
String resolveUriResult;
|
|
9
|
+
int argOffset;
|
|
10
|
+
|
|
11
|
+
if (args.isNotEmpty &&
|
|
12
|
+
(args[0].startsWith('ws://') || args[0].startsWith('http://'))) {
|
|
13
|
+
resolveUriResult = args[0];
|
|
14
|
+
argOffset = 1;
|
|
15
|
+
} else {
|
|
16
|
+
// Try file (we assume we are running from project root where .flutter_skill_uri might exist)
|
|
17
|
+
// NOTE: If global activated, we might not be in the right dir?
|
|
18
|
+
// Users should run this in their project root.
|
|
19
|
+
final file = File('.flutter_skill_uri');
|
|
20
|
+
if (await file.exists()) {
|
|
21
|
+
resolveUriResult = (await file.readAsString()).trim();
|
|
22
|
+
} else {
|
|
23
|
+
print('Usage: flutter_skill act [vm-uri] <action> <params...>');
|
|
24
|
+
exit(1);
|
|
25
|
+
}
|
|
26
|
+
argOffset = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (args.length <= argOffset) {
|
|
30
|
+
print('Missing action argument');
|
|
31
|
+
exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
uri = resolveUriResult;
|
|
35
|
+
String action = args[argOffset];
|
|
36
|
+
final client = FlutterSkillClient(uri);
|
|
37
|
+
|
|
38
|
+
String? param1;
|
|
39
|
+
String? param2;
|
|
40
|
+
if (args.length > argOffset + 1) param1 = args[argOffset + 1];
|
|
41
|
+
if (args.length > argOffset + 2) param2 = args[argOffset + 2];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await client.connect();
|
|
45
|
+
|
|
46
|
+
switch (action) {
|
|
47
|
+
case 'tap':
|
|
48
|
+
if (param1 == null) throw ArgumentError('tap requires a key or text');
|
|
49
|
+
await client.tap(key: param1);
|
|
50
|
+
print('Tapped "$param1"');
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case 'enter_text':
|
|
54
|
+
if (param1 == null || param2 == null)
|
|
55
|
+
throw ArgumentError('enter_text requires key and text');
|
|
56
|
+
await client.enterText(param1, param2);
|
|
57
|
+
print('Entered text "$param2" into "$param1"');
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case 'scroll_to':
|
|
61
|
+
if (param1 == null)
|
|
62
|
+
throw ArgumentError('scroll_to requires a key or text');
|
|
63
|
+
await client.scrollTo(key: param1);
|
|
64
|
+
print('Scrolled to "$param1"');
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'assert_visible':
|
|
68
|
+
if (param1 == null)
|
|
69
|
+
throw ArgumentError('assert_visible requires a key or text');
|
|
70
|
+
final target = param1;
|
|
71
|
+
final elements = await client.getInteractiveElements();
|
|
72
|
+
if (_findTarget(elements, target)) {
|
|
73
|
+
print('Assertion Passed: "$target" is visible.');
|
|
74
|
+
} else {
|
|
75
|
+
throw Exception('Assertion Failed: "$target" is NOT visible.');
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case 'assert_gone':
|
|
80
|
+
if (param1 == null)
|
|
81
|
+
throw ArgumentError('assert_gone requires a key or text');
|
|
82
|
+
final target = param1;
|
|
83
|
+
final elements = await client.getInteractiveElements();
|
|
84
|
+
if (!_findTarget(elements, target)) {
|
|
85
|
+
print('Assertion Passed: "$target" is gone.');
|
|
86
|
+
} else {
|
|
87
|
+
throw Exception('Assertion Failed: "$target" is STILL visible.');
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
print('Unknown action: $action');
|
|
93
|
+
exit(1);
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
print('Error: $e');
|
|
97
|
+
exit(1);
|
|
98
|
+
} finally {
|
|
99
|
+
await client.disconnect();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bool _findTarget(List<dynamic> elements, String target) {
|
|
104
|
+
for (final e in elements) {
|
|
105
|
+
if (e is! Map) continue;
|
|
106
|
+
final key = e['key']?.toString();
|
|
107
|
+
final text = e['text']?.toString();
|
|
108
|
+
if (key == target || (text != null && text.contains(target))) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (e.containsKey('children')) {
|
|
112
|
+
if (_findTarget((e['children'] as List).cast<dynamic>(), target)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import '../flutter_skill_client.dart'; // Relative import to sibling dir
|
|
3
|
+
|
|
4
|
+
Future<void> runInspect(List<String> args) async {
|
|
5
|
+
// No initial arg check, let resolveUri handle it
|
|
6
|
+
// if (args.isEmpty) ...
|
|
7
|
+
|
|
8
|
+
String uri;
|
|
9
|
+
try {
|
|
10
|
+
uri = await FlutterSkillClient.resolveUri(args);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
print(e);
|
|
13
|
+
exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
final client = FlutterSkillClient(uri);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await client.connect();
|
|
20
|
+
final elements = await client.getInteractiveElements();
|
|
21
|
+
|
|
22
|
+
// Print simplified tree for LLM consumption
|
|
23
|
+
print('Interactive Elements:');
|
|
24
|
+
if (elements.isEmpty) {
|
|
25
|
+
print('(No interactive elements found)');
|
|
26
|
+
} else {
|
|
27
|
+
for (final e in elements) {
|
|
28
|
+
_printElement(e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
print('Error: $e');
|
|
33
|
+
exit(1);
|
|
34
|
+
} finally {
|
|
35
|
+
await client.disconnect();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
void _printElement(dynamic element, [String prefix = '']) {
|
|
40
|
+
if (element is! Map) return;
|
|
41
|
+
|
|
42
|
+
// Try to extract useful info
|
|
43
|
+
final type = element['type'] ?? 'Widget';
|
|
44
|
+
final key = element['key'];
|
|
45
|
+
final text = element['text'];
|
|
46
|
+
final tooltip = element['tooltip'];
|
|
47
|
+
|
|
48
|
+
var buffer = StringBuffer();
|
|
49
|
+
buffer.write(prefix);
|
|
50
|
+
buffer.write('- **$type**');
|
|
51
|
+
|
|
52
|
+
if (key != null) buffer.write(' [Key: "$key"]');
|
|
53
|
+
if (text != null && text.toString().isNotEmpty)
|
|
54
|
+
buffer.write(' [Text: "$text"]');
|
|
55
|
+
if (tooltip != null) buffer.write(' [Tooltip: "$tooltip"]');
|
|
56
|
+
|
|
57
|
+
print(buffer.toString());
|
|
58
|
+
|
|
59
|
+
// Recursively print children if any
|
|
60
|
+
if (element.containsKey('children') && element['children'] is List) {
|
|
61
|
+
for (final child in element['children']) {
|
|
62
|
+
_printElement(child, '$prefix ');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
import 'setup.dart'; // Import setup logic
|
|
4
|
+
|
|
5
|
+
// File to store the last connected URI
|
|
6
|
+
final _uriFile = File('.flutter_skill_uri');
|
|
7
|
+
|
|
8
|
+
Future<void> runLaunch(List<String> args) async {
|
|
9
|
+
// Extract project path. Everything else is passed to flutter run.
|
|
10
|
+
// We assume: flutter_skill launch [project_path] [flutter_args...]
|
|
11
|
+
// But wait, standard args might be tricky.
|
|
12
|
+
// Let's say: first arg is project path if it doesn't start with -?
|
|
13
|
+
|
|
14
|
+
String projectPath = '.';
|
|
15
|
+
List<String> flutterArgs = [];
|
|
16
|
+
|
|
17
|
+
if (args.isNotEmpty) {
|
|
18
|
+
if (!args[0].startsWith('-')) {
|
|
19
|
+
projectPath = args[0];
|
|
20
|
+
flutterArgs = args.sublist(1);
|
|
21
|
+
} else {
|
|
22
|
+
// Current dir, all args are for flutter
|
|
23
|
+
flutterArgs = args;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
print('Running auto-setup...');
|
|
28
|
+
try {
|
|
29
|
+
// Call the setup logic directly
|
|
30
|
+
await runSetup(projectPath);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
print('Setup failed: $e');
|
|
33
|
+
print('Proceeding with launch anyway...');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
print('Launching Flutter app in: $projectPath with args: $flutterArgs');
|
|
37
|
+
|
|
38
|
+
final process = await Process.start(
|
|
39
|
+
'flutter',
|
|
40
|
+
['run', ...flutterArgs],
|
|
41
|
+
workingDirectory: projectPath,
|
|
42
|
+
mode: ProcessStartMode.normal,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
print(
|
|
46
|
+
'Flutter process started (PID: ${process.pid}). Waiting for connection URI...');
|
|
47
|
+
|
|
48
|
+
process.stdout
|
|
49
|
+
.transform(utf8.decoder)
|
|
50
|
+
.transform(const LineSplitter())
|
|
51
|
+
.listen((line) {
|
|
52
|
+
print('[Flutter]: $line');
|
|
53
|
+
_checkForUri(line);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
process.stderr
|
|
57
|
+
.transform(utf8.decoder)
|
|
58
|
+
.transform(const LineSplitter())
|
|
59
|
+
.listen((line) {
|
|
60
|
+
print('[Flutter Error]: $line');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Forward stdin to flutter run
|
|
64
|
+
if (stdin.hasTerminal) {
|
|
65
|
+
stdin.listen((data) => process.stdin.add(data));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
final exitCode = await process.exitCode;
|
|
69
|
+
print('Flutter app exited with code $exitCode');
|
|
70
|
+
exit(exitCode);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void _checkForUri(String line) {
|
|
74
|
+
if (line.contains('ws://')) {
|
|
75
|
+
final uriRegex = RegExp(r'ws://[^\s]+');
|
|
76
|
+
final match = uriRegex.firstMatch(line);
|
|
77
|
+
if (match != null) {
|
|
78
|
+
final uri = match.group(0)!;
|
|
79
|
+
print('\nCode-Skill: Found VM Service URI: $uri');
|
|
80
|
+
try {
|
|
81
|
+
_uriFile.writeAsStringSync(uri);
|
|
82
|
+
print(
|
|
83
|
+
'Code-Skill: URI saved to .flutter_skill_uri. You can now run scripts without arguments.');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
print('Code-Skill: Failed to save URI: $e');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|