dashcam 0.8.4 → 1.0.1-beta.2

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 +542 -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,234 @@
1
+ import { server } from './websocket/server.js';
2
+ import { WebTrackerManager } from './extension-logs/manager.js';
3
+ import { WebLogsTracker } from './extension-logs/index.js';
4
+ import { logger, setVerbose } from './logger.js';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+
8
+ const DAEMON_DIR = path.join(process.cwd(), '.dashcam');
9
+ const DAEMON_PID_FILE = path.join(DAEMON_DIR, 'daemon.pid');
10
+ const DAEMON_CONFIG_FILE = path.join(DAEMON_DIR, 'web-config.json');
11
+
12
+ // Ensure daemon directory exists
13
+ if (!fs.existsSync(DAEMON_DIR)) {
14
+ fs.mkdirSync(DAEMON_DIR, { recursive: true });
15
+ }
16
+
17
+ class WebLogsDaemon {
18
+ constructor() {
19
+ this.webTrackerManager = null;
20
+ this.webWatchTracker = null;
21
+ this.isRunning = false;
22
+ }
23
+
24
+ async start() {
25
+ if (this.isRunning) {
26
+ logger.debug('Web logs daemon already running, skipping start');
27
+ return;
28
+ }
29
+
30
+ // Enable verbose logging for daemon
31
+ setVerbose(true);
32
+ logger.info('Starting web logs daemon...');
33
+ try {
34
+ // Start WebSocket server
35
+ logger.debug('Initializing WebSocket server...');
36
+ await server.start();
37
+ logger.info(`WebSocket server started on port ${server.port}`);
38
+
39
+ // Create web tracker manager
40
+ logger.debug('Creating WebTrackerManager...');
41
+ this.webTrackerManager = new WebTrackerManager(server);
42
+
43
+ // Load existing config if available
44
+ logger.debug('Loading daemon configuration...');
45
+ const config = this.loadConfig();
46
+ logger.debug('Loaded config:', { configKeys: Object.keys(config), configCount: Object.keys(config).length });
47
+
48
+ this.webWatchTracker = new WebLogsTracker({
49
+ config,
50
+ webTrackerManager: this.webTrackerManager
51
+ });
52
+
53
+ this.isRunning = true;
54
+
55
+ // Write PID file
56
+ logger.debug('Writing daemon PID file...');
57
+ fs.writeFileSync(DAEMON_PID_FILE, process.pid.toString());
58
+
59
+ logger.info('Web logs daemon started successfully');
60
+
61
+ // Keep process alive
62
+ process.on('SIGTERM', () => {
63
+ logger.info('Received SIGTERM, stopping daemon...');
64
+ this.stop();
65
+ });
66
+ process.on('SIGINT', () => {
67
+ logger.info('Received SIGINT, stopping daemon...');
68
+ this.stop();
69
+ });
70
+
71
+ } catch (error) {
72
+ logger.error('Failed to start web logs daemon', { error: error.message, stack: error.stack });
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ stop() {
78
+ if (!this.isRunning) {
79
+ logger.debug('Web logs daemon not running, skipping stop');
80
+ return;
81
+ }
82
+
83
+ logger.info('Stopping web logs daemon...');
84
+
85
+ if (this.webWatchTracker) {
86
+ logger.debug('Destroying web watch tracker...');
87
+ this.webWatchTracker.destroy();
88
+ }
89
+
90
+ if (this.webTrackerManager) {
91
+ logger.debug('Destroying web tracker manager...');
92
+ this.webTrackerManager.destroy();
93
+ }
94
+
95
+ logger.debug('Stopping WebSocket server...');
96
+ server.stop();
97
+
98
+ // Remove PID file
99
+ if (fs.existsSync(DAEMON_PID_FILE)) {
100
+ logger.debug('Removing daemon PID file...');
101
+ fs.unlinkSync(DAEMON_PID_FILE);
102
+ }
103
+
104
+ this.isRunning = false;
105
+ logger.info('Web logs daemon stopped');
106
+ process.exit(0);
107
+ }
108
+
109
+ updateConfig(config) {
110
+ logger.debug('Updating daemon config...', { configKeys: Object.keys(config) });
111
+ if (this.webWatchTracker) {
112
+ this.webWatchTracker.updateConfig(config);
113
+ }
114
+ this.saveConfig(config);
115
+ logger.info('Daemon config updated successfully');
116
+ }
117
+
118
+ loadConfig() {
119
+ try {
120
+ if (fs.existsSync(DAEMON_CONFIG_FILE)) {
121
+ logger.debug('Loading daemon config from file...', { configFile: DAEMON_CONFIG_FILE });
122
+ const data = fs.readFileSync(DAEMON_CONFIG_FILE, 'utf8');
123
+ const config = JSON.parse(data);
124
+ logger.debug('Daemon config loaded successfully', { configKeys: Object.keys(config) });
125
+ return config;
126
+ } else {
127
+ logger.debug('No daemon config file found, using empty config');
128
+ }
129
+ } catch (error) {
130
+ logger.error('Failed to load daemon config', { error: error.message, configFile: DAEMON_CONFIG_FILE });
131
+ }
132
+ return {};
133
+ }
134
+
135
+ saveConfig(config) {
136
+ try {
137
+ logger.debug('Saving daemon config to file...', { configFile: DAEMON_CONFIG_FILE, configKeys: Object.keys(config) });
138
+ fs.writeFileSync(DAEMON_CONFIG_FILE, JSON.stringify(config, null, 2));
139
+ logger.debug('Daemon config saved successfully');
140
+ } catch (error) {
141
+ logger.error('Failed to save daemon config', { error: error.message, configFile: DAEMON_CONFIG_FILE });
142
+ }
143
+ }
144
+
145
+ static isDaemonRunning() {
146
+ try {
147
+ if (!fs.existsSync(DAEMON_PID_FILE)) {
148
+ logger.debug('Daemon PID file does not exist, daemon not running');
149
+ return false;
150
+ }
151
+
152
+ const pid = parseInt(fs.readFileSync(DAEMON_PID_FILE, 'utf8').trim());
153
+ if (isNaN(pid)) {
154
+ logger.warn('Invalid PID in daemon PID file', { pidFile: DAEMON_PID_FILE });
155
+ return false;
156
+ }
157
+
158
+ logger.debug('Checking if daemon process is still running...', { pid });
159
+ // Check if process is still running
160
+ process.kill(pid, 0);
161
+ logger.debug('Daemon process is running', { pid });
162
+ return true;
163
+ } catch (error) {
164
+ logger.debug('Daemon process not running or not accessible', { error: error.message });
165
+ return false;
166
+ }
167
+ }
168
+
169
+ static async ensureDaemonRunning() {
170
+ if (!WebLogsDaemon.isDaemonRunning()) {
171
+ logger.info('Web logs daemon not running, starting it...');
172
+
173
+ // Spawn daemon process
174
+ const { spawn } = await import('child_process');
175
+ const child = spawn('node', [
176
+ path.join(process.cwd(), 'bin/dashcam.js'),
177
+ '_internal_daemon'
178
+ ], {
179
+ detached: true,
180
+ stdio: 'inherit' // Changed from 'ignore' to 'inherit' for debugging
181
+ });
182
+
183
+ logger.debug('Spawned daemon process', { pid: child.pid });
184
+ child.unref();
185
+
186
+ // Wait a moment for daemon to start
187
+ logger.debug('Waiting for daemon to start...');
188
+ await new Promise(resolve => setTimeout(resolve, 1000));
189
+
190
+ if (!WebLogsDaemon.isDaemonRunning()) {
191
+ logger.error('Failed to start web logs daemon after spawn attempt');
192
+ throw new Error('Failed to start web logs daemon');
193
+ } else {
194
+ logger.info('Web logs daemon started successfully');
195
+ }
196
+ } else {
197
+ logger.debug('Web logs daemon already running');
198
+ }
199
+ }
200
+
201
+ static stopDaemon() {
202
+ try {
203
+ if (!fs.existsSync(DAEMON_PID_FILE)) {
204
+ logger.debug('No daemon PID file found, daemon not running');
205
+ return false;
206
+ }
207
+
208
+ const pid = parseInt(fs.readFileSync(DAEMON_PID_FILE, 'utf8').trim());
209
+ if (isNaN(pid)) {
210
+ logger.warn('Invalid PID in daemon PID file');
211
+ return false;
212
+ }
213
+
214
+ logger.info('Stopping daemon process...', { pid });
215
+ process.kill(pid, 'SIGTERM');
216
+
217
+ // Wait for cleanup
218
+ setTimeout(() => {
219
+ if (fs.existsSync(DAEMON_PID_FILE)) {
220
+ logger.debug('Removing daemon PID file after cleanup timeout');
221
+ fs.unlinkSync(DAEMON_PID_FILE);
222
+ }
223
+ }, 1000);
224
+
225
+ logger.info('Daemon stop signal sent');
226
+ return true;
227
+ } catch (error) {
228
+ logger.error('Failed to stop daemon', { error: error.message });
229
+ return false;
230
+ }
231
+ }
232
+ }
233
+
234
+ export { WebLogsDaemon, DAEMON_CONFIG_FILE };
@@ -0,0 +1,223 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { logger } from '../logger.js';
3
+
4
+ // Simple ref implementation for reactivity (adapted from Vue's ref)
5
+ function ref(value) {
6
+ return {
7
+ value,
8
+ _isRef: true
9
+ };
10
+ }
11
+
12
+ class WSServer {
13
+ #socket = null;
14
+ #handlers = {};
15
+
16
+ constructor(ports) {
17
+ this.ports = ports;
18
+ this.port = null;
19
+ this.isListening = ref(false);
20
+ }
21
+
22
+ on(event, handler) {
23
+ if (!this.#handlers[event]) this.#handlers[event] = [];
24
+ if (this.#handlers[event].find((cb) => cb === handler))
25
+ throw new Error('Handler already registered');
26
+ this.#handlers[event].push(handler);
27
+ return () => {
28
+ this.#handlers[event] = this.#handlers[event].filter((cb) => cb !== handler);
29
+ };
30
+ }
31
+
32
+ emit(event, payload) {
33
+ if (!this.#handlers[event]) return;
34
+ for (const cb of this.#handlers[event]) {
35
+ try {
36
+ cb(payload);
37
+ } catch (err) {
38
+ logger.error(
39
+ 'Failed calling handler for event ' +
40
+ event +
41
+ ' with error ' +
42
+ err.message
43
+ );
44
+ }
45
+ }
46
+ }
47
+
48
+ async start() {
49
+ if (this.#socket) {
50
+ logger.error('The websocket server is already started');
51
+ return;
52
+ }
53
+
54
+ logger.debug('WebSocketServer: Starting server, trying ports...', { ports: this.ports });
55
+ for (const port of this.ports) {
56
+ const ws = await new Promise((resolve) => {
57
+ logger.debug('WebSocketServer: Trying port ' + port);
58
+ const ws = new WebSocketServer({ port, host: '127.0.0.1' });
59
+ ws.on('error', () => {
60
+ logger.debug('WebSocketServer: Failed to listen on port ' + port);
61
+ resolve();
62
+ });
63
+ ws.on('listening', () => {
64
+ logger.debug('WebSocketServer: Successfully listening on port ' + port);
65
+ resolve(ws);
66
+ });
67
+ });
68
+
69
+ if (ws) return this.#setup(ws, port);
70
+ }
71
+ throw new Error('Unable to listen to any of the provided ports');
72
+ }
73
+
74
+ #setup(socket, port) {
75
+ logger.debug('WebSocketServer: Setting up server on port', { port });
76
+
77
+ const states = {
78
+ NEW: 'NEW',
79
+ SENT_HEADERS: 'SENT_HEADERS',
80
+ CONNECTED: 'CONNECTED',
81
+ };
82
+
83
+ this.port = port;
84
+ this.#socket = socket;
85
+
86
+ this.#socket.on('close', (arg) => {
87
+ logger.debug('WebSocketServer: Server closed', { arg });
88
+ this.#socket = null;
89
+ this.isListening.value = false;
90
+ this.emit('close', arg);
91
+ });
92
+ this.#socket.on('error', (arg) => {
93
+ logger.error('WebSocketServer: Server error', { error: arg });
94
+ this.#socket = null;
95
+ this.isListening.value = false;
96
+ this.emit('error', arg);
97
+ });
98
+
99
+ this.#socket.on('connection', (client) => {
100
+ logger.info('WebSocketServer: New client connection established');
101
+
102
+ let state = states.NEW;
103
+ const failValidation = () => {
104
+ logger.warn('WebSocketServer: Client validation failed, closing connection');
105
+ client.send(
106
+ JSON.stringify({ error: 'Unidentified client, closing socket' })
107
+ );
108
+ client.close();
109
+ };
110
+
111
+ const timeout = setTimeout(() => {
112
+ if (state === states.CONNECTED) return;
113
+ logger.warn('WebSocketServer: Client validation timeout, closing connection');
114
+ failValidation();
115
+ clearTimeout(timeout);
116
+ }, 10000);
117
+
118
+ client.on('message', (data, isBinary) => {
119
+ let message = isBinary ? data : data.toString();
120
+ logger.debug('WebSocketServer: Received message from client', {
121
+ isBinary,
122
+ messageLength: message.length,
123
+ state
124
+ });
125
+
126
+ try {
127
+ message = JSON.parse(message);
128
+ logger.debug('WebSocketServer: Parsed message', { type: message.type, hasPayload: !!message.payload });
129
+ } catch (err) {
130
+ logger.debug('WebSocketServer: Message is not JSON, treating as raw string');
131
+ }
132
+
133
+ if (state === states.SENT_HEADERS) {
134
+ if (message === 'dashcam_extension_socket_confirm') {
135
+ state = states.CONNECTED;
136
+ logger.info('WebSocketServer: Client successfully validated and connected');
137
+ this.emit('connection', client);
138
+ return;
139
+ }
140
+ logger.warn('WebSocketServer: Invalid confirmation message from client');
141
+ failValidation();
142
+ clearTimeout(timeout);
143
+ }
144
+
145
+ this.emit('message', message, client);
146
+ });
147
+
148
+ logger.debug('WebSocketServer: Sending connection header to client');
149
+ client.send('dashcam_desktop_socket_connected', (err) => {
150
+ if (err) {
151
+ logger.error('WebSocketServer: Failed to send connection header', { error: err.message });
152
+ client.close();
153
+ clearTimeout(timeout);
154
+ } else {
155
+ logger.debug('WebSocketServer: Connection header sent, waiting for confirmation');
156
+ state = states.SENT_HEADERS;
157
+ }
158
+ });
159
+ });
160
+
161
+ this.isListening.value = true;
162
+ logger.info('WebSocketServer: Setup complete, server is listening', { port });
163
+ this.emit('listening', port);
164
+ }
165
+
166
+ broadcast(message) {
167
+ if (!this.#socket) {
168
+ logger.error('WebSocketServer: Cannot broadcast, server not currently running');
169
+ throw new Error('Server not currently running');
170
+ }
171
+
172
+ logger.debug('WebSocketServer: Broadcasting message to all clients', {
173
+ clientCount: this.#socket.clients.size,
174
+ messageType: message.type || 'raw'
175
+ });
176
+
177
+ this.#socket.clients.forEach((client) => {
178
+ try {
179
+ this.send(client, message);
180
+ } catch (err) {
181
+ logger.error('WebSocketServer: Failed to send message to client', { error: err.message });
182
+ }
183
+ });
184
+ }
185
+
186
+ send(client, message) {
187
+ if (!this.#socket) {
188
+ logger.error('WebSocketServer: Cannot send, server not currently running');
189
+ throw new Error('Server not currently running');
190
+ }
191
+
192
+ logger.debug('WebSocketServer: Sending message to client', {
193
+ messageType: message.type || 'raw',
194
+ messageLength: JSON.stringify(message).length
195
+ });
196
+
197
+ client.send(
198
+ typeof message === 'string' ? message : JSON.stringify(message)
199
+ );
200
+ }
201
+
202
+ async stop() {
203
+ if (this.#socket) {
204
+ logger.debug('WebSocketServer: Stopping server...');
205
+ this.#socket.close();
206
+ this.#socket = null;
207
+ this.isListening.value = false;
208
+ logger.info('WebSocketServer: Server stopped');
209
+ } else {
210
+ logger.debug('WebSocketServer: Server already stopped');
211
+ }
212
+ }
213
+ }
214
+
215
+ // This list of ports is randomly generated of ports between 10000 and 65000
216
+ // This exact same list of ports needs to be used on the desktop app's websocket server
217
+ const server = new WSServer([
218
+ 10368, 16240, 21855, 24301, 25928,
219
+ // 27074, 31899, 34205, 36109, 37479, 38986,
220
+ // 39618, 41890, 47096, 48736, 49893, 53659, 55927, 56001, 62895,
221
+ ]);
222
+
223
+ export { server, ref };
package/package.json CHANGED
@@ -1,33 +1,62 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "0.8.4",
4
- "description": "Fix bugs, close pulls, and update your team with desktop instant replay.",
5
- "main": "index.js",
3
+ "version": "1.0.1-beta.2",
4
+ "description": "Minimal CLI version of Dashcam desktop app",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "dashcam": "./bin/dashcam.js"
8
+ },
6
9
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
8
- "prepare": "husky install"
10
+ "start": "node bin/dashcam.js",
11
+ "prebuild": "npm run bundle",
12
+ "bundle": "esbuild bin/dashcam.js --bundle --platform=node --target=node20 --format=cjs --outfile=dist/bundle.cjs --external:@mapbox/node-pre-gyp --external:file-icon --external:get-windows --banner:js=\"const importMetaUrl = typeof document === 'undefined' ? require('url').pathToFileURL(__filename).href : document.currentScript && document.currentScript.src || new URL('index.js', document.baseURI).href;\" --define:import.meta.url='importMetaUrl'",
13
+ "build": "pkg dist/bundle.cjs --out-path dist --compress GZip --public",
14
+ "build:macos": "pkg dist/bundle.cjs --targets node20-macos-x64 --output dist/dashcam-macos-x64 --compress GZip --public && pkg dist/bundle.cjs --targets node20-macos-arm64 --output dist/dashcam-macos-arm64 --compress GZip --public",
15
+ "build:linux": "pkg dist/bundle.cjs --targets node20-linux-x64 --output dist/dashcam-linux-x64 --compress GZip --public && pkg dist/bundle.cjs --targets node20-linux-arm64 --output dist/dashcam-linux-arm64 --compress GZip --public",
16
+ "build:windows": "pkg dist/bundle.cjs --targets node20-win-x64 --output dist/dashcam-windows-x64.exe --compress GZip --public && pkg dist/bundle.cjs --targets node20-win-arm64 --output dist/dashcam-windows-arm64.exe --compress GZip --public",
17
+ "build:all": "npm run build:macos && npm run build:linux && npm run build:windows"
9
18
  },
