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