@pheem49/mint 1.3.0 → 1.4.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.
Files changed (38) hide show
  1. package/.codex +0 -0
  2. package/README.md +174 -126
  3. package/main.js +21 -1
  4. package/mint-cli-logic.js +21 -1
  5. package/mint-cli.js +287 -45
  6. package/package.json +13 -2
  7. package/src/AI_Brain/Gemini_API.js +331 -64
  8. package/src/AI_Brain/agent_orchestrator.js +73 -0
  9. package/src/AI_Brain/autonomous_brain.js +2 -0
  10. package/src/AI_Brain/memory_store.js +318 -0
  11. package/src/AI_Brain/proactive_engine.js +2 -8
  12. package/src/Automation_Layer/file_operations.js +123 -4
  13. package/src/Automation_Layer/open_app.js +72 -43
  14. package/src/Automation_Layer/open_website.js +3 -3
  15. package/src/CLI/chat_router.js +57 -9
  16. package/src/CLI/chat_ui.js +117 -11
  17. package/src/CLI/code_agent.js +249 -36
  18. package/src/CLI/onboarding.js +53 -6
  19. package/src/CLI/workspace_manager.js +90 -0
  20. package/src/Plugins/docker.js +12 -10
  21. package/src/Plugins/spotify.js +168 -40
  22. package/src/Plugins/system_monitor.js +72 -0
  23. package/src/System/config_manager.js +35 -2
  24. package/src/System/custom_workflows.js +9 -2
  25. package/src/System/notifications.js +23 -0
  26. package/src/UI/settings.html +143 -65
  27. package/src/UI/settings.js +155 -41
  28. package/tests/agent_orchestrator.test.js +41 -0
  29. package/tests/chat_router.test.js +42 -0
  30. package/tests/code_agent.test.js +69 -0
  31. package/tests/config_manager.test.js +141 -0
  32. package/tests/docker.test.js +46 -0
  33. package/tests/file_operations.test.js +57 -0
  34. package/tests/memory_store.test.js +185 -0
  35. package/tests/provider_routing.test.js +67 -0
  36. package/tests/spotify.test.js +201 -0
  37. package/tests/system_monitor.test.js +37 -0
  38. package/tests/workspace_manager.test.js +56 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Mint Long-Term Memory Store
