dashcam 1.0.1-beta.8 → 1.0.2-beta.1
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/.github/workflows/publish.yml +26 -20
- package/691cc08dc2fc02f59ae66f08 (1).mp4 +0 -0
- package/NPM_PUBLISH_FIX.md +104 -0
- package/SINGLE_FRAME_VIDEO_FIX.md +129 -0
- package/bin/dashcam.js +27 -27
- package/lib/auth.js +5 -5
- package/lib/config.js +1 -1
- package/lib/ffmpeg.js +9 -39
- package/lib/processManager.js +23 -36
- package/lib/recorder.js +130 -26
- package/lib/systemInfo.js +141 -0
- package/lib/tracking/FileTracker.js +1 -1
- package/lib/tracking/icons/index.js +3 -2
- package/lib/tracking/icons/linux.js +370 -0
- package/lib/uploader.js +13 -7
- package/package.json +2 -1
- package/scripts/sync-version.sh +48 -0
- package/test-short-recording.js +287 -0
- package/test_workflow.sh +19 -14
- package/BACKWARD_COMPATIBILITY.md +0 -177
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { logger } from "../../logger.js";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find icon for a Linux application using various strategies
|
|
9
|
+
*/
|
|
10
|
+
const findLinuxIcon = async (appName) => {
|
|
11
|
+
// Strategy 1: Look for .desktop file
|
|
12
|
+
const desktopFile = await findDesktopFile(appName);
|
|
13
|
+
if (desktopFile) {
|
|
14
|
+
const iconName = await extractIconFromDesktop(desktopFile);
|
|
15
|
+
if (iconName) {
|
|
16
|
+
const iconPath = await findIconInTheme(iconName);
|
|
17
|
+
if (iconPath) {
|
|
18
|
+
logger.debug("Found icon via .desktop file", { appName, iconPath });
|
|
19
|
+
return iconPath;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Strategy 2: Try to find icon directly in icon themes
|
|
25
|
+
const iconPath = await findIconInTheme(appName);
|
|
26
|
+
if (iconPath) {
|
|
27
|
+
logger.debug("Found icon in theme", { appName, iconPath });
|
|
28
|
+
return iconPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Strategy 3: Common application paths
|
|
32
|
+
const commonPaths = [
|
|
33
|
+
`/usr/share/pixmaps/${appName}.png`,
|
|
34
|
+
`/usr/share/pixmaps/${appName}.svg`,
|
|
35
|
+
`/usr/share/icons/hicolor/48x48/apps/${appName}.png`,
|
|
36
|
+
`/usr/share/icons/hicolor/scalable/apps/${appName}.svg`,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const iconPath of commonPaths) {
|
|
40
|
+
if (fs.existsSync(iconPath)) {
|
|
41
|
+
logger.debug("Found icon in common path", { appName, iconPath });
|
|
42
|
+
return iconPath;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.debug("No icon found for Linux app", { appName });
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calculate string similarity (Levenshtein distance based)
|
|
52
|
+
* Returns a value between 0 (no match) and 1 (perfect match)
|
|
53
|
+
*/
|
|
54
|
+
const calculateSimilarity = (str1, str2) => {
|
|
55
|
+
const s1 = str1.toLowerCase();
|
|
56
|
+
const s2 = str2.toLowerCase();
|
|
57
|
+
|
|
58
|
+
// Exact match
|
|
59
|
+
if (s1 === s2) return 1.0;
|
|
60
|
+
|
|
61
|
+
// Check if one contains the other
|
|
62
|
+
if (s1.includes(s2) || s2.includes(s1)) {
|
|
63
|
+
return 0.8;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Simple Levenshtein-based similarity
|
|
67
|
+
const longer = s1.length > s2.length ? s1 : s2;
|
|
68
|
+
const shorter = s1.length > s2.length ? s2 : s1;
|
|
69
|
+
|
|
70
|
+
if (longer.length === 0) return 1.0;
|
|
71
|
+
|
|
72
|
+
const editDistance = levenshteinDistance(s1, s2);
|
|
73
|
+
return (longer.length - editDistance) / longer.length;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculate Levenshtein distance between two strings
|
|
78
|
+
*/
|
|
79
|
+
const levenshteinDistance = (str1, str2) => {
|
|
80
|
+
const matrix = [];
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
83
|
+
matrix[i] = [i];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
87
|
+
matrix[0][j] = j;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
91
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
92
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
93
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
94
|
+
} else {
|
|
95
|
+
matrix[i][j] = Math.min(
|
|
96
|
+
matrix[i - 1][j - 1] + 1, // substitution
|
|
97
|
+
matrix[i][j - 1] + 1, // insertion
|
|
98
|
+
matrix[i - 1][j] + 1 // deletion
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return matrix[str2.length][str1.length];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find .desktop file for an application
|
|
109
|
+
*/
|
|
110
|
+
const findDesktopFile = async (appName) => {
|
|
111
|
+
const desktopDirs = [
|
|
112
|
+
"/usr/share/applications",
|
|
113
|
+
"/usr/local/share/applications",
|
|
114
|
+
path.join(os.homedir(), ".local/share/applications"),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// Try exact match first
|
|
118
|
+
for (const dir of desktopDirs) {
|
|
119
|
+
const desktopFile = path.join(dir, `${appName}.desktop`);
|
|
120
|
+
if (fs.existsSync(desktopFile)) {
|
|
121
|
+
logger.debug("Found desktop file (exact match)", { appName, desktopFile });
|
|
122
|
+
return desktopFile;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Try case-insensitive exact match
|
|
127
|
+
for (const dir of desktopDirs) {
|
|
128
|
+
try {
|
|
129
|
+
if (!fs.existsSync(dir)) continue;
|
|
130
|
+
|
|
131
|
+
const files = fs.readdirSync(dir);
|
|
132
|
+
const match = files.find(
|
|
133
|
+
(f) => f.toLowerCase() === `${appName.toLowerCase()}.desktop`
|
|
134
|
+
);
|
|
135
|
+
if (match) {
|
|
136
|
+
logger.debug("Found desktop file (case-insensitive)", { appName, match });
|
|
137
|
+
return path.join(dir, match);
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.debug("Error reading desktop directory", { dir, error: error.message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Try fuzzy matching - find best match based on string similarity
|
|
145
|
+
let bestMatch = null;
|
|
146
|
+
let bestScore = 0.6; // Minimum similarity threshold
|
|
147
|
+
|
|
148
|
+
for (const dir of desktopDirs) {
|
|
149
|
+
try {
|
|
150
|
+
if (!fs.existsSync(dir)) continue;
|
|
151
|
+
|
|
152
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.desktop'));
|
|
153
|
+
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const baseName = file.replace('.desktop', '');
|
|
156
|
+
const similarity = calculateSimilarity(appName, baseName);
|
|
157
|
+
|
|
158
|
+
if (similarity > bestScore) {
|
|
159
|
+
bestScore = similarity;
|
|
160
|
+
bestMatch = path.join(dir, file);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logger.debug("Error in fuzzy desktop file search", { dir, error: error.message });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (bestMatch) {
|
|
169
|
+
logger.debug("Found desktop file (fuzzy match)", {
|
|
170
|
+
appName,
|
|
171
|
+
desktopFile: bestMatch,
|
|
172
|
+
similarity: bestScore.toFixed(2)
|
|
173
|
+
});
|
|
174
|
+
return bestMatch;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
logger.debug("No desktop file found", { appName });
|
|
178
|
+
return null;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract icon name from .desktop file
|
|
183
|
+
*/
|
|
184
|
+
const extractIconFromDesktop = async (desktopFilePath) => {
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(desktopFilePath, "utf8");
|
|
187
|
+
const iconMatch = content.match(/^Icon=(.+)$/m);
|
|
188
|
+
if (iconMatch) {
|
|
189
|
+
return iconMatch[1].trim();
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.debug("Error reading desktop file", {
|
|
193
|
+
desktopFilePath,
|
|
194
|
+
error: error.message
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Find icon in XDG icon themes
|
|
202
|
+
*/
|
|
203
|
+
const findIconInTheme = async (iconName) => {
|
|
204
|
+
// Common icon theme locations and sizes
|
|
205
|
+
const iconThemes = ["hicolor", "gnome", "Adwaita", "breeze", "oxygen"];
|
|
206
|
+
const iconSizes = ["48x48", "64x64", "scalable", "128x128", "256x256"];
|
|
207
|
+
const iconFormats = ["png", "svg", "xpm"];
|
|
208
|
+
|
|
209
|
+
const searchPaths = [
|
|
210
|
+
"/usr/share/icons",
|
|
211
|
+
"/usr/local/share/icons",
|
|
212
|
+
path.join(os.homedir(), ".local/share/icons"),
|
|
213
|
+
path.join(os.homedir(), ".icons"),
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const basePath of searchPaths) {
|
|
217
|
+
if (!fs.existsSync(basePath)) continue;
|
|
218
|
+
|
|
219
|
+
for (const theme of iconThemes) {
|
|
220
|
+
const themePath = path.join(basePath, theme);
|
|
221
|
+
if (!fs.existsSync(themePath)) continue;
|
|
222
|
+
|
|
223
|
+
for (const size of iconSizes) {
|
|
224
|
+
const sizePath = path.join(themePath, size, "apps");
|
|
225
|
+
if (!fs.existsSync(sizePath)) continue;
|
|
226
|
+
|
|
227
|
+
for (const format of iconFormats) {
|
|
228
|
+
const iconPath = path.join(sizePath, `${iconName}.${format}`);
|
|
229
|
+
if (fs.existsSync(iconPath)) {
|
|
230
|
+
return iconPath;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Convert image to PNG if needed
|
|
242
|
+
*/
|
|
243
|
+
const convertToPng = async (iconPath) => {
|
|
244
|
+
const ext = path.extname(iconPath).toLowerCase();
|
|
245
|
+
|
|
246
|
+
// If already PNG, read and return
|
|
247
|
+
if (ext === ".png") {
|
|
248
|
+
return fs.readFileSync(iconPath);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// For SVG, try to convert using ImageMagick or rsvg-convert
|
|
252
|
+
if (ext === ".svg") {
|
|
253
|
+
const tmpPngPath = path.join(os.tmpdir(), `icon-${Date.now()}.png`);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// Try rsvg-convert first (commonly available on Linux)
|
|
257
|
+
await execa("rsvg-convert", [
|
|
258
|
+
"-w", "48",
|
|
259
|
+
"-h", "48",
|
|
260
|
+
"-o", tmpPngPath,
|
|
261
|
+
iconPath
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const buffer = fs.readFileSync(tmpPngPath);
|
|
265
|
+
fs.unlinkSync(tmpPngPath);
|
|
266
|
+
return buffer;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logger.debug("rsvg-convert failed, trying ImageMagick", { error: error.message });
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// Fallback to ImageMagick convert
|
|
272
|
+
await execa("convert", [
|
|
273
|
+
"-background", "none",
|
|
274
|
+
"-resize", "48x48",
|
|
275
|
+
iconPath,
|
|
276
|
+
tmpPngPath
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
const buffer = fs.readFileSync(tmpPngPath);
|
|
280
|
+
fs.unlinkSync(tmpPngPath);
|
|
281
|
+
return buffer;
|
|
282
|
+
} catch (convertError) {
|
|
283
|
+
logger.debug("ImageMagick convert failed", { error: convertError.message });
|
|
284
|
+
|
|
285
|
+
// Clean up temp file if it exists
|
|
286
|
+
if (fs.existsSync(tmpPngPath)) {
|
|
287
|
+
fs.unlinkSync(tmpPngPath);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// For XPM, try ImageMagick
|
|
296
|
+
if (ext === ".xpm") {
|
|
297
|
+
const tmpPngPath = path.join(os.tmpdir(), `icon-${Date.now()}.png`);
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await execa("convert", [
|
|
301
|
+
"-background", "none",
|
|
302
|
+
"-resize", "48x48",
|
|
303
|
+
iconPath,
|
|
304
|
+
tmpPngPath
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const buffer = fs.readFileSync(tmpPngPath);
|
|
308
|
+
fs.unlinkSync(tmpPngPath);
|
|
309
|
+
return buffer;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.debug("Failed to convert XPM to PNG", { error: error.message });
|
|
312
|
+
|
|
313
|
+
// Clean up temp file if it exists
|
|
314
|
+
if (fs.existsSync(tmpPngPath)) {
|
|
315
|
+
fs.unlinkSync(tmpPngPath);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
logger.debug("Unsupported icon format", { ext, iconPath });
|
|
323
|
+
return null;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get icon as buffer for Linux application
|
|
328
|
+
* @param {string} appPath - Path to the application or process name
|
|
329
|
+
*/
|
|
330
|
+
const getIconAsBuffer = async (appPath) => {
|
|
331
|
+
try {
|
|
332
|
+
// Extract app name from path
|
|
333
|
+
let appName = path.basename(appPath);
|
|
334
|
+
|
|
335
|
+
// Remove common extensions
|
|
336
|
+
appName = appName.replace(/\.(exe|bin|sh|py|js)$/i, "");
|
|
337
|
+
|
|
338
|
+
logger.debug("Extracting icon for Linux app", { appName, appPath });
|
|
339
|
+
|
|
340
|
+
// Find the icon file
|
|
341
|
+
const iconPath = await findLinuxIcon(appName);
|
|
342
|
+
if (!iconPath) {
|
|
343
|
+
logger.debug("No icon found for Linux app", { appName });
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Convert to PNG if needed
|
|
348
|
+
const buffer = await convertToPng(iconPath);
|
|
349
|
+
if (!buffer) {
|
|
350
|
+
logger.debug("Failed to convert icon to PNG", { iconPath });
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
logger.debug("Successfully extracted Linux icon", {
|
|
355
|
+
appName,
|
|
356
|
+
iconPath,
|
|
357
|
+
bufferSize: buffer.length,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return { extension: "png", buffer };
|
|
361
|
+
} catch (error) {
|
|
362
|
+
logger.warn("Failed to extract Linux icon", {
|
|
363
|
+
appPath,
|
|
364
|
+
error: error.message,
|
|
365
|
+
});
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export { getIconAsBuffer };
|
package/lib/uploader.js
CHANGED
|
@@ -5,6 +5,7 @@ import { logger, logFunctionCall } from './logger.js';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { auth } from './auth.js';
|
|
7
7
|
import got from 'got';
|
|
8
|
+
import { getSystemInfo } from './systemInfo.js';
|
|
8
9
|
|
|
9
10
|
class Uploader {
|
|
10
11
|
constructor() {
|
|
@@ -200,6 +201,15 @@ export async function upload(filePath, metadata = {}) {
|
|
|
200
201
|
hasProject: !!metadata.project
|
|
201
202
|
});
|
|
202
203
|
|
|
204
|
+
// Collect system information
|
|
205
|
+
logger.debug('Collecting system information...');
|
|
206
|
+
const systemInfo = await getSystemInfo();
|
|
207
|
+
logger.verbose('System information collected for upload', {
|
|
208
|
+
cpuBrand: systemInfo.cpu?.brand,
|
|
209
|
+
osDistro: systemInfo.os?.distro,
|
|
210
|
+
totalMemGB: systemInfo.mem?.total ? (systemInfo.mem.total / (1024 * 1024 * 1024)).toFixed(2) : 'unknown'
|
|
211
|
+
});
|
|
212
|
+
|
|
203
213
|
// Handle project ID - use provided project or fetch first available project
|
|
204
214
|
let projectId = metadata.project;
|
|
205
215
|
if (!projectId) {
|
|
@@ -229,11 +239,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
229
239
|
duration: metadata.duration || 0,
|
|
230
240
|
apps: metadata.apps && metadata.apps.length > 0 ? metadata.apps : ['Screen Recording'], // Use tracked apps or fallback
|
|
231
241
|
title: metadata.title || defaultTitle,
|
|
232
|
-
system:
|
|
233
|
-
platform: process.platform,
|
|
234
|
-
arch: process.arch,
|
|
235
|
-
nodeVersion: process.version
|
|
236
|
-
},
|
|
242
|
+
system: systemInfo, // Include system information
|
|
237
243
|
clientStartDate: metadata.clientStartDate || Date.now() // Use actual recording start time
|
|
238
244
|
};
|
|
239
245
|
|
|
@@ -255,7 +261,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
255
261
|
|
|
256
262
|
let newReplay;
|
|
257
263
|
try {
|
|
258
|
-
newReplay = await got.post('https://api.
|
|
264
|
+
newReplay = await got.post('https://testdriver-api.onrender.com/api/v1/replay', {
|
|
259
265
|
headers: {
|
|
260
266
|
Authorization: `Bearer ${token}`
|
|
261
267
|
},
|
|
@@ -430,7 +436,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
430
436
|
|
|
431
437
|
// Publish the replay (like the desktop app does)
|
|
432
438
|
logger.debug('Publishing replay...');
|
|
433
|
-
await got.post('https://api.
|
|
439
|
+
await got.post('https://testdriver-api.onrender.com/api/v1/replay/publish', {
|
|
434
440
|
headers: {
|
|
435
441
|
Authorization: `Bearer ${token}`
|
|
436
442
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dashcam",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2-beta.1",
|
|
4
4
|
"description": "Minimal CLI version of Dashcam desktop app",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"mask-sensitive-data": "^0.11.5",
|
|
48
48
|
"node-fetch": "^2.7.0",
|
|
49
49
|
"open": "^9.1.0",
|
|
50
|
+
"systeminformation": "^5.18.15",
|
|
50
51
|
"tail": "^2.2.6",
|
|
51
52
|
"winston": "^3.11.0",
|
|
52
53
|
"ws": "^8.18.3"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Sync local version with npm registry
|
|
4
|
+
# This fixes version drift when publishes fail
|
|
5
|
+
|
|
6
|
+
echo "🔍 Checking version sync..."
|
|
7
|
+
|
|
8
|
+
# Get current version in package.json
|
|
9
|
+
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
|
10
|
+
echo "📦 Local version: $LOCAL_VERSION"
|
|
11
|
+
|
|
12
|
+
# Get current beta version from npm
|
|
13
|
+
NPM_BETA_VERSION=$(npm view dashcam dist-tags.beta 2>/dev/null)
|
|
14
|
+
if [ -z "$NPM_BETA_VERSION" ]; then
|
|
15
|
+
echo "⚠️ No beta tag found on npm, checking latest version..."
|
|
16
|
+
NPM_BETA_VERSION=$(npm view dashcam versions --json | jq -r '.[] | select(contains("beta"))' | tail -1)
|
|
17
|
+
fi
|
|
18
|
+
echo "📡 NPM beta version: $NPM_BETA_VERSION"
|
|
19
|
+
|
|
20
|
+
# Get all local git tags
|
|
21
|
+
echo ""
|
|
22
|
+
echo "🏷️ Local git tags:"
|
|
23
|
+
git tag | grep beta | tail -5
|
|
24
|
+
|
|
25
|
+
echo ""
|
|
26
|
+
echo "📡 NPM published versions:"
|
|
27
|
+
npm view dashcam versions --json | jq -r '.[] | select(contains("beta"))' | tail -5
|
|
28
|
+
|
|
29
|
+
echo ""
|
|
30
|
+
if [ "$LOCAL_VERSION" != "$NPM_BETA_VERSION" ]; then
|
|
31
|
+
echo "⚠️ Version mismatch detected!"
|
|
32
|
+
echo " Local: $LOCAL_VERSION"
|
|
33
|
+
echo " NPM: $NPM_BETA_VERSION"
|
|
34
|
+
echo ""
|
|
35
|
+
read -p "Do you want to sync package.json to $NPM_BETA_VERSION? (y/n) " -n 1 -r
|
|
36
|
+
echo
|
|
37
|
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
38
|
+
npm version $NPM_BETA_VERSION --no-git-tag-version --allow-same-version
|
|
39
|
+
echo "✅ Synced package.json to $NPM_BETA_VERSION"
|
|
40
|
+
echo ""
|
|
41
|
+
echo "⚠️ Note: You may have unpublished git tags. To clean them up:"
|
|
42
|
+
echo " git tag | grep beta | tail -10 # Review tags"
|
|
43
|
+
echo " git tag -d v1.0.1-beta.XX # Delete unpublished tags"
|
|
44
|
+
echo " git push origin :refs/tags/v1.0.1-beta.XX # Delete remote tags"
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
echo "✅ Versions are in sync!"
|
|
48
|
+
fi
|