@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.
@@ -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
+ }