dashcam 0.8.4 → 1.0.1-beta.3

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 (55) hide show
  1. package/.dashcam/cli-config.json +3 -0
  2. package/.dashcam/recording.log +135 -0
  3. package/.dashcam/web-config.json +11 -0
  4. package/.github/RELEASE.md +59 -0
  5. package/.github/workflows/build.yml +103 -0
  6. package/.github/workflows/publish.yml +43 -0
  7. package/.github/workflows/release.yml +107 -0
  8. package/LOG_TRACKING_GUIDE.md +225 -0
  9. package/README.md +709 -155
  10. package/bin/dashcam.cjs +8 -0
  11. package/bin/dashcam.js +549 -0
  12. package/bin/index.js +63 -0
  13. package/examples/execute-script.js +152 -0
  14. package/examples/simple-test.js +37 -0
  15. package/lib/applicationTracker.js +311 -0
  16. package/lib/auth.js +222 -0
  17. package/lib/binaries.js +21 -0
  18. package/lib/config.js +34 -0
  19. package/lib/extension-logs/helpers.js +182 -0
  20. package/lib/extension-logs/index.js +347 -0
  21. package/lib/extension-logs/manager.js +344 -0
  22. package/lib/ffmpeg.js +156 -0
  23. package/lib/logTracker.js +23 -0
  24. package/lib/logger.js +118 -0
  25. package/lib/logs/index.js +432 -0
  26. package/lib/permissions.js +85 -0
  27. package/lib/processManager.js +255 -0
  28. package/lib/recorder.js +675 -0
  29. package/lib/store.js +58 -0
  30. package/lib/tracking/FileTracker.js +98 -0
  31. package/lib/tracking/FileTrackerManager.js +62 -0
  32. package/lib/tracking/LogsTracker.js +147 -0
  33. package/lib/tracking/active-win.js +212 -0
  34. package/lib/tracking/icons/darwin.js +39 -0
  35. package/lib/tracking/icons/index.js +167 -0
  36. package/lib/tracking/icons/windows.js +27 -0
  37. package/lib/tracking/idle.js +82 -0
  38. package/lib/tracking.js +23 -0
  39. package/lib/uploader.js +449 -0
  40. package/lib/utilities/jsonl.js +77 -0
  41. package/lib/webLogsDaemon.js +234 -0
  42. package/lib/websocket/server.js +223 -0
  43. package/package.json +50 -21
  44. package/recording.log +814 -0
  45. package/sea-bundle.mjs +34595 -0
  46. package/test-page.html +15 -0
  47. package/test.log +1 -0
  48. package/test_run.log +48 -0
  49. package/test_workflow.sh +80 -0
  50. package/examples/crash-test.js +0 -11
  51. package/examples/github-issue.sh +0 -1
  52. package/examples/protocol.html +0 -22
  53. package/index.js +0 -158
  54. package/lib.js +0 -199
  55. package/recorder.js +0 -85
