@nclamvn/vibecode-cli 2.2.1 → 3.0.0
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/bin/vibecode.js +86 -0
- package/package.json +1 -1
- package/src/commands/config.js +42 -4
- package/src/commands/deploy.js +728 -0
- package/src/commands/favorite.js +412 -0
- package/src/commands/feedback.js +473 -0
- package/src/commands/go.js +128 -0
- package/src/commands/history.js +249 -0
- package/src/commands/images.js +465 -0
- package/src/commands/voice.js +580 -0
- package/src/commands/watch.js +3 -20
- package/src/index.js +46 -1
- package/src/services/image-service.js +513 -0
- package/src/utils/history.js +357 -0
- package/src/utils/notifications.js +343 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - History & Favorites Utility
|
|
3
|
+
// Phase M8: Command history and favorite prompts management
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Configuration
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const HISTORY_DIR = path.join(os.homedir(), '.vibecode');
|
|
15
|
+
const HISTORY_FILE = path.join(HISTORY_DIR, 'history.json');
|
|
16
|
+
const FAVORITES_FILE = path.join(HISTORY_DIR, 'favorites.json');
|
|
17
|
+
const MAX_HISTORY = 100;
|
|
18
|
+
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// History Functions
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Add a command to history
|
|
25
|
+
* @param {string} command - The command that was run
|
|
26
|
+
* @param {string} description - Description of the command
|
|
27
|
+
* @param {Object} metadata - Additional metadata
|
|
28
|
+
*/
|
|
29
|
+
export async function addToHistory(command, description = '', metadata = {}) {
|
|
30
|
+
const history = await loadHistory();
|
|
31
|
+
|
|
32
|
+
history.unshift({
|
|
33
|
+
id: Date.now(),
|
|
34
|
+
command,
|
|
35
|
+
description,
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
...metadata
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Keep only last MAX_HISTORY items
|
|
42
|
+
if (history.length > MAX_HISTORY) {
|
|
43
|
+
history.length = MAX_HISTORY;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await saveHistory(history);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load history from disk
|
|
51
|
+
* @returns {Promise<Array>} History array
|
|
52
|
+
*/
|
|
53
|
+
export async function loadHistory() {
|
|
54
|
+
try {
|
|
55
|
+
await fs.mkdir(HISTORY_DIR, { recursive: true });
|
|
56
|
+
const content = await fs.readFile(HISTORY_FILE, 'utf-8');
|
|
57
|
+
return JSON.parse(content);
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Save history to disk
|
|
65
|
+
* @param {Array} history - History array to save
|
|
66
|
+
*/
|
|
67
|
+
export async function saveHistory(history) {
|
|
68
|
+
await fs.mkdir(HISTORY_DIR, { recursive: true });
|
|
69
|
+
await fs.writeFile(HISTORY_FILE, JSON.stringify(history, null, 2));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all history
|
|
74
|
+
*/
|
|
75
|
+
export async function clearHistory() {
|
|
76
|
+
await saveHistory([]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Search history by query
|
|
81
|
+
* @param {string} query - Search query
|
|
82
|
+
* @returns {Promise<Array>} Matching history items
|
|
83
|
+
*/
|
|
84
|
+
export async function searchHistory(query) {
|
|
85
|
+
const history = await loadHistory();
|
|
86
|
+
const q = query.toLowerCase();
|
|
87
|
+
|
|
88
|
+
return history.filter(item =>
|
|
89
|
+
item.command.toLowerCase().includes(q) ||
|
|
90
|
+
(item.description && item.description.toLowerCase().includes(q))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get history item by index (1-based)
|
|
96
|
+
* @param {number} index - Item index (1-based)
|
|
97
|
+
* @returns {Promise<Object|null>} History item or null
|
|
98
|
+
*/
|
|
99
|
+
export async function getHistoryItem(index) {
|
|
100
|
+
const history = await loadHistory();
|
|
101
|
+
return history[index - 1] || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get history stats
|
|
106
|
+
* @returns {Promise<Object>} History statistics
|
|
107
|
+
*/
|
|
108
|
+
export async function getHistoryStats() {
|
|
109
|
+
const history = await loadHistory();
|
|
110
|
+
|
|
111
|
+
if (history.length === 0) {
|
|
112
|
+
return { total: 0, oldest: null, newest: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
total: history.length,
|
|
117
|
+
oldest: history[history.length - 1]?.timestamp,
|
|
118
|
+
newest: history[0]?.timestamp
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
// Favorites Functions
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Load favorites from disk
|
|
128
|
+
* @returns {Promise<Array>} Favorites array
|
|
129
|
+
*/
|
|
130
|
+
export async function loadFavorites() {
|
|
131
|
+
try {
|
|
132
|
+
await fs.mkdir(HISTORY_DIR, { recursive: true });
|
|
133
|
+
const content = await fs.readFile(FAVORITES_FILE, 'utf-8');
|
|
134
|
+
return JSON.parse(content);
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Save favorites to disk
|
|
142
|
+
* @param {Array} favorites - Favorites array to save
|
|
143
|
+
*/
|
|
144
|
+
export async function saveFavorites(favorites) {
|
|
145
|
+
await fs.mkdir(HISTORY_DIR, { recursive: true });
|
|
146
|
+
await fs.writeFile(FAVORITES_FILE, JSON.stringify(favorites, null, 2));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add a new favorite
|
|
151
|
+
* @param {string} name - Display name for the favorite
|
|
152
|
+
* @param {string} command - The command to save
|
|
153
|
+
* @param {string} description - Description of what it does
|
|
154
|
+
* @param {Array<string>} tags - Optional tags for searching
|
|
155
|
+
* @returns {Promise<Object>} Result object
|
|
156
|
+
*/
|
|
157
|
+
export async function addFavorite(name, command, description = '', tags = []) {
|
|
158
|
+
const favorites = await loadFavorites();
|
|
159
|
+
|
|
160
|
+
// Check for duplicate
|
|
161
|
+
const exists = favorites.some(f => f.command === command);
|
|
162
|
+
if (exists) {
|
|
163
|
+
return { success: false, message: 'Already in favorites' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
favorites.push({
|
|
167
|
+
id: Date.now(),
|
|
168
|
+
name: name || description.substring(0, 30),
|
|
169
|
+
command,
|
|
170
|
+
description,
|
|
171
|
+
tags,
|
|
172
|
+
createdAt: new Date().toISOString(),
|
|
173
|
+
usageCount: 0
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await saveFavorites(favorites);
|
|
177
|
+
return { success: true, message: 'Added to favorites' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Remove a favorite by identifier (index or name)
|
|
182
|
+
* @param {string|number} identifier - Index (1-based) or name/command fragment
|
|
183
|
+
* @returns {Promise<Object>} Result object
|
|
184
|
+
*/
|
|
185
|
+
export async function removeFavorite(identifier) {
|
|
186
|
+
const favorites = await loadFavorites();
|
|
187
|
+
|
|
188
|
+
let index = -1;
|
|
189
|
+
|
|
190
|
+
// Try by index first
|
|
191
|
+
if (typeof identifier === 'number' || /^\d+$/.test(identifier)) {
|
|
192
|
+
index = parseInt(identifier) - 1;
|
|
193
|
+
} else {
|
|
194
|
+
// Try by name/command
|
|
195
|
+
index = favorites.findIndex(f =>
|
|
196
|
+
f.name.toLowerCase().includes(identifier.toLowerCase()) ||
|
|
197
|
+
f.command.toLowerCase().includes(identifier.toLowerCase())
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (index >= 0 && index < favorites.length) {
|
|
202
|
+
const removed = favorites.splice(index, 1)[0];
|
|
203
|
+
await saveFavorites(favorites);
|
|
204
|
+
return { success: true, removed };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { success: false, message: 'Favorite not found' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get a favorite by identifier (index or name)
|
|
212
|
+
* @param {string|number} identifier - Index (1-based) or name/command fragment
|
|
213
|
+
* @returns {Promise<Object|null>} Favorite object or null
|
|
214
|
+
*/
|
|
215
|
+
export async function getFavorite(identifier) {
|
|
216
|
+
const favorites = await loadFavorites();
|
|
217
|
+
|
|
218
|
+
// Try by index
|
|
219
|
+
if (typeof identifier === 'number' || /^\d+$/.test(identifier)) {
|
|
220
|
+
return favorites[parseInt(identifier) - 1] || null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Try by name/command
|
|
224
|
+
return favorites.find(f =>
|
|
225
|
+
f.name.toLowerCase().includes(identifier.toLowerCase()) ||
|
|
226
|
+
f.command.toLowerCase().includes(identifier.toLowerCase())
|
|
227
|
+
) || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Update favorite usage count
|
|
232
|
+
* @param {number} id - Favorite ID
|
|
233
|
+
*/
|
|
234
|
+
export async function updateFavoriteUsage(id) {
|
|
235
|
+
const favorites = await loadFavorites();
|
|
236
|
+
const favorite = favorites.find(f => f.id === id);
|
|
237
|
+
|
|
238
|
+
if (favorite) {
|
|
239
|
+
favorite.usageCount = (favorite.usageCount || 0) + 1;
|
|
240
|
+
favorite.lastUsed = new Date().toISOString();
|
|
241
|
+
await saveFavorites(favorites);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Search favorites by query
|
|
247
|
+
* @param {string} query - Search query
|
|
248
|
+
* @returns {Promise<Array>} Matching favorites
|
|
249
|
+
*/
|
|
250
|
+
export async function searchFavorites(query) {
|
|
251
|
+
const favorites = await loadFavorites();
|
|
252
|
+
const q = query.toLowerCase();
|
|
253
|
+
|
|
254
|
+
return favorites.filter(f =>
|
|
255
|
+
f.name.toLowerCase().includes(q) ||
|
|
256
|
+
f.command.toLowerCase().includes(q) ||
|
|
257
|
+
(f.description && f.description.toLowerCase().includes(q)) ||
|
|
258
|
+
(f.tags && f.tags.some(t => t.toLowerCase().includes(q)))
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Export favorites as JSON
|
|
264
|
+
* @returns {Promise<Array>} Favorites array
|
|
265
|
+
*/
|
|
266
|
+
export async function exportFavorites() {
|
|
267
|
+
return await loadFavorites();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Import favorites from JSON data
|
|
272
|
+
* @param {Array} data - Favorites data to import
|
|
273
|
+
* @param {boolean} merge - Whether to merge with existing (true) or replace (false)
|
|
274
|
+
* @returns {Promise<Object>} Import result
|
|
275
|
+
*/
|
|
276
|
+
export async function importFavorites(data, merge = true) {
|
|
277
|
+
const existing = merge ? await loadFavorites() : [];
|
|
278
|
+
|
|
279
|
+
// Filter out duplicates
|
|
280
|
+
const newFavorites = data.filter(item =>
|
|
281
|
+
!existing.some(e => e.command === item.command)
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Ensure each imported item has required fields
|
|
285
|
+
const processedFavorites = newFavorites.map(item => ({
|
|
286
|
+
id: item.id || Date.now() + Math.random(),
|
|
287
|
+
name: item.name || item.description?.substring(0, 30) || 'Untitled',
|
|
288
|
+
command: item.command,
|
|
289
|
+
description: item.description || '',
|
|
290
|
+
tags: item.tags || [],
|
|
291
|
+
createdAt: item.createdAt || new Date().toISOString(),
|
|
292
|
+
usageCount: item.usageCount || 0
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
const merged = [...existing, ...processedFavorites];
|
|
296
|
+
await saveFavorites(merged);
|
|
297
|
+
|
|
298
|
+
return { imported: processedFavorites.length, total: merged.length };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Clear all favorites
|
|
303
|
+
*/
|
|
304
|
+
export async function clearFavorites() {
|
|
305
|
+
await saveFavorites([]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get favorites stats
|
|
310
|
+
* @returns {Promise<Object>} Favorites statistics
|
|
311
|
+
*/
|
|
312
|
+
export async function getFavoritesStats() {
|
|
313
|
+
const favorites = await loadFavorites();
|
|
314
|
+
|
|
315
|
+
if (favorites.length === 0) {
|
|
316
|
+
return { total: 0, mostUsed: null, totalUsage: 0 };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const mostUsed = favorites.reduce((max, f) =>
|
|
320
|
+
(f.usageCount || 0) > (max.usageCount || 0) ? f : max
|
|
321
|
+
, favorites[0]);
|
|
322
|
+
|
|
323
|
+
const totalUsage = favorites.reduce((sum, f) => sum + (f.usageCount || 0), 0);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
total: favorites.length,
|
|
327
|
+
mostUsed: mostUsed.name,
|
|
328
|
+
totalUsage
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
333
|
+
// Exports
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
export default {
|
|
337
|
+
// History
|
|
338
|
+
addToHistory,
|
|
339
|
+
loadHistory,
|
|
340
|
+
saveHistory,
|
|
341
|
+
clearHistory,
|
|
342
|
+
searchHistory,
|
|
343
|
+
getHistoryItem,
|
|
344
|
+
getHistoryStats,
|
|
345
|
+
// Favorites
|
|
346
|
+
loadFavorites,
|
|
347
|
+
saveFavorites,
|
|
348
|
+
addFavorite,
|
|
349
|
+
removeFavorite,
|
|
350
|
+
getFavorite,
|
|
351
|
+
updateFavoriteUsage,
|
|
352
|
+
searchFavorites,
|
|
353
|
+
exportFavorites,
|
|
354
|
+
importFavorites,
|
|
355
|
+
clearFavorites,
|
|
356
|
+
getFavoritesStats
|
|
357
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Desktop Notifications Utility
|
|
3
|
+
// Phase M7: Cross-platform notification support
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import { execSync, exec } from 'child_process';
|
|
7
|
+
import { platform } from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Configuration
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const NOTIFICATION_TYPES = {
|
|
15
|
+
success: { icon: '✅', sound: true },
|
|
16
|
+
error: { icon: '❌', sound: true },
|
|
17
|
+
warning: { icon: '⚠️', sound: false },
|
|
18
|
+
info: { icon: 'ℹ️', sound: false },
|
|
19
|
+
build: { icon: '🏗️', sound: true },
|
|
20
|
+
deploy: { icon: '🚀', sound: true },
|
|
21
|
+
test: { icon: '🧪', sound: true },
|
|
22
|
+
watch: { icon: '👁️', sound: false }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const APP_NAME = 'Vibecode';
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Platform Detection
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get current platform
|
|
33
|
+
* @returns {'macos'|'linux'|'windows'|'unknown'}
|
|
34
|
+
*/
|
|
35
|
+
function getPlatform() {
|
|
36
|
+
const p = platform();
|
|
37
|
+
if (p === 'darwin') return 'macos';
|
|
38
|
+
if (p === 'linux') return 'linux';
|
|
39
|
+
if (p === 'win32') return 'windows';
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if notifications are supported on current platform
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function isNotificationSupported() {
|
|
48
|
+
const p = getPlatform();
|
|
49
|
+
|
|
50
|
+
if (p === 'macos') {
|
|
51
|
+
return true; // AppleScript always available
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (p === 'linux') {
|
|
55
|
+
try {
|
|
56
|
+
execSync('which notify-send', { stdio: 'ignore' });
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (p === 'windows') {
|
|
64
|
+
return true; // PowerShell always available
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
// Platform-Specific Implementations
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Send notification on macOS using AppleScript
|
|
76
|
+
*/
|
|
77
|
+
function notifyMacOS(title, message, options = {}) {
|
|
78
|
+
const { sound = false, subtitle = '' } = options;
|
|
79
|
+
|
|
80
|
+
// Escape special characters for AppleScript
|
|
81
|
+
const escapeAS = (str) => str.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
|
|
82
|
+
|
|
83
|
+
let script = `display notification "${escapeAS(message)}" with title "${escapeAS(title)}"`;
|
|
84
|
+
|
|
85
|
+
if (subtitle) {
|
|
86
|
+
script += ` subtitle "${escapeAS(subtitle)}"`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (sound) {
|
|
90
|
+
script += ' sound name "Glass"';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
execSync(`osascript -e '${script}'`, { stdio: 'ignore' });
|
|
95
|
+
return true;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Send notification on Linux using notify-send
|
|
103
|
+
*/
|
|
104
|
+
function notifyLinux(title, message, options = {}) {
|
|
105
|
+
const { icon = 'dialog-information', urgency = 'normal', timeout = 5000 } = options;
|
|
106
|
+
|
|
107
|
+
// Escape special characters for shell
|
|
108
|
+
const escapeShell = (str) => str.replace(/'/g, "'\\''");
|
|
109
|
+
|
|
110
|
+
const args = [
|
|
111
|
+
`'${escapeShell(title)}'`,
|
|
112
|
+
`'${escapeShell(message)}'`,
|
|
113
|
+
`--urgency=${urgency}`,
|
|
114
|
+
`-t ${timeout}`,
|
|
115
|
+
`-i ${icon}`
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
execSync(`notify-send ${args.join(' ')}`, { stdio: 'ignore' });
|
|
120
|
+
return true;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Send notification on Windows using PowerShell
|
|
128
|
+
*/
|
|
129
|
+
function notifyWindows(title, message, options = {}) {
|
|
130
|
+
const { icon = 'Information' } = options;
|
|
131
|
+
|
|
132
|
+
// Escape special characters for PowerShell
|
|
133
|
+
const escapePS = (str) => str.replace(/'/g, "''").replace(/`/g, '``');
|
|
134
|
+
|
|
135
|
+
const script = `
|
|
136
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
137
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
138
|
+
|
|
139
|
+
$template = @"
|
|
140
|
+
<toast>
|
|
141
|
+
<visual>
|
|
142
|
+
<binding template="ToastText02">
|
|
143
|
+
<text id="1">${escapePS(title)}</text>
|
|
144
|
+
<text id="2">${escapePS(message)}</text>
|
|
145
|
+
</binding>
|
|
146
|
+
</visual>
|
|
147
|
+
</toast>
|
|
148
|
+
"@
|
|
149
|
+
|
|
150
|
+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
151
|
+
$xml.LoadXml($template)
|
|
152
|
+
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
|
153
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("${APP_NAME}").Show($toast)
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Use simpler balloon notification as fallback
|
|
158
|
+
const simpleScript = `
|
|
159
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
160
|
+
$balloon = New-Object System.Windows.Forms.NotifyIcon
|
|
161
|
+
$balloon.Icon = [System.Drawing.SystemIcons]::${icon}
|
|
162
|
+
$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::${icon}
|
|
163
|
+
$balloon.BalloonTipTitle = '${escapePS(title)}'
|
|
164
|
+
$balloon.BalloonTipText = '${escapePS(message)}'
|
|
165
|
+
$balloon.Visible = $true
|
|
166
|
+
$balloon.ShowBalloonTip(5000)
|
|
167
|
+
Start-Sleep -Seconds 1
|
|
168
|
+
$balloon.Dispose()
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
exec(`powershell -Command "${simpleScript.replace(/\n/g, '; ')}"`, { stdio: 'ignore' });
|
|
172
|
+
return true;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
// Main Notification Function
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Send a desktop notification
|
|
184
|
+
* @param {string} message - Notification message
|
|
185
|
+
* @param {string} type - Notification type (success, error, warning, info, build, deploy, test, watch)
|
|
186
|
+
* @param {Object} options - Additional options
|
|
187
|
+
* @param {string} options.title - Custom title (default: "Vibecode")
|
|
188
|
+
* @param {string} options.subtitle - Subtitle (macOS only)
|
|
189
|
+
* @param {boolean} options.sound - Play sound
|
|
190
|
+
* @returns {boolean} - Whether notification was sent successfully
|
|
191
|
+
*/
|
|
192
|
+
export function notify(message, type = 'info', options = {}) {
|
|
193
|
+
const config = NOTIFICATION_TYPES[type] || NOTIFICATION_TYPES.info;
|
|
194
|
+
const title = options.title || `${config.icon} ${APP_NAME}`;
|
|
195
|
+
const sound = options.sound !== undefined ? options.sound : config.sound;
|
|
196
|
+
|
|
197
|
+
const p = getPlatform();
|
|
198
|
+
|
|
199
|
+
switch (p) {
|
|
200
|
+
case 'macos':
|
|
201
|
+
return notifyMacOS(title, message, { ...options, sound });
|
|
202
|
+
case 'linux':
|
|
203
|
+
return notifyLinux(title, message, options);
|
|
204
|
+
case 'windows':
|
|
205
|
+
return notifyWindows(title, message, options);
|
|
206
|
+
default:
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
212
|
+
// Convenience Functions
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Notify build completion
|
|
217
|
+
* @param {boolean} success - Whether build succeeded
|
|
218
|
+
* @param {string} projectName - Project name
|
|
219
|
+
* @param {Object} options - Additional options
|
|
220
|
+
*/
|
|
221
|
+
export function notifyBuildComplete(success, projectName = 'Project', options = {}) {
|
|
222
|
+
const message = success
|
|
223
|
+
? `${projectName} built successfully!`
|
|
224
|
+
: `${projectName} build failed`;
|
|
225
|
+
|
|
226
|
+
return notify(message, success ? 'success' : 'error', {
|
|
227
|
+
title: '🏗️ Build Complete',
|
|
228
|
+
subtitle: projectName,
|
|
229
|
+
...options
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Notify deploy completion
|
|
235
|
+
* @param {boolean} success - Whether deploy succeeded
|
|
236
|
+
* @param {string} platform - Deploy platform (vercel, netlify, etc.)
|
|
237
|
+
* @param {string} url - Deployment URL
|
|
238
|
+
*/
|
|
239
|
+
export function notifyDeployComplete(success, platform = 'Cloud', url = '') {
|
|
240
|
+
const message = success
|
|
241
|
+
? `Deployed to ${platform}!${url ? ` ${url}` : ''}`
|
|
242
|
+
: `Deploy to ${platform} failed`;
|
|
243
|
+
|
|
244
|
+
return notify(message, success ? 'deploy' : 'error', {
|
|
245
|
+
title: '🚀 Deploy Complete',
|
|
246
|
+
subtitle: platform
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Notify file change detected (watch mode)
|
|
252
|
+
* @param {string} filePath - Changed file path
|
|
253
|
+
* @param {string} event - Event type (change, add, unlink)
|
|
254
|
+
*/
|
|
255
|
+
export function notifyWatchChange(filePath, event = 'change') {
|
|
256
|
+
const fileName = path.basename(filePath);
|
|
257
|
+
const eventIcons = {
|
|
258
|
+
change: '📝',
|
|
259
|
+
add: '➕',
|
|
260
|
+
unlink: '🗑️'
|
|
261
|
+
};
|
|
262
|
+
const icon = eventIcons[event] || '👁️';
|
|
263
|
+
|
|
264
|
+
return notify(`${icon} ${fileName}`, 'watch', {
|
|
265
|
+
title: '👁️ File Changed',
|
|
266
|
+
subtitle: event,
|
|
267
|
+
sound: false
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Notify test completion
|
|
273
|
+
* @param {boolean} success - Whether tests passed
|
|
274
|
+
* @param {number} passed - Number of passed tests
|
|
275
|
+
* @param {number} failed - Number of failed tests
|
|
276
|
+
*/
|
|
277
|
+
export function notifyTestComplete(success, passed = 0, failed = 0) {
|
|
278
|
+
const message = success
|
|
279
|
+
? `All ${passed} tests passed!`
|
|
280
|
+
: `${failed} test${failed > 1 ? 's' : ''} failed, ${passed} passed`;
|
|
281
|
+
|
|
282
|
+
return notify(message, success ? 'success' : 'error', {
|
|
283
|
+
title: '🧪 Tests Complete'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Notify error occurred
|
|
289
|
+
* @param {string} message - Error message
|
|
290
|
+
* @param {string} context - Error context
|
|
291
|
+
*/
|
|
292
|
+
export function notifyError(message, context = '') {
|
|
293
|
+
return notify(message, 'error', {
|
|
294
|
+
title: '❌ Error',
|
|
295
|
+
subtitle: context
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Notify success
|
|
301
|
+
* @param {string} message - Success message
|
|
302
|
+
* @param {string} context - Success context
|
|
303
|
+
*/
|
|
304
|
+
export function notifySuccess(message, context = '') {
|
|
305
|
+
return notify(message, 'success', {
|
|
306
|
+
title: '✅ Success',
|
|
307
|
+
subtitle: context
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Notify agent module completion
|
|
313
|
+
* @param {number} moduleNum - Module number
|
|
314
|
+
* @param {number} total - Total modules
|
|
315
|
+
* @param {boolean} success - Whether module succeeded
|
|
316
|
+
*/
|
|
317
|
+
export function notifyAgentProgress(moduleNum, total, success = true) {
|
|
318
|
+
const message = success
|
|
319
|
+
? `Module ${moduleNum}/${total} completed`
|
|
320
|
+
: `Module ${moduleNum}/${total} failed`;
|
|
321
|
+
|
|
322
|
+
return notify(message, success ? 'info' : 'warning', {
|
|
323
|
+
title: '🤖 Agent Mode',
|
|
324
|
+
subtitle: `Progress: ${Math.round((moduleNum / total) * 100)}%`
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─────────────────────────────────────════════════════════════════════════════
|
|
329
|
+
// Exports
|
|
330
|
+
// ─────────────────────────────════════════════════════════════════════════════
|
|
331
|
+
|
|
332
|
+
export default {
|
|
333
|
+
notify,
|
|
334
|
+
notifyBuildComplete,
|
|
335
|
+
notifyDeployComplete,
|
|
336
|
+
notifyWatchChange,
|
|
337
|
+
notifyTestComplete,
|
|
338
|
+
notifyError,
|
|
339
|
+
notifySuccess,
|
|
340
|
+
notifyAgentProgress,
|
|
341
|
+
isNotificationSupported,
|
|
342
|
+
getPlatform
|
|
343
|
+
};
|