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.
- package/.dashcam/cli-config.json +3 -0
- package/.dashcam/recording.log +135 -0
- package/.dashcam/web-config.json +11 -0
- package/.github/RELEASE.md +59 -0
- package/.github/workflows/publish.yml +43 -0
- package/BACKWARD_COMPATIBILITY.md +177 -0
- package/LOG_TRACKING_GUIDE.md +225 -0
- package/README.md +709 -155
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.cjs +8 -0
- package/bin/dashcam.js +703 -0
- package/bin/index.js +63 -0
- package/examples/execute-script.js +152 -0
- package/examples/simple-test.js +37 -0
- package/lib/applicationTracker.js +311 -0
- package/lib/auth.js +222 -0
- package/lib/binaries.js +21 -0
- package/lib/config.js +34 -0
- package/lib/extension-logs/helpers.js +182 -0
- package/lib/extension-logs/index.js +347 -0
- package/lib/extension-logs/manager.js +344 -0
- package/lib/ffmpeg.js +155 -0
- package/lib/logTracker.js +23 -0
- package/lib/logger.js +118 -0
- package/lib/logs/index.js +488 -0
- package/lib/permissions.js +85 -0
- package/lib/processManager.js +317 -0
- package/lib/recorder.js +690 -0
- package/lib/store.js +58 -0
- package/lib/tracking/FileTracker.js +105 -0
- package/lib/tracking/FileTrackerManager.js +62 -0
- package/lib/tracking/LogsTracker.js +161 -0
- package/lib/tracking/active-win.js +212 -0
- package/lib/tracking/icons/darwin.js +39 -0
- package/lib/tracking/icons/index.js +167 -0
- package/lib/tracking/icons/windows.js +27 -0
- package/lib/tracking/idle.js +82 -0
- package/lib/tracking.js +23 -0
- package/lib/uploader.js +456 -0
- package/lib/utilities/jsonl.js +77 -0
- package/lib/webLogsDaemon.js +234 -0
- package/lib/websocket/server.js +223 -0
- package/package.json +53 -21
- package/recording.log +814 -0
- package/sea-bundle.mjs +34595 -0
- package/test-page.html +15 -0
- package/test.log +1 -0
- package/test_run.log +48 -0
- package/test_workflow.sh +154 -0
- package/examples/crash-test.js +0 -11
- package/examples/github-issue.sh +0 -1
- package/examples/protocol.html +0 -22
- package/index.js +0 -158
- package/lib.js +0 -199
- 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(`[](${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();
|