dashcam 1.1.0-beta.5 → 1.3.1-beta

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.
@@ -8,7 +8,7 @@ on:
8
8
  jobs:
9
9
  publish-beta:
10
10
  runs-on: ubuntu-latest
11
-
11
+
12
12
  steps:
13
13
  - uses: actions/checkout@v4
14
14
  with:
@@ -25,39 +25,25 @@ jobs:
25
25
  git config --global user.name "github-actions[bot]"
26
26
  git config --global user.email "github-actions[bot]@users.noreply.github.com"
27
27
 
28
- - name: Check if this is a version bump commit
29
- id: check_commit
30
- run: |
31
- if echo "${{ github.event.head_commit.message }}" | grep -q "chore: bump beta version"; then
32
- echo "skip=true" >> $GITHUB_OUTPUT
33
- else
34
- echo "skip=false" >> $GITHUB_OUTPUT
35
- fi
36
-
37
28
  - name: Install dependencies
38
- if: steps.check_commit.outputs.skip != 'true'
39
29
  run: npm install
40
30
 
41
31
  - name: Commit dependency changes
42
- if: steps.check_commit.outputs.skip != 'true'
43
32
  run: |
44
33
  git add package-lock.json
45
34
  git diff --staged --quiet || git commit -m "chore: update package-lock.json"
46
35
 
47
36
  - name: Bump version (prerelease beta)
48
- if: steps.check_commit.outputs.skip != 'true'
49
37
  run: |
50
38
  npm version prerelease --preid=beta -m "chore: bump beta version to %s"
51
39
  env:
52
40
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53
41
 
54
42
  - name: Publish to npm under beta tag
55
- if: steps.check_commit.outputs.skip != 'true'
56
43
  run: npm publish --tag beta
57
44
  env:
58
45
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
59
46
 
60
47
  - name: Push version bump commit + tag
61
- if: steps.check_commit.outputs.skip != 'true'
62
48
  run: |
63
49
  git push --follow-tags
@@ -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
@@ -5,7 +5,7 @@ import { upload } from '../lib/uploader.js';
5
5
  import { logger, setVerbose } from '../lib/logger.js';
6
6
  import { APP } from '../lib/config.js';
7
7
  import { createPattern } from '../lib/tracking.js';
8
- import { startRecording, stopRecording, getRecordingStatus } from '../lib/recorder.js';
8
+ import { processManager } from '../lib/processManager.js';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { dirname } from 'path';
11
11
  import path from 'path';
