dashcam 0.8.4 → 1.0.1-beta.11

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 +703 -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 -158
  54. package/lib.js +0 -199
  55. package/recorder.js +0 -85
package/bin/dashcam.js ADDED
@@ -0,0 +1,703 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { auth } from '../lib/auth.js';
4
+ import { upload } from '../lib/uploader.js';
5
+ import { logger, setVerbose } from '../lib/logger.js';
6
+ import { APP } from '../lib/config.js';
7
+ import { createPattern } from '../lib/tracking.js';
8
+ import { processManager } from '../lib/processManager.js';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname } from 'path';
11
+ import path from 'path';
12
+ import fs from 'fs';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ // Ensure config directory exists
18
+ if (!fs.existsSync(APP.configDir)) {
19
+ fs.mkdirSync(APP.configDir, { recursive: true });
20
+ }
21
+
22
+ // Ensure recordings directory exists
23
+ if (!fs.existsSync(APP.recordingsDir)) {
24
+ fs.mkdirSync(APP.recordingsDir, { recursive: true });
25
+ }
26
+
27
+ program
28
+ .name('dashcam')
29
+ .description('Capture the steps to reproduce every bug.')
30
+ .version(APP.version)
31
+ .option('-v, --verbose', 'Enable verbose logging output')
32
+ .option('-t, --title <string>', 'Title of the replay (used with create/default action)')
33
+ .option('-d, --description [text]', 'Replay markdown body (used with create/default action)')
34
+ .option('--md', 'Returns code for a rich markdown image link (used with create/default action)')
35
+ .option('-k, --project <project>', 'Project ID to publish to (used with create/default action)')
36
+ .hook('preAction', (thisCommand) => {
37
+ // Enable verbose logging if the flag is set
38
+ if (thisCommand.opts().verbose) {
39
+ setVerbose(true);
40
+ logger.info('Verbose logging enabled');
41
+ }
42
+ });
43
+
44
+ program
45
+ .command('auth')
46
+ .description("Authenticate the dashcam desktop using a team's apiKey")
47
+ .argument('<api-key>', 'Your team API key')
48
+ .action(async (apiKey, options, command) => {
49
+ try {
50
+ logger.verbose('Starting authentication process', {
51
+ apiKeyProvided: !!apiKey,
52
+ globalOptions: command.parent.opts()
53
+ });
54
+
55
+ await auth.login(apiKey);
56
+ console.log('Successfully authenticated with API key');
57
+ process.exit(0);
58
+ } catch (error) {
59
+ console.error('Authentication failed:', error.message);
60
+ logger.error('Authentication failed with details:', {
61
+ error: error.message,
62
+ stack: error.stack
63
+ });
64
+ process.exit(1);
65
+ }
66
+ });
67
+
68
+ program
69
+ .command('logout')
70
+ .description('Logout from your Dashcam account')
71
+ .action(async () => {
72
+ try {
73
+ await auth.logout();
74
+ console.log('Successfully logged out');
75
+ process.exit(0);
76
+ } catch (error) {
77
+ logger.error('Logout failed:', error);
78
+ process.exit(1);
79
+ }
80
+ });
81
+
82
+ // Shared recording action to avoid duplication
83
+ async function recordingAction(options, command) {
84
+ try {
85
+ const silent = options.silent;
86
+ const log = (...args) => { if (!silent) console.log(...args); };
87
+ const logError = (...args) => { if (!silent) console.error(...args); };
88
+
89
+ // Check if recording is already active
90
+ if (processManager.isRecordingActive()) {
91
+ const status = processManager.getActiveStatus();
92
+ const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
93
+ log('Recording already in progress');
94
+ log(`Duration: ${duration} seconds`);
95
+ log(`PID: ${status.pid}`);
96
+ log('Use "dashcam stop" to stop the recording');
97
+ process.exit(0);
98
+ }
99
+
100
+ // Check authentication
101
+ if (!await auth.isAuthenticated()) {
102
+ log('You need to login first. Run: dashcam auth <api-key>');
103
+ process.exit(1);
104
+ }
105
+
106
+ // Check for piped input (description from stdin) if description option not set
107
+ let description = options.description;
108
+ if (!description && !process.stdin.isTTY) {
109
+ const chunks = [];
110
+ for await (const chunk of process.stdin) {
111
+ chunks.push(chunk);
112
+ }
113
+ description = Buffer.concat(chunks).toString('utf-8');
114
+ }
115
+
116
+ // Check screen recording permissions (macOS only)
117
+ const { ensurePermissions } = await import('../lib/permissions.js');
118
+ const hasPermissions = await ensurePermissions();
119
+ if (!hasPermissions) {
120
+ log('\n⚠️ Cannot start recording without screen recording permission.');
121
+ process.exit(1);
122
+ }
123
+
124
+ // Start recording in background mode
125
+ log('Starting recording in background...');
126
+
127
+ try {
128
+ const result = await processManager.startRecording({
129
+ fps: parseInt(options.fps) || 30,
130
+ audio: options.audio,
131
+ output: options.output,
132
+ title: options.title,
133
+ description: description,
134
+ project: options.project || options.k // Support both -p and -k for project
135
+ });
136
+
137
+ log(`✅ Recording started successfully (PID: ${result.pid})`);
138
+ log(`Output: ${result.outputPath}`);
139
+ log('');
140
+ log('Use "dashcam status" to check progress');
141
+ log('Use "dashcam stop" to stop recording and upload');
142
+
143
+ process.exit(0);
144
+ } catch (error) {
145
+ logError('Failed to start recording:', error.message);
146
+ process.exit(1);
147
+ }
148
+ } catch (error) {
149
+ logger.error('Failed to start recording:', error);
150
+ if (!options.silent) console.error('Failed to start recording:', error.message);
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ // Shared create/clip action
156
+ async function createClipAction(options) {
157
+ try {
158
+ // Check for piped input (description from stdin)
159
+ let description = options.description;
160
+ if (!description && !process.stdin.isTTY) {
161
+ const chunks = [];
162
+ for await (const chunk of process.stdin) {
163
+ chunks.push(chunk);
164
+ }
165
+ description = Buffer.concat(chunks).toString('utf-8');
166
+ }
167
+
168
+ if (!processManager.isRecordingActive()) {
169
+ console.log('No active recording to create clip from');
170
+ console.log('Start a recording first with "dashcam record" or "dashcam start"');
171
+ process.exit(0);
172
+ }
173
+
174
+ const activeStatus = processManager.getActiveStatus();
175
+
176
+ console.log('Creating clip from recording...');
177
+
178
+ const result = await processManager.stopActiveRecording();
179
+
180
+ if (!result) {
181
+ console.log('Failed to stop recording');
182
+ process.exit(1);
183
+ }
184
+
185
+ console.log('Recording stopped successfully');
186
+
187
+ // Upload the recording
188
+ console.log('Uploading clip...');
189
+ try {
190
+ const uploadResult = await upload(result.outputPath, {
191
+ title: options.title || activeStatus?.options?.title || 'Dashcam Recording',
192
+ description: description || activeStatus?.options?.description,
193
+ project: options.project || options.k || activeStatus?.options?.project,
194
+ duration: result.duration,
195
+ clientStartDate: result.clientStartDate,
196
+ apps: result.apps,
197
+ icons: result.icons,
198
+ gifPath: result.gifPath,
199
+ snapshotPath: result.snapshotPath
200
+ });
201
+
202
+ // Output based on format option
203
+ if (options.md) {
204
+ const replayId = uploadResult.replay?.id;
205
+ const shareKey = uploadResult.shareLink.split('share=')[1];
206
+ console.log(`[![Dashcam - ${options.title || 'New Replay'}](https://replayable-api-production.herokuapp.com/replay/${replayId}/gif?shareKey=${shareKey})](${uploadResult.shareLink})`);
207
+ console.log('');
208
+ console.log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
209
+ } else {
210
+ console.log(uploadResult.shareLink);
211
+ }
212
+ } catch (uploadError) {
213
+ console.error('Upload failed:', uploadError.message);
214
+ console.log('Recording saved locally:', result.outputPath);
215
+ }
216
+
217
+ process.exit(0);
218
+ } catch (error) {
219
+ logger.error('Error creating clip:', error);
220
+ console.error('Failed to create clip:', error.message);
221
+ process.exit(1);
222
+ }
223
+ }
224
+
225
+ // 'create' command - creates a clip from current recording (like stop but with more options)
226
+ program
227
+ .command('create')
228
+ .description('Create a clip and output the resulting url or markdown. Will launch desktop app for local editing before publishing.')
229
+ .option('-t, --title <string>', 'Title of the replay. Automatically generated if not supplied.')
230
+ .option('-d, --description [text]', 'Replay markdown body. This may also be piped in: `cat README.md | dashcam create`')
231
+ .option('--md', 'Returns code for a rich markdown image link.')
232
+ .option('-k, --project <project>', 'Project ID to publish to')
233
+ .action(createClipAction);
234
+
235
+ // 'record' command - the main recording command with all options
236
+ program
237
+ .command('record')
238
+ .description('Start a recording terminal to be included in your dashcam video recording')
239
+ .option('-a, --audio', 'Include audio in the recording')
240
+ .option('-f, --fps <fps>', 'Frames per second (default: 30)', '30')
241
+ .option('-o, --output <path>', 'Custom output path')
242
+ .option('-t, --title <title>', 'Title for the recording')
243
+ .option('-d, --description <description>', 'Description for the recording (supports markdown)')
244
+ .option('-p, --project <project>', 'Project ID to upload the recording to')
245
+ .option('-s, --silent', 'Silent mode - suppress all output')
246
+ .action(recordingAction);
247
+
248
+ program
249
+ .command('pipe')
250
+ .description('Pipe command output to dashcam to be included in recorded video')
251
+ .action(async () => {
252
+ try {
253
+ // Check if recording is active
254
+ if (!processManager.isRecordingActive()) {
255
+ console.error('No active recording. Start a recording first with "dashcam record" or "dashcam start"');
256
+ process.exit(1);
257
+ }
258
+
259
+ // Read from stdin
260
+ const chunks = [];
261
+ for await (const chunk of process.stdin) {
262
+ chunks.push(chunk);
263
+ // Also output to stdout so pipe continues to work
264
+ process.stdout.write(chunk);
265
+ }
266
+ const content = Buffer.concat(chunks).toString('utf-8');
267
+
268
+ // Import the log tracker to add the piped content
269
+ const { logsTrackerManager } = await import('../lib/logs/index.js');
270
+
271
+ // Add piped content as a log entry
272
+ logsTrackerManager.addPipedLog(content);
273
+
274
+ process.exit(0);
275
+ } catch (error) {
276
+ logger.error('Failed to pipe content:', error);
277
+ console.error('Failed to pipe content:', error.message);
278
+ process.exit(1);
279
+ }
280
+ });
281
+
282
+ program
283
+ .command('status')
284
+ .description('Show current recording status')
285
+ .action(() => {
286
+ const activeStatus = processManager.getActiveStatus();
287
+ if (activeStatus) {
288
+ const duration = ((Date.now() - activeStatus.startTime) / 1000).toFixed(1);
289
+ console.log('Recording in progress');
290
+ console.log(`Duration: ${duration} seconds`);
291
+ console.log(`PID: ${activeStatus.pid}`);
292
+ console.log(`Started: ${new Date(activeStatus.startTime).toLocaleString()}`);
293
+ if (activeStatus.options.title) {
294
+ console.log(`Title: ${activeStatus.options.title}`);
295
+ }
296
+ } else {
297
+ console.log('No active recording');
298
+ }
299
+ process.exit(0);
300
+ });
301
+
302
+
303
+
304
+ // 'start' command - alias for record with simple instant replay mode
305
+ program
306
+ .command('start')
307
+ .description('Start instant replay recording on dashcam')
308
+ .action(async () => {
309
+ // Call recordingAction with minimal options for instant replay
310
+ await recordingAction({
311
+ fps: '30',
312
+ audio: false,
313
+ silent: false
314
+ }, null);
315
+ });
316
+
317
+ program
318
+ .command('track')
319
+ .description('Add a logs config to Dashcam')
320
+ .option('--name <name>', 'Name for the tracking configuration (required)')
321
+ .option('--type <type>', 'Type of tracker: "application" or "web" (required)')
322
+ .option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
323
+ return previous ? previous.concat([value]) : [value];
324
+ })
325
+ .option('--web <pattern>', 'Web URL pattern to track (can use wildcards like *) - deprecated, use --type=web --pattern instead')
326
+ .option('--app <pattern>', 'Application file pattern to track (can use wildcards like *) - deprecated, use --type=application --pattern instead')
327
+ .action(async (options) => {
328
+ try {
329
+ // Support both old and new syntax
330
+ // New syntax: --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"
331
+ // Old syntax: --web <pattern> --app <pattern>
332
+
333
+ if (options.type && options.pattern) {
334
+ // New syntax validation
335
+ if (!options.name) {
336
+ console.error('Error: --name is required when using --type and --pattern');
337
+ console.log('Example: dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
338
+ process.exit(1);
339
+ }
340
+
341
+ if (options.type !== 'web' && options.type !== 'application') {
342
+ console.error('Error: --type must be either "web" or "application"');
343
+ process.exit(1);
344
+ }
345
+
346
+ const config = {
347
+ name: options.name,
348
+ type: options.type,
349
+ patterns: options.pattern,
350
+ enabled: true
351
+ };
352
+
353
+ await createPattern(config);
354
+ console.log(`${options.type === 'web' ? 'Web' : 'Application'} tracking pattern added successfully:`, options.name);
355
+ console.log('Patterns:', options.pattern.join(', '));
356
+
357
+ } else if (options.web || options.app) {
358
+ // Old syntax for backward compatibility
359
+ if (options.web) {
360
+ const config = {
361
+ name: options.name || 'Web Pattern',
362
+ type: 'web',
363
+ patterns: [options.web],
364
+ enabled: true
365
+ };
366
+
367
+ await createPattern(config);
368
+ console.log('Web tracking pattern added successfully:', options.web);
369
+ }
370
+
371
+ if (options.app) {
372
+ const config = {
373
+ name: options.name || 'App Pattern',
374
+ type: 'application',
375
+ patterns: [options.app],
376
+ enabled: true
377
+ };
378
+
379
+ await createPattern(config);
380
+ console.log('Application tracking pattern added successfully:', options.app);
381
+ }
382
+ } else {
383
+ console.error('Error: Must provide either:');
384
+ console.log(' --name --type --pattern (new syntax)');
385
+ console.log(' --web or --app (old syntax)');
386
+ console.log('\nExamples:');
387
+ console.log(' dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
388
+ console.log(' dashcam track --web "*facebook.com*"');
389
+ process.exit(1);
390
+ }
391
+
392
+ process.exit(0);
393
+ } catch (error) {
394
+ console.error('Failed to add tracking pattern:', error.message);
395
+ process.exit(1);
396
+ }
397
+ });
398
+
399
+ program
400
+ .command('stop')
401
+ .description('Stop the current recording and wait for upload completion')
402
+ .action(async () => {
403
+ try {
404
+ // Enable verbose logging for stop command
405
+ setVerbose(true);
406
+
407
+ if (!processManager.isRecordingActive()) {
408
+ console.log('No active recording to stop');
409
+ process.exit(0);
410
+ }
411
+
412
+ const activeStatus = processManager.getActiveStatus();
413
+ const logFile = path.join(process.cwd(), '.dashcam', 'recording.log');
414
+
415
+ console.log('Stopping recording...');
416
+
417
+ try {
418
+ const result = await processManager.stopActiveRecording();
419
+
420
+ if (!result) {
421
+ console.log('Failed to stop recording');
422
+ process.exit(1);
423
+ }
424
+
425
+ console.log('Recording stopped successfully');
426
+
427
+ // Check if files still exist - if not, background process already uploaded
428
+ const filesExist = fs.existsSync(result.outputPath) &&
429
+ (!result.gifPath || fs.existsSync(result.gifPath)) &&
430
+ (!result.snapshotPath || fs.existsSync(result.snapshotPath));
431
+
432
+ if (!filesExist) {
433
+ // Files were deleted, meaning background process uploaded
434
+ // Wait for the upload result to be written
435
+ logger.debug('Waiting for upload result from background process');
436
+ await new Promise(resolve => setTimeout(resolve, 2000));
437
+
438
+ // Try to read the upload result from the background process
439
+ const uploadResult = processManager.readUploadResult();
440
+ logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
441
+
442
+ if (uploadResult && uploadResult.shareLink) {
443
+ console.log('📹 Watch your recording:', uploadResult.shareLink);
444
+ // Clean up the result file now that we've read it
445
+ processManager.cleanup();
446
+ } else {
447
+ console.log('✅ Recording uploaded (share link not available)');
448
+ logger.warn('Upload result not available from background process');
449
+ }
450
+
451
+ process.exit(0);
452
+ }
453
+
454
+ // Always attempt to upload - let upload function find project if needed
455
+ console.log('Uploading recording...');
456
+ try {
457
+ const uploadResult = await upload(result.outputPath, {
458
+ title: activeStatus?.options?.title,
459
+ description: activeStatus?.options?.description,
460
+ project: activeStatus?.options?.project, // May be undefined, that's ok
461
+ duration: result.duration,
462
+ clientStartDate: result.clientStartDate,
463
+ apps: result.apps,
464
+ icons: result.icons,
465
+ gifPath: result.gifPath,
466
+ snapshotPath: result.snapshotPath
467
+ });
468
+
469
+ console.log('📹 Watch your recording:', uploadResult.shareLink);
470
+ } catch (uploadError) {
471
+ console.error('Upload failed:', uploadError.message);
472
+ console.log('Recording saved locally:', result.outputPath);
473
+ }
474
+ } catch (error) {
475
+ console.error('Failed to stop recording:', error.message);
476
+ process.exit(1);
477
+ }
478
+
479
+ process.exit(0);
480
+ } catch (error) {
481
+ logger.error('Error stopping recording:', error);
482
+ console.error('Failed to stop recording:', error.message);
483
+ process.exit(1);
484
+ }
485
+ });
486
+
487
+ program
488
+ .command('logs')
489
+ .description('Manage log tracking for recordings')
490
+ .option('--add', 'Add a new log tracker')
491
+ .option('--remove <id>', 'Remove a log tracker by ID')
492
+ .option('--list', 'List all configured log trackers')
493
+ .option('--status', 'Show log tracking status')
494
+ .option('--name <name>', 'Name for the log tracker (required with --add)')
495
+ .option('--type <type>', 'Type of tracker: "web" or "file" (required with --add)')
496
+ .option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
497
+ return previous ? previous.concat([value]) : [value];
498
+ })
499
+ .option('--file <file>', 'File path for file type trackers')
500
+ .action(async (options) => {
501
+ try {
502
+ // Import logsTrackerManager only when needed to avoid unwanted initialization
503
+ const { logsTrackerManager } = await import('../lib/logs/index.js');
504
+
505
+ if (options.add) {
506
+ // Validate required options for add
507
+ if (!options.name) {
508
+ console.error('Error: --name is required when adding a tracker');
509
+ console.log('Example: dashcam logs --add --name=social --type=web --pattern="*facebook.com*"');
510
+ process.exit(1);
511
+ }
512
+ if (!options.type) {
513
+ console.error('Error: --type is required when adding a tracker (web or file)');
514
+ process.exit(1);
515
+ }
516
+ if (options.type !== 'web' && options.type !== 'file') {
517
+ console.error('Error: --type must be either "web" or "file"');
518
+ process.exit(1);
519
+ }
520
+
521
+ if (options.type === 'web') {
522
+ if (!options.pattern || options.pattern.length === 0) {
523
+ console.error('Error: At least one --pattern is required for web trackers');
524
+ console.log('Example: dashcam logs --add --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
525
+ process.exit(1);
526
+ }
527
+
528
+ const webConfig = {
529
+ id: options.name.toLowerCase().replace(/[^a-z0-9]/g, '-'),
530
+ name: options.name,
531
+ type: 'web',
532
+ enabled: true,
533
+ patterns: options.pattern
534
+ };
535
+
536
+ logsTrackerManager.addWebTracker(webConfig);
537
+ console.log(`Added web tracker "${options.name}" with patterns:`, options.pattern);
538
+ } else if (options.type === 'file') {
539
+ if (!options.file) {
540
+ console.error('Error: --file is required for file trackers');
541
+ console.log('Example: dashcam logs --add --name=app-logs --type=file --file=/var/log/app.log');
542
+ process.exit(1);
543
+ }
544
+ if (!fs.existsSync(options.file)) {
545
+ console.error('Log file does not exist:', options.file);
546
+ process.exit(1);
547
+ }
548
+
549
+ logsTrackerManager.addCliLogFile(options.file);
550
+ console.log(`Added file tracker "${options.name}" for:`, options.file);
551
+ }
552
+ } else if (options.remove) {
553
+ logsTrackerManager.removeTracker(options.remove);
554
+ console.log('Removed tracker:', options.remove);
555
+ } else if (options.list) {
556
+ const status = logsTrackerManager.getStatus();
557
+ console.log('Currently configured trackers:');
558
+
559
+ if (status.cliFiles.length > 0) {
560
+ console.log('\nFile trackers:');
561
+ status.cliFiles.forEach((filePath, index) => {
562
+ console.log(` file-${index + 1}: ${filePath}`);
563
+ });
564
+ }
565
+
566
+ if (status.webApps.length > 0) {
567
+ console.log('\nWeb trackers:');
568
+ status.webApps.forEach(app => {
569
+ console.log(` ${app.id}: ${app.name}`);
570
+ console.log(` Patterns: ${app.patterns.join(', ')}`);
571
+ });
572
+ }
573
+
574
+ if (status.cliFiles.length === 0 && status.webApps.length === 0) {
575
+ console.log(' (none configured)');
576
+ console.log('\nExamples:');
577
+ console.log(' dashcam logs --add --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
578
+ console.log(' dashcam logs --add --name=app-logs --type=file --file=/var/log/app.log');
579
+ }
580
+ } else if (options.status) {
581
+ const status = logsTrackerManager.getStatus();
582
+ console.log('Log tracking status:');
583
+ console.log(` Active recording instances: ${status.activeInstances}`);
584
+ console.log(` File trackers: ${status.cliFilesCount}`);
585
+ console.log(` Web trackers: ${status.webAppsCount}`);
586
+ console.log(` Total recent events: ${status.totalEvents}`);
587
+
588
+ if (status.fileTrackerStats.length > 0) {
589
+ console.log('\n File tracker activity (last minute):');
590
+ status.fileTrackerStats.forEach(stat => {
591
+ console.log(` ${stat.filePath}: ${stat.count} events`);
592
+ });
593
+ }
594
+ } else {
595
+ console.log('Please specify an action: --add, --remove, --list, or --status');
596
+ console.log('\nExamples:');
597
+ console.log(' dashcam logs --add --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
598
+ console.log(' dashcam logs --add --name=app-logs --type=file --file=/var/log/app.log');
599
+ console.log(' dashcam logs --list');
600
+ console.log(' dashcam logs --status');
601
+ console.log('\nUse "dashcam logs --help" for more information');
602
+ }
603
+
604
+ // Exit successfully to prevent hanging
605
+ process.exit(0);
606
+ } catch (error) {
607
+ logger.error('Error managing logs:', error);
608
+ console.error('Failed to manage logs:', error.message);
609
+ process.exit(1);
610
+ }
611
+ });
612
+
613
+ program
614
+ .command('upload')
615
+ .description('Upload a completed recording file or recover from interrupted recording')
616
+ .argument('[filePath]', 'Path to the recording file to upload (optional)')
617
+ .option('-t, --title <title>', 'Title for the recording')
618
+ .option('-d, --description <description>', 'Description for the recording')
619
+ .option('-p, --project <project>', 'Project ID to upload to')
620
+ .option('--recover', 'Attempt to recover and upload from interrupted recording')
621
+ .action(async (filePath, options) => {
622
+ try {
623
+ let targetFile = filePath;
624
+
625
+ if (options.recover) {
626
+ // Try to recover from interrupted recording
627
+ const tempFileInfoPath = path.join(process.cwd(), '.dashcam', 'temp-file.json');
628
+
629
+ if (fs.existsSync(tempFileInfoPath)) {
630
+ console.log('Found interrupted recording, attempting recovery...');
631
+
632
+ const tempFileInfo = JSON.parse(fs.readFileSync(tempFileInfoPath, 'utf8'));
633
+ const tempFile = tempFileInfo.tempFile;
634
+
635
+ if (fs.existsSync(tempFile) && fs.statSync(tempFile).size > 0) {
636
+ console.log('Recovering recording from temp file...');
637
+
638
+ // Import recorder to finalize the interrupted recording
639
+ const { stopRecording } = await import('../lib/recorder.js');
640
+
641
+ try {
642
+ // This will attempt to finalize the temp file
643
+ const result = await stopRecording();
644
+ targetFile = result.outputPath;
645
+ console.log('Recovery successful:', result.outputPath);
646
+ } catch (error) {
647
+ console.error('Recovery failed:', error.message);
648
+ console.log('You can try uploading the temp file directly:', tempFile);
649
+ targetFile = tempFile;
650
+ }
651
+
652
+ // Clean up temp file info after recovery attempt
653
+ fs.unlinkSync(tempFileInfoPath);
654
+ } else {
655
+ console.log('No valid temp file found for recovery');
656
+ process.exit(1);
657
+ }
658
+ } else {
659
+ console.log('No interrupted recording found');
660
+ process.exit(1);
661
+ }
662
+ }
663
+
664
+ if (!targetFile) {
665
+ console.error('Please provide a file path or use --recover option');
666
+ console.log('Examples:');
667
+ console.log(' dashcam upload /path/to/recording.webm');
668
+ console.log(' dashcam upload --recover');
669
+ process.exit(1);
670
+ }
671
+
672
+ if (!fs.existsSync(targetFile)) {
673
+ console.error('File not found:', targetFile);
674
+ process.exit(1);
675
+ }
676
+
677
+ console.log('Uploading recording...');
678
+ const uploadResult = await upload(targetFile, {
679
+ title: options.title,
680
+ description: options.description,
681
+ project: options.project
682
+ });
683
+
684
+ console.log('✅ Upload complete! Share link:', uploadResult.shareLink);
685
+ process.exit(0);
686
+
687
+ } catch (error) {
688
+ console.error('Upload failed:', error.message);
689
+ process.exit(1);
690
+ }
691
+ });
692
+
693
+ // If no command specified, treat as create command
694
+ program.action((options, command) => {
695
+ // Merge global options with command options
696
+ const mergedOptions = {
697
+ ...command.opts(),
698
+ ...options
699
+ };
700
+ return createClipAction(mergedOptions);
701
+ });
702
+
703
+ program.parse();