3
+ * ---------------------------
4
+ * Persists user preferences, session summaries, and usage patterns
5
+ * across all Mint sessions using SQLite (same DB as knowledge_base).
6
+ *
7
+ * Auto-injects a "User Context" block into the system prompt so Mint
8
+ * remembers who it's talking to even after restart.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const crypto = require('crypto');
15
+ const { readConfig } = require('../System/config_manager');
16
+
17
+ // ── Electron-safe app path ──────────────────────────────────────────────────
18
+ let app;
19
+ try {
20
+ const electron = require('electron');
21
+ app = electron.app;
22
+ } catch (_) {
23
+ app = null;
24
+ }
25
+
26
+ function getDbPath() {
27
+ const fileName = 'mint-knowledge.sqlite'; // shared DB with knowledge_base
28
+ if (app && app.getPath) {
29
+ return path.join(app.getPath('userData'), fileName);
30
+ }
31
+ const mintDir = path.join(os.homedir(), '.mint');
32
+ if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
33
+ return path.join(mintDir, fileName);
34
+ }
35
+
36
+ // ── Lazy DatabaseSync init ─────────────────────────────────────────────────
37
+ let DatabaseSync = null;
38
+ function getDatabaseSync() {
39
+ if (!DatabaseSync) ({ DatabaseSync } = require('node:sqlite'));
40
+ return DatabaseSync;
41
+ }
42
+
43
+ let dbInstance = null;
44
+ function getDb() {
45
+ if (dbInstance) return dbInstance;
46
+ const Database = getDatabaseSync();
47
+ dbInstance = new Database(getDbPath());
48
+
49
+ dbInstance.exec(`
50
+ -- User profile: arbitrary key-value pairs
51
+ CREATE TABLE IF NOT EXISTS user_profile (
52
+ key TEXT PRIMARY KEY,
53
+ value TEXT,
54
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
55
+ );
56
+
57
+ -- Condensed summaries of past sessions
58
+ CREATE TABLE IF NOT EXISTS session_memories (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ summary TEXT NOT NULL,
61
+ tags TEXT DEFAULT '',
62
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
63
+ );
64
+
65
+ -- Frequently used topics / commands
66
+ CREATE TABLE IF NOT EXISTS usage_patterns (
67
+ pattern TEXT PRIMARY KEY,
68
+ count INTEGER DEFAULT 1,
69
+ last_used DATETIME DEFAULT CURRENT_TIMESTAMP
70
+ );
71
+
72
+ -- Response Cache: For repetitive exact queries
73
+ CREATE TABLE IF NOT EXISTS response_cache (
74
+ query_hash TEXT PRIMARY KEY,
75
+ response TEXT NOT NULL,
76
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
77
+ );
78
+ `);
79
+
80
+ return dbInstance;
81
+ }
82
+
83
+ // ── Profile helpers ────────────────────────────────────────────────────────
84
+ function setProfile(key, value) {
85
+ try {
86
+ const db = getDb();
87
+ db.prepare(`
88
+ INSERT INTO user_profile (key, value, updated_at)
89
+ VALUES (?, ?, CURRENT_TIMESTAMP)
90
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
91
+ `).run(key, String(value));
92
+ } catch (err) {
93
+ console.error('[Memory] setProfile error:', err.message);
94
+ }
95
+ }
96
+
97
+ function getProfile(key, defaultValue = null) {
98
+ try {
99
+ const row = getDb().prepare('SELECT value FROM user_profile WHERE key = ?').get(key);
100
+ return row ? row.value : defaultValue;
101
+ } catch (_) {
102
+ return defaultValue;
103
+ }
104
+ }
105
+
106
+ function getAllProfile() {
107
+ try {
108
+ const rows = getDb().prepare('SELECT key, value FROM user_profile').all();
109
+ return Object.fromEntries(rows.map(r => [r.key, r.value]));
110
+ } catch (_) {
111
+ return {};
112
+ }
113
+ }
114
+
115
+ // ── Session memory helpers ─────────────────────────────────────────────────
116
+ const MAX_SESSION_MEMORIES = 20; // keep last N summaries
117
+
118
+ function addSessionMemory(summary, tags = []) {
119
+ try {
120
+ const db = getDb();
121
+ db.prepare('INSERT INTO session_memories (summary, tags) VALUES (?, ?)').run(
122
+ summary.slice(0, 800), // cap length
123
+ tags.join(',')
124
+ );
125
+ // Prune oldest beyond limit
126
+ db.exec(`
127
+ DELETE FROM session_memories WHERE id NOT IN (
128
+ SELECT id FROM session_memories ORDER BY id DESC LIMIT ${MAX_SESSION_MEMORIES}
129
+ )
130
+ `);
131
+ } catch (err) {
132
+ console.error('[Memory] addSessionMemory error:', err.message);
133
+ }
134
+ }
135
+
136
+ function getRecentMemories(limit = 5) {
137
+ try {
138
+ return getDb()
139
+ .prepare('SELECT summary, tags, created_at FROM session_memories ORDER BY id DESC LIMIT ?')
140
+ .all(limit);
141
+ } catch (_) {
142
+ return [];
143
+ }
144
+ }
145
+
146
+ // ── Usage pattern helpers ──────────────────────────────────────────────────
147
+ function recordPattern(pattern) {
148
+ try {
149
+ const db = getDb();
150
+ db.prepare(`
151
+ INSERT INTO usage_patterns (pattern, count, last_used)
152
+ VALUES (?, 1, CURRENT_TIMESTAMP)
153
+ ON CONFLICT(pattern) DO UPDATE
154
+ SET count = count + 1, last_used = CURRENT_TIMESTAMP
155
+ `).run(pattern.slice(0, 120));
156
+ } catch (_) {}
157
+ }
158
+
159
+ function getTopPatterns(limit = 8) {
160
+ try {
161
+ return getDb()
162
+ .prepare('SELECT pattern, count FROM usage_patterns ORDER BY count DESC, last_used DESC LIMIT ?')
163
+ .all(limit);
164
+ } catch (_) {
165
+ return [];
166
+ }
167
+ }
168
+
169
+ // ── Simple keyword extractor (no external deps) ────────────────────────────
170
+ const STOP_WORDS = new Set([
171
+ 'ที่', 'ให้', 'และ', 'ของ', 'กับ', 'ใน', 'บน', 'เป็น', 'อยู่', 'มี', 'ได้', 'the', 'a', 'an',
172
+ 'is', 'are', 'was', 'were', 'it', 'in', 'on', 'at', 'for', 'to', 'of', 'with', 'and', 'or',
173
+ 'this', 'that', 'i', 'you', 'me', 'my', 'your', 'can', 'do', 'be', 'will', 'please', 'how',
174
+ 'what', 'which', 'when', 'where', 'why', 'help', 'want', 'need', 'make', 'create', 'get', 'run'
175
+ ]);
176
+
177
+ function extractKeywords(text) {
178
+ return text
179
+ .toLowerCase()
180
+ .replace(/[^\w\u0E00-\u0E7F\s]/g, ' ')
181
+ .split(/\s+/)
182
+ .filter(w => w.length > 2 && !STOP_WORDS.has(w))
183
+ .slice(0, 6);
184
+ }
185
+
186
+ // ── Main public API ────────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Called after every successful chat turn.
190
+ * Extracts patterns & infers preferences — runs async, non-blocking.
191
+ */
192
+ function recordInteraction(userMessage, aiResponseText) {
193
+ try {
194
+ if (!userMessage || !aiResponseText) return;
195
+
196
+ // Extract keywords as usage patterns
197
+ const keywords = extractKeywords(userMessage);
198
+ keywords.forEach(kw => recordPattern(kw));
199
+
200
+ // Detect preferred language
201
+ const thaiRatio = (userMessage.match(/[\u0E00-\u0E7F]/g) || []).length / userMessage.length;
202
+ if (thaiRatio > 0.3) setProfile('preferred_language', 'thai');
203
+ else setProfile('preferred_language', 'english');
204
+
205
+ // Detect coding intent (update project activity)
206
+ const codingKeywords = ['code', 'fix', 'debug', 'function', 'class', 'import', 'script',
207
+ 'แก้', 'เขียน', 'โค้ด', 'สคริปต์', 'ฟังก์ชัน'];
208
+ if (codingKeywords.some(k => userMessage.toLowerCase().includes(k))) {
209
+ const cwd = process.cwd();
210
+ if (cwd !== os.homedir()) {
211
+ setProfile('last_active_project', path.basename(cwd));
212
+ setProfile('last_active_project_path', cwd);
213
+ }
214
+ }
215
+
216
+ // Update interaction counter
217
+ const count = parseInt(getProfile('total_interactions', '0'), 10);
218
+ setProfile('total_interactions', String(count + 1));
219
+ setProfile('last_seen', new Date().toISOString());
220
+ } catch (err) {
221
+ console.error('[Memory] recordInteraction error:', err.message);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Saves a condensed summary of a completed conversation.
227
+ * Call this when user clears history or after N turns.
228
+ */
229
+ function saveSessionSummary(summary, tags = []) {
230
+ if (!summary || summary.trim().length < 10) return;
231
+ addSessionMemory(summary.trim(), tags);
232
+ }
233
+
234
+ /**
235
+ * Returns a formatted context string to inject into the AI system prompt.
236
+ * Lightweight — no async calls.
237
+ */
238
+ function getUserContext() {
239
+ try {
240
+ const profile = getAllProfile();
241
+ const patterns = getTopPatterns(6);
242
+ const memories = getRecentMemories(3);
243
+
244
+ const lines = ['\n\n[LONG-TERM USER CONTEXT — use this to personalize responses]'];
245
+
246
+ // Profile info
247
+ if (Object.keys(profile).length > 0) {
248
+ if (profile.preferred_language)
249
+ lines.push(`• Preferred language: ${profile.preferred_language}`);
250
+ if (profile.last_active_project)
251
+ lines.push(`• Last active project: ${profile.last_active_project} (${profile.last_active_project_path || ''})`);
252
+ if (profile.total_interactions)
253
+ lines.push(`• Total interactions with Mint: ${profile.total_interactions}`);
254
+ if (profile.last_seen) {
255
+ const d = new Date(profile.last_seen);
256
+ lines.push(`• Last session: ${d.toLocaleDateString('th-TH')} ${d.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' })}`);
257
+ }
258
+ }
259
+
260
+ // Usage patterns
261
+ if (patterns.length > 0) {
262
+ const topTopics = patterns.map(p => p.pattern).join(', ');
263
+ lines.push(`• Frequent topics/tools: ${topTopics}`);
264
+ }
265
+
266
+ // Past session memories
267
+ if (memories.length > 0) {
268
+ lines.push('\nRecent session summaries:');
269
+ memories.forEach((m, i) => lines.push(` ${i + 1}. ${m.summary}`));
270
+ }
271
+
272
+ if (lines.length === 1) return ''; // nothing to add
273
+ lines.push('[END USER CONTEXT]\n');
274
+ return lines.join('\n');
275
+ } catch (err) {
276
+ console.error('[Memory] getUserContext error:', err.message);
277
+ return '';
278
+ }
279
+ }
280
+
281
+ // ── Response Cache helpers ────────────────────────────────────────────────
282
+ function getCachedResponse(query) {
283
+ try {
284
+ const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
285
+ const row = getDb().prepare('SELECT response, created_at FROM response_cache WHERE query_hash = ?').get(hash);
286
+ if (row) {
287
+ // Optional: check TTL (e.g., 24 hours)
288
+ const age = Date.now() - new Date(row.created_at).getTime();
289
+ if (age < 24 * 60 * 60 * 1000) {
290
+ return JSON.parse(row.response);
291
+ }
292
+ }
293
+ } catch (_) {}
294
+ return null;
295
+ }
296
+
297
+ function cacheResponse(query, responseObj) {
298
+ try {
299
+ const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
300
+ getDb().prepare(`
301
+ INSERT INTO response_cache (query_hash, response, created_at)
302
+ VALUES (?, ?, CURRENT_TIMESTAMP)
303
+ ON CONFLICT(query_hash) DO UPDATE SET response = excluded.response, created_at = CURRENT_TIMESTAMP
304
+ `).run(hash, JSON.stringify(responseObj));
305
+ } catch (_) {}
306
+ }
307
+
308
+ module.exports = {
309
+ recordInteraction,
310
+ saveSessionSummary,
311
+ getUserContext,
312
+ setProfile,
313
+ getProfile,
314
+ getTopPatterns,
315
+ getRecentMemories,
316
+ getCachedResponse,
317
+ cacheResponse
318
+ };
@@ -1,7 +1,4 @@
1
1
  const { GoogleGenAI } = require('@google/genai');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const { app } = require('electron');
5
2
  const { readConfig } = require('../System/config_manager');
6
3
 
7
4
  // ============================================================
@@ -76,11 +73,8 @@ function resolveGeminiModel() {
76
73
 
77
74
  function getMinSuggestionIntervalMs() {
78
75
  try {
79
- const CONFIG_PATH = path.join(app.getPath('userData'), 'mint-config.json');
80
- if (fs.existsSync(CONFIG_PATH)) {
81
- const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
82
- return (cfg.proactiveCooldown || 120) * 1000;
83
- }
76
+ const cfg = readConfig();
77
+ return (cfg.proactiveCooldown || 120) * 1000;
84
78
  } catch {
85
79
  // ignore
86
80
  }
@@ -1,4 +1,4 @@
1
- const { exec } = require('child_process');
1
+ const { execFile } = require('child_process');
2
2
  let shell;
3
3
  try {
4
4
  shell = require('electron').shell;
@@ -9,6 +9,22 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
 
12
+ const IGNORED_DIRECTORY_NAMES = new Set([
13
+ '.git',
14
+ 'node_modules',
15
+ '.cache',
16
+ 'dist',
17
+ 'build',
18
+ 'coverage'
19
+ ]);
20
+
21
+ function getSearchRoots() {
22
+ return Array.from(new Set([
23
+ process.cwd(),
24
+ os.homedir()
25
+ ]));
26
+ }
27
+
12
28
  /**
13
29
  * Smartly resolves a path.
14
30
  * If a path starts with '/' but doesn't exist at root, checks if it exists relative to home.
@@ -53,6 +69,109 @@ function resolveSmartPath(target) {
53
69
  return target;
54
70
  }
55
71
 
72
+ function findPath(target, options = {}) {
73
+ if (!target || !target.trim()) {
74
+ return { success: false, message: 'No search query provided.', matches: [] };
75
+ }
76
+
77
+ const normalizedType = ['file', 'dir', 'any'].includes(options.type) ? options.type : 'any';
78
+ const loweredQuery = target.trim().toLowerCase();
79
+ const exactMatches = [];
80
+ const partialMatches = [];
81
+ const visited = new Set();
82
+ const maxResults = options.maxResults || 20;
83
+ const searchRoots = Array.isArray(options.roots) && options.roots.length > 0
84
+ ? options.roots
85
+ : getSearchRoots();
86
+
87
+ function buildMatch(entryPath, entryType, rootPath, exactNameMatch) {
88
+ const relativeToCwd = path.relative(process.cwd(), entryPath);
89
+ const pathDepth = entryPath.split(path.sep).length;
90
+ return {
91
+ path: entryPath,
92
+ type: entryType,
93
+ exactNameMatch,
94
+ inCurrentWorkspace: !relativeToCwd.startsWith('..') && !path.isAbsolute(relativeToCwd),
95
+ pathDepth,
96
+ rootPath
97
+ };
98
+ }
99
+
100
+ function sortMatches(matches) {
101
+ return matches.sort((a, b) => {
102
+ if (a.exactNameMatch !== b.exactNameMatch) return a.exactNameMatch ? -1 : 1;
103
+ if (a.inCurrentWorkspace !== b.inCurrentWorkspace) return a.inCurrentWorkspace ? -1 : 1;
104
+ if (a.pathDepth !== b.pathDepth) return a.pathDepth - b.pathDepth;
105
+ return a.path.localeCompare(b.path);
106
+ });
107
+ }
108
+
109
+ function visit(currentPath, rootPath) {
110
+ let entries = [];
111
+ try {
112
+ entries = fs.readdirSync(currentPath, { withFileTypes: true });
113
+ } catch (_) {
114
+ return;
115
+ }
116
+
117
+ for (const entry of entries) {
118
+ const absoluteEntryPath = path.join(currentPath, entry.name);
119
+ if (visited.has(absoluteEntryPath)) continue;
120
+ visited.add(absoluteEntryPath);
121
+
122
+ const entryType = entry.isDirectory() ? 'dir' : 'file';
123
+ if (entry.isDirectory() && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
124
+ continue;
125
+ }
126
+ const relativePath = path.relative(rootPath, absoluteEntryPath);
127
+ const searchablePath = relativePath || entry.name;
128
+ const matchesType = normalizedType === 'any' || normalizedType === entryType;
129
+ const lowerEntryName = entry.name.toLowerCase();
130
+ const exactNameMatch = lowerEntryName === loweredQuery;
131
+ const partialMatch = lowerEntryName.includes(loweredQuery) || searchablePath.toLowerCase().includes(loweredQuery);
132
+
133
+ if (matchesType && partialMatch) {
134
+ const match = buildMatch(absoluteEntryPath, entryType, rootPath, exactNameMatch);
135
+ if (exactNameMatch) {
136
+ exactMatches.push(match);
137
+ if (exactMatches.length >= maxResults) return;
138
+ } else if (exactMatches.length === 0) {
139
+ partialMatches.push(match);
140
+ if (partialMatches.length >= maxResults) return;
141
+ }
142
+ }
143
+
144
+ if (entry.isDirectory() && exactMatches.length < maxResults && partialMatches.length < maxResults) {
145
+ visit(absoluteEntryPath, rootPath);
146
+ if (exactMatches.length >= maxResults || partialMatches.length >= maxResults) return;
147
+ }
148
+ }
149
+ }
150
+
151
+ for (const rootPath of searchRoots) {
152
+ if (!fs.existsSync(rootPath)) continue;
153
+ visit(rootPath, rootPath);
154
+ if (exactMatches.length >= maxResults || partialMatches.length >= maxResults) break;
155
+ }
156
+
157
+ const matches = exactMatches.length > 0
158
+ ? sortMatches(exactMatches).slice(0, maxResults)
159
+ : sortMatches(partialMatches).slice(0, maxResults);
160
+
161
+ if (matches.length === 0) {
162
+ return {
163
+ success: false,
164
+ message: `ไม่พบ${normalizedType === 'dir' ? 'โฟลเดอร์' : normalizedType === 'file' ? 'ไฟล์' : 'ไฟล์หรือโฟลเดอร์'}ที่ตรงกับ "${target}" ค่ะ`,
165
+ matches: []
166
+ };
167
+ }
168
+
169
+ return {
170
+ success: true,
171
+ matches: matches.map(({ path: matchPath, type }) => ({ path: matchPath, type }))
172
+ };
173
+ }
174
+
56
175
  /**
57
176
  * สร้างโฟลเดอร์ใหม่
58
177
  * target: ชื่อโฟลเดอร์ หรือ absolute path
@@ -99,7 +218,7 @@ async function openFile(target) {
99
218
  }
100
219
  } else {
101
220
  return new Promise((resolve) => {
102
- exec(`xdg-open "${resolvedPath}"`, (err) => {
221
+ execFile('xdg-open', [resolvedPath], (err) => {
103
222
  if (err) {
104
223
  console.error("Failed to open path via xdg-open:", err);
105
224
  resolve(`ไม่สามารถเปิดไฟล์ได้ค่ะ: ${err.message}`);
@@ -129,7 +248,7 @@ async function deleteFile(target) {
129
248
  }
130
249
  } else {
131
250
  return new Promise((resolve) => {
132
- exec(`gio trash "${resolvedPath}"`, (err) => {
251
+ execFile('gio', ['trash', resolvedPath], (err) => {
133
252
  if (err) {
134
253
  console.error("Failed to trash item via gio trash:", err);
135
254
  resolve({ success: false, message: err.message });
@@ -141,4 +260,4 @@ async function deleteFile(target) {
141
260
  }
142
261
  }
143
262
 
144
- module.exports = { createFolder, openFile, deleteFile };
263
+ module.exports = { createFolder, openFile, deleteFile, findPath };
@@ -1,56 +1,85 @@
1
- const { exec } = require('child_process');
1
+ const { execFile } = require('child_process');
2
2
 
3
- function openApp(target) {
3
+ function execFilePromise(command, args) {
4
+ return new Promise((resolve, reject) => {
5
+ execFile(command, args, (error) => {
6
+ if (error) {
7
+ reject(error);
8
+ return;
9
+ }
10
+ resolve();
11
+ });
12
+ });
13
+ }
14
+
15
+ async function tryCommands(commands) {
16
+ let lastError = null;
17
+ for (const { command, args } of commands) {
18
+ try {
19
+ await execFilePromise(command, args);
20
+ return true;
21
+ } catch (error) {
22
+ lastError = error;
23
+ }
24
+ }
25
+
26
+ if (lastError) {
27
+ console.error(`exec error: ${lastError}`);
28
+ }
29
+ return false;
30
+ }
31
+
32
+ async function openApp(target) {
4
33
  if (!target) return;
5
34
 
6
- let cmd = '';
7
35
  if (process.platform === 'win32') {
8
- cmd = `start "" "${target}"`;
9
- } else if (process.platform === 'darwin') {
10
- if (!target.includes('/')) {
11
- cmd = `open -X -a "${target}" || open -a "${target}"`;
12
- } else {
13
- cmd = `open "${target}"`;
14
- }
15
- } else {
16
- const tLower = target.toLowerCase();
17
- const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
18
-
19
- // Try common linux patterns: gtk-launch, exact name, lowercase, flatpak
36
+ await execFilePromise('cmd.exe', ['/c', 'start', '', target]).catch((error) => {
37
+ console.error(`exec error: ${error}`);
38
+ });
39
+ return;
40
+ }
41
+
42
+ if (process.platform === 'darwin') {
20
43
  if (!target.includes('/')) {
21
- const patterns = [
22
- `gtk-launch ${target}`,
23
- `gtk-launch ${tLower}`,
24
- `gtk-launch ${tCapitalized}`,
25
- `gtk-launch com.${tLower}app.${tCapitalized}`,
26
- `gtk-launch com.${tLower}.${tCapitalized}`,
27
- target,
28
- tLower,
29
- `flatpak run ${target}`, // In case target is already ID
30
- `flatpak run com.${tLower}app.${tCapitalized}`,
31
- `flatpak run com.${tLower}.${tCapitalized}`,
32
- `flatpak run com.${tLower}.Browser`,
33
- `flatpak run com.${tLower}.${target}`,
34
- `flatpak run com.valvesoftware.Steam`,
35
- `flatpak run net.lutris.Lutris`,
36
- `snap run ${tLower}`
37
- ];
38
- cmd = patterns.join(' || ');
44
+ await tryCommands([
45
+ { command: 'open', args: ['-X', '-a', target] },
46
+ { command: 'open', args: ['-a', target] }
47
+ ]);
39
48
  } else {
40
- cmd = `xdg-open "${target}"`;
49
+ await execFilePromise('open', [target]).catch((error) => {
50
+ console.error(`exec error: ${error}`);
51
+ });
41
52
  }
53
+ return;
42
54
  }
43
55
 
44
- exec(cmd, (error) => {
45
- if (error) {
56
+ const tLower = target.toLowerCase();
57
+ const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
58
+
59
+ if (target.includes('/')) {
60
+ await execFilePromise('xdg-open', [target]).catch((error) => {
46
61
  console.error(`exec error: ${error}`);
47
- if (process.platform !== 'win32') {
48
- exec(target.toLowerCase(), (err2) => {
49
- if (err2) console.error("Fallback lowercase exec failed:", err2);
50
- });
51
- }
52
- }
53
- });
62
+ });
63
+ return;
64
+ }
65
+
66
+ await tryCommands([
67
+ { command: 'gtk-launch', args: [target] },
68
+ { command: 'gtk-launch', args: [tLower] },
69
+ { command: 'gtk-launch', args: [tCapitalized] },
70
+ { command: 'gtk-launch', args: [`com.${tLower}app.${tCapitalized}`] },
71
+ { command: 'gtk-launch', args: [`com.${tLower}.${tCapitalized}`] },
72
+ { command: target, args: [] },
73
+ { command: tLower, args: [] },
74
+ { command: 'flatpak', args: ['run', target] },
75
+ { command: 'flatpak', args: ['run', `com.${tLower}app.${tCapitalized}`] },
76
+ { command: 'flatpak', args: ['run', `com.${tLower}.${tCapitalized}`] },
77
+ { command: 'flatpak', args: ['run', `com.${tLower}.Browser`] },
78
+ { command: 'flatpak', args: ['run', `com.${tLower}.${target}`] },
79
+ { command: 'flatpak', args: ['run', 'com.valvesoftware.Steam'] },
80
+ { command: 'flatpak', args: ['run', 'net.lutris.Lutris'] },
81
+ { command: 'snap', args: ['run', tLower] }
82
+ ]);
54
83
  }
55
84
 
56
85
  module.exports = { openApp };
@@ -1,4 +1,4 @@
1
- const { exec } = require('child_process');
1
+ const { execFile } = require('child_process');
2
2
 
3
3
  let shell;
4
4
  try {
@@ -17,7 +17,7 @@ function openWebsite(targetUrl) {
17
17
  shell.openExternal(url);
18
18
  } else {
19
19
  // Fallback for Node.js (Linux focus)
20
- exec(`xdg-open "${url}"`, (err) => {
20
+ execFile('xdg-open', [url], (err) => {
21
21
  if (err) console.error("Failed to open URL via xdg_open:", err);
22
22
  });
23
23
  }
@@ -29,7 +29,7 @@ function openSearch(query) {
29
29
  if (shell) {
30
30
  shell.openExternal(url);
31
31
  } else {
32
- exec(`xdg-open "${url}"`, (err) => {
32
+ execFile('xdg-open', [url], (err) => {
33
33
  if (err) console.error("Failed to open search via xdg-open:", err);
34
34
  });
35
35
  }