dashcam 0.8.3 → 1.0.1-beta.10

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/publish.yml +43 -0
  6. package/BACKWARD_COMPATIBILITY.md +177 -0
  7. package/LOG_TRACKING_GUIDE.md +225 -0
  8. package/README.md +709 -155
  9. package/bin/dashcam-background.js +177 -0
  10. package/bin/dashcam.cjs +8 -0
  11. package/bin/dashcam.js +696 -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 +155 -0
  23. package/lib/logTracker.js +23 -0
  24. package/lib/logger.js +118 -0
  25. package/lib/logs/index.js +488 -0
  26. package/lib/permissions.js +85 -0
  27. package/lib/processManager.js +317 -0
  28. package/lib/recorder.js +690 -0
  29. package/lib/store.js +58 -0
  30. package/lib/tracking/FileTracker.js +105 -0
  31. package/lib/tracking/FileTrackerManager.js +62 -0
  32. package/lib/tracking/LogsTracker.js +161 -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 +456 -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 +53 -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 +154 -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 -177
  54. package/lib.js +0 -199
  55. package/recorder.js +0 -85
package/lib/ffmpeg.js ADDED
@@ -0,0 +1,155 @@
1
+ import { execa } from 'execa';
2
+ import { logger } from './logger.js';
3
+ import { getFfmpegPath, getFfprobePath } from './binaries.js';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import fs from 'fs';
7
+
8
+ /**
9
+ * Create a snapshot (PNG) from a video at a specific timestamp
10
+ */
11
+ export async function createSnapshot(inputVideoPath, outputSnapshotPath, snapshotTimeSeconds = 0) {
12
+ logger.debug('Creating snapshot', { inputVideoPath, outputSnapshotPath, snapshotTimeSeconds });
13
+
14
+ const ffmpegPath = await getFfmpegPath();
15
+
16
+ const command = [
17
+ '-ss', snapshotTimeSeconds,
18
+ '-i', inputVideoPath,
19
+ '-frames:v', '1',
20
+ '-vf', 'scale=640:-1:force_original_aspect_ratio=decrease:eval=frame',
21
+ '-compression_level', '6', // Use default compression (was 100, which is extremely slow)
22
+ outputSnapshotPath,
23
+ '-y',
24
+ '-hide_banner'
25
+ ];
26
+
27
+ await execa(ffmpegPath, command);
28
+ }
29
+
30
+ /**
31
+ * Create an animated GIF from a video
32
+ */
33
+ export async function createGif(inputVideoPath, outputGifPath) {
34
+ logger.debug('Creating GIF', { inputVideoPath, outputGifPath });
35
+
36
+ const ffmpegPath = await getFfmpegPath();
37
+ const ffprobePath = await getFfprobePath();
38
+
39
+ // Function to check if video is ready
40
+ const isVideoReady = async () => {
41
+ try {
42
+ // Check if file exists and is not empty
43
+ if (!fs.existsSync(inputVideoPath) || fs.statSync(inputVideoPath).size === 0) {
44
+ return false;
45
+ }
46
+
47
+ // Try to read video info with ffprobe
48
+ const { exitCode } = await execa(ffprobePath, [
49
+ '-v', 'error',
50
+ '-select_streams', 'v:0',
51
+ '-show_entries', 'stream=codec_type',
52
+ '-of', 'default=noprint_wrappers=1:nokey=1',
53
+ inputVideoPath
54
+ ], { reject: false });
55
+
56
+ return exitCode === 0;
57
+ } catch (error) {
58
+ return false;
59
+ }
60
+ };
61
+
62
+ // Wait for up to 5 seconds for the video to be ready
63
+ for (let i = 0; i < 10; i++) {
64
+ if (await isVideoReady()) {
65
+ break;
66
+ }
67
+ await new Promise(resolve => setTimeout(resolve, 500));
68
+ }
69
+
70
+ // Final check
71
+ if (!await isVideoReady()) {
72
+ throw new Error('Video file is not ready or is corrupted');
73
+ }
74
+
75
+ const gifFps = 4;
76
+ const gifDuration = 10;
77
+ const gifFrames = Math.ceil(gifDuration * gifFps);
78
+
79
+ // Get video duration in seconds
80
+ const { stdout } = await execa(ffprobePath, [
81
+ '-v', 'error',
82
+ '-show_entries', 'format=duration',
83
+ '-of', 'default=noprint_wrappers=1:nokey=1',
84
+ inputVideoPath
85
+ ]);
86
+
87
+ const videoDuration = parseFloat(stdout);
88
+ const id = (Math.random() + 1).toString(36).substring(7);
89
+
90
+ // Handle NaN or invalid duration
91
+ if (!videoDuration || isNaN(videoDuration) || videoDuration <= 0) {
92
+ logger.warn('Video duration unavailable or invalid, using default sampling for GIF', {
93
+ duration: videoDuration,
94
+ stdout
95
+ });
96
+
97
+ // Fallback: Just sample the video at a fixed rate (e.g., 1 frame every 3 seconds)
98
+ const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
99
+ await execa(ffmpegPath, [
100
+ '-i', inputVideoPath,
101
+ '-vf', `fps=1/3`, // Sample 1 frame every 3 seconds
102
+ framesPath
103
+ ]);
104
+
105
+ // Create GIF from frames
106
+ await execa(ffmpegPath, [
107
+ '-framerate', `${gifFps}`,
108
+ '-i', framesPath,
109
+ '-loop', '0',
110
+ outputGifPath,
111
+ '-y',
112
+ '-hide_banner'
113
+ ]);
114
+
115
+ // Clean up temporary frame files
116
+ const framesToDelete = fs.readdirSync(os.tmpdir())
117
+ .filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
118
+ .map(file => path.join(os.tmpdir(), file));
119
+
120
+ for (const frame of framesToDelete) {
121
+ fs.unlinkSync(frame);
122
+ }
123
+
124
+ return;
125
+ }
126
+
127
+ const extractedFramesInterval = videoDuration / gifFrames;
128
+
129
+ // Extract frames
130
+ const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
131
+ await execa(ffmpegPath, [
132
+ '-i', inputVideoPath,
133
+ '-vf', `fps=1/${extractedFramesInterval}`,
134
+ framesPath
135
+ ]);
136
+
137
+ // Create GIF from frames
138
+ await execa(ffmpegPath, [
139
+ '-framerate', `${gifFps}`,
140
+ '-i', framesPath,
141
+ '-loop', '0',
142
+ outputGifPath,
143
+ '-y',
144
+ '-hide_banner'
145
+ ]);
146
+
147
+ // Clean up temporary frame files
148
+ const framesToDelete = fs.readdirSync(os.tmpdir())
149
+ .filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
150
+ .map(file => path.join(os.tmpdir(), file));
151
+
152
+ for (const frame of framesToDelete) {
153
+ fs.unlinkSync(frame);
154
+ }
155
+ }
@@ -0,0 +1,23 @@
1
+ import { LogsTracker } from './tracking/LogsTracker.js';
2
+ import { FileTrackerManager } from './tracking/FileTrackerManager.js';
3
+
4
+ // Create a shared file tracker manager for efficient resource usage
5
+ const fileTrackerManager = new FileTrackerManager();
6
+
7
+ // Create a singleton instance for watch-only mode (no directory)
8
+ const logTracker = new LogsTracker({
9
+ config: {},
10
+ fileTrackerManager
11
+ });
12
+
13
+ // Helper function to create a new tracker for recording (with directory)
14
+ export function createRecordingTracker(directory, config = {}) {
15
+ return new LogsTracker({
16
+ config,
17
+ directory,
18
+ fileTrackerManager
19
+ });
20
+ }
21
+
22
+ // Export the singleton for backwards compatibility
23
+ export { logTracker, fileTrackerManager };
package/lib/logger.js ADDED
@@ -0,0 +1,118 @@
1
+ import winston from 'winston';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ // Constants that match the desktop app
6
+ const DD_TOKEN = process.env.DD_TOKEN || 'pubfd7949e46d22d1e71fc8fa6d95ecc5f2';
7
+ const ENV = process.env.NODE_ENV || 'production';
8
+
9
+ // Configure HTTP transport for Datadog
10
+ const httpTransportOptions = {
11
+ host: 'http-intake.logs.datadoghq.com',
12
+ path: `/api/v2/logs?dd-api-key=${DD_TOKEN}`,
13
+ ssl: true,
14
+ level: 'silly'
15
+ };
16
+
17
+ // Metadata that matches the desktop app
18
+ const httpMeta = {
19
+ ddsource: 'nodejs',
20
+ service: 'dashcam-cli',
21
+ env: ENV,
22
+ os_type: os.type(),
23
+ os_release: os.release()
24
+ };
25
+
26
+ // Verbose level configuration
27
+ let isVerbose = false;
28
+
29
+ // Create custom format to include version and user info
30
+ const updateFormat = winston.format((info) => {
31
+ info.version = process.env.npm_package_version;
32
+ return info;
33
+ });
34
+
35
+ // Custom format for console output with more detail
36
+ const verboseConsoleFormat = winston.format.combine(
37
+ winston.format.timestamp({ format: 'HH:mm:ss.SSS' }),
38
+ winston.format.colorize(),
39
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
40
+ let output = `${timestamp} [${level}] ${message}`;
41
+
42
+ // Add metadata if present and in verbose mode
43
+ if (isVerbose && Object.keys(meta).length > 0) {
44
+ // Filter out internal winston metadata
45
+ const cleanMeta = Object.fromEntries(
46
+ Object.entries(meta).filter(([key]) =>
47
+ !['timestamp', 'level', 'message', 'ddsource', 'service', 'env', 'os_type', 'os_release', 'version'].includes(key)
48
+ )
49
+ );
50
+
51
+ if (Object.keys(cleanMeta).length > 0) {
52
+ output += `\n ${JSON.stringify(cleanMeta, null, 2).split('\n').join('\n ')}`;
53
+ }
54
+ }
55
+
56
+ return output;
57
+ })
58
+ );
59
+
60
+ // Function to set verbose mode
61
+ export function setVerbose(verbose = true) {
62
+ isVerbose = verbose;
63
+ // Update console transport level based on verbose mode
64
+ const consoleTransport = logger.transports.find(t => t.constructor.name === 'Console');
65
+ if (consoleTransport) {
66
+ consoleTransport.level = verbose ? 'silly' : (ENV === 'production' ? 'info' : 'debug');
67
+ }
68
+ }
69
+
70
+ // Create the logger instance
71
+ export const logger = winston.createLogger({
72
+ format: winston.format.combine(
73
+ updateFormat(),
74
+ winston.format.timestamp(),
75
+ winston.format.json()
76
+ ),
77
+ defaultMeta: {
78
+ ...httpMeta
79
+ },
80
+ transports: [
81
+ // Log to console in development with enhanced formatting
82
+ new winston.transports.Console({
83
+ format: verboseConsoleFormat,
84
+ level: ENV === 'production' ? 'info' : 'debug'
85
+ }),
86
+ // Log to files
87
+ new winston.transports.File({
88
+ filename: path.join(os.homedir(), '.dashcam', 'logs', 'error.log'),
89
+ level: 'error'
90
+ }),
91
+ new winston.transports.File({
92
+ filename: path.join(os.homedir(), '.dashcam', 'logs', 'combined.log'),
93
+ level: 'silly' // Capture all levels in file
94
+ }),
95
+ // Log debug info to separate file when verbose
96
+ new winston.transports.File({
97
+ filename: path.join(os.homedir(), '.dashcam', 'logs', 'debug.log'),
98
+ level: 'debug'
99
+ }),
100
+ // Send to Datadog in production
101
+ ...(ENV === 'production' ? [new winston.transports.Http(httpTransportOptions)] : [])
102
+ ]
103
+ });
104
+
105
+ // Add convenience methods for common logging patterns
106
+ logger.verbose = (message, meta) => logger.debug(`[VERBOSE] ${message}`, meta);
107
+ logger.trace = (message, meta) => logger.silly(`[TRACE] ${message}`, meta);
108
+
109
+ // Function to log function entry/exit for debugging
110
+ export function logFunctionCall(functionName, args = {}) {
111
+ if (isVerbose) {
112
+ logger.debug(`→ Entering ${functionName}`, { args });
113
+ return (result) => {
114
+ logger.debug(`← Exiting ${functionName}`, { result: typeof result });
115
+ };
116
+ }
117
+ return () => {}; // No-op if not verbose
118
+ }