@@ -83,13 +83,14 @@ async function recordingAction(options, command) {
83
83
  const logError = (...args) => { if (!silent) console.error(...args); };
84
84
 
85
85
  // Check if recording is already active
86
- const status = getRecordingStatus();
87
- if (status.isRecording) {
88
- const duration = (status.duration / 1000).toFixed(1);
86
+ if (processManager.isRecordingActive()) {
87
+ const status = processManager.getActiveStatus();
88
+ const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
89
89
  log('Recording already in progress');
90
90
  log(`Duration: ${duration} seconds`);
91
- log('Stop the current recording before starting a new one');
92
- process.exit(1);
91
+ log(`PID: ${status.pid}`);
92
+ log('Use "dashcam stop" to stop the recording');
93
+ process.exit(0);
93
94
  }
94
95
 
95
96
  // Check authentication
@@ -116,77 +117,26 @@ async function recordingAction(options, command) {
116
117
  process.exit(1);
117
118
  }
118
119
 
119
- // Start recording
120
- log('Starting recording...');
120
+ // Start recording in background mode
121
+ log('Starting recording in background...');
121
122
 
122
123
  try {
123
- const recordingOptions = {
124
+ const result = await processManager.startRecording({
124
125
  fps: parseInt(options.fps) || 30,
125
- includeAudio: options.audio || false,
126
- customOutputPath: options.output
127
- };
128
-
129
- const recordingMetadata = {
126
+ audio: options.audio,
127
+ output: options.output,
130
128
  title: options.title,
131
129
  description: description,
132
- project: options.project || options.k
133
- };
134
-
135
- await startRecording(recordingOptions);
130
+ project: options.project || options.k // Support both -p and -k for project
131
+ });
136
132
 
137
- log(`✅ Recording started successfully`);
133
+ log(`✅ Recording started successfully (PID: ${result.pid})`);
134
+ log(`Output: ${result.outputPath}`);
138
135
  log('');
139
- log('Press Ctrl+C to stop recording and upload');
140
-
141
- // Set up graceful shutdown handlers
142
- const handleShutdown = async (signal) => {
143
- log('\nStopping recording...');
144
-
145
- try {
146
- const result = await stopRecording();
147
-
148
- if (!result) {
149
- log('Failed to stop recording');
150
- process.exit(1);
151
- }
152
-
153
- log('Recording stopped successfully');
154
-
155
- // Upload the recording
156
- log('Uploading recording...');
157
- try {
158
- const uploadResult = await upload(result.outputPath, {
159
- title: recordingMetadata.title || 'Dashcam Recording',
160
- description: recordingMetadata.description,
161
- project: recordingMetadata.project,
162
- duration: result.duration,
163
- clientStartDate: result.clientStartDate,
164
- apps: result.apps,
165
- icons: result.icons,
166
- logs: result.logs,
167
- gifPath: result.gifPath,
168
- snapshotPath: result.snapshotPath
169
- });
170
-
171
- log('📹 Watch your recording:', uploadResult.shareLink);
172
- process.exit(0);
173
- } catch (uploadError) {
174
- logError('Upload failed:', uploadError.message);
175
- log('Recording saved locally:', result.outputPath);
176
- process.exit(1);
177
- }
178
- } catch (error) {
179
- logError('Failed to stop recording:', error.message);
180
- process.exit(1);
181
- }
182
- };
183
-
184
- process.on('SIGINT', () => handleShutdown('SIGINT'));
185
- process.on('SIGTERM', () => handleShutdown('SIGTERM'));
186
-
187
- // Keep process alive
188
- await new Promise(() => {});
136
+ log('Use "dashcam status" to check progress');
137
+ log('Use "dashcam stop" to stop recording and upload');
189
138
 
139
+ process.exit(0);
190
140
  } catch (error) {
191
141
  logError('Failed to start recording:', error.message);
192
142
  process.exit(1);
@@ -198,6 +148,83 @@ async function recordingAction(options, command) {
198
148
  }
199
149
  }
200
150
 
151
+ // 'create' command - creates a clip from current recording (like stop but with more options)
152
+ program
153
+ .command('create')
154
+ .description('Create a clip and output the resulting url or markdown. Will launch desktop app for local editing before publishing.')
155
+ .option('-t, --title <string>', 'Title of the replay. Automatically generated if not supplied.')
156
+ .option('-d, --description [text]', 'Replay markdown body. This may also be piped in: `cat README.md | dashcam create`')
157
+ .option('--md', 'Returns code for a rich markdown image link.')
158
+ .option('-k, --project <project>', 'Project ID to publish to')
159
+ .action(async (options) => {
160
+ try {
161
+ // Check for piped input (description from stdin)
162
+ let description = options.description;
163
+ if (!description && !process.stdin.isTTY) {
164
+ const chunks = [];
165
+ for await (const chunk of process.stdin) {
166
+ chunks.push(chunk);
167
+ }
168
+ description = Buffer.concat(chunks).toString('utf-8');
169
+ }
170
+
171
+ if (!processManager.isRecordingActive()) {
172
+ console.log('No active recording to create clip from');
173
+ console.log('Start a recording first with "dashcam record" or "dashcam start"');
174
+ process.exit(0);
175
+ }
176
+
177
+ const activeStatus = processManager.getActiveStatus();
178
+
179
+ console.log('Creating clip from recording...');
180
+
181
+ const result = await processManager.stopActiveRecording();
182
+
183
+ if (!result) {
184
+ console.log('Failed to stop recording');
185
+ process.exit(1);
186
+ }
187
+
188
+ console.log('Recording stopped successfully');
189
+
190
+ // Upload the recording
191
+ console.log('Uploading clip...');
192
+ try {
193
+ const uploadResult = await upload(result.outputPath, {
194
+ title: options.title || activeStatus?.options?.title || 'Dashcam Recording',
195
+ description: description || activeStatus?.options?.description,
196
+ project: options.project || options.k || activeStatus?.options?.project,
197
+ duration: result.duration,
198
+ clientStartDate: result.clientStartDate,
199
+ apps: result.apps,
200
+ icons: result.icons,
201
+ gifPath: result.gifPath,
202
+ snapshotPath: result.snapshotPath
203
+ });
204
+
205
+ // Output based on format option
206
+ if (options.md) {
207
+ const replayId = uploadResult.replay?.id;
208
+ const shareKey = uploadResult.shareLink.split('share=')[1];
209
+ console.log(`[![Dashcam - ${options.title || 'New Replay'}](https://replayable-api-production.herokuapp.com/replay/${replayId}/gif?shareKey=${shareKey})](${uploadResult.shareLink})`);
210
+ console.log('');
211
+ console.log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
212
+ } else {
213
+ console.log(uploadResult.shareLink);
214
+ }
215
+ } catch (uploadError) {
216
+ console.error('Upload failed:', uploadError.message);
217
+ console.log('Recording saved locally:', result.outputPath);
218
+ }
219
+
220
+ process.exit(0);
221
+ } catch (error) {
222
+ logger.error('Error creating clip:', error);
223
+ console.error('Failed to create clip:', error.message);
224
+ process.exit(1);
225
+ }
226
+ });
227
+
201
228
  // 'record' command - the main recording command with all options
202
229
  program
203
230
  .command('record')
@@ -211,6 +238,254 @@ program
211
238
  .option('-s, --silent', 'Silent mode - suppress all output')
212
239
  .action(recordingAction);
213
240
 
241
+ program
242
+ .command('pipe')
243
+ .description('Pipe command output to dashcam to be included in recorded video')
244
+ .action(async () => {
245
+ try {
246
+ // Check if recording is active
247
+ if (!processManager.isRecordingActive()) {
248
+ console.error('No active recording. Start a recording first with "dashcam record" or "dashcam start"');
249
+ process.exit(1);
250
+ }
251
+
252
+ // Read from stdin
253
+ const chunks = [];
254
+ for await (const chunk of process.stdin) {
255
+ chunks.push(chunk);
256
+ // Also output to stdout so pipe continues to work
257
+ process.stdout.write(chunk);
258
+ }
259
+ const content = Buffer.concat(chunks).toString('utf-8');
260
+
261
+ // Import the log tracker to add the piped content
262
+ const { logsTrackerManager } = await import('../lib/logs/index.js');
263
+
264
+ // Add piped content as a log entry
265
+ logsTrackerManager.addPipedLog(content);
266
+
267
+ process.exit(0);
268
+ } catch (error) {
269
+ logger.error('Failed to pipe content:', error);
270
+ console.error('Failed to pipe content:', error.message);
271
+ process.exit(1);
272
+ }
273
+ });
274
+
275
+ program
276
+ .command('status')
277
+ .description('Show current recording status')
278
+ .action(() => {
279
+ const activeStatus = processManager.getActiveStatus();
280
+ if (activeStatus) {
281
+ const duration = ((Date.now() - activeStatus.startTime) / 1000).toFixed(1);
282
+ console.log('Recording in progress');
283
+ console.log(`Duration: ${duration} seconds`);
284
+ console.log(`PID: ${activeStatus.pid}`);
285
+ console.log(`Started: ${new Date(activeStatus.startTime).toLocaleString()}`);
286
+ if (activeStatus.options.title) {
287
+ console.log(`Title: ${activeStatus.options.title}`);
288
+ }
289
+ } else {
290
+ console.log('No active recording');
291
+ }
292
+ process.exit(0);
293
+ });
294
+
295
+
296
+
297
+ // 'start' command - alias for record with simple instant replay mode
298
+ program
299
+ .command('start')
300
+ .description('Start instant replay recording on dashcam')
301
+ .action(async () => {
302
+ // Call recordingAction with minimal options for instant replay
303
+ await recordingAction({
304
+ fps: '30',
305
+ audio: false,
306
+ silent: false
307
+ }, null);
308
+ });
309
+
310
+ program
311
+ .command('track')
312
+ .description('Add a logs config to Dashcam')
313
+ .option('--name <name>', 'Name for the tracking configuration (required)')
314
+ .option('--type <type>', 'Type of tracker: "application" or "web" (required)')
315
+ .option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
316
+ return previous ? previous.concat([value]) : [value];
317
+ })
318
+ .option('--web <pattern>', 'Web URL pattern to track (can use wildcards like *) - deprecated, use --type=web --pattern instead')
319
+ .option('--app <pattern>', 'Application file pattern to track (can use wildcards like *) - deprecated, use --type=application --pattern instead')
320
+ .action(async (options) => {
321
+ try {
322
+ // Support both old and new syntax
323
+ // New syntax: --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"
324
+ // Old syntax: --web <pattern> --app <pattern>
325
+
326
+ if (options.type && options.pattern) {
327
+ // New syntax validation
328
+ if (!options.name) {
329
+ console.error('Error: --name is required when using --type and --pattern');
330
+ console.log('Example: dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
331
+ process.exit(1);
332
+ }
333
+
334
+ if (options.type !== 'web' && options.type !== 'application') {
335
+ console.error('Error: --type must be either "web" or "application"');
336
+ process.exit(1);
337
+ }
338
+
339
+ const config = {
340
+ name: options.name,
341
+ type: options.type,
342
+ patterns: options.pattern,
343
+ enabled: true
344
+ };
345
+
346
+ await createPattern(config);
347
+ console.log(`${options.type === 'web' ? 'Web' : 'Application'} tracking pattern added successfully:`, options.name);
348
+ console.log('Patterns:', options.pattern.join(', '));
349
+
350
+ } else if (options.web || options.app) {
351
+ // Old syntax for backward compatibility
352
+ if (options.web) {
353
+ const config = {
354
+ name: options.name || 'Web Pattern',
355
+ type: 'web',
356
+ patterns: [options.web],
357
+ enabled: true
358
+ };
359
+
360
+ await createPattern(config);
361
+ console.log('Web tracking pattern added successfully:', options.web);
362
+ }
363
+
364
+ if (options.app) {
365
+ const config = {
366
+ name: options.name || 'App Pattern',
367
+ type: 'application',
368
+ patterns: [options.app],
369
+ enabled: true
370
+ };
371
+
372
+ await createPattern(config);
373
+ console.log('Application tracking pattern added successfully:', options.app);
374
+ }
375
+ } else {
376
+ console.error('Error: Must provide either:');
377
+ console.log(' --name --type --pattern (new syntax)');
378
+ console.log(' --web or --app (old syntax)');
379
+ console.log('\nExamples:');
380
+ console.log(' dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
381
+ console.log(' dashcam track --web "*facebook.com*"');
382
+ process.exit(1);
383
+ }
384
+
385
+ process.exit(0);
386
+ } catch (error) {
387
+ console.error('Failed to add tracking pattern:', error.message);
388
+ process.exit(1);
389
+ }
390
+ });
391
+
392
+ program
393
+ .command('stop')
394
+ .description('Stop the current recording and wait for upload completion')
395
+ .action(async () => {
396
+ try {
397
+ // Enable verbose logging for stop command
398
+ setVerbose(true);
399
+
400
+ if (!processManager.isRecordingActive()) {
401
+ console.log('No active recording to stop');
402
+ process.exit(0);
403
+ }
404
+
405
+ const activeStatus = processManager.getActiveStatus();
406
+ const logFile = path.join(process.cwd(), '.dashcam', 'recording.log');
407
+
408
+ console.log('Stopping recording...');
409
+
410
+ try {
411
+ const result = await processManager.stopActiveRecording();
412
+
413
+ if (!result) {
414
+ console.log('Failed to stop recording');
415
+ process.exit(1);
416
+ }
417
+
418
+ console.log('Recording stopped successfully');
419
+
420
+ // Wait for upload to complete (background process handles this)
421
+ logger.debug('Waiting for background upload to complete...');
422
+ console.log('⏳ Uploading recording...');
423
+
424
+ // Wait up to 2 minutes for upload result to appear
425
+ const maxWaitForUpload = 120000; // 2 minutes
426
+ const startWaitForUpload = Date.now();
427
+ let uploadResult = null;
428
+
429
+ while (!uploadResult && (Date.now() - startWaitForUpload) < maxWaitForUpload) {
430
+ uploadResult = processManager.readUploadResult();
431
+ if (!uploadResult) {
432
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
433
+ }
434
+ }
435
+
436
+ logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
437
+
438
+ if (uploadResult && uploadResult.shareLink) {
439
+ console.log('📹 Watch your recording:', uploadResult.shareLink);
440
+ // Clean up the result file now that we've read it
441
+ processManager.cleanup();
442
+ process.exit(0);
443
+ }
444
+
445
+ // Check if files still exist - if not, background process already uploaded
446
+ const filesExist = fs.existsSync(result.outputPath) &&
447
+ (!result.gifPath || fs.existsSync(result.gifPath)) &&
448
+ (!result.snapshotPath || fs.existsSync(result.snapshotPath));
449
+
450
+ if (!filesExist) {
451
+ console.log('✅ Recording uploaded by background process');
452
+ logger.info('Files were cleaned up by background process');
453
+ process.exit(0);
454
+ }
455
+
456
+ // Always attempt to upload - let upload function find project if needed
457
+ console.log('Uploading recording...');
458
+ try {
459
+ const uploadResult = await upload(result.outputPath, {
460
+ title: activeStatus?.options?.title,
461
+ description: activeStatus?.options?.description,
462
+ project: activeStatus?.options?.project, // May be undefined, that's ok
463
+ duration: result.duration,
464
+ clientStartDate: result.clientStartDate,
465
+ apps: result.apps,
466
+ icons: result.icons,
467
+ gifPath: result.gifPath,
468
+ snapshotPath: result.snapshotPath
469
+ });
470
+
471
+ console.log('📹 Watch your recording:', uploadResult.shareLink);
472
+ } catch (uploadError) {
473
+ console.error('Upload failed:', uploadError.message);
474
+ console.log('Recording saved locally:', result.outputPath);
475
+ }
476
+ } catch (error) {
477
+ console.error('Failed to stop recording:', error.message);
478
+ process.exit(1);
479
+ }
480
+
481
+ process.exit(0);
482
+ } catch (error) {
483
+ logger.error('Error stopping recording:', error);
484
+ console.error('Failed to stop recording:', error.message);
485
+ process.exit(1);
486
+ }
487
+ });
488
+
214
489
  program
215
490
  .command('logs')
216
491
  .description('Manage log tracking for recordings')
@@ -337,4 +612,84 @@ program
337
612
  }
338
613
  });
