flutter-skill 0.7.4
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 +440 -0
- package/bin/cli.js +316 -0
- package/dart/bin/server.dart +3 -0
- package/dart/lib/flutter_skill.dart +1283 -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 +654 -0
- package/dart/lib/src/cli/setup.dart +76 -0
- package/dart/lib/src/flutter_skill_client.dart +294 -0
- package/dart/pubspec.yaml +26 -0
- package/index.js +7 -0
- package/package.json +45 -0
- package/scripts/build.js +67 -0
- package/scripts/check-dart.js +39 -0
- package/scripts/postinstall.js +142 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
3
|
+
Future<void> runSetup(String projectPath) async {
|
|
4
|
+
final pubspecFile = File('$projectPath/pubspec.yaml');
|
|
5
|
+
final mainFile = File('$projectPath/lib/main.dart');
|
|
6
|
+
|
|
7
|
+
if (!pubspecFile.existsSync()) {
|
|
8
|
+
print('Error: pubspec.yaml not found at $projectPath');
|
|
9
|
+
exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!mainFile.existsSync()) {
|
|
13
|
+
print('Error: lib/main.dart not found at $projectPath');
|
|
14
|
+
exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
print('Checking dependencies in ${pubspecFile.path}...');
|
|
18
|
+
final pubspecContent = pubspecFile.readAsStringSync();
|
|
19
|
+
|
|
20
|
+
if (!pubspecContent.contains('flutter_skill:')) {
|
|
21
|
+
print('Adding flutter_skill dependency...');
|
|
22
|
+
final result = await Process.run(
|
|
23
|
+
'flutter',
|
|
24
|
+
['pub', 'add', 'flutter_skill'],
|
|
25
|
+
workingDirectory: projectPath,
|
|
26
|
+
);
|
|
27
|
+
if (result.exitCode != 0) {
|
|
28
|
+
print('Failed to add dependency: ${result.stderr}');
|
|
29
|
+
exit(1);
|
|
30
|
+
}
|
|
31
|
+
print('Dependency added.');
|
|
32
|
+
} else {
|
|
33
|
+
print('Dependency flutter_skill already exists.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
print('Checking instrumentation in ${mainFile.path}...');
|
|
37
|
+
String mainContent = mainFile.readAsStringSync();
|
|
38
|
+
|
|
39
|
+
bool changed = false;
|
|
40
|
+
|
|
41
|
+
// 1. Check Import
|
|
42
|
+
if (!mainContent.contains('package:flutter_skill/flutter_skill.dart')) {
|
|
43
|
+
mainContent =
|
|
44
|
+
"import 'package:flutter_skill/flutter_skill.dart';\nimport 'package:flutter/foundation.dart'; // For kDebugMode\n" +
|
|
45
|
+
mainContent;
|
|
46
|
+
changed = true;
|
|
47
|
+
print('Added imports.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Check Initialization
|
|
51
|
+
if (!mainContent.contains('FlutterSkillBinding.ensureInitialized()')) {
|
|
52
|
+
final mainRegex = RegExp(r'void\s+main\s*\(\s*\)\s*\{');
|
|
53
|
+
final match = mainRegex.firstMatch(mainContent);
|
|
54
|
+
|
|
55
|
+
if (match != null) {
|
|
56
|
+
final end = match.end;
|
|
57
|
+
const injection =
|
|
58
|
+
'\n if (kDebugMode) {\n FlutterSkillBinding.ensureInitialized();\n }\n';
|
|
59
|
+
mainContent = mainContent.replaceRange(end, end, injection);
|
|
60
|
+
changed = true;
|
|
61
|
+
print('Added FlutterSkillBinding initialization.');
|
|
62
|
+
} else {
|
|
63
|
+
print(
|
|
64
|
+
'Warning: Could not find "void main() {" to inject code. Manual setup required.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (changed) {
|
|
69
|
+
mainFile.writeAsStringSync(mainContent);
|
|
70
|
+
print('Updated lib/main.dart.');
|
|
71
|
+
} else {
|
|
72
|
+
print('No changes needed for lib/main.dart.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
print('Setup complete.');
|
|
76
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:vm_service/vm_service.dart';
|
|
3
|
+
import 'package:vm_service/vm_service_io.dart';
|
|
4
|
+
|
|
5
|
+
class FlutterSkillClient {
|
|
6
|
+
final String wsUri;
|
|
7
|
+
VmService? _service;
|
|
8
|
+
String? _isolateId;
|
|
9
|
+
|
|
10
|
+
FlutterSkillClient(this.wsUri);
|
|
11
|
+
|
|
12
|
+
Future<void> connect() async {
|
|
13
|
+
print('DEBUG: Connecting to $wsUri');
|
|
14
|
+
_service = await vmServiceConnectUri(wsUri);
|
|
15
|
+
print('DEBUG: Connected to VM Service');
|
|
16
|
+
|
|
17
|
+
final vm = await _service!.getVM();
|
|
18
|
+
print('DEBUG: Got VM info');
|
|
19
|
+
final isolates = vm.isolates;
|
|
20
|
+
if (isolates == null || isolates.isEmpty) {
|
|
21
|
+
throw Exception('No isolates found');
|
|
22
|
+
}
|
|
23
|
+
_isolateId = isolates.first.id!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Future<void> disconnect() async {
|
|
27
|
+
await _service?.dispose();
|
|
28
|
+
_service = null;
|
|
29
|
+
_isolateId = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Future<Map<String, dynamic>> _call(String method,
|
|
33
|
+
[Map<String, dynamic>? args]) async {
|
|
34
|
+
if (_service == null || _isolateId == null) {
|
|
35
|
+
throw Exception('Not connected');
|
|
36
|
+
}
|
|
37
|
+
final response = await _service!.callServiceExtension(
|
|
38
|
+
method,
|
|
39
|
+
isolateId: _isolateId!,
|
|
40
|
+
args: args,
|
|
41
|
+
);
|
|
42
|
+
return response.json ?? {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ==================== EXISTING METHODS ====================
|
|
46
|
+
|
|
47
|
+
Future<List<dynamic>> getInteractiveElements() async {
|
|
48
|
+
final result = await _call('ext.flutter.flutter_skill.interactive');
|
|
49
|
+
print('DEBUG: Interactive Result type: ${result.runtimeType}');
|
|
50
|
+
print('DEBUG: Interactive Result: $result');
|
|
51
|
+
|
|
52
|
+
if (result.containsKey('elements')) {
|
|
53
|
+
return result['elements'] as List<dynamic>;
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Future<void> tap({String? key, String? text}) async {
|
|
59
|
+
if (key == null && text == null) {
|
|
60
|
+
throw ArgumentError('Must provide key or text for tap');
|
|
61
|
+
}
|
|
62
|
+
await _call('ext.flutter.flutter_skill.tap', {
|
|
63
|
+
if (key != null) 'key': key,
|
|
64
|
+
if (text != null) 'text': text,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Future<void> enterText(String key, String text) async {
|
|
69
|
+
await _call('ext.flutter.flutter_skill.enterText', {
|
|
70
|
+
'key': key,
|
|
71
|
+
'text': text,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Future<void> scrollTo({String? key, String? text}) async {
|
|
76
|
+
await _call('ext.flutter.flutter_skill.scroll', {
|
|
77
|
+
if (key != null) 'key': key,
|
|
78
|
+
if (text != null) 'text': text,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ==================== UI INSPECTION ====================
|
|
83
|
+
|
|
84
|
+
Future<Map<String, dynamic>> getWidgetTree({int maxDepth = 10}) async {
|
|
85
|
+
final result = await _call('ext.flutter.flutter_skill.getWidgetTree', {
|
|
86
|
+
'maxDepth': maxDepth.toString(),
|
|
87
|
+
});
|
|
88
|
+
return result['tree'] ?? {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Future<Map<String, dynamic>?> getWidgetProperties(String key) async {
|
|
92
|
+
final result =
|
|
93
|
+
await _call('ext.flutter.flutter_skill.getWidgetProperties', {
|
|
94
|
+
'key': key,
|
|
95
|
+
});
|
|
96
|
+
return result['properties'];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Future<List<dynamic>> getTextContent() async {
|
|
100
|
+
final result = await _call('ext.flutter.flutter_skill.getTextContent');
|
|
101
|
+
return result['texts'] ?? [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Future<List<dynamic>> findByType(String type) async {
|
|
105
|
+
final result = await _call('ext.flutter.flutter_skill.findByType', {
|
|
106
|
+
'type': type,
|
|
107
|
+
});
|
|
108
|
+
return result['elements'] ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ==================== MORE INTERACTIONS ====================
|
|
112
|
+
|
|
113
|
+
Future<bool> longPress(
|
|
114
|
+
{String? key, String? text, int duration = 500}) async {
|
|
115
|
+
final result = await _call('ext.flutter.flutter_skill.longPress', {
|
|
116
|
+
if (key != null) 'key': key,
|
|
117
|
+
if (text != null) 'text': text,
|
|
118
|
+
'duration': duration.toString(),
|
|
119
|
+
});
|
|
120
|
+
return result['success'] == true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Future<bool> swipe(
|
|
124
|
+
{required String direction, double distance = 300, String? key}) async {
|
|
125
|
+
final result = await _call('ext.flutter.flutter_skill.swipe', {
|
|
126
|
+
'direction': direction,
|
|
127
|
+
'distance': distance.toString(),
|
|
128
|
+
if (key != null) 'key': key,
|
|
129
|
+
});
|
|
130
|
+
return result['success'] == true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Future<bool> drag({required String fromKey, required String toKey}) async {
|
|
134
|
+
final result = await _call('ext.flutter.flutter_skill.drag', {
|
|
135
|
+
'fromKey': fromKey,
|
|
136
|
+
'toKey': toKey,
|
|
137
|
+
});
|
|
138
|
+
return result['success'] == true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Future<bool> doubleTap({String? key, String? text}) async {
|
|
142
|
+
final result = await _call('ext.flutter.flutter_skill.doubleTap', {
|
|
143
|
+
if (key != null) 'key': key,
|
|
144
|
+
if (text != null) 'text': text,
|
|
145
|
+
});
|
|
146
|
+
return result['success'] == true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ==================== STATE & VALIDATION ====================
|
|
150
|
+
|
|
151
|
+
Future<String?> getTextValue(String key) async {
|
|
152
|
+
final result = await _call('ext.flutter.flutter_skill.getTextValue', {
|
|
153
|
+
'key': key,
|
|
154
|
+
});
|
|
155
|
+
return result['value'];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Future<bool?> getCheckboxState(String key) async {
|
|
159
|
+
final result = await _call('ext.flutter.flutter_skill.getCheckboxState', {
|
|
160
|
+
'key': key,
|
|
161
|
+
});
|
|
162
|
+
return result['checked'];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Future<double?> getSliderValue(String key) async {
|
|
166
|
+
final result = await _call('ext.flutter.flutter_skill.getSliderValue', {
|
|
167
|
+
'key': key,
|
|
168
|
+
});
|
|
169
|
+
return result['value']?.toDouble();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Future<bool> waitForElement(
|
|
173
|
+
{String? key, String? text, int timeout = 5000}) async {
|
|
174
|
+
final result = await _call('ext.flutter.flutter_skill.waitForElement', {
|
|
175
|
+
if (key != null) 'key': key,
|
|
176
|
+
if (text != null) 'text': text,
|
|
177
|
+
'timeout': timeout.toString(),
|
|
178
|
+
});
|
|
179
|
+
return result['found'] == true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Future<bool> waitForGone(
|
|
183
|
+
{String? key, String? text, int timeout = 5000}) async {
|
|
184
|
+
final result = await _call('ext.flutter.flutter_skill.waitForGone', {
|
|
185
|
+
if (key != null) 'key': key,
|
|
186
|
+
if (text != null) 'text': text,
|
|
187
|
+
'timeout': timeout.toString(),
|
|
188
|
+
});
|
|
189
|
+
return result['gone'] == true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ==================== SCREENSHOT ====================
|
|
193
|
+
|
|
194
|
+
Future<String?> takeScreenshot() async {
|
|
195
|
+
final result = await _call('ext.flutter.flutter_skill.screenshot');
|
|
196
|
+
return result['image'];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Future<String?> takeElementScreenshot(String key) async {
|
|
200
|
+
final result = await _call('ext.flutter.flutter_skill.screenshotElement', {
|
|
201
|
+
'key': key,
|
|
202
|
+
});
|
|
203
|
+
return result['image'];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ==================== NAVIGATION ====================
|
|
207
|
+
|
|
208
|
+
Future<String?> getCurrentRoute() async {
|
|
209
|
+
final result = await _call('ext.flutter.flutter_skill.getCurrentRoute');
|
|
210
|
+
return result['route'];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Future<bool> goBack() async {
|
|
214
|
+
final result = await _call('ext.flutter.flutter_skill.goBack');
|
|
215
|
+
return result['success'] == true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Future<List<String>> getNavigationStack() async {
|
|
219
|
+
final result = await _call('ext.flutter.flutter_skill.getNavigationStack');
|
|
220
|
+
return (result['stack'] as List?)?.cast<String>() ?? [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ==================== DEBUG & LOGS ====================
|
|
224
|
+
|
|
225
|
+
Future<List<String>> getLogs() async {
|
|
226
|
+
final result = await _call('ext.flutter.flutter_skill.getLogs');
|
|
227
|
+
return (result['logs'] as List?)?.cast<String>() ?? [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Future<List<dynamic>> getErrors() async {
|
|
231
|
+
final result = await _call('ext.flutter.flutter_skill.getErrors');
|
|
232
|
+
return result['errors'] ?? [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
Future<void> clearLogs() async {
|
|
236
|
+
await _call('ext.flutter.flutter_skill.clearLogs');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Future<Map<String, dynamic>> getPerformance() async {
|
|
240
|
+
return await _call('ext.flutter.flutter_skill.getPerformance');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ==================== EXISTING HELPERS ====================
|
|
244
|
+
|
|
245
|
+
Future<void> hotReload() async {
|
|
246
|
+
if (_service == null || _isolateId == null) {
|
|
247
|
+
throw Exception('Not connected');
|
|
248
|
+
}
|
|
249
|
+
await _service!.reloadSources(_isolateId!);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Future<void> hotRestart() async {
|
|
253
|
+
if (_service == null || _isolateId == null) {
|
|
254
|
+
throw Exception('Not connected');
|
|
255
|
+
}
|
|
256
|
+
await _service!.reloadSources(_isolateId!);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
Future<Map<String, dynamic>> getLayoutTree() async {
|
|
260
|
+
try {
|
|
261
|
+
final groupName =
|
|
262
|
+
'flutter_skill_${DateTime.now().millisecondsSinceEpoch}';
|
|
263
|
+
final result =
|
|
264
|
+
await _call('ext.flutter.inspector.getRootWidgetSummaryTree', {
|
|
265
|
+
'objectGroup': groupName,
|
|
266
|
+
});
|
|
267
|
+
return result;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
rethrow;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
bool get isConnected => _service != null && _isolateId != null;
|
|
274
|
+
|
|
275
|
+
static Future<String> resolveUri(List<String> args) async {
|
|
276
|
+
if (args.isNotEmpty) {
|
|
277
|
+
final arg = args[0];
|
|
278
|
+
if (arg.startsWith('ws://') || arg.startsWith('http://')) {
|
|
279
|
+
return arg;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
final file = File('.flutter_skill_uri');
|
|
284
|
+
if (await file.exists()) {
|
|
285
|
+
final uri = (await file.readAsString()).trim();
|
|
286
|
+
if (uri.isNotEmpty) {
|
|
287
|
+
return uri;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
throw ArgumentError(
|
|
292
|
+
'No URI provided and .flutter_skill_uri not found/empty. Run `flutter_skill launch` or provide URI as first argument.');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: flutter_skill
|
|
2
|
+
description: Give your AI Agent eyes and hands inside your Flutter app.
|
|
3
|
+
version: 0.2.0
|
|
4
|
+
homepage: https://github.com/ai-dashboad/flutter-skill
|
|
5
|
+
repository: https://github.com/ai-dashboad/flutter-skill
|
|
6
|
+
# publish_to: 'none' # Remove this when ready to publish to pub.dev
|
|
7
|
+
|
|
8
|
+
executables:
|
|
9
|
+
flutter_skill: flutter_skill
|
|
10
|
+
|
|
11
|
+
environment:
|
|
12
|
+
sdk: '>=3.0.0 <4.0.0'
|
|
13
|
+
flutter: ">=3.0.0"
|
|
14
|
+
|
|
15
|
+
dependencies:
|
|
16
|
+
flutter:
|
|
17
|
+
sdk: flutter
|
|
18
|
+
vm_service: ^14.0.0
|
|
19
|
+
http: ^1.1.0
|
|
20
|
+
path: ^1.8.0
|
|
21
|
+
logging: ^1.2.0
|
|
22
|
+
|
|
23
|
+
dev_dependencies:
|
|
24
|
+
flutter_test:
|
|
25
|
+
sdk: flutter
|
|
26
|
+
lints: ^3.0.0
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flutter-skill",
|
|
3
|
+
"version": "0.7.4",
|
|
4
|
+
"description": "MCP Server for app automation - Give your AI Agent eyes and hands inside any app (Flutter, React, Web, Native)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flutter-skill-mcp": "./bin/cli.js",
|
|
8
|
+
"flutter-skill": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node scripts/postinstall.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"flutter",
|
|
15
|
+
"react-native",
|
|
16
|
+
"web",
|
|
17
|
+
"mcp",
|
|
18
|
+
"ai",
|
|
19
|
+
"automation",
|
|
20
|
+
"testing",
|
|
21
|
+
"claude",
|
|
22
|
+
"cursor",
|
|
23
|
+
"windsurf"
|
|
24
|
+
],
|
|
25
|
+
"author": "ai-dashboad",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/ai-dashboad/flutter-skill.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/ai-dashboad/flutter-skill",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/ai-dashboad/flutter-skill/issues"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16.0.0"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin/",
|
|
40
|
+
"scripts/",
|
|
41
|
+
"dart/",
|
|
42
|
+
"web/",
|
|
43
|
+
"README.md"
|
|
44
|
+
]
|
|
45
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const rootDir = path.join(__dirname, '..', '..');
|
|
7
|
+
const npmDir = path.join(__dirname, '..');
|
|
8
|
+
const dartDir = path.join(npmDir, 'dart');
|
|
9
|
+
|
|
10
|
+
// Files to copy
|
|
11
|
+
const filesToCopy = [
|
|
12
|
+
'pubspec.yaml',
|
|
13
|
+
'lib/flutter_skill.dart',
|
|
14
|
+
'lib/src/flutter_skill_client.dart',
|
|
15
|
+
'lib/src/cli/server.dart',
|
|
16
|
+
'lib/src/cli/setup.dart',
|
|
17
|
+
'lib/src/cli/launch.dart',
|
|
18
|
+
'lib/src/cli/inspect.dart',
|
|
19
|
+
'lib/src/cli/act.dart',
|
|
20
|
+
'bin/server.dart',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Create directories
|
|
24
|
+
function mkdirp(dir) {
|
|
25
|
+
if (!fs.existsSync(dir)) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Copy file
|
|
31
|
+
function copyFile(src, dest) {
|
|
32
|
+
mkdirp(path.dirname(dest));
|
|
33
|
+
fs.copyFileSync(src, dest);
|
|
34
|
+
console.log(`Copied: ${path.relative(rootDir, src)} -> ${path.relative(npmDir, dest)}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Main
|
|
38
|
+
console.log('Building npm package...\n');
|
|
39
|
+
|
|
40
|
+
// Clean dart directory
|
|
41
|
+
if (fs.existsSync(dartDir)) {
|
|
42
|
+
fs.rmSync(dartDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
mkdirp(dartDir);
|
|
45
|
+
|
|
46
|
+
// Copy files
|
|
47
|
+
for (const file of filesToCopy) {
|
|
48
|
+
const src = path.join(rootDir, file);
|
|
49
|
+
const dest = path.join(dartDir, file);
|
|
50
|
+
if (fs.existsSync(src)) {
|
|
51
|
+
copyFile(src, dest);
|
|
52
|
+
} else {
|
|
53
|
+
console.warn(`Warning: ${file} not found`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update version in package.json from pubspec.yaml
|
|
58
|
+
const pubspec = fs.readFileSync(path.join(rootDir, 'pubspec.yaml'), 'utf8');
|
|
59
|
+
const versionMatch = pubspec.match(/version:\s*(\S+)/);
|
|
60
|
+
if (versionMatch) {
|
|
61
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(npmDir, 'package.json'), 'utf8'));
|
|
62
|
+
packageJson.version = versionMatch[1];
|
|
63
|
+
fs.writeFileSync(path.join(npmDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
64
|
+
console.log(`\nUpdated package.json version to ${versionMatch[1]}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('\nBuild complete!');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function checkCommand(cmd) {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`${cmd} --version`, { stdio: 'ignore' });
|
|
8
|
+
return true;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hasDart = checkCommand('dart');
|
|
15
|
+
const hasFlutter = checkCommand('flutter');
|
|
16
|
+
|
|
17
|
+
if (!hasDart && !hasFlutter) {
|
|
18
|
+
console.log('\n' + '='.repeat(60));
|
|
19
|
+
console.log('flutter-skill requires Dart SDK');
|
|
20
|
+
console.log('='.repeat(60));
|
|
21
|
+
console.log('\nPlease install Flutter (includes Dart):');
|
|
22
|
+
console.log(' https://docs.flutter.dev/get-started/install\n');
|
|
23
|
+
console.log('Or install Dart standalone:');
|
|
24
|
+
console.log(' https://dart.dev/get-dart\n');
|
|
25
|
+
} else if (hasDart && !hasFlutter) {
|
|
26
|
+
console.log('\nNote: Flutter SDK not found. Some features may be limited.');
|
|
27
|
+
console.log('Install Flutter for full functionality:');
|
|
28
|
+
console.log(' https://docs.flutter.dev/get-started/install\n');
|
|
29
|
+
} else {
|
|
30
|
+
console.log('\nflutter-skill installed successfully!');
|
|
31
|
+
console.log('\nMCP Config:');
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
"flutter-skill": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["flutter-skill"]
|
|
36
|
+
}
|
|
37
|
+
}, null, 2));
|
|
38
|
+
console.log('\n');
|
|
39
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const packageJson = require('../package.json');
|
|
9
|
+
const VERSION = packageJson.version;
|
|
10
|
+
|
|
11
|
+
const cacheDir = path.join(os.homedir(), '.flutter-skill');
|
|
12
|
+
const binDir = path.join(cacheDir, 'bin');
|
|
13
|
+
|
|
14
|
+
function getBinaryName() {
|
|
15
|
+
const platform = os.platform();
|
|
16
|
+
const arch = os.arch();
|
|
17
|
+
|
|
18
|
+
if (platform === 'darwin') {
|
|
19
|
+
return arch === 'arm64' ? 'flutter-skill-macos-arm64' : 'flutter-skill-macos-x64';
|
|
20
|
+
} else if (platform === 'linux') {
|
|
21
|
+
return 'flutter-skill-linux-x64';
|
|
22
|
+
} else if (platform === 'win32') {
|
|
23
|
+
return 'flutter-skill-windows-x64.exe';
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function downloadBinary(url, destPath) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
31
|
+
|
|
32
|
+
const file = fs.createWriteStream(destPath);
|
|
33
|
+
|
|
34
|
+
const request = (url) => {
|
|
35
|
+
https.get(url, (response) => {
|
|
36
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
37
|
+
request(response.headers.location);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (response.statusCode !== 200) {
|
|
42
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const totalBytes = parseInt(response.headers['content-length'], 10);
|
|
47
|
+
let downloadedBytes = 0;
|
|
48
|
+
|
|
49
|
+
response.on('data', (chunk) => {
|
|
50
|
+
downloadedBytes += chunk.length;
|
|
51
|
+
if (totalBytes) {
|
|
52
|
+
const percent = Math.round((downloadedBytes / totalBytes) * 100);
|
|
53
|
+
process.stdout.write(`\r[flutter-skill] Downloading native binary... ${percent}%`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
response.pipe(file);
|
|
58
|
+
file.on('finish', () => {
|
|
59
|
+
file.close();
|
|
60
|
+
fs.chmodSync(destPath, 0o755);
|
|
61
|
+
console.log('\n[flutter-skill] Native binary installed successfully!');
|
|
62
|
+
resolve(destPath);
|
|
63
|
+
});
|
|
64
|
+
}).on('error', reject);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
request(url);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
const binaryName = getBinaryName();
|
|
73
|
+
if (!binaryName) {
|
|
74
|
+
console.log('[flutter-skill] No native binary available for this platform, using Dart runtime');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const localPath = path.join(binDir, `${binaryName}-v${VERSION}`);
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(localPath)) {
|
|
81
|
+
console.log('[flutter-skill] Native binary already installed');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const downloadUrl = `https://github.com/ai-dashboad/flutter-skill/releases/download/v${VERSION}/${binaryName}`;
|
|
86
|
+
|
|
87
|
+
console.log(`[flutter-skill] Installing native binary for faster startup...`);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await downloadBinary(downloadUrl, localPath);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.log(`[flutter-skill] Could not download native binary (${error.message}), will use Dart runtime`);
|
|
93
|
+
console.log('[flutter-skill] This is normal for new releases, Dart fallback works fine');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Install tool priority rules for Claude Code
|
|
98
|
+
function installToolPriorityRules() {
|
|
99
|
+
const homeDir = os.homedir();
|
|
100
|
+
const promptsDir = path.join(homeDir, '.claude', 'prompts');
|
|
101
|
+
const targetFile = path.join(promptsDir, 'flutter-tool-priority.md');
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(targetFile)) {
|
|
104
|
+
return Promise.resolve(); // Already installed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Download from GitHub
|
|
108
|
+
const url = 'https://raw.githubusercontent.com/ai-dashboad/flutter-skill/main/docs/prompts/tool-priority.md';
|
|
109
|
+
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
fs.mkdirSync(promptsDir, { recursive: true });
|
|
112
|
+
const file = fs.createWriteStream(targetFile);
|
|
113
|
+
|
|
114
|
+
const request = (reqUrl) => {
|
|
115
|
+
https.get(reqUrl, (response) => {
|
|
116
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
117
|
+
request(response.headers.location);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (response.statusCode !== 200) {
|
|
121
|
+
resolve(); // Silent fail
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
response.pipe(file);
|
|
125
|
+
file.on('finish', () => {
|
|
126
|
+
file.close();
|
|
127
|
+
console.log('[flutter-skill] Tool priority rules installed for Claude Code');
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
}).on('error', () => resolve());
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
request(url);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main()
|
|
138
|
+
.then(() => installToolPriorityRules())
|
|
139
|
+
.catch(() => {
|
|
140
|
+
// Silent fail - Dart fallback will work
|
|
141
|
+
installToolPriorityRules().catch(() => {});
|
|
142
|
+
});
|