@@ -0,0 +1,432 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { logger } from '../logger.js';
4
+ import { LogsTracker } from '../tracking/LogsTracker.js';
5
+ import { FileTrackerManager } from '../tracking/FileTrackerManager.js';
6
+ import { WebLogsDaemon, DAEMON_CONFIG_FILE } from '../webLogsDaemon.js';
7
+ import { WebLogsTracker } from '../extension-logs/index.js';
8
+ import { WebTrackerManager } from '../extension-logs/manager.js';
9
+ import { server } from '../websocket/server.js';
10
+ import { jsonl } from '../utilities/jsonl.js';
11
+ import { filterWebEvents } from '../extension-logs/helpers.js';
12
+ import fs from 'fs';
13
+
14
+ const CLI_CONFIG_FILE = path.join(process.cwd(), '.dashcam', 'cli-config.json');
15
+
16
+ // Simple trim function for CLI (adapted from desktop app)
17
+ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipId) {
18
+ logger.info('Trimming logs', { count: groupLogStatuses.length });
19
+
20
+ const REPLAY_DIR = path.join(os.tmpdir(), 'dashcam', 'recordings');
21
+
22
+ // Filter out logs with no content
23
+ groupLogStatuses = groupLogStatuses.filter((status) => status.count);
24
+
25
+ let webHandled = false;
26
+
27
+ groupLogStatuses.forEach((status) => {
28
+ if (!status.fileLocation || !status.count) return;
29
+
30
+ try {
31
+ const parsed = path.parse(status.fileLocation);
32
+ const content = jsonl.read(status.fileLocation);
33
+
34
+ if (!content || !Array.isArray(content)) return;
35
+
36
+ let events = content;
37
+
38
+ // Convert events to relative time
39
+ let relativeEvents = events.map((event) => {
40
+ event.time = parseInt(event.time + '') - startMS;
41
+ // Check if it's not already relative time
42
+ if (event.time > 1_000_000_000_000) {
43
+ // relative time = absolute time - clip start time
44
+ event.time = event.time - clientStartDate;
45
+ }
46
+ return event;
47
+ });
48
+
49
+ const duration = endMS - startMS;
50
+ let filteredEvents = relativeEvents;
51
+
52
+ if (status.type === 'application' || status.type === 'cli') {
53
+ // Filter events within the time range
54
+ filteredEvents = filteredEvents.filter((event) => {
55
+ return event.time >= 0 && event.time <= duration;
56
+ });
57
+
58
+ if (status.type === 'cli') {
59
+ // Remap logFile indices for CLI logs
60
+ let map = {};
61
+ filteredEvents = filteredEvents.map((event) => {
62
+ let name = map[event.logFile] ?? Object.keys(map).length + 1;
63
+ if (!map[event.logFile]) map[event.logFile] = name;
64
+ return {
65
+ ...event,
66
+ logFile: name,
67
+ };
68
+ });
69
+ }
70
+ } else if (status.type === 'web' && !webHandled) {
71
+ logger.debug('Found web groupLog, handling all web groupLogs at once');
72
+ // We do this because weblogs have a single shared jsonl file
73
+ // shared between all web logs
74
+ filteredEvents = filterWebEvents(
75
+ filteredEvents,
76
+ groupLogStatuses.filter((status) => status.type === 'web'),
77
+ 0,
78
+ duration
79
+ );
80
+
81
+ webHandled = true;
82
+ } else if (status.type === 'web') {
83
+ // Skip processing for additional web logs - already handled
84
+ status.trimmedFileLocation = path.join(
85
+ REPLAY_DIR,
86
+ [clipId, parsed.base].join('_')
87
+ );
88
+ status.count = filteredEvents.length;
89
+ return;
90
+ }
91
+
92
+ logger.debug('Filtered events', {
93
+ source: events.length,
94
+ filtered: filteredEvents.length,
95
+ difference: events.length - filteredEvents.length,
96
+ });
97
+
98
+ status.count = filteredEvents.length;
99
+ status.trimmedFileLocation = jsonl.write(
100
+ REPLAY_DIR,
101
+ [clipId, parsed.base].join('_'),
102
+ filteredEvents
103
+ );
104
+ } catch (error) {
105
+ logger.error('Error trimming log file', { file: status.fileLocation, error });
106
+ }
107
+ });
108
+
109
+ // Handle shared web log file location
110
+ const firstWebLog = groupLogStatuses.find(
111
+ (status) => status.type === 'web' && status.trimmedFileLocation
112
+ );
113
+ if (firstWebLog) {
114
+ groupLogStatuses
115
+ .filter((status) => status.type === 'web')
116
+ .forEach(
117
+ (status) =>
118
+ (status.trimmedFileLocation = firstWebLog.trimmedFileLocation)
119
+ );
120
+ }
121
+
122
+ return groupLogStatuses;
123
+ }
124
+
125
+ class LogsTrackerManager {
126
+ constructor() {
127
+ this.instances = {};
128
+ this.cliConfig = {};
129
+ this.webLogsConfig = {};
130
+ this.fileTrackerManager = new FileTrackerManager();
131
+
132
+ // Load persisted configs
133
+ this.loadCliConfig();
134
+ this.loadWebConfig();
135
+
136
+ // Create the singleton watch-only tracker for CLI files
137
+ this.watchTracker = new LogsTracker({
138
+ config: this.cliConfig,
139
+ fileTrackerManager: this.fileTrackerManager
140
+ });
141
+ }
142
+
143
+ async ensureWebDaemonRunning() {
144
+ try {
145
+ await WebLogsDaemon.ensureDaemonRunning();
146
+ } catch (error) {
147
+ logger.error('Failed to ensure web daemon is running', { error });
148
+ }
149
+ }
150
+
151
+ loadWebConfig() {
152
+ try {
153
+ if (fs.existsSync(DAEMON_CONFIG_FILE)) {
154
+ const data = fs.readFileSync(DAEMON_CONFIG_FILE, 'utf8');
155
+ this.webLogsConfig = JSON.parse(data);
156
+ }
157
+ } catch (error) {
158
+ logger.error('Failed to load web config', { error });
159
+ }
160
+ }
161
+
162
+ saveWebConfig() {
163
+ try {
164
+ fs.writeFileSync(DAEMON_CONFIG_FILE, JSON.stringify(this.webLogsConfig, null, 2));
165
+ } catch (error) {
166
+ logger.error('Failed to save web config', { error });
167
+ }
168
+ }
169
+
170
+ loadCliConfig() {
171
+ try {
172
+ if (fs.existsSync(CLI_CONFIG_FILE)) {
173
+ const data = fs.readFileSync(CLI_CONFIG_FILE, 'utf8');
174
+ this.cliConfig = JSON.parse(data);
175
+ }
176
+ } catch (error) {
177
+ logger.error('Failed to load CLI config', { error });
178
+ }
179
+ }
180
+
181
+ saveCliConfig() {
182
+ try {
183
+ // Ensure directory exists
184
+ const dir = path.dirname(CLI_CONFIG_FILE);
185
+ if (!fs.existsSync(dir)) {
186
+ fs.mkdirSync(dir, { recursive: true });
187
+ }
188
+ fs.writeFileSync(CLI_CONFIG_FILE, JSON.stringify(this.cliConfig, null, 2));
189
+ } catch (error) {
190
+ logger.error('Failed to save CLI config', { error });
191
+ }
192
+ }
193
+
194
+ updateLogsConfig(config) {
195
+ // Update web logs config
196
+ const webConfigs = Array.isArray(config) ?
197
+ config.filter(app => app.type === 'web' && app.enabled === true)
198
+ .reduce((map, config) => {
199
+ map[config.id] = config;
200
+ return map;
201
+ }, {}) : {};
202
+
203
+ this.webLogsConfig = webConfigs;
204
+ this.saveWebConfig();
205
+
206
+ logger.info('Updated logs config', { webConfigs });
207
+ }
208
+
209
+ async addWebTracker(config) {
210
+ this.webLogsConfig[config.id] = config;
211
+ this.saveWebConfig();
212
+
213
+ // Ensure daemon is running and will pick up the new config
214
+ await this.ensureWebDaemonRunning();
215
+
216
+ logger.info(`Added web tracker: ${config.name}`, { patterns: config.patterns });
217
+ }
218
+
219
+ pushCliTrackedPath(filePath) {
220
+ if (!this.cliConfig[filePath]) {
221
+ this.cliConfig[filePath] = true;
222
+ this.watchTracker.updateConfig(this.cliConfig);
223
+ this.saveCliConfig();
224
+ logger.info(`Added CLI tracked path: ${filePath}`);
225
+ }
226
+ }
227
+
228
+ removeCliTrackedPath(filePath) {
229
+ if (this.cliConfig[filePath]) {
230
+ delete this.cliConfig[filePath];
231
+ this.watchTracker.updateConfig(this.cliConfig);
232
+ this.saveCliConfig();
233
+ logger.info(`Removed CLI tracked path: ${filePath}`);
234
+ }
235
+ }
236
+
237
+ // CLI interface methods
238
+ addCliLogFile(filePath) {
239
+ this.pushCliTrackedPath(filePath);
240
+ }
241
+
242
+ removeCliLogFile(filePath) {
243
+ this.removeCliTrackedPath(filePath);
244
+ }
245
+
246
+ removeTracker(id) {
247
+ // Try removing from web trackers first
248
+ if (this.webLogsConfig[id]) {
249
+ delete this.webLogsConfig[id];
250
+ this.saveWebConfig();
251
+ logger.info(`Removed web tracker: ${id}`);
252
+ return;
253
+ }
254
+
255
+ // Check if it's a file tracker (format: file-1, file-2, etc.)
256
+ if (id.startsWith('file-')) {
257
+ const fileIndex = parseInt(id.split('-')[1]) - 1;
258
+ const cliFiles = Object.keys(this.cliConfig);
259
+ if (fileIndex >= 0 && fileIndex < cliFiles.length) {
260
+ const filePath = cliFiles[fileIndex];
261
+ this.removeCliTrackedPath(filePath);
262
+ logger.info(`Removed file tracker: ${filePath}`);
263
+ return;
264
+ }
265
+ }
266
+
267
+ logger.warn(`Tracker not found: ${id}`);
268
+ }
269
+
270
+ getStatus() {
271
+ // Load current web config
272
+ this.loadWebConfig();
273
+
274
+ const activeInstances = Object.keys(this.instances).length;
275
+ const cliFilesCount = Object.keys(this.cliConfig).length;
276
+ const webAppsCount = Object.keys(this.webLogsConfig).length;
277
+
278
+ // Get file tracker stats
279
+ const fileTrackerStats = Object.keys(this.cliConfig).map(filePath => {
280
+ const stats = this.fileTrackerManager.getStats(filePath);
281
+ return {
282
+ filePath,
283
+ count: stats.count
284
+ };
285
+ });
286
+
287
+ const totalEvents = fileTrackerStats.reduce((sum, stat) => sum + stat.count, 0);
288
+
289
+ return {
290
+ activeInstances,
291
+ cliFilesCount,
292
+ webAppsCount,
293
+ totalEvents,
294
+ fileTrackerStats,
295
+ cliFiles: Object.keys(this.cliConfig),
296
+ webApps: Object.values(this.webLogsConfig).map(config => ({
297
+ id: config.id,
298
+ name: config.name,
299
+ patterns: config.patterns
300
+ })),
301
+ webDaemonRunning: WebLogsDaemon.isDaemonRunning()
302
+ };
303
+ }
304
+
305
+ async startNew({ recorderId, screenId, directory }) {
306
+ logger.debug('LogsTrackerManager: Starting new logs tracker instance', { recorderId, screenId, directory });
307
+
308
+ const instanceKey = `${recorderId}_${screenId}`;
309
+
310
+ // Create recording tracker for CLI logs
311
+ const cliTracker = new LogsTracker({
312
+ directory,
313
+ config: { ...this.cliConfig }, // Copy current config
314
+ fileTrackerManager: this.fileTrackerManager,
315
+ });
316
+
317
+ // Start WebSocket server if not already running
318
+ if (!server.isListening.value) {
319
+ logger.debug('LogsTrackerManager: Starting WebSocket server...');
320
+ await server.start();
321
+ logger.info('LogsTrackerManager: WebSocket server started on port', { port: server.port });
322
+ } else {
323
+ logger.debug('LogsTrackerManager: WebSocket server already running on port', { port: server.port });
324
+ }
325
+
326
+ // Create a WebTrackerManager instance for this recording
327
+ logger.debug('LogsTrackerManager: Creating WebTrackerManager for recording...');
328
+ const webTrackerManager = new WebTrackerManager(server);
329
+
330
+ // Create recording tracker for web logs with directory to write events to file
331
+ logger.debug('LogsTrackerManager: Creating WebLogsTracker for recording...', {
332
+ directory,
333
+ webConfigCount: Object.keys(this.webLogsConfig).length
334
+ });
335
+ const webTracker = new WebLogsTracker({
336
+ config: { ...this.webLogsConfig }, // Copy current web config
337
+ webTrackerManager,
338
+ directory // This makes it NOT watch-only, so events will be written to file
339
+ });
340
+
341
+ this.instances[instanceKey] = {
342
+ recorderId,
343
+ screenId,
344
+ directory,
345
+ trackers: {
346
+ cli: cliTracker,
347
+ web: webTracker,
348
+ webTrackerManager // Store this so we can clean it up later
349
+ },
350
+ startTime: Date.now(),
351
+ endTime: undefined,
352
+ };
353
+
354
+ logger.info(`Started new logs tracker instance with web support`, {
355
+ recorderId,
356
+ screenId,
357
+ directory,
358
+ webConfigCount: Object.keys(this.webLogsConfig).length
359
+ });
360
+ return this.instances[instanceKey];
361
+ }
362
+
363
+ async stop({ recorderId, screenId }) {
364
+ const instanceKey = `${recorderId}_${screenId}`;
365
+ const instance = this.instances[instanceKey];
366
+
367
+ if (!instance) {
368
+ logger.warn(`No logs tracker instance found for ${instanceKey}`);
369
+ return [];
370
+ }
371
+
372
+ delete this.instances[instanceKey];
373
+
374
+ // Stop CLI tracker
375
+ const cliStatus = instance.trackers.cli.destroy();
376
+
377
+ // Stop web tracker if it exists
378
+ let webStatus = [];
379
+ if (instance.trackers.web) {
380
+ logger.debug('LogsTrackerManager: Stopping web tracker...', { recorderId, screenId });
381
+ webStatus = instance.trackers.web.destroy();
382
+ }
383
+
384
+ // Clean up WebTrackerManager if it exists
385
+ if (instance.trackers.webTrackerManager) {
386
+ logger.debug('LogsTrackerManager: Destroying WebTrackerManager...', { recorderId, screenId });
387
+ instance.trackers.webTrackerManager.destroy();
388
+ }
389
+
390
+ // Stop WebSocket server if no more recording instances are active
391
+ const remainingInstances = Object.keys(this.instances).length;
392
+ if (remainingInstances === 0 && server.isListening.value) {
393
+ logger.debug('LogsTrackerManager: No more recording instances, stopping WebSocket server...');
394
+ await server.stop();
395
+ logger.info('LogsTrackerManager: WebSocket server stopped');
396
+ } else {
397
+ logger.debug('LogsTrackerManager: WebSocket server kept running', { remainingInstances });
398
+ }
399
+
400
+ logger.info(`Stopped logs tracker instance with web support`, {
401
+ recorderId,
402
+ screenId,
403
+ cliStatusCount: cliStatus.length,
404
+ webStatusCount: webStatus.length
405
+ });
406
+
407
+ // Combine CLI and web statuses
408
+ return [...cliStatus, ...webStatus];
409
+ }
410
+
411
+ stopAll() {
412
+ logger.info('Stopping all logs tracker instances');
413
+ const results = [];
414
+ for (const instanceKey of Object.keys(this.instances)) {
415
+ const [recorderId, screenId] = instanceKey.split('_');
416
+ results.push(...this.stop({ recorderId, screenId }));
417
+ }
418
+ return results;
419
+ }
420
+
421
+ destroy() {
422
+ this.stopAll();
423
+ this.watchTracker.destroy();
424
+ this.fileTrackerManager.destroy();
425
+ // Note: Don't stop the web daemon here as it should persist
426
+ }
427
+ }
428
+
429
+ // Create singleton instance
430
+ const logsTrackerManager = new LogsTrackerManager();
431
+
432
+ export { trimLogs, logsTrackerManager };
@@ -0,0 +1,85 @@
1
+ import { logger } from './logger.js';
2
+ import { execa } from 'execa';
3
+ import os from 'os';
4
+
5
+ /**
6
+ * Check if the application has screen recording permissions on macOS
7
+ */
8
+ async function checkScreenRecordingPermission() {
9
+ const platform = os.platform();
10
+
11
+ if (platform !== 'darwin') {
12
+ // Not macOS, permissions check not needed
13
+ return { hasPermission: true, platform };
14
+ }
15
+
16
+ try {
17
+ // Try to capture a single frame to test permissions
18
+ // This is a quick test that will fail immediately if permissions are denied
19
+ const { stderr } = await execa('screencapture', ['-x', '-t', 'png', '/tmp/dashcam_permission_test.png'], {
20
+ timeout: 2000,
21
+ reject: false
22
+ });
23
+
24
+ // Clean up test file
25
+ try {
26
+ const fs = await import('fs');
27
+ if (fs.existsSync('/tmp/dashcam_permission_test.png')) {
28
+ fs.unlinkSync('/tmp/dashcam_permission_test.png');
29
+ }
30
+ } catch (e) {
31
+ // Ignore cleanup errors
32
+ }
33
+
34
+ const hasPermission = !stderr || !stderr.includes('not permitted');
35
+
36
+ return { hasPermission, platform: 'darwin' };
37
+ } catch (error) {
38
+ logger.debug('Permission check failed', { error: error.message });
39
+ return { hasPermission: false, platform: 'darwin', error: error.message };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Display instructions for granting screen recording permissions
45
+ */
46
+ function showPermissionInstructions() {
47
+ const platform = os.platform();
48
+
49
+ if (platform !== 'darwin') {
50
+ return;
51
+ }
52
+
53
+ console.log('\n⚠️ Screen Recording Permission Required\n');
54
+ console.log('Dashcam needs screen recording permission to capture your screen.');
55
+ console.log('\nTo grant permission:');
56
+ console.log('1. Open System Settings (or System Preferences)');
57
+ console.log('2. Go to Privacy & Security > Screen Recording');
58
+ console.log('3. Click the lock icon and enter your password');
59
+ console.log('4. Enable screen recording for Terminal (or your terminal app)');
60
+ console.log(' - If using the standalone binary, you may need to add the binary itself');
61
+ console.log('5. Restart your terminal and try again\n');
62
+ console.log('Note: You may need to fully quit and restart your terminal application.\n');
63
+ }
64
+
65
+ /**
66
+ * Check permissions and show instructions if needed
67
+ */
68
+ async function ensurePermissions() {
69
+ const result = await checkScreenRecordingPermission();
70
+
71
+ if (!result.hasPermission) {
72
+ logger.warn('Screen recording permission not granted');
73
+ showPermissionInstructions();
74
+ return false;
75
+ }
76
+
77
+ logger.debug('Screen recording permission check passed');
78
+ return true;
79
+ }
80
+
81
+ export {
82
+ checkScreenRecordingPermission,
83
+ showPermissionInstructions,
84
+ ensurePermissions
85
+ };