339
614
 
615
+ program
616
+ .command('upload')
617
+ .description('Upload a completed recording file or recover from interrupted recording')
618
+ .argument('[filePath]', 'Path to the recording file to upload (optional)')
619
+ .option('-t, --title <title>', 'Title for the recording')
620
+ .option('-d, --description <description>', 'Description for the recording')
621
+ .option('-p, --project <project>', 'Project ID to upload to')
622
+ .option('--recover', 'Attempt to recover and upload from interrupted recording')
623
+ .action(async (filePath, options) => {
624
+ try {
625
+ let targetFile = filePath;
626
+
627
+ if (options.recover) {
628
+ // Try to recover from interrupted recording
629
+ const tempFileInfoPath = path.join(process.cwd(), '.dashcam', 'temp-file.json');
630
+
631
+ if (fs.existsSync(tempFileInfoPath)) {
632
+ console.log('Found interrupted recording, attempting recovery...');
633
+
634
+ const tempFileInfo = JSON.parse(fs.readFileSync(tempFileInfoPath, 'utf8'));
635
+ const tempFile = tempFileInfo.tempFile;
636
+
637
+ if (fs.existsSync(tempFile) && fs.statSync(tempFile).size > 0) {
638
+ console.log('Recovering recording from temp file...');
639
+
640
+ // Import recorder to finalize the interrupted recording
641
+ const { stopRecording } = await import('../lib/recorder.js');
642
+
643
+ try {
644
+ // This will attempt to finalize the temp file
645
+ const result = await stopRecording();
646
+ targetFile = result.outputPath;
647
+ console.log('Recovery successful:', result.outputPath);
648
+ } catch (error) {
649
+ console.error('Recovery failed:', error.message);
650
+ console.log('You can try uploading the temp file directly:', tempFile);
651
+ targetFile = tempFile;
652
+ }
653
+
654
+ // Clean up temp file info after recovery attempt
655
+ fs.unlinkSync(tempFileInfoPath);
656
+ } else {
657
+ console.log('No valid temp file found for recovery');
658
+ process.exit(1);
659
+ }
660
+ } else {
661
+ console.log('No interrupted recording found');
662
+ process.exit(1);
663
+ }
664
+ }
665
+
666
+ if (!targetFile) {
667
+ console.error('Please provide a file path or use --recover option');
668
+ console.log('Examples:');
669
+ console.log(' dashcam upload /path/to/recording.webm');
670
+ console.log(' dashcam upload --recover');
671
+ process.exit(1);
672
+ }
673
+
674
+ if (!fs.existsSync(targetFile)) {
675
+ console.error('File not found:', targetFile);
676
+ process.exit(1);
677
+ }
678
+
679
+ console.log('Uploading recording...');
680
+ const uploadResult = await upload(targetFile, {
681
+ title: options.title,
682
+ description: options.description,
683
+ project: options.project
684
+ });
685
+
686
+ console.log('✅ Upload complete! Share link:', uploadResult.shareLink);
687
+ process.exit(0);
688
+
689
+ } catch (error) {
690
+ console.error('Upload failed:', error.message);
691
+ process.exit(1);
692
+ }
693
+ });
694
+
340
695
  program.parse();
