dashcam 1.0.1-beta.6 → 1.0.1-beta.7

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,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Background recording process for dashcam CLI
4
+ * This script runs detached from the parent process to handle long-running recordings
5
+ */
6
+
7
+ import { startRecording, stopRecording } from '../lib/recorder.js';
8
+ import { upload } from '../lib/uploader.js';
9
+ import { logger, setVerbose } from '../lib/logger.js';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+
14
+ // Get process directory for status files
15
+ const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
16
+ const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
17
+ const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
18
+
19
+ // Parse options from command line argument
20
+ const optionsJson = process.argv[2];
21
+ if (!optionsJson) {
22
+ console.error('No options provided to background process');
23
+ process.exit(1);
24
+ }
25
+
26
+ const options = JSON.parse(optionsJson);
27
+
28
+ // Enable verbose logging in background
29
+ setVerbose(true);
30
+
31
+ logger.info('Background recording process started', {
32
+ pid: process.pid,
33
+ options
34
+ });
35
+
36
+ // Write status file
37
+ function writeStatus(status) {
38
+ try {
39
+ fs.writeFileSync(STATUS_FILE, JSON.stringify({
40
+ ...status,
41
+ timestamp: Date.now(),
42
+ pid: process.pid
43
+ }, null, 2));
44
+ } catch (error) {
45
+ logger.error('Failed to write status file', { error });
46
+ }
47
+ }
48
+
49
+ // Write upload result file
50
+ function writeUploadResult(result) {
51
+ try {
52
+ logger.info('Writing upload result to file', { path: RESULT_FILE, shareLink: result.shareLink });
53
+ fs.writeFileSync(RESULT_FILE, JSON.stringify({
54
+ ...result,
55
+ timestamp: Date.now()
56
+ }, null, 2));
57
+ logger.info('Successfully wrote upload result to file');
58
+ } catch (error) {
59
+ logger.error('Failed to write upload result file', { error });
60
+ }
61
+ }
62
+
63
+ // Main recording function
64
+ async function runBackgroundRecording() {
65
+ let recordingResult = null;
66
+ let isShuttingDown = false;
67
+
68
+ try {
69
+ // Start the recording
70
+ const recordingOptions = {
71
+ fps: parseInt(options.fps) || 10,
72
+ includeAudio: options.audio || false,
73
+ customOutputPath: options.output || null
74
+ };
75
+
76
+ logger.info('Starting recording with options', { recordingOptions });
77
+
78
+ recordingResult = await startRecording(recordingOptions);
79
+
80
+ // Write status to track the recording
81
+ writeStatus({
82
+ isRecording: true,
83
+ startTime: recordingResult.startTime,
84
+ options,
85
+ pid: process.pid,
86
+ outputPath: recordingResult.outputPath
87
+ });
88
+
89
+ logger.info('Recording started successfully', {
90
+ outputPath: recordingResult.outputPath,
91
+ startTime: recordingResult.startTime
92
+ });
93
+
94
+ // Set up signal handlers for graceful shutdown
95
+ const handleShutdown = async (signal) => {
96
+ if (isShuttingDown) {
97
+ logger.info('Shutdown already in progress...');
98
+ return;
99
+ }
100
+ isShuttingDown = true;
101
+
102
+ logger.info(`Received ${signal}, stopping background recording...`);
103
+
104
+ try {
105
+ // Stop the recording
106
+ const stopResult = await stopRecording();
107
+
108
+ if (stopResult) {
109
+ logger.info('Recording stopped successfully', {
110
+ outputPath: stopResult.outputPath,
111
+ duration: stopResult.duration
112
+ });
113
+
114
+ // Upload the recording
115
+ logger.info('Starting upload...');
116
+ const uploadResult = await upload(stopResult.outputPath, {
117
+ title: options.title || 'Dashcam Recording',
118
+ description: options.description || 'Recorded with Dashcam CLI',
119
+ project: options.project || options.k,
120
+ duration: stopResult.duration,
121
+ clientStartDate: stopResult.clientStartDate,
122
+ apps: stopResult.apps,
123
+ logs: stopResult.logs,
124
+ gifPath: stopResult.gifPath,
125
+ snapshotPath: stopResult.snapshotPath
126
+ });
127
+
128
+ logger.info('Upload complete', { shareLink: uploadResult.shareLink });
129
+
130
+ // Write upload result for stop command to read
131
+ writeUploadResult({
132
+ shareLink: uploadResult.shareLink,
133
+ replayId: uploadResult.replay?.id
134
+ });
135
+ }
136
+
137
+ // Update status to indicate recording stopped
138
+ writeStatus({
139
+ isRecording: false,
140
+ completedTime: Date.now(),
141
+ pid: process.pid
142
+ });
143
+
144
+ logger.info('Background process exiting successfully');
145
+ process.exit(0);
146
+ } catch (error) {
147
+ logger.error('Error during shutdown:', error);
148
+ process.exit(1);
149
+ }
150
+ };
151
+
152
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
153
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
154
+
155
+ // Keep the process alive
156
+ logger.info('Background recording is now running. Waiting for stop signal...');
157
+ await new Promise(() => {}); // Wait indefinitely for signals
158
+
159
+ } catch (error) {
160
+ logger.error('Background recording failed:', error);
161
+
162
+ // Update status to indicate failure
163
+ writeStatus({
164
+ isRecording: false,
165
+ error: error.message,
166
+ pid: process.pid
167
+ });
168
+
169
+ process.exit(1);
170
+ }
171
+ }
172
+
173
+ // Run the background recording
174
+ runBackgroundRecording().catch(error => {
175
+ logger.error('Fatal error in background process:', error);
176
+ process.exit(1);
177
+ });
package/bin/dashcam.js CHANGED
@@ -117,8 +117,8 @@ async function recordingAction(options, command) {
117
117
  process.exit(1);
118
118
  }
