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
package/bin/cli.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// Package info
|
|
10
|
+
const packageJson = require('../package.json');
|
|
11
|
+
const VERSION = packageJson.version;
|
|
12
|
+
|
|
13
|
+
// Update check config
|
|
14
|
+
const CHECK_INTERVAL_HOURS = 24;
|
|
15
|
+
const UPDATE_CHECK_FILE = path.join(os.homedir(), '.flutter-skill', 'update-check.json');
|
|
16
|
+
|
|
17
|
+
// Paths
|
|
18
|
+
const cacheDir = path.join(os.homedir(), '.flutter-skill');
|
|
19
|
+
const binDir = path.join(cacheDir, 'bin');
|
|
20
|
+
|
|
21
|
+
// Get platform-specific binary name
|
|
22
|
+
function getBinaryName() {
|
|
23
|
+
const platform = os.platform();
|
|
24
|
+
const arch = os.arch();
|
|
25
|
+
|
|
26
|
+
if (platform === 'darwin') {
|
|
27
|
+
return arch === 'arm64' ? 'flutter-skill-macos-arm64' : 'flutter-skill-macos-x64';
|
|
28
|
+
} else if (platform === 'linux') {
|
|
29
|
+
return 'flutter-skill-linux-x64';
|
|
30
|
+
} else if (platform === 'win32') {
|
|
31
|
+
return 'flutter-skill-windows-x64.exe';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get the local binary path
|
|
37
|
+
function getLocalBinaryPath() {
|
|
38
|
+
const binaryName = getBinaryName();
|
|
39
|
+
if (!binaryName) return null;
|
|
40
|
+
return path.join(binDir, `${binaryName}-v${VERSION}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Download binary from GitHub releases
|
|
44
|
+
function downloadBinary(url, destPath) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
// Ensure directory exists
|
|
47
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
48
|
+
|
|
49
|
+
const file = fs.createWriteStream(destPath);
|
|
50
|
+
|
|
51
|
+
const request = (url) => {
|
|
52
|
+
https.get(url, (response) => {
|
|
53
|
+
// Handle redirects
|
|
54
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
55
|
+
request(response.headers.location);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (response.statusCode !== 200) {
|
|
60
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
response.pipe(file);
|
|
65
|
+
file.on('finish', () => {
|
|
66
|
+
file.close();
|
|
67
|
+
// Make executable
|
|
68
|
+
fs.chmodSync(destPath, 0o755);
|
|
69
|
+
resolve(destPath);
|
|
70
|
+
});
|
|
71
|
+
}).on('error', (err) => {
|
|
72
|
+
fs.unlink(destPath, () => {});
|
|
73
|
+
reject(err);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
request(url);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try to use native binary, fallback to Dart
|
|
82
|
+
async function main() {
|
|
83
|
+
const binaryName = getBinaryName();
|
|
84
|
+
const localBinaryPath = getLocalBinaryPath();
|
|
85
|
+
|
|
86
|
+
// Try to use existing native binary
|
|
87
|
+
if (localBinaryPath && fs.existsSync(localBinaryPath)) {
|
|
88
|
+
runNativeBinary(localBinaryPath);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try to download native binary
|
|
93
|
+
if (binaryName && localBinaryPath) {
|
|
94
|
+
const downloadUrl = `https://github.com/ai-dashboad/flutter-skill/releases/download/v${VERSION}/${binaryName}`;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Download in background, don't block startup for first time
|
|
98
|
+
// For now, just fall through to Dart
|
|
99
|
+
// Future: implement async download with progress
|
|
100
|
+
console.error(`[flutter-skill] Native binary not found, using Dart runtime`);
|
|
101
|
+
console.error(`[flutter-skill] To install native binary for faster startup:`);
|
|
102
|
+
console.error(`[flutter-skill] curl -L ${downloadUrl} -o ${localBinaryPath} && chmod +x ${localBinaryPath}`);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Ignore download errors, fall back to Dart
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fallback to Dart
|
|
109
|
+
runWithDart();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Run using native binary
|
|
113
|
+
function runNativeBinary(binaryPath) {
|
|
114
|
+
const args = process.argv.slice(2);
|
|
115
|
+
// Default to 'server' command if no args
|
|
116
|
+
if (args.length === 0) {
|
|
117
|
+
args.push('server');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const server = spawn(binaryPath, args, {
|
|
121
|
+
stdio: 'inherit'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
server.on('close', (code) => {
|
|
125
|
+
process.exit(code || 0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
process.on('SIGINT', () => server.kill('SIGINT'));
|
|
129
|
+
process.on('SIGTERM', () => server.kill('SIGTERM'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Run using Dart
|
|
133
|
+
function runWithDart() {
|
|
134
|
+
const dartDir = path.join(__dirname, '..', 'dart');
|
|
135
|
+
const serverScript = path.join(dartDir, 'bin', 'server.dart');
|
|
136
|
+
|
|
137
|
+
// Check if Dart is installed
|
|
138
|
+
try {
|
|
139
|
+
execSync('dart --version', { stdio: 'ignore' });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error('Error: Dart SDK not found. Please install Flutter/Dart first.');
|
|
142
|
+
console.error(' https://docs.flutter.dev/get-started/install');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if server script exists
|
|
147
|
+
if (!fs.existsSync(serverScript)) {
|
|
148
|
+
console.error('Error: Server script not found at:', serverScript);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Get dependencies silently
|
|
153
|
+
try {
|
|
154
|
+
const pubCmd = checkFlutter() ? 'flutter' : 'dart';
|
|
155
|
+
execSync(`${pubCmd} pub get`, {
|
|
156
|
+
cwd: dartDir,
|
|
157
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
158
|
+
});
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Ignore pub get errors
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Start with Dart
|
|
164
|
+
const args = process.argv.slice(2);
|
|
165
|
+
if (args.length === 0) {
|
|
166
|
+
args.push('server');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const dartArgs = ['run', serverScript, ...args];
|
|
170
|
+
const server = spawn('dart', dartArgs, {
|
|
171
|
+
cwd: dartDir,
|
|
172
|
+
stdio: 'inherit'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
server.on('close', (code) => {
|
|
176
|
+
process.exit(code || 0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
process.on('SIGINT', () => server.kill('SIGINT'));
|
|
180
|
+
process.on('SIGTERM', () => server.kill('SIGTERM'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function checkFlutter() {
|
|
184
|
+
try {
|
|
185
|
+
execSync('flutter --version', { stdio: 'ignore' });
|
|
186
|
+
return true;
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check for updates (non-blocking, runs in background)
|
|
193
|
+
function checkForUpdates() {
|
|
194
|
+
// Only check periodically
|
|
195
|
+
try {
|
|
196
|
+
const checkDir = path.dirname(UPDATE_CHECK_FILE);
|
|
197
|
+
if (!fs.existsSync(checkDir)) {
|
|
198
|
+
fs.mkdirSync(checkDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let lastCheck = 0;
|
|
202
|
+
let skippedVersion = null;
|
|
203
|
+
|
|
204
|
+
if (fs.existsSync(UPDATE_CHECK_FILE)) {
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf-8'));
|
|
207
|
+
lastCheck = data.lastCheck || 0;
|
|
208
|
+
skippedVersion = data.skippedVersion;
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
const hoursSinceLastCheck = (now - lastCheck) / (1000 * 60 * 60);
|
|
214
|
+
|
|
215
|
+
if (hoursSinceLastCheck < CHECK_INTERVAL_HOURS) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Update last check time
|
|
220
|
+
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({
|
|
221
|
+
lastCheck: now,
|
|
222
|
+
skippedVersion
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
// Check npm registry for latest version (async, non-blocking)
|
|
226
|
+
https.get('https://registry.npmjs.org/flutter-skill', (res) => {
|
|
227
|
+
let data = '';
|
|
228
|
+
res.on('data', (chunk) => data += chunk);
|
|
229
|
+
res.on('end', () => {
|
|
230
|
+
try {
|
|
231
|
+
const json = JSON.parse(data);
|
|
232
|
+
const latestVersion = json['dist-tags'].latest;
|
|
233
|
+
|
|
234
|
+
if (latestVersion && compareVersions(latestVersion, VERSION) > 0) {
|
|
235
|
+
if (skippedVersion !== latestVersion) {
|
|
236
|
+
console.error(`\n[flutter-skill] Update available: ${VERSION} → ${latestVersion}`);
|
|
237
|
+
console.error(`[flutter-skill] Run: npm update -g flutter-skill\n`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch {}
|
|
241
|
+
});
|
|
242
|
+
}).on('error', () => {});
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function compareVersions(v1, v2) {
|
|
247
|
+
const parts1 = v1.split('.').map(Number);
|
|
248
|
+
const parts2 = v2.split('.').map(Number);
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
251
|
+
const p1 = parts1[i] || 0;
|
|
252
|
+
const p2 = parts2[i] || 0;
|
|
253
|
+
if (p1 > p2) return 1;
|
|
254
|
+
if (p1 < p2) return -1;
|
|
255
|
+
}
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Show tips when running interactively without arguments
|
|
260
|
+
function showTips() {
|
|
261
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
262
|
+
const args = process.argv.slice(2);
|
|
263
|
+
|
|
264
|
+
if (!isInteractive || args.length > 0) {
|
|
265
|
+
return false; // Not interactive or has args, don't show tips
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(`Flutter Skill v${VERSION} - AI Agent Bridge for Flutter Apps`);
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log('Commands:');
|
|
271
|
+
console.log(' server Start MCP server (default when launched by IDE)');
|
|
272
|
+
console.log(' launch Launch and connect to a Flutter app');
|
|
273
|
+
console.log(' inspect Inspect interactive elements in running app');
|
|
274
|
+
console.log(' act Perform actions (tap, scroll, enter_text)');
|
|
275
|
+
console.log(' doctor Check installation and environment health');
|
|
276
|
+
console.log(' setup Install tool priority rules for Claude Code');
|
|
277
|
+
console.log(' --version Show version');
|
|
278
|
+
console.log('');
|
|
279
|
+
console.log('Quick Start:');
|
|
280
|
+
console.log(' flutter-skill doctor Check your environment is ready');
|
|
281
|
+
console.log(' flutter-skill launch ./my_app Launch and connect to your app');
|
|
282
|
+
console.log('');
|
|
283
|
+
console.log('What can AI agents do with Flutter Skill?');
|
|
284
|
+
console.log(' - Launch your Flutter app and auto-connect');
|
|
285
|
+
console.log(' - Inspect UI: find buttons, text fields, lists');
|
|
286
|
+
console.log(' - Tap, swipe, scroll, and enter text');
|
|
287
|
+
console.log(' - Take screenshots to verify visual changes');
|
|
288
|
+
console.log(' - Read app logs and debug issues');
|
|
289
|
+
console.log(' - Hot reload after code changes');
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log('Example: Ask your AI agent:');
|
|
292
|
+
console.log(' "Launch my Flutter app and tap the login button"');
|
|
293
|
+
console.log(' "Take a screenshot and check if the list is showing"');
|
|
294
|
+
console.log(' "Enter \'hello@test.com\' in the email field and submit"');
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log('Docs: https://pub.dev/packages/flutter_skill');
|
|
297
|
+
console.log('');
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Handle --version / -v directly (fast, no binary needed)
|
|
302
|
+
const cliArgs = process.argv.slice(2);
|
|
303
|
+
if (cliArgs.includes('--version') || cliArgs.includes('-v')) {
|
|
304
|
+
console.log(VERSION);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Show tips if interactive, otherwise run server
|
|
309
|
+
if (showTips()) {
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Run update check in background (non-blocking)
|
|
314
|
+
checkForUpdates();
|
|
315
|
+
|
|
316
|
+
main().catch(console.error);
|