dashcam 1.0.1-beta.5 → 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.
package/bin/dashcam.js CHANGED
@@ -26,7 +26,7 @@ if (!fs.existsSync(APP.recordingsDir)) {
26
26
 
27
27
  program
28
28
  .name('dashcam')
29
- .description('CLI version of Dashcam screen recorder')
29
+ .description('Capture the steps to reproduce every bug.')
30
30
  .version(APP.version)
31
31
  .option('-v, --verbose', 'Enable verbose logging output')
32
32
  .hook('preAction', (thisCommand) => {
@@ -39,8 +39,8 @@ program
39
39
 
40
40
  program
41
41
  .command('auth')
42
- .description('Authenticate with TestDriver using an API key')
43
- .argument('<apiKey>', 'Your TestDriver API key')
42
+ .description("Authenticate the dashcam desktop using a team's apiKey")
43
+ .argument('<api-key>', 'Your team API key')
44
44
  .action(async (apiKey, options, command) => {
45
45
  try {
46
46
  logger.verbose('Starting authentication process', {
@@ -75,9 +75,160 @@ program
75
75
  }
76
76
  });
77
77
 
78
+ // Shared recording action to avoid duplication
79
+ async function recordingAction(options, command) {
80
+ try {
81
+ const silent = options.silent;
82
+ const log = (...args) => { if (!silent) console.log(...args); };
83
+ const logError = (...args) => { if (!silent) console.error(...args); };
84
+
85
+ // Check if recording is already active
86
+ if (processManager.isRecordingActive()) {
87
+ const status = processManager.getActiveStatus();
88
+ const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
89
+ log('Recording already in progress');
90
+ log(`Duration: ${duration} seconds`);
91
+ log(`PID: ${status.pid}`);
92
+ log('Use "dashcam stop" to stop the recording');
93
+ process.exit(0);
94
+ }
95
+
96
+ // Check authentication
97
+ if (!await auth.isAuthenticated()) {
98
+ log('You need to login first. Run: dashcam auth <api-key>');
99
+ process.exit(1);
100
+ }
101
+
102
+ // Check for piped input (description from stdin) if description option not set
103
+ let description = options.description;
104
+ if (!description && !process.stdin.isTTY) {
105
+ const chunks = [];
106
+ for await (const chunk of process.stdin) {
107
+ chunks.push(chunk);
108
+ }
109
+ description = Buffer.concat(chunks).toString('utf-8');
110
+ }
111
+
112
+ // Check screen recording permissions (macOS only)
113
+ const { ensurePermissions } = await import('../lib/permissions.js');
114
+ const hasPermissions = await ensurePermissions();
115
+ if (!hasPermissions) {
116
+ log('\n⚠️ Cannot start recording without screen recording permission.');
117
+ process.exit(1);
118
+ }
119
+
120
+ // Start recording in background mode
121
+ log('Starting recording in background...');
122
+
123
+ try {
124
+ const result = await processManager.startRecording({
125
+ fps: parseInt(options.fps) || 30,
126
+ audio: options.audio,
127
+ output: options.output,
128
+ title: options.title,
129
+ description: description,
130
+ project: options.project || options.k // Support both -p and -k for project
131
+ });
132
+
133
+ log(`✅ Recording started successfully (PID: ${result.pid})`);
134
+ log(`Output: ${result.outputPath}`);
135
+ log('');
136
+ log('Use "dashcam status" to check progress');
137
+ log('Use "dashcam stop" to stop recording and upload');
138
+
139
+ process.exit(0);
140
+ } catch (error) {
141
+ logError('Failed to start recording:', error.message);
142
+ process.exit(1);
143
+ }
144
+ } catch (error) {
145
+ logger.error('Failed to start recording:', error);
146
+ if (!options.silent) console.error('Failed to start recording:', error.message);
147
+ process.exit(1);
148
+ }
149
+ }
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
+
228
+ // 'record' command - the main recording command with all options
78
229
  program
79
230
  .command('record')
80
- .description('Start a background screen recording')
231
+ .description('Start a recording terminal to be included in your dashcam video recording')
81
232
  .option('-a, --audio', 'Include audio in the recording')
82
233
  .option('-f, --fps <fps>', 'Frames per second (default: 30)', '30')
83
234
  .option('-o, --output <path>', 'Custom output path')
@@ -85,123 +236,38 @@ program
85
236
  .option('-d, --description <description>', 'Description for the recording (supports markdown)')
86
237
  .option('-p, --project <project>', 'Project ID to upload the recording to')
87
238
  .option('-s, --silent', 'Silent mode - suppress all output')
88
- .action(async (options, command) => {
89
- try {
90
- const silent = options.silent;
91
- const log = (...args) => { if (!silent) console.log(...args); };
92
- const logError = (...args) => { if (!silent) console.error(...args); };
93
-
94
- // Check if recording is already active
95
- if (processManager.isRecordingActive()) {
96
- const status = processManager.getActiveStatus();
97
- const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
98
- log('Recording already in progress');
99
- log(`Duration: ${duration} seconds`);
100
- log(`PID: ${status.pid}`);
101
- log('Use "dashcam stop" to stop the recording');
102
- process.exit(0);
103
- }
239
+ .action(recordingAction);
104
240
 
105
- // Check authentication
106
- if (!await auth.isAuthenticated()) {
107
- log('You need to login first. Run: dashcam auth <api-key>');
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"');
108
249
  process.exit(1);
109
250
  }
110
251
 
111
- // Check screen recording permissions (macOS only)
112
- const { ensurePermissions } = await import('../lib/permissions.js');
113
- const hasPermissions = await ensurePermissions();
114
- if (!hasPermissions) {
115
- log('\n⚠️ Cannot start recording without screen recording permission.');
116
- process.exit(1);
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);
117
258
  }
259
+ const content = Buffer.concat(chunks).toString('utf-8');
118
260
 
119
- // Always use background mode
120
- log('Starting recording...');
261
+ // Import the log tracker to add the piped content
262
+ const { logsTrackerManager } = await import('../lib/logs/index.js');
121
263
 
122
- try {
123
- const result = await processManager.startRecording({
124
- fps: parseInt(options.fps) || 30,
125
- audio: options.audio,
126
- output: options.output,
127
- title: options.title,
128
- description: options.description,
129
- project: options.project
130
- });
131
-
132
- log(`Recording started successfully (PID: ${result.pid})`);
133
- log(`Output: ${result.outputPath}`);
134
- log('Use "dashcam status" to check progress');
135
- log('Use "dashcam stop" to stop recording and upload');
136
-
137
- // Keep this process alive for background recording
138
- log('Recording is running in background...');
139
-
140
- // Set up signal handlers for graceful shutdown
141
- let isShuttingDown = false;
142
- const handleShutdown = async (signal) => {
143
- if (isShuttingDown) {
144
- log('Shutdown already in progress...');
145
- return;
146
- }
147
- isShuttingDown = true;
148
-
149
- log(`\nReceived ${signal}, stopping background recording...`);
150
- try {
151
- // Stop the recording using the recorder directly (not processManager)
152
- const { stopRecording } = await import('../lib/recorder.js');
153
- const stopResult = await stopRecording();
154
-
155
- if (stopResult) {
156
- log('Recording stopped:', stopResult.outputPath);
157
-
158
- // Import and call upload function with the correct format
159
- const { upload } = await import('../lib/uploader.js');
160
-
161
- log('Starting upload...');
162
- const uploadResult = await upload(stopResult.outputPath, {
163
- title: options.title || 'Dashcam Recording',
164
- description: options.description || 'Recorded with Dashcam CLI',
165
- project: options.project,
166
- duration: stopResult.duration,
167
- clientStartDate: stopResult.clientStartDate,
168
- apps: stopResult.apps,
169
- logs: stopResult.logs,
170
- gifPath: stopResult.gifPath,
171
- snapshotPath: stopResult.snapshotPath
172
- });
173
-
174
- // Write upload result for stop command to read
175
- processManager.writeUploadResult({
176
- shareLink: uploadResult.shareLink,
177
- replayId: uploadResult.replay?.id
178
- });
179
-
180
- log('✅ Upload complete!');
181
- log('📹 Watch your recording:', uploadResult.shareLink);
182
- }
183
-
184
- // Clean up process files, but preserve upload result for stop command
185
- processManager.cleanup({ preserveResult: true });
186
- } catch (error) {
187
- logError('Error during shutdown:', error.message);
188
- logger.error('Error during shutdown:', error);
189
- }
190
- process.exit(0);
191
- };
192
-
193
- process.on('SIGINT', () => handleShutdown('SIGINT'));
194
- process.on('SIGTERM', () => handleShutdown('SIGTERM'));
195
-
196
- // Keep the process alive
197
- await new Promise(() => {});
198
- } catch (error) {
199
- logError('Failed to start recording:', error.message);
200
- process.exit(1);
201
- }
264
+ // Add piped content as a log entry
265
+ logsTrackerManager.addPipedLog(content);
266
+
267
+ process.exit(0);
202
268
  } catch (error) {
203
- logger.error('Failed to start recording:', error);
204
- if (!options.silent) console.error('Failed to start recording:', error.message);
269
+ logger.error('Failed to pipe content:', error);
270
+ console.error('Failed to pipe content:', error.message);
205
271
  process.exit(1);
206
272
  }
207
273
  });
@@ -228,43 +294,94 @@ program
228
294
 
229
295
 
230
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
+
231
310
  program
232
311
  .command('track')
233
- .description('Track logs from web URLs or application files')
234
- .option('--web <pattern>', 'Web URL pattern to track (can use wildcards like *)')
235
- .option('--app <pattern>', 'Application file pattern to track (can use wildcards like *)')
236
- .option('--name <name>', 'Name for the tracking configuration')
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')
237
320
  .action(async (options) => {
238
321
  try {
239
- // Validate that at least one pattern is provided
240
- if (!options.web && !options.app) {
241
- console.error('Error: Must provide either --web or --app pattern');
242
- process.exit(1);
243
- }
244
-
245
- if (options.web) {
246
- const config = {
247
- name: options.name || 'Web Pattern',
248
- type: 'web',
249
- patterns: [options.web],
250
- enabled: true
251
- };
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
+ }
252
338
 
253
- await createPattern(config);
254
- console.log('Web tracking pattern added successfully:', options.web);
255
- }
256
-
257
- if (options.app) {
258
339
  const config = {
259
- name: options.name || 'App Pattern',
260
- type: 'application',
261
- patterns: [options.app],
340
+ name: options.name,
341
+ type: options.type,
342
+ patterns: options.pattern,
262
343
  enabled: true
263
344
  };
264
345
 
265
346
  await createPattern(config);
266
- console.log('Application tracking pattern added successfully:', options.app);
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);
267
383
  }
384
+
268
385
  process.exit(0);
269
386
  } catch (error) {
270
387
  console.error('Failed to add tracking pattern:', error.message);
@@ -566,4 +683,13 @@ program
566
683
  }
567
684
  });
568
685
 
686
+ // If no command specified and there are options like --md, treat as create command
687
+ program.action(async (options) => {
688
+ // Default to create command when running just "dashcam"
689
+ const createCommand = program.commands.find(cmd => cmd.name() === 'create');
690
+ if (createCommand && createCommand._actionHandler) {
691
+ await createCommand._actionHandler(options);
692
+ }
693
+ });
694
+
569
695
  program.parse();
package/lib/logs/index.js CHANGED
@@ -15,7 +15,13 @@ const CLI_CONFIG_FILE = path.join(process.cwd(), '.dashcam', 'cli-config.json');
15
15
 
16
16
  // Simple trim function for CLI (adapted from desktop app)
17
17
  async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipId) {
18
- logger.info('Trimming logs', { count: groupLogStatuses.length });
18
+ logger.info('Trimming logs', {
19
+ count: groupLogStatuses.length,
20
+ startMS,
21
+ endMS,
22
+ clientStartDate,
23
+ clientStartDateReadable: new Date(clientStartDate).toISOString()
24
+ });
19
25
 
20
26
  const REPLAY_DIR = path.join(os.tmpdir(), 'dashcam', 'recordings');
21
27
 
@@ -36,12 +42,24 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
36
42
  let events = content;
37
43
 
38
44
  // Convert events to relative time
39
- let relativeEvents = events.map((event) => {
45
+ let relativeEvents = events.map((event, index) => {
46
+ const originalTime = event.time;
40
47
  event.time = parseInt(event.time + '') - startMS;
41
48
  // Check if it's not already relative time
42
49
  if (event.time > 1_000_000_000_000) {
43
50
  // relative time = absolute time - clip start time
44
51
  event.time = event.time - clientStartDate;
52
+ if (index === 0) {
53
+ // Log first event for debugging
54
+ logger.info('First event timestamp conversion', {
55
+ originalTime,
56
+ clientStartDate,
57
+ relativeTime: event.time,
58
+ relativeSeconds: (event.time / 1000).toFixed(2),
59
+ startMS,
60
+ line: event.line ? event.line.substring(0, 50) : 'N/A'
61
+ });
62
+ }
45
63
  }
46
64
  return event;
47
65
  });
@@ -56,16 +74,19 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
56
74
  });
57
75
 
58
76
  if (status.type === 'cli') {
59
- // Remap logFile indices for CLI logs
60
- let map = {};
61
- filteredEvents = filteredEvents.map((event) => {
62
- let name = map[event.logFile] ?? Object.keys(map).length + 1;
63
- if (!map[event.logFile]) map[event.logFile] = name;
64
- return {
65
- ...event,
66
- logFile: name,
67
- };
77
+ // For CLI logs, keep the file paths in logFile field for UI display
78
+ // No need to remap to numeric indices
79
+
80
+ // Create items array showing unique files and their event counts
81
+ const fileCounts = {};
82
+ filteredEvents.forEach(event => {
83
+ fileCounts[event.logFile] = (fileCounts[event.logFile] || 0) + 1;
68
84
  });
85
+
86
+ status.items = Object.entries(fileCounts).map(([filePath, count]) => ({
87
+ item: filePath,
88
+ count
89
+ }));
69
90
  }
70
91
  } else if (status.type === 'web' && !webHandled) {
71
92
  logger.debug('Found web groupLog, handling all web groupLogs at once');
@@ -243,6 +264,41 @@ class LogsTrackerManager {
243
264
  this.removeCliTrackedPath(filePath);
244
265
  }
245
266
 
267
+ addPipedLog(content) {
268
+ // Find active recording instance
269
+ const activeInstances = Object.values(this.instances);
270
+ if (activeInstances.length === 0) {
271
+ logger.warn('No active recording to add piped content to');
272
+ return;
273
+ }
274
+
275
+ // Add to the most recent active instance (last one)
276
+ const instance = activeInstances[activeInstances.length - 1];
277
+ const timestamp = Date.now();
278
+
279
+ // Write to CLI tracker's temp file
280
+ if (instance.trackers && instance.trackers.cli) {
281
+ const pipedLog = {
282
+ time: timestamp,
283
+ logFile: 'piped-input',
284
+ content: content
285
+ };
286
+
287
+ // Write to the CLI tracker's output file
288
+ try {
289
+ const outputFile = path.join(instance.directory, 'cli-logs.jsonl');
290
+ const logLine = JSON.stringify(pipedLog) + '\n';
291
+ fs.appendFileSync(outputFile, logLine);
292
+ logger.info('Added piped content to recording', {
293
+ recorderId: instance.recorderId,
294
+ contentLength: content.length
295
+ });
296
+ } catch (error) {
297
+ logger.error('Failed to write piped content', { error });
298
+ }
299
+ }
300
+ }
301
+
246
302
  removeTracker(id) {
247
303
  // Try removing from web trackers first
248
304
  if (this.webLogsConfig[id]) {