package/lib/config.js CHANGED
@@ -1,10 +1,16 @@
1
1
  import { fileURLToPath } from 'url';
2
2
  import { dirname, join } from 'path';
3
3
  import { homedir } from 'os';
4
+ import { readFileSync } from 'fs';
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
8
 
9
+ // Read version from package.json
10
+ const packageJson = JSON.parse(
11
+ readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
12
+ );
13
+
8
14
  export const ENV = process.env.NODE_ENV || 'production';
9
15
 
10
16
  export const auth0Config = {
@@ -26,7 +32,7 @@ export const API_ENDPOINT = apiEndpoints[ENV];
26
32
  export const APP = {
27
33
  id: 'dashcam-cli',
28
34
  name: ENV === 'production' ? 'Dashcam CLI' : `Dashcam CLI - ${ENV}`,
29
- version: process.env.npm_package_version || '1.0.0',
35
+ version: packageJson.version,
30
36
  configDir: join(homedir(), '.dashcam'),
31
37
  logsDir: join(homedir(), '.dashcam', 'logs'),
32
38
  recordingsDir: join(homedir(), '.dashcam', 'recordings'),
@@ -0,0 +1,257 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import { logger } from './logger.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ // Use a fixed directory in the user's home directory for cross-process communication
11
+ const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
12
+ const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
13
+ const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
14
+ const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
15
+
16
+ // Ensure process directory exists
17
+ if (!fs.existsSync(PROCESS_DIR)) {
18
+ fs.mkdirSync(PROCESS_DIR, { recursive: true });
19
+ }
20
+
21
+ class ProcessManager {
22
+ constructor() {
23
+ this.isBackgroundMode = false;
24
+ this.isStopping = false;
25
+ }
26
+
27
+ setBackgroundMode(enabled = true) {
28
+ this.isBackgroundMode = enabled;
29
+ }
30
+
31
+ writeStatus(status) {
32
+ try {
33
+ fs.writeFileSync(STATUS_FILE, JSON.stringify({
34
+ ...status,
35
+ timestamp: Date.now(),
36
+ pid: process.pid
37
+ }, null, 2));
38
+ } catch (error) {
39
+ logger.error('Failed to write status file', { error });
40
+ }
41
+ }
42
+
43
+ readStatus() {
44
+ try {
45
+ if (!fs.existsSync(STATUS_FILE)) return null;
46
+ const data = fs.readFileSync(STATUS_FILE, 'utf8');
47
+ return JSON.parse(data);
48
+ } catch (error) {
49
+ logger.error('Failed to read status file', { error });
50
+ return null;
51
+ }
52
+ }
53
+
54
+ writeUploadResult(result) {
55
+ try {
56
+ logger.info('Writing upload result to file', { path: RESULT_FILE, shareLink: result.shareLink });
57
+ fs.writeFileSync(RESULT_FILE, JSON.stringify({
58
+ ...result,
59
+ timestamp: Date.now()
60
+ }, null, 2));
61
+ logger.info('Successfully wrote upload result to file');
62
+ // Verify it was written
63
+ if (fs.existsSync(RESULT_FILE)) {
64
+ logger.info('Verified upload result file exists');
65
+ } else {
66
+ logger.error('Upload result file does not exist after write!');
67
+ }
68
+ } catch (error) {
69
+ logger.error('Failed to write upload result file', { error });
70
+ }
71
+ }
72
+
73
+ readUploadResult() {
74
+ try {
75
+ if (!fs.existsSync(RESULT_FILE)) return null;
76
+ const data = fs.readFileSync(RESULT_FILE, 'utf8');
77
+ return JSON.parse(data);
78
+ } catch (error) {
79
+ logger.error('Failed to read upload result file', { error });
80
+ return null;
81
+ }
82
+ }
83
+
84
+ writePid(pid = process.pid) {
85
+ try {
86
+ fs.writeFileSync(PID_FILE, pid.toString());
87
+ } catch (error) {
88
+ logger.error('Failed to write PID file', { error });
89
+ }
90
+ }
91
+
92
+ readPid() {
93
+ try {
94
+ if (!fs.existsSync(PID_FILE)) return null;
95
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
96
+ return isNaN(pid) ? null : pid;
97
+ } catch (error) {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ isProcessRunning(pid) {
103
+ if (!pid) return false;
104
+ try {
105
+ process.kill(pid, 0); // Signal 0 just checks if process exists
106
+ return true;
107
+ } catch (error) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ isRecordingActive() {
113
+ const pid = this.readPid();
114
+ const status = this.readStatus();
115
+
116
+ if (!pid || !this.isProcessRunning(pid)) {
117
+ // Clean up but preserve upload result in case the background process just finished uploading
118
+ this.cleanup({ preserveResult: true });
119
+ return false;
120
+ }
121
+
122
+ return status && status.isRecording;
123
+ }
124
+
125
+ getActiveStatus() {
126
+ if (!this.isRecordingActive()) return null;
127
+ return this.readStatus();
128
+ }
129
+
130
+ cleanup(options = {}) {
131
+ const { preserveResult = false } = options;
132
+ try {
133
+ if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
134
+ if (fs.existsSync(STATUS_FILE)) fs.unlinkSync(STATUS_FILE);
135
+ if (!preserveResult && fs.existsSync(RESULT_FILE)) fs.unlinkSync(RESULT_FILE);
136
+ } catch (error) {
137
+ logger.error('Failed to cleanup process files', { error });
138
+ }
139
+ }
140
+
141
+ async stopActiveRecording() {
142
+ if (this.isStopping) {
143
+ logger.info('Stop already in progress, ignoring additional stop request');
144
+ return false;
145
+ }
146
+
147
+ this.isStopping = true;
148
+
149
+ try {
150
+ const status = this.readStatus();
151
+
152
+ if (!status || !status.isRecording) {
153
+ logger.warn('No active recording found');
154
+ return false;
155
+ }
156
+
157
+ // Import recorder module
158
+ const { stopRecording } = await import('./recorder.js');
159
+
160
+ logger.info('Stopping recording directly');
161
+
162
+ // Stop the recording
163
+ const result = await stopRecording();
164
+
165
+ logger.info('Recording stopped successfully', {
166
+ outputPath: result.outputPath,
167
+ duration: result.duration
168
+ });
169
+
170
+ // Update status to indicate recording stopped
171
+ this.writeStatus({
172
+ isRecording: false,
173
+ completedTime: Date.now(),
174
+ pid: process.pid
175
+ });
176
+
177
+ this.cleanup({ preserveResult: true });
178
+
179
+ return result;
180
+ } catch (error) {
181
+ logger.error('Failed to stop recording', { error });
182
+ throw error;
183
+ } finally {
184
+ this.isStopping = false;
185
+ }
186
+ }
187
+
188
+ async startRecording(options) {
189
+ // Check if recording is already active
190
+ if (this.isRecordingActive()) {
191
+ throw new Error('Recording already in progress');
192
+ }
193
+
194
+ // Import recorder module
195
+ const { startRecording } = await import('./recorder.js');
196
+
197
+ logger.info('Starting recording directly');
198
+
199
+ // Start recording with the provided options
200
+ const recordingOptions = {
201
+ fps: parseInt(options.fps) || 10,
202
+ includeAudio: options.audio || false,
203
+ customOutputPath: options.output || null
204
+ };
205
+
206
+ logger.info('Starting recording with options', { recordingOptions });
207
+
208
+ const recordingResult = await startRecording(recordingOptions);
209
+
210
+ // Write PID and status files for status tracking
211
+ this.writePid(process.pid);
212
+
213
+ this.writeStatus({
214
+ isRecording: true,
215
+ startTime: recordingResult.startTime,
216
+ options,
217
+ pid: process.pid,
218
+ outputPath: recordingResult.outputPath
219
+ });
220
+
221
+ logger.info('Recording started successfully', {
222
+ outputPath: recordingResult.outputPath,
223
+ startTime: recordingResult.startTime
224
+ });
225
+
226
+ return {
227
+ pid: process.pid,
228
+ outputPath: recordingResult.outputPath,
229
+ startTime: recordingResult.startTime
230
+ };
231
+ }
232
+
233
+ async gracefulExit() {
234
+ logger.info('Graceful exit requested');
235
+
236
+ // If we're currently recording, stop it properly
237
+ if (this.isRecordingActive()) {
238
+ try {
239
+ logger.info('Stopping active recording before exit');
240
+ await this.stopActiveRecording();
241
+ logger.info('Recording stopped successfully during graceful exit');
242
+ } catch (error) {
243
+ logger.error('Failed to stop recording during graceful exit', { error });
244
+ this.cleanup(); // Fallback cleanup
245
+ }
246
+ } else {
247
+ // Just cleanup if no recording is active
248
+ this.cleanup();
249
+ }
250
+
251
+ process.exit(0);
252
+ }
253
+ }
254
+
255
+ const processManager = new ProcessManager();
256
+
257
+ export { processManager };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.1.0-beta.5",
3
+ "version": "1.3.1-beta",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
- "main": "bin/index.js",
5
+ "main": "bin/dashcam.js",
6
6
  "bin": {
7
7
  "dashcam": "./bin/dashcam.js"
8
8
  },
package/test_workflow.sh CHANGED
@@ -30,16 +30,16 @@ if [ ! -f "$TEMP_FILE" ]; then
30
30
  fi
31
31
  echo "✅ File tracking configured"
32
32
 
33
- # 4. Start dashcam recording in background with timeout to auto-stop
33
+ # 4. Start dashcam recording in background
34
34
  echo ""
35
- echo "4. Starting dashcam recording..."
36
- # Start recording in background and set up auto-stop after test duration
35
+ echo "4. Starting dashcam recording in background..."
36
+ # Start recording and redirect output to a log file so we can still monitor it
37
37
  ./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1 &
38
38
  RECORD_PID=$!
39
39
 
40
40
  # Wait for recording to initialize and log tracker to start
41
41
  echo "Waiting for recording to initialize (PID: $RECORD_PID)..."
42
- sleep 2
42
+ sleep 1
43
43
 
44
44
  # Write first event after log tracker is fully ready
45
45
  RECORDING_START=$(date +%s)
@@ -53,7 +53,6 @@ if ps -p $RECORD_PID > /dev/null; then
53
53
  echo "✅ Recording started successfully"
54
54
  else
55
55
  echo "❌ Recording process died, check /tmp/dashcam-recording.log"
56
- cat /tmp/dashcam-recording.log
57
56
  exit 1
58
57
  fi
59
58
 
@@ -110,28 +109,15 @@ echo ""
110
109
  echo "Waiting 2 seconds to ensure all events are captured..."
111
110
  sleep 2
112
111
 
113
- # 6. Stop recording by sending SIGINT (Ctrl+C) to the recording process
112
+ # 6. Stop recording and upload (this will kill the background recording process)
114
113
  echo ""
115
- echo "6. Stopping recording (sending SIGINT)..."
114
+ echo "6. Stopping recording and uploading..."
116
115
  # Check if recording is still active
117
- if ps -p $RECORD_PID > /dev/null; then
118
- kill -INT $RECORD_PID
119
- echo "Sent SIGINT to recording process, waiting for upload to complete..."
120
-
121
- # Wait for the process to finish (it will stop recording and upload)
122
- wait $RECORD_PID 2>/dev/null || true
123
-
124
- echo "✅ Recording stopped and upload completed"
125
-
126
- # Show the output from the recording process
127
- echo ""
128
- echo "Recording output:"
129
- cat /tmp/dashcam-recording.log
116
+ if ./bin/dashcam.js status | grep -q "Recording in progress"; then
117
+ ./bin/dashcam.js stop
118
+ echo " Recording stopped and uploaded"
130
119
  else
131
- echo "⚠️ Recording process already completed"
132
- echo ""
133
- echo "Recording output:"
134
- cat /tmp/dashcam-recording.log
120
+ echo "⚠️ Recording already completed (this is expected with background mode)"
135
121
  fi
136
122
 
137
123
  echo ""
@@ -141,6 +127,11 @@ echo ""
141
127
  echo "🎉 Test workflow completed successfully!"
142
128
  echo "======================================"
143
129
 
130
+ # Show final status
131
+ echo ""
132
+ echo "📊 Final Status:"
133
+ ./bin/dashcam.js status
134
+
144
135
  echo ""
145
136
  echo "╔════════════════════════════════════════════════════════════════╗"
146
137
  echo "║ SYNC VERIFICATION GUIDE ║"