10
- "bin": {
11
- "dashcam": "index.js"
19
+ "pkg": {
20
+ "scripts": "dist/bundle.cjs",
21
+ "targets": [
22
+ "node20-macos-x64",
23
+ "node20-macos-arm64",
24
+ "node20-linux-x64",
25
+ "node20-linux-arm64",
26
+ "node20-win-x64",
27
+ "node20-win-arm64"
28
+ ],
29
+ "outputPath": "dist",
30
+ "ignore": [
31
+ "node_modules/get-windows/**",
32
+ "node_modules/file-icon/**"
33
+ ]
12
34
  },
13
- "author": "",
14
- "license": "ISC",
15
35
  "dependencies": {
16
- "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0",
17
- "cli-color": "^2.0.3",
18
- "commander": "^9.4.0",
19
- "find-process": "^1.4.7",
20
- "husky": "^7.0.4",
21
- "node-ipc": "^10.1.0",
22
- "yargs": "^17.3.1"
36
+ "@aws-sdk/client-s3": "^3.919.0",
37
+ "@aws-sdk/client-sts": "^3.919.0",
38
+ "@aws-sdk/lib-storage": "^3.919.0",
39
+ "auth0": "^4.0.0",
40
+ "commander": "^11.0.0",
41
+ "execa": "^9.6.0",
42
+ "ffmpeg-static": "^5.2.0",
43
+ "ffprobe-static": "^3.1.0",
44
+ "file-icon": "^6.0.0",
45
+ "get-windows": "^9.2.3",
46
+ "got": "^11.8.6",
47
+ "mask-sensitive-data": "^0.11.5",
48
+ "node-fetch": "^2.7.0",
49
+ "open": "^9.1.0",
50
+ "tail": "^2.2.6",
51
+ "winston": "^3.11.0",
52
+ "ws": "^8.18.3"
23
53
  },
24
54
  "devDependencies": {
25
- "husky": "^7.0.0"
55
+ "@yao-pkg/pkg": "^6.10.1",
56
+ "esbuild": "^0.19.0"
26
57
  },
58
+ "type": "module",
27
59
  "engines": {
28
- "node": ">=22.19.0"
29
- },
30
- "volta": {
31
- "node": "22.19.0"
60
+ "node": ">=20.0.0"
32
61
  }
33
62
  }