119
119
 
120
- // Always use background mode
121
- log('Starting recording...');
120
+ // Start recording in background mode
121
+ log('Starting recording in background...');
122
122
 
123
123
  try {
124
124
  const result = await processManager.startRecording({
@@ -130,81 +130,13 @@ async function recordingAction(options, command) {
130
130
  project: options.project || options.k // Support both -p and -k for project
131
131
  });
132
132
 
133
- log(`Recording started successfully (PID: ${result.pid})`);
133
+ log(`✅ Recording started successfully (PID: ${result.pid})`);
134
134
  log(`Output: ${result.outputPath}`);
135
+ log('');
135
136
  log('Use "dashcam status" to check progress');
136
137
  log('Use "dashcam stop" to stop recording and upload');
137
138
 
138
- // Keep this process alive for background recording
139
- log('Recording is running in background...');
140
-
141
- // Set up signal handlers for graceful shutdown
142
- let isShuttingDown = false;
143
- const handleShutdown = async (signal) => {
144
- if (isShuttingDown) {
145
- log('Shutdown already in progress...');
146
- return;
147
- }
148
- isShuttingDown = true;
149
-
150
- log(`\nReceived ${signal}, stopping background recording...`);
151
- try {
152
- // Stop the recording using the recorder directly (not processManager)
153
- const { stopRecording } = await import('../lib/recorder.js');
154
- const stopResult = await stopRecording();
155
-
156
- if (stopResult) {
157
- log('Recording stopped:', stopResult.outputPath);
158
-
159
- // Import and call upload function with the correct format
160
- const { upload } = await import('../lib/uploader.js');
161
-
162
- log('Starting upload...');
163
- const uploadResult = await upload(stopResult.outputPath, {
164
- title: options.title || 'Dashcam Recording',
165
- description: description || 'Recorded with Dashcam CLI',
166
- project: options.project || options.k,
167
- duration: stopResult.duration,
168
- clientStartDate: stopResult.clientStartDate,
169
- apps: stopResult.apps,
170
- logs: stopResult.logs,
171
- gifPath: stopResult.gifPath,
172
- snapshotPath: stopResult.snapshotPath
173
- });
174
-
175
- // Write upload result for stop command to read
176
- processManager.writeUploadResult({
177
- shareLink: uploadResult.shareLink,
178
- replayId: uploadResult.replay?.id
179
- });
180
-
181
- // Output based on format option (for create/markdown mode)
182
- if (options.md) {
183
- const replayId = uploadResult.replay?.id;
184
- const shareKey = uploadResult.shareLink.split('share=')[1];
185
- log(`[![Dashcam - ${options.title || 'New Replay'}](https://replayable-api-production.herokuapp.com/replay/${replayId}/gif?shareKey=${shareKey})](${uploadResult.shareLink})`);
186
- log('');
187
- log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
188
- } else {
189
- log('✅ Upload complete!');
190
- log('📹 Watch your recording:', uploadResult.shareLink);
191
- }
192
- }
193
-
194
- // Clean up process files, but preserve upload result for stop command
195
- processManager.cleanup({ preserveResult: true });
196
- } catch (error) {
197
- logError('Error during shutdown:', error.message);
198
- logger.error('Error during shutdown:', error);
199
- }
200
- process.exit(0);
201
- };
202
-
203
- process.on('SIGINT', () => handleShutdown('SIGINT'));
204
- process.on('SIGTERM', () => handleShutdown('SIGTERM'));
205
-
206
- // Keep the process alive
207
- await new Promise(() => {});
139
+ process.exit(0);
208
140
  } catch (error) {
209
141
  logError('Failed to start recording:', error.message);
210
142
  process.exit(1);
@@ -2,8 +2,12 @@ import { spawn } from 'child_process';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
+ import { fileURLToPath } from 'url';
5
6
  import { logger } from './logger.js';
6
7
 
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
7
11
  // Use a fixed directory in the user's home directory for cross-process communication
8
12
  const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
9
13
  const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
@@ -222,44 +226,68 @@ class ProcessManager {
222
226
  throw new Error('Recording already in progress');
223
227
  }
224
228
 
225
- try {
226
- // Import and call the recording function directly
227
- const { startRecording } = await import('./recorder.js');
228
-
229
- const recordingOptions = {
230
- fps: parseInt(options.fps) || 10,
231
- includeAudio: options.audio || false,
232
- customOutputPath: options.output || null
233
- };
229
+ // Always run in background mode by spawning a detached process
230
+ logger.info('Starting recording in background mode');
231
+
232
+ // Get the path to the CLI binary
233
+ const binPath = path.join(__dirname, '..', 'bin', 'dashcam-background.js');
234
+
235
+ logger.debug('Background process path', { binPath, exists: fs.existsSync(binPath) });
236
+
237
+ // Create log files for background process
238
+ const logDir = PROCESS_DIR;
239
+ const stdoutLog = path.join(logDir, 'background-stdout.log');
240
+ const stderrLog = path.join(logDir, 'background-stderr.log');
241
+
242
+ const stdoutFd = fs.openSync(stdoutLog, 'a');
243
+ const stderrFd = fs.openSync(stderrLog, 'a');
244
+
245
+ // Spawn a detached process that will handle the recording
246
+ const backgroundProcess = spawn(process.execPath, [
247
+ binPath,
248
+ JSON.stringify(options)
249
+ ], {
250
+ detached: true,
251
+ stdio: ['ignore', stdoutFd, stderrFd], // Log stdout and stderr
252
+ env: {
253
+ ...process.env,
254
+ DASHCAM_BACKGROUND: 'true'
255
+ }
256
+ });
257
+
258
+ // Close the file descriptors in the parent process
259
+ fs.closeSync(stdoutFd);
260
+ fs.closeSync(stderrFd);
234
261
 
235
- logger.info('Starting recording directly', { options: recordingOptions });
262
+ // Get the background process PID before unreffing
263
+ const backgroundPid = backgroundProcess.pid;
264
+
265
+ // Allow the parent process to exit independently
266
+ backgroundProcess.unref();
236
267
 
237
- const result = await startRecording(recordingOptions);
268
+ // Wait a moment for the background process to initialize
269
+ await new Promise(resolve => setTimeout(resolve, 2000));
238
270
 
239
- // Write status to track the recording
240
- this.writePid(process.pid);
241
- this.writeStatus({
242
- isRecording: true,
243
- startTime: result.startTime, // Use actual recording start time from recorder
244
- options,
245
- pid: process.pid,
246
- outputPath: result.outputPath
247
- });
271
+ // Read the status file to get recording details
272
+ const status = this.readStatus();
273
+
274
+ if (!status || !status.isRecording) {
275
+ throw new Error('Background process failed to start recording');
276
+ }
248
277
 
249
- logger.info('Recording started successfully', {
250
- outputPath: result.outputPath,
251
- startTime: result.startTime
252
- });
278
+ // Write PID file so other commands can find the background process
279
+ this.writePid(status.pid);
253
280
 
254
- return {
255
- pid: process.pid,
256
- outputPath: result.outputPath,
257
- startTime: result.startTime
258
- };
259
- } catch (error) {
260
- logger.error('Failed to start recording', { error });
261
- throw error;
262
- }
281
+ logger.info('Background recording process started', {
282
+ pid: status.pid,
283
+ outputPath: status.outputPath
284
+ });
285
+
286
+ return {
287
+ pid: status.pid,
288
+ outputPath: status.outputPath,
289
+ startTime: status.startTime
290
+ };
263
291
  }
264
292
 
265
293
  async gracefulExit() {
package/lib/uploader.js CHANGED
@@ -445,8 +445,7 @@ export async function upload(filePath, metadata = {}) {
445
445
 
446
446
  logExit();
447
447
 
448
- // Replace app.dashcam.io with app.testdriver.ai in the share link
449
- const shareLink = newReplay.replay.shareLink?.replace('app.dashcam.io', 'app.testdriver.ai') || newReplay.replay.shareLink;
448
+ const shareLink = newReplay.replay.shareLink;
450
449
 
451
450
  return {
452
451
  replay: newReplay.replay,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.0.1-beta.6",
3
+ "version": "1.0.1-beta.7",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/index.js",
6
6
  "bin": {