@hypothesi/tauri-mcp-server 0.1.1
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/LICENSE +39 -0
- package/dist/config.js +45 -0
- package/dist/driver/app-discovery.js +148 -0
- package/dist/driver/plugin-client.js +208 -0
- package/dist/driver/plugin-commands.js +142 -0
- package/dist/driver/protocol.js +7 -0
- package/dist/driver/scripts/find-element.js +48 -0
- package/dist/driver/scripts/focus.js +17 -0
- package/dist/driver/scripts/get-styles.js +41 -0
- package/dist/driver/scripts/html2canvas-loader.js +86 -0
- package/dist/driver/scripts/index.js +94 -0
- package/dist/driver/scripts/interact.js +103 -0
- package/dist/driver/scripts/keyboard.js +76 -0
- package/dist/driver/scripts/swipe.js +88 -0
- package/dist/driver/scripts/wait-for.js +44 -0
- package/dist/driver/session-manager.js +121 -0
- package/dist/driver/webview-executor.js +334 -0
- package/dist/driver/webview-interactions.js +213 -0
- package/dist/index.js +80 -0
- package/dist/manager/cli.js +128 -0
- package/dist/manager/config.js +142 -0
- package/dist/manager/docs.js +213 -0
- package/dist/manager/mobile.js +83 -0
- package/dist/monitor/logs.js +98 -0
- package/dist/tools-registry.js +292 -0
- package/package.json +60 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
export const RunCommandSchema = z.object({
|
|
6
|
+
command: z.string().describe('Any Tauri CLI command (e.g., "init", "dev", "build", "android dev", "migrate", "plugin add", etc.)'),
|
|
7
|
+
args: z.array(z.string()).optional().describe('Additional arguments to pass to the command'),
|
|
8
|
+
cwd: z.string().describe('The project directory'),
|
|
9
|
+
timeout: z.number().optional().describe('Command timeout in milliseconds (default: 180000)'),
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Get available Tauri commands by running 'tauri --help'
|
|
13
|
+
*/
|
|
14
|
+
export async function listTauriCommands(cwd) {
|
|
15
|
+
try {
|
|
16
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
17
|
+
const hasTauriScript = await fs
|
|
18
|
+
.readFile(packageJsonPath, 'utf8')
|
|
19
|
+
.then((content) => {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(content);
|
|
22
|
+
return Boolean(pkg.scripts && pkg.scripts.tauri);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.catch(() => { return false; });
|
|
29
|
+
let result;
|
|
30
|
+
if (hasTauriScript) {
|
|
31
|
+
result = await execa('npm', ['run', 'tauri', '--', '--help'], {
|
|
32
|
+
cwd,
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
result = await execa('tauri', ['--help'], {
|
|
38
|
+
cwd,
|
|
39
|
+
timeout: 10000,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return result.stdout;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
throw new Error(`Failed to get Tauri commands: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runTauriCommand(command, cwd, args = [], timeout = 180000) {
|
|
50
|
+
try {
|
|
51
|
+
// Split command if it contains spaces (e.g., "android dev" or "plugin add")
|
|
52
|
+
const commandParts = command.split(' ');
|
|
53
|
+
const allArgs = [...commandParts, ...args];
|
|
54
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
55
|
+
const hasTauriScript = await fs
|
|
56
|
+
.readFile(packageJsonPath, 'utf8')
|
|
57
|
+
.then((content) => {
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(content);
|
|
60
|
+
return Boolean(pkg.scripts && pkg.scripts.tauri);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch(() => { return false; });
|
|
67
|
+
const isDevLikeCommand = commandParts[0] === 'dev' ||
|
|
68
|
+
command.startsWith('android dev') ||
|
|
69
|
+
command.startsWith('ios dev');
|
|
70
|
+
const spawnTauri = () => {
|
|
71
|
+
if (hasTauriScript) {
|
|
72
|
+
return execa('npm', ['run', 'tauri', '--', ...allArgs], {
|
|
73
|
+
cwd,
|
|
74
|
+
timeout: isDevLikeCommand ? undefined : timeout,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return execa('tauri', allArgs, {
|
|
78
|
+
cwd,
|
|
79
|
+
timeout: isDevLikeCommand ? undefined : timeout,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const child = spawnTauri();
|
|
83
|
+
if (isDevLikeCommand) {
|
|
84
|
+
// For long-running dev commands, start the process and return once the
|
|
85
|
+
// command appears healthy
|
|
86
|
+
child.catch(() => { return; });
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
setTimeout(resolve, 3000);
|
|
89
|
+
});
|
|
90
|
+
if (child.exitCode !== null) {
|
|
91
|
+
if (child.exitCode === 0) {
|
|
92
|
+
return 'Tauri dev command completed successfully.';
|
|
93
|
+
}
|
|
94
|
+
throw new Error('Tauri dev command exited unexpectedly. Check your project configuration and logs for details.');
|
|
95
|
+
}
|
|
96
|
+
const text = [
|
|
97
|
+
'Tauri dev command started. It may still be initializing; check your terminal',
|
|
98
|
+
'or devtools logs for live output. Use Ctrl+C in the appropriate terminal to',
|
|
99
|
+
'stop it.',
|
|
100
|
+
];
|
|
101
|
+
return text.join(' ');
|
|
102
|
+
}
|
|
103
|
+
const result = await child;
|
|
104
|
+
if (Array.isArray(result.stdout)) {
|
|
105
|
+
return result.stdout.join('\n');
|
|
106
|
+
}
|
|
107
|
+
if (result.stdout instanceof Uint8Array) {
|
|
108
|
+
return new TextDecoder().decode(result.stdout);
|
|
109
|
+
}
|
|
110
|
+
return result.stdout || 'Command completed successfully';
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error), stderr = error && typeof error === 'object' && 'stderr' in error ? String(error.stderr) : '';
|
|
114
|
+
// Provide more helpful error messages
|
|
115
|
+
if (message.includes('Unknown command')) {
|
|
116
|
+
throw new Error(`Unknown Tauri command: "${command}". Run 'tauri --help' to see available commands.\n${stderr}`);
|
|
117
|
+
}
|
|
118
|
+
if (message.includes('spawn tauri ENOENT')) {
|
|
119
|
+
const msgParts = [
|
|
120
|
+
'Failed to run the Tauri CLI. Either add a "tauri" script to your',
|
|
121
|
+
'package.json (e.g., "tauri": "tauri") or install the global Tauri CLI',
|
|
122
|
+
'as described in the documentation, then try again.',
|
|
123
|
+
];
|
|
124
|
+
throw new Error(msgParts.join(' '));
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Tauri command failed: ${message}${stderr ? `\n${stderr}` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
// Tauri supports multiple configuration files
|
|
5
|
+
const TAURI_CONFIG_FILES = [
|
|
6
|
+
'tauri.conf.json',
|
|
7
|
+
'tauri.conf.json5',
|
|
8
|
+
'Tauri.toml',
|
|
9
|
+
// Platform-specific configs
|
|
10
|
+
'tauri.windows.conf.json',
|
|
11
|
+
'tauri.linux.conf.json',
|
|
12
|
+
'tauri.macos.conf.json',
|
|
13
|
+
'tauri.android.conf.json',
|
|
14
|
+
'tauri.ios.conf.json',
|
|
15
|
+
// Build-specific configs
|
|
16
|
+
'tauri.conf.dev.json',
|
|
17
|
+
'tauri.conf.prod.json',
|
|
18
|
+
// Project files
|
|
19
|
+
'Cargo.toml',
|
|
20
|
+
'package.json',
|
|
21
|
+
// Mobile-specific files
|
|
22
|
+
'Info.plist',
|
|
23
|
+
'AndroidManifest.xml',
|
|
24
|
+
];
|
|
25
|
+
export const ReadConfigSchema = z.object({
|
|
26
|
+
projectPath: z.string(),
|
|
27
|
+
file: z.enum(TAURI_CONFIG_FILES).describe('Tauri configuration file to read'),
|
|
28
|
+
});
|
|
29
|
+
export const WriteConfigSchema = z.object({
|
|
30
|
+
projectPath: z.string(),
|
|
31
|
+
file: z.enum(TAURI_CONFIG_FILES).describe('Tauri configuration file to write'),
|
|
32
|
+
content: z.string().describe('The new content of the file'),
|
|
33
|
+
});
|
|
34
|
+
export async function readConfig(projectPath, file) {
|
|
35
|
+
const filePath = path.join(projectPath, getRelativePath(file));
|
|
36
|
+
try {
|
|
37
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
38
|
+
return content;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Failed to read ${file}: ${error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* List all available Tauri configuration files in the project
|
|
46
|
+
*/
|
|
47
|
+
export async function listConfigFiles(projectPath) {
|
|
48
|
+
const availableFiles = [];
|
|
49
|
+
for (const file of TAURI_CONFIG_FILES) {
|
|
50
|
+
const filePath = path.join(projectPath, getRelativePath(file));
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(filePath);
|
|
53
|
+
availableFiles.push(file);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
// File doesn't exist, skip it
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return availableFiles;
|
|
60
|
+
}
|
|
61
|
+
export async function writeConfig(projectPath, file, content) {
|
|
62
|
+
const filePath = path.join(projectPath, getRelativePath(file));
|
|
63
|
+
// Validate JSON files
|
|
64
|
+
if (file.endsWith('.json')) {
|
|
65
|
+
try {
|
|
66
|
+
JSON.parse(content);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
throw new Error(`Invalid JSON content for ${file}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Validate JSON5 files
|
|
73
|
+
if (file.endsWith('.json5')) {
|
|
74
|
+
// JSON5 is a superset of JSON, basic validation
|
|
75
|
+
// In production, you'd want to use a json5 parser
|
|
76
|
+
try {
|
|
77
|
+
// At minimum, check for basic syntax
|
|
78
|
+
if (!content.trim()) {
|
|
79
|
+
throw new Error(`Empty content for ${file}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
throw new Error(`Invalid content for ${file}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Validate TOML files
|
|
87
|
+
if (file.endsWith('.toml')) {
|
|
88
|
+
// Basic TOML validation - in production use a TOML parser
|
|
89
|
+
if (!content.trim()) {
|
|
90
|
+
throw new Error(`Empty content for ${file}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Validate XML files
|
|
94
|
+
if (file.endsWith('.xml')) {
|
|
95
|
+
// Basic XML validation
|
|
96
|
+
if (!content.includes('<') || !content.includes('>')) {
|
|
97
|
+
throw new Error(`Invalid XML content for ${file}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Validate plist files
|
|
101
|
+
if (file.endsWith('.plist')) {
|
|
102
|
+
// plist files are XML-based
|
|
103
|
+
if (!content.includes('<?xml') || !content.includes('<plist')) {
|
|
104
|
+
throw new Error(`Invalid plist content for ${file}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
109
|
+
return `Successfully wrote to ${file}`;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new Error(`Failed to write ${file}: ${error}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function getRelativePath(file) {
|
|
116
|
+
// Main Tauri config files
|
|
117
|
+
if (file === 'tauri.conf.json' || file === 'tauri.conf.json5' || file === 'Tauri.toml') {
|
|
118
|
+
return `src-tauri/${file}`;
|
|
119
|
+
}
|
|
120
|
+
// Platform-specific Tauri configs
|
|
121
|
+
if (file.startsWith('tauri.') && (file.endsWith('.conf.json') || file.endsWith('.conf.json5'))) {
|
|
122
|
+
return `src-tauri/${file}`;
|
|
123
|
+
}
|
|
124
|
+
// Cargo.toml
|
|
125
|
+
if (file === 'Cargo.toml') {
|
|
126
|
+
return 'src-tauri/Cargo.toml';
|
|
127
|
+
}
|
|
128
|
+
// iOS Info.plist
|
|
129
|
+
if (file === 'Info.plist') {
|
|
130
|
+
return 'src-tauri/gen/apple/Runner/Info.plist';
|
|
131
|
+
}
|
|
132
|
+
// Android manifest
|
|
133
|
+
if (file === 'AndroidManifest.xml') {
|
|
134
|
+
return 'src-tauri/gen/android/app/src/main/AndroidManifest.xml';
|
|
135
|
+
}
|
|
136
|
+
// Package.json is at root
|
|
137
|
+
if (file === 'package.json') {
|
|
138
|
+
return 'package.json';
|
|
139
|
+
}
|
|
140
|
+
// Default: assume it's relative to project root
|
|
141
|
+
return file;
|
|
142
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
export const GetDocsSchema = z.object({
|
|
6
|
+
projectPath: z.string().describe('Path to the Tauri project'),
|
|
7
|
+
});
|
|
8
|
+
export async function getDocs(projectPath) {
|
|
9
|
+
try {
|
|
10
|
+
const version = await getTauriVersion(projectPath), isV2 = version.startsWith('2'), docs = await fetchDocs(version, isV2);
|
|
11
|
+
return docs;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return `Error getting docs: ${error}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function getTauriVersion(projectPath) {
|
|
18
|
+
try {
|
|
19
|
+
// 1. Try 'cargo tree' to get the exact resolved version of the tauri crate
|
|
20
|
+
try {
|
|
21
|
+
const { stdout } = await execa('cargo', ['tree', '-p', 'tauri', '--depth', '0'], { cwd: path.join(projectPath, 'src-tauri') }), match = stdout.match(/tauri v([\d.]+)/);
|
|
22
|
+
if (match && match[1]) {
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
// Ignore
|
|
28
|
+
}
|
|
29
|
+
// 2. Try 'npm list' for the CLI version
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execa('npm', ['list', '@tauri-apps/cli', '--depth=0', '--json'], { cwd: projectPath }), pkg = JSON.parse(stdout);
|
|
32
|
+
if (pkg.dependencies?.['@tauri-apps/cli']?.version) {
|
|
33
|
+
return pkg.dependencies['@tauri-apps/cli'].version;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
// Ignore
|
|
38
|
+
}
|
|
39
|
+
// 3. Fallback: Read Cargo.toml manually
|
|
40
|
+
const cargoPath = path.join(projectPath, 'src-tauri', 'Cargo.toml'), cargoContent = await fs.readFile(cargoPath, 'utf-8'), match = cargoContent.match(/tauri\s*=\s*{?[^}]*version\s*=\s*"([^"]+)"/);
|
|
41
|
+
if (match && match[1]) {
|
|
42
|
+
return match[1];
|
|
43
|
+
}
|
|
44
|
+
// 4. Fallback: Read package.json
|
|
45
|
+
const pkgPath = path.join(projectPath, 'package.json'), pkgContent = await fs.readFile(pkgPath, 'utf-8'), pkgJson = JSON.parse(pkgContent), cliVersion = pkgJson.devDependencies?.['@tauri-apps/cli'] || pkgJson.dependencies?.['@tauri-apps/cli'];
|
|
46
|
+
if (cliVersion) {
|
|
47
|
+
return cliVersion.replace('^', '').replace('~', '');
|
|
48
|
+
}
|
|
49
|
+
return 'unknown';
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return 'unknown';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Exported for testing
|
|
56
|
+
export async function fetchDocs(version, isV2) {
|
|
57
|
+
const branch = isV2 ? 'v2' : 'v1', treeUrl = `https://api.github.com/repos/tauri-apps/tauri-docs/git/trees/${branch}?recursive=1`, rawBaseUrl = `https://raw.githubusercontent.com/tauri-apps/tauri-docs/${branch}`;
|
|
58
|
+
try {
|
|
59
|
+
// Fetching file tree from ${treeUrl}...
|
|
60
|
+
const treeResponse = await fetch(treeUrl, {
|
|
61
|
+
headers: {
|
|
62
|
+
'User-Agent': 'mcp-server-tauri', // GitHub API requires User-Agent
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!treeResponse.ok) {
|
|
66
|
+
throw new Error(`Failed to fetch file tree: ${treeResponse.status} ${treeResponse.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
const treeData = (await treeResponse.json()), relevantFiles = filterRelevantFiles(treeData.tree, isV2);
|
|
69
|
+
// Found ${relevantFiles.length} relevant documentation files.
|
|
70
|
+
let combinedDocs = `# Tauri ${isV2 ? 'v2' : 'v1'} Documentation (Dynamically Fetched)\n\n`;
|
|
71
|
+
combinedDocs += `Version Detected: ${version}\n`;
|
|
72
|
+
combinedDocs += `Source: ${rawBaseUrl}\n\n`;
|
|
73
|
+
// Fetch content in batches to be polite and avoid timeouts
|
|
74
|
+
const batchSize = 5;
|
|
75
|
+
for (let i = 0; i < relevantFiles.length; i += batchSize) {
|
|
76
|
+
const batch = relevantFiles.slice(i, i + batchSize), results = await Promise.all(batch.map((file) => { return fetchContent(rawBaseUrl, file); }));
|
|
77
|
+
combinedDocs += results.join('\n\n');
|
|
78
|
+
}
|
|
79
|
+
return combinedDocs;
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
// Error fetching dynamic docs: ${e}
|
|
83
|
+
return `Error fetching dynamic docs: ${e}\n\n` + getStaticFallback(version, isV2);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function isExcludedPath(filePath) {
|
|
87
|
+
// Check for common exclusions
|
|
88
|
+
if (filePath.includes('/blog/') || filePath.includes('/_') || filePath.includes('/translations/')) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// Check file extensions
|
|
92
|
+
if (!filePath.endsWith('.md') && !filePath.endsWith('.mdx')) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// Check for hidden or node_modules
|
|
96
|
+
if (filePath.startsWith('.') || filePath.includes('/node_modules/')) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// Check for blog posts
|
|
100
|
+
if (filePath.startsWith('blog/') || filePath.includes('/blog/')) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function isTranslationPath(filePath) {
|
|
106
|
+
const langCodes = ['fr', 'es', 'it', 'ja', 'ko', 'zh-cn', 'zh-tw', 'pt-br', 'ru', 'de'];
|
|
107
|
+
for (const lang of langCodes) {
|
|
108
|
+
const hasLang = filePath.includes(`/${lang}/`) || filePath.includes(`/_${lang}/`) ||
|
|
109
|
+
filePath.startsWith(`${lang}/`) || filePath.startsWith(`_${lang}/`);
|
|
110
|
+
if (hasLang) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function filterRelevantFiles(tree, isV2) {
|
|
117
|
+
return tree.filter((item) => {
|
|
118
|
+
if (item.type !== 'blob') {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const file = item;
|
|
122
|
+
if (isExcludedPath(file.path) || isTranslationPath(file.path)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (isV2) {
|
|
126
|
+
// v2 docs are in src/content/docs
|
|
127
|
+
if (!file.path.startsWith('src/content/docs/')) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
// Exclude fragments and templates
|
|
131
|
+
if (file.path.includes('/_fragments/') || file.path.includes('/.templates/')) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// v1 docs are in docs/
|
|
137
|
+
if (!file.path.startsWith('docs/')) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
// Exclude templates
|
|
141
|
+
if (file.path.includes('/.templates/')) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function fetchContent(baseUrl, file) {
|
|
149
|
+
try {
|
|
150
|
+
const url = `${baseUrl}/${file.path}`, response = await fetch(url);
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
return `## ${file.path}\n\n(Failed to fetch: ${response.status})\n`;
|
|
153
|
+
}
|
|
154
|
+
const text = await response.text();
|
|
155
|
+
// Remove frontmatter
|
|
156
|
+
const cleanText = text.replace(/^---[\s\S]*?---\n/, '');
|
|
157
|
+
return `## ${file.path}\n\n${cleanText}\n\n---\n`;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return `## ${file.path}\n\n(Error fetching content: ${e})\n`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function getStaticFallback(version, isV2) {
|
|
164
|
+
if (isV2) {
|
|
165
|
+
return `# Tauri v2 Static Fallback
|
|
166
|
+
(Dynamic fetching failed. This is a minimal fallback.)
|
|
167
|
+
|
|
168
|
+
## Core Concepts
|
|
169
|
+
- **Frontend**: Web technologies (HTML/CSS/JS).
|
|
170
|
+
- **Backend**: Rust.
|
|
171
|
+
- **IPC**: Use \`invoke\` to call Rust commands.
|
|
172
|
+
|
|
173
|
+
## Security
|
|
174
|
+
- Enable capabilities in \`src-tauri/capabilities\`.
|
|
175
|
+
- Configure permissions in \`tauri.conf.json\`.
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
return `# Tauri v1 LLM Cheat Sheet
|
|
179
|
+
Version Detected: ${version}
|
|
180
|
+
Documentation: https://tauri.app/v1/api/
|
|
181
|
+
|
|
182
|
+
## Core Concepts
|
|
183
|
+
- **Frontend**: Web technologies.
|
|
184
|
+
- **Backend**: Rust.
|
|
185
|
+
- **IPC**: \`invoke\` and \`#[tauri::command]\`.
|
|
186
|
+
|
|
187
|
+
## Key APIs (Frontend)
|
|
188
|
+
\`\`\`typescript
|
|
189
|
+
import { invoke } from '@tauri-apps/api/tauri';
|
|
190
|
+
|
|
191
|
+
// Call Rust command
|
|
192
|
+
await invoke('my_command', { arg: 'value' });
|
|
193
|
+
\`\`\`
|
|
194
|
+
|
|
195
|
+
## Key APIs (Rust)
|
|
196
|
+
\`\`\`rust
|
|
197
|
+
#[tauri::command]
|
|
198
|
+
fn my_command(arg: String) -> String {
|
|
199
|
+
format!("Hello {}", arg)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn main() {
|
|
203
|
+
tauri::Builder::default()
|
|
204
|
+
.invoke_handler(tauri::generate_handler![my_command])
|
|
205
|
+
.run(tauri::generate_context!())
|
|
206
|
+
.expect("error while running tauri application");
|
|
207
|
+
}
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
## Configuration
|
|
211
|
+
- **tauri.conf.json**: Uses \`allowlist\` to enable features.
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
export const ListDevicesSchema = z.object({});
|
|
4
|
+
async function getAndroidDevices() {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout } = await execa('adb', ['devices', '-l']);
|
|
7
|
+
return stdout
|
|
8
|
+
.split('\n')
|
|
9
|
+
.slice(1)
|
|
10
|
+
.filter((line) => { return line.trim().length > 0; })
|
|
11
|
+
.map((line) => { return line.trim(); });
|
|
12
|
+
}
|
|
13
|
+
catch (_) {
|
|
14
|
+
// Android SDK not available or adb command failed
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function getIOSSimulators() {
|
|
19
|
+
if (process.platform !== 'darwin') {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await execa('xcrun', ['simctl', 'list', 'devices', 'booted']);
|
|
24
|
+
return stdout
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter((line) => { return line.trim().length > 0 && !line.includes('== Devices =='); });
|
|
27
|
+
}
|
|
28
|
+
catch (_) {
|
|
29
|
+
// Xcode not installed or xcrun command failed
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function listDevices() {
|
|
34
|
+
const [android, ios] = await Promise.all([
|
|
35
|
+
getAndroidDevices(),
|
|
36
|
+
getIOSSimulators(),
|
|
37
|
+
]);
|
|
38
|
+
return { android, ios };
|
|
39
|
+
}
|
|
40
|
+
export const LaunchEmulatorSchema = z.object({
|
|
41
|
+
platform: z.enum(['android', 'ios']),
|
|
42
|
+
name: z.string().describe('Name of the AVD or Simulator'),
|
|
43
|
+
});
|
|
44
|
+
export async function launchEmulator(platform, name) {
|
|
45
|
+
if (platform === 'android') {
|
|
46
|
+
try {
|
|
47
|
+
// Launch Android Emulator - Try to spawn, but immediately await to catch ENOENT
|
|
48
|
+
await execa('emulator', ['-avd', name], {
|
|
49
|
+
detached: true,
|
|
50
|
+
stdio: 'ignore',
|
|
51
|
+
timeout: 1000, // Just check if it spawns
|
|
52
|
+
});
|
|
53
|
+
return `Launching Android AVD: ${name}`;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
throw new Error(`Failed to launch Android emulator: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (platform === 'ios') {
|
|
61
|
+
// Check if we're on macOS first
|
|
62
|
+
if (process.platform !== 'darwin') {
|
|
63
|
+
throw new Error('iOS simulators are only available on macOS');
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
// Launch iOS Simulator
|
|
67
|
+
await execa('xcrun', ['simctl', 'boot', name]);
|
|
68
|
+
await execa('open', ['-a', 'Simulator']);
|
|
69
|
+
return `Booted iOS Simulator: ${name}`;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
// Provide more helpful error messages
|
|
74
|
+
if (message.includes('xcrun: error')) {
|
|
75
|
+
throw new Error('Xcode is not installed. Please install Xcode from the App Store to use iOS simulators.');
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Failed to launch iOS simulator: ${message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
throw new Error(`Unsupported platform: ${platform}. Use 'android' or 'ios'.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
export const ReadLogsSchema = z.object({
|
|
4
|
+
source: z.enum(['android', 'ios', 'system']),
|
|
5
|
+
lines: z.number().default(50),
|
|
6
|
+
filter: z.string().optional().describe('Regex or keyword to filter logs'),
|
|
7
|
+
since: z.string().optional().describe('ISO timestamp to filter logs since (e.g. 2023-10-27T10:00:00Z)'),
|
|
8
|
+
});
|
|
9
|
+
export async function readLogs(source, lines, filter, since) {
|
|
10
|
+
try {
|
|
11
|
+
let output = '';
|
|
12
|
+
if (source === 'android') {
|
|
13
|
+
const args = ['logcat', '-d'];
|
|
14
|
+
if (since) {
|
|
15
|
+
// adb logcat -T expects "MM-DD HH:MM:SS.mmm"
|
|
16
|
+
const date = new Date(since);
|
|
17
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
18
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
19
|
+
const hours = date.getHours().toString().padStart(2, '0');
|
|
20
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
21
|
+
const seconds = date.getSeconds().toString().padStart(2, '0');
|
|
22
|
+
const ms = date.getMilliseconds().toString().padStart(3, '0');
|
|
23
|
+
const adbTime = `${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
24
|
+
args.push('-T', adbTime);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
args.push('-t', lines.toString());
|
|
28
|
+
}
|
|
29
|
+
const { stdout } = await execa('logcat', ['-d', '-t', lines.toString()], { timeout: 5000 });
|
|
30
|
+
output = stdout;
|
|
31
|
+
}
|
|
32
|
+
else if (source === 'ios') {
|
|
33
|
+
// iOS / macOS
|
|
34
|
+
const args = ['log', 'show', '--style', 'syslog'];
|
|
35
|
+
if (source === 'ios') {
|
|
36
|
+
args.unshift('xcrun', 'simctl', 'spawn', 'booted');
|
|
37
|
+
}
|
|
38
|
+
if (since) {
|
|
39
|
+
// log show --start "YYYY-MM-DD HH:MM:SS"
|
|
40
|
+
// It accepts ISO-like formats too usually, but let's be safe with
|
|
41
|
+
// local time format if possible
|
|
42
|
+
// Actually 'log show' on macOS is picky. ISO 8601 works in recent versions.
|
|
43
|
+
args.push('--start', since);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Default to last 1m if no since provided, as 'lines' isn't
|
|
47
|
+
// directly supported by log show time window
|
|
48
|
+
args.push('--last', '1m');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await execa(args[0], args.slice(1));
|
|
52
|
+
// We still apply line limit manually if we didn't use -t (adb)
|
|
53
|
+
let outLines = stdout.split('\n');
|
|
54
|
+
if (!since) {
|
|
55
|
+
outLines = outLines.slice(-lines);
|
|
56
|
+
}
|
|
57
|
+
output = outLines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return `Error reading logs: ${e}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// System (same as iOS essentially but local)
|
|
65
|
+
const args = ['log', 'show', '--style', 'syslog'];
|
|
66
|
+
if (since) {
|
|
67
|
+
args.push('--start', since);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
args.push('--last', '1m');
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const { stdout } = await execa('log', args.slice(1)); // 'log' is the command
|
|
74
|
+
let outLines = stdout.split('\n');
|
|
75
|
+
if (!since) {
|
|
76
|
+
outLines = outLines.slice(-lines);
|
|
77
|
+
}
|
|
78
|
+
output = outLines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return `Error reading system logs: ${e}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (filter) {
|
|
85
|
+
try {
|
|
86
|
+
const regex = new RegExp(filter, 'i');
|
|
87
|
+
return output.split('\n').filter((line) => { return regex.test(line); }).join('\n');
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
return `Invalid filter regex: ${e}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return `Error reading logs: ${error}`;
|
|
97
|
+
}
|
|
98
|
+
}
|