chrome-relay 0.5.20 → 0.5.22
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/dist/cli.js +156 -41
- package/dist/index.js +1 -1
- package/dist/native-host.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1101,7 +1101,7 @@ var init_dist = __esm({
|
|
|
1101
1101
|
import { Command } from "commander";
|
|
1102
1102
|
|
|
1103
1103
|
// src/index.ts
|
|
1104
|
-
var CHROME_RELAY_VERSION = true ? "0.5.
|
|
1104
|
+
var CHROME_RELAY_VERSION = true ? "0.5.22" : "0.0.0-dev";
|
|
1105
1105
|
|
|
1106
1106
|
// src/commands/shared.ts
|
|
1107
1107
|
init_dist();
|
|
@@ -1237,6 +1237,7 @@ function isToolName(name) {
|
|
|
1237
1237
|
init_dist();
|
|
1238
1238
|
import os from "os";
|
|
1239
1239
|
import path from "path";
|
|
1240
|
+
import { spawnSync } from "child_process";
|
|
1240
1241
|
import { chmod, mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
1241
1242
|
import { fileURLToPath } from "url";
|
|
1242
1243
|
var APP_DIR = path.join(os.homedir(), ".chrome-relay");
|
|
@@ -1254,18 +1255,52 @@ function getDefaultAllowedOrigins() {
|
|
|
1254
1255
|
function formatKnownExtensionIds() {
|
|
1255
1256
|
return KNOWN_EXTENSION_IDS.map(([label, id]) => `${label}: ${id}`).join(", ");
|
|
1256
1257
|
}
|
|
1257
|
-
function
|
|
1258
|
+
function getChromiumBrowserTargets() {
|
|
1259
|
+
const home = os.homedir();
|
|
1258
1260
|
if (process.platform === "darwin") {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
"
|
|
1262
|
-
|
|
1261
|
+
const appSupport = path.join(home, "Library/Application Support");
|
|
1262
|
+
return [
|
|
1263
|
+
{ label: "Google Chrome", installRoot: path.join(appSupport, "Google/Chrome"), manifestDir: path.join(appSupport, "Google/Chrome/NativeMessagingHosts") },
|
|
1264
|
+
{ label: "Google Chrome Canary", installRoot: path.join(appSupport, "Google/Chrome Canary"), manifestDir: path.join(appSupport, "Google/Chrome Canary/NativeMessagingHosts") },
|
|
1265
|
+
{ label: "Chromium", installRoot: path.join(appSupport, "Chromium"), manifestDir: path.join(appSupport, "Chromium/NativeMessagingHosts") },
|
|
1266
|
+
{ label: "Microsoft Edge", installRoot: path.join(appSupport, "Microsoft Edge"), manifestDir: path.join(appSupport, "Microsoft Edge/NativeMessagingHosts") },
|
|
1267
|
+
{ label: "Brave", installRoot: path.join(appSupport, "BraveSoftware/Brave-Browser"), manifestDir: path.join(appSupport, "BraveSoftware/Brave-Browser/NativeMessagingHosts") },
|
|
1268
|
+
{ label: "Vivaldi", installRoot: path.join(appSupport, "Vivaldi"), manifestDir: path.join(appSupport, "Vivaldi/NativeMessagingHosts") },
|
|
1269
|
+
{ label: "Arc", installRoot: path.join(appSupport, "Arc/User Data"), manifestDir: path.join(appSupport, "Arc/User Data/NativeMessagingHosts") },
|
|
1270
|
+
{ label: "Opera", installRoot: path.join(appSupport, "com.operasoftware.Opera"), manifestDir: path.join(appSupport, "com.operasoftware.Opera/NativeMessagingHosts") }
|
|
1271
|
+
];
|
|
1263
1272
|
}
|
|
1264
1273
|
if (process.platform === "linux") {
|
|
1265
|
-
|
|
1274
|
+
const config = path.join(home, ".config");
|
|
1275
|
+
return [
|
|
1276
|
+
{ label: "Google Chrome", installRoot: path.join(config, "google-chrome"), manifestDir: path.join(config, "google-chrome/NativeMessagingHosts") },
|
|
1277
|
+
{ label: "Chromium", installRoot: path.join(config, "chromium"), manifestDir: path.join(config, "chromium/NativeMessagingHosts") },
|
|
1278
|
+
{ label: "Microsoft Edge", installRoot: path.join(config, "microsoft-edge"), manifestDir: path.join(config, "microsoft-edge/NativeMessagingHosts") },
|
|
1279
|
+
{ label: "Brave", installRoot: path.join(config, "BraveSoftware/Brave-Browser"), manifestDir: path.join(config, "BraveSoftware/Brave-Browser/NativeMessagingHosts") },
|
|
1280
|
+
{ label: "Vivaldi", installRoot: path.join(config, "vivaldi"), manifestDir: path.join(config, "vivaldi/NativeMessagingHosts") },
|
|
1281
|
+
{ label: "Opera", installRoot: path.join(config, "opera"), manifestDir: path.join(config, "opera/NativeMessagingHosts") }
|
|
1282
|
+
];
|
|
1266
1283
|
}
|
|
1267
1284
|
throw new Error(`Unsupported platform for install: ${process.platform}`);
|
|
1268
1285
|
}
|
|
1286
|
+
async function pathExists(p) {
|
|
1287
|
+
try {
|
|
1288
|
+
await stat(p);
|
|
1289
|
+
return true;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async function getInstalledBrowsers() {
|
|
1295
|
+
const all = getChromiumBrowserTargets();
|
|
1296
|
+
const installed = [];
|
|
1297
|
+
for (const target of all) {
|
|
1298
|
+
if (await pathExists(target.installRoot)) {
|
|
1299
|
+
installed.push(target);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return installed;
|
|
1303
|
+
}
|
|
1269
1304
|
function getDistDir() {
|
|
1270
1305
|
return path.dirname(fileURLToPath(import.meta.url));
|
|
1271
1306
|
}
|
|
@@ -1279,10 +1314,7 @@ exec "${process.execPath}" "${hostPath}"
|
|
|
1279
1314
|
await chmod(wrapperPath, 493);
|
|
1280
1315
|
return wrapperPath;
|
|
1281
1316
|
}
|
|
1282
|
-
async function
|
|
1283
|
-
const manifestDir = getChromeManifestDir();
|
|
1284
|
-
await mkdir(manifestDir, { recursive: true });
|
|
1285
|
-
const manifestPath = path.join(manifestDir, `${NATIVE_HOST_NAME}.json`);
|
|
1317
|
+
async function writeManifestsForBrowsers(wrapperPath, browsers) {
|
|
1286
1318
|
const manifest = {
|
|
1287
1319
|
name: NATIVE_HOST_NAME,
|
|
1288
1320
|
description: "Native host for Chrome Relay",
|
|
@@ -1290,32 +1322,104 @@ async function writeManifest(wrapperPath) {
|
|
|
1290
1322
|
type: "stdio",
|
|
1291
1323
|
allowed_origins: getDefaultAllowedOrigins()
|
|
1292
1324
|
};
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1325
|
+
const body = `${JSON.stringify(manifest, null, 2)}
|
|
1326
|
+
`;
|
|
1327
|
+
const written = [];
|
|
1328
|
+
for (const target of browsers) {
|
|
1329
|
+
await mkdir(target.manifestDir, { recursive: true });
|
|
1330
|
+
const manifestPath = path.join(target.manifestDir, `${NATIVE_HOST_NAME}.json`);
|
|
1331
|
+
await writeFile(manifestPath, body, "utf8");
|
|
1332
|
+
written.push({ browser: target.label, manifestPath });
|
|
1333
|
+
}
|
|
1334
|
+
return written;
|
|
1335
|
+
}
|
|
1336
|
+
function killStaleNativeHosts() {
|
|
1337
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
1338
|
+
return { killed: 0 };
|
|
1339
|
+
}
|
|
1340
|
+
const ps = spawnSync("ps", ["-A", "-o", "pid=,command="], { encoding: "utf8" });
|
|
1341
|
+
if (ps.status !== 0 || !ps.stdout) return { killed: 0 };
|
|
1342
|
+
let killed = 0;
|
|
1343
|
+
for (const raw of ps.stdout.split("\n")) {
|
|
1344
|
+
const line = raw.trim();
|
|
1345
|
+
if (!line) continue;
|
|
1346
|
+
if (!line.includes("chrome-relay") || !line.includes("native-host.js")) continue;
|
|
1347
|
+
const m = line.match(/^(\d+)\s/);
|
|
1348
|
+
if (!m) continue;
|
|
1349
|
+
const pid = Number.parseInt(m[1], 10);
|
|
1350
|
+
if (pid === process.pid) continue;
|
|
1351
|
+
try {
|
|
1352
|
+
process.kill(pid, "SIGTERM");
|
|
1353
|
+
killed++;
|
|
1354
|
+
} catch {
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return { killed };
|
|
1296
1358
|
}
|
|
1297
1359
|
async function runInstall() {
|
|
1298
1360
|
const distDir = getDistDir();
|
|
1299
1361
|
const hostPath = path.join(distDir, "native-host.js");
|
|
1300
1362
|
const wrapperPath = await writeWrapperScript(hostPath);
|
|
1301
|
-
const
|
|
1363
|
+
const installed = await getInstalledBrowsers();
|
|
1364
|
+
if (installed.length === 0) {
|
|
1365
|
+
const all = getChromiumBrowserTargets();
|
|
1366
|
+
const fallback = all.find((t) => t.label === "Google Chrome");
|
|
1367
|
+
if (fallback) installed.push(fallback);
|
|
1368
|
+
}
|
|
1369
|
+
const writtenManifests = await writeManifestsForBrowsers(wrapperPath, installed);
|
|
1370
|
+
const { killed } = killStaleNativeHosts();
|
|
1302
1371
|
console.log(`Installed Chrome Relay native host.`);
|
|
1303
1372
|
console.log(`Wrapper: ${wrapperPath}`);
|
|
1304
|
-
console.log(`
|
|
1373
|
+
console.log(`Manifests written:`);
|
|
1374
|
+
for (const m of writtenManifests) {
|
|
1375
|
+
console.log(` \u2022 ${m.browser}: ${m.manifestPath}`);
|
|
1376
|
+
}
|
|
1305
1377
|
console.log(`Local bridge port: ${DEFAULT_HTTP_PORT}`);
|
|
1306
1378
|
console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
|
|
1379
|
+
if (killed > 0) {
|
|
1380
|
+
console.log(`Reaped ${killed} stale native-host process${killed === 1 ? "" : "es"}; browsers will respawn from the new manifest.`);
|
|
1381
|
+
}
|
|
1307
1382
|
}
|
|
1308
1383
|
async function runDoctor() {
|
|
1309
1384
|
try {
|
|
1310
1385
|
const wrapperPath = path.join(APP_DIR, "run-host.sh");
|
|
1311
|
-
const manifestPath = path.join(getChromeManifestDir(), `${NATIVE_HOST_NAME}.json`);
|
|
1312
1386
|
await stat(wrapperPath);
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
(
|
|
1318
|
-
|
|
1387
|
+
console.log(`Wrapper present: yes`);
|
|
1388
|
+
const installed = await getInstalledBrowsers();
|
|
1389
|
+
if (installed.length === 0) {
|
|
1390
|
+
console.log(`No Chromium-based browsers detected.`);
|
|
1391
|
+
console.log(`Tip: install Chrome / Arc / Brave / Edge / Chromium / Vivaldi / Opera then re-run "chrome-relay install".`);
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
const required = getDefaultAllowedOrigins();
|
|
1395
|
+
let allHealthy = true;
|
|
1396
|
+
console.log(`Detected browsers (${installed.length}):`);
|
|
1397
|
+
for (const target of installed) {
|
|
1398
|
+
const manifestPath = path.join(target.manifestDir, `${NATIVE_HOST_NAME}.json`);
|
|
1399
|
+
const exists = await pathExists(manifestPath);
|
|
1400
|
+
if (!exists) {
|
|
1401
|
+
allHealthy = false;
|
|
1402
|
+
console.log(` \u2022 ${target.label}: manifest MISSING (${manifestPath})`);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1407
|
+
const allowedOrigins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : [];
|
|
1408
|
+
const missingOrigins = required.filter((o) => !allowedOrigins.includes(o));
|
|
1409
|
+
if (missingOrigins.length > 0) {
|
|
1410
|
+
allHealthy = false;
|
|
1411
|
+
console.log(` \u2022 ${target.label}: manifest present but missing origins: ${missingOrigins.join(", ")}`);
|
|
1412
|
+
} else {
|
|
1413
|
+
console.log(` \u2022 ${target.label}: ok`);
|
|
1414
|
+
}
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
allHealthy = false;
|
|
1417
|
+
console.log(` \u2022 ${target.label}: manifest unreadable (${e instanceof Error ? e.message : String(e)})`);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
if (!allHealthy) {
|
|
1421
|
+
console.log(`Tip: run "chrome-relay install" to refresh manifests for every detected browser.`);
|
|
1422
|
+
}
|
|
1319
1423
|
let serverReachable = false;
|
|
1320
1424
|
try {
|
|
1321
1425
|
const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/ping`);
|
|
@@ -1323,19 +1427,12 @@ async function runDoctor() {
|
|
|
1323
1427
|
} catch {
|
|
1324
1428
|
serverReachable = false;
|
|
1325
1429
|
}
|
|
1326
|
-
console.log(`Wrapper present: yes`);
|
|
1327
|
-
console.log(`Manifest present: yes`);
|
|
1328
1430
|
console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
|
|
1329
|
-
console.log(`Allowed origins: ${(manifest.allowed_origins ?? ["missing"]).join(", ")}`);
|
|
1330
|
-
if (missingOrigins.length > 0) {
|
|
1331
|
-
console.log(`Manifest missing origins: ${missingOrigins.join(", ")}`);
|
|
1332
|
-
console.log(`Tip: run "chrome-relay install" to refresh the native host manifest.`);
|
|
1333
|
-
}
|
|
1334
1431
|
console.log(`Local bridge reachable: ${serverReachable ? "yes" : "no"}`);
|
|
1335
1432
|
if (!serverReachable) {
|
|
1336
|
-
console.log(`Tip: load the extension so it can launch the native host.`);
|
|
1433
|
+
console.log(`Tip: load the extension in one of the detected browsers so it can launch the native host.`);
|
|
1337
1434
|
}
|
|
1338
|
-
return
|
|
1435
|
+
return allHealthy;
|
|
1339
1436
|
} catch (error) {
|
|
1340
1437
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1341
1438
|
return false;
|
|
@@ -1344,6 +1441,17 @@ async function runDoctor() {
|
|
|
1344
1441
|
|
|
1345
1442
|
// src/release-notes.ts
|
|
1346
1443
|
var RELEASE_NOTES = {
|
|
1444
|
+
"0.5.22": [
|
|
1445
|
+
"Multi-browser install. `chrome-relay install` now writes the native-messaging manifest into every detected Chromium-fork browser's NativeMessagingHosts dir, not just Google Chrome's. Detected: Chrome, Chrome Canary, Chromium, Edge, Brave, Vivaldi, Arc, Opera (macOS + Linux paths). Detection is parent-dir existence \u2014 we never speculatively create profile dirs for browsers that aren't installed.",
|
|
1446
|
+
"Why this matters: the extension installs fine via Chrome Web Store in any Chromium fork, but the bridge silently failed because the host manifest was only at Chrome's path. Arc + Brave users hit `connectNative()` errors with no obvious cause.",
|
|
1447
|
+
"`chrome-relay doctor` now reports per-browser manifest status, so when a refresh is needed the failure points at the specific browser whose manifest drifted.",
|
|
1448
|
+
"Fallback: if no Chromium browser is detected on the machine, we still drop the manifest at Chrome's path so a later Chrome install picks it up."
|
|
1449
|
+
],
|
|
1450
|
+
"0.5.21": [
|
|
1451
|
+
"Fix: `chrome-relay update` and `chrome-relay install` now SIGTERM any running native-host.js process before exiting, and `update` re-runs `install` from the freshly-installed binary. Chrome respawns the host from the new manifest on its next native-messaging request.",
|
|
1452
|
+
"Why this matters: Chrome's native messaging keeps the host process alive for the session. Pre-0.5.21, `chrome-relay update` refreshed the on-disk package but Chrome kept routing through the OLD host. The HTTP bridge served by that old host then reported its own embedded `CHROME_RELAY_VERSION`, which falsely tripped the cli-outdated nudge against the newer extension. Users running the very command the nudge told them to run found the nudge still firing afterwards \u2014 the worst kind of UX bug.",
|
|
1453
|
+
"Best-effort and silent on failure: kill is only attempted on darwin/linux, only matches `native-host.js` paths that also contain `chrome-relay`, and ignores individual kill errors (already gone, no permission). Won't kill the running CLI itself."
|
|
1454
|
+
],
|
|
1347
1455
|
"0.5.20": [
|
|
1348
1456
|
"BREAKING \u2014 `chrome-relay navigate` no longer steals focus by default. Background is now the implicit behavior; agents pass `--active` when they actually want the user looking at the new tab. The whole product pitch is 'operate without stealing focus' \u2014 the default needed to match.",
|
|
1349
1457
|
"`--inactive` flag removed entirely. It was the opt-in for the previous (wrong) default; now it'd be a no-op, and no-op flags are dead code. Agents that were passing `--inactive` should drop it (the behavior is now the default). Commander will reject the unknown flag \u2014 that's the right signal to update.",
|
|
@@ -1511,7 +1619,7 @@ function registerInstallUpdate(program) {
|
|
|
1511
1619
|
});
|
|
1512
1620
|
program.command("update").description("Update chrome-relay CLI to the latest version and print what changed (agent-readable JSON).").option("--dry-run", "skip the install; just show what changed since the current version").action(async (opts) => {
|
|
1513
1621
|
const fromVersion = CHROME_RELAY_VERSION;
|
|
1514
|
-
const { spawnSync } = await import("child_process");
|
|
1622
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
1515
1623
|
const out = {
|
|
1516
1624
|
updatedFrom: fromVersion,
|
|
1517
1625
|
updatedTo: fromVersion,
|
|
@@ -1531,7 +1639,7 @@ function registerInstallUpdate(program) {
|
|
|
1531
1639
|
};
|
|
1532
1640
|
process.stderr.write(`[chrome-relay] updating from ${fromVersion} via ${pm}...
|
|
1533
1641
|
`);
|
|
1534
|
-
const install =
|
|
1642
|
+
const install = spawnSync2(cmd[0], cmd[1], { stdio: "inherit" });
|
|
1535
1643
|
out.install.status = install.status;
|
|
1536
1644
|
if (install.status !== 0) {
|
|
1537
1645
|
process.stderr.write(`[chrome-relay] install failed (${pm} exited ${install.status}). Try manually: ${cmd[0]} ${cmd[1].join(" ")}
|
|
@@ -1543,15 +1651,22 @@ function registerInstallUpdate(program) {
|
|
|
1543
1651
|
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
1544
1652
|
process.exit(1);
|
|
1545
1653
|
}
|
|
1546
|
-
const which =
|
|
1654
|
+
const which = spawnSync2("which", ["chrome-relay"]);
|
|
1547
1655
|
const newBin = which.stdout?.toString().trim();
|
|
1548
1656
|
if (which.status === 0 && newBin) {
|
|
1549
|
-
const versionOut =
|
|
1657
|
+
const versionOut = spawnSync2(newBin, ["--version"]);
|
|
1550
1658
|
const newVersion = (versionOut.stdout?.toString() ?? "").trim();
|
|
1551
1659
|
out.binary.path = newBin;
|
|
1552
1660
|
if (newVersion && newVersion !== fromVersion) {
|
|
1553
1661
|
out.updatedTo = newVersion;
|
|
1554
|
-
const
|
|
1662
|
+
const install2 = spawnSync2(newBin, ["install"], { stdio: "inherit" });
|
|
1663
|
+
if (install2.status !== 0) {
|
|
1664
|
+
out.warnings.push({
|
|
1665
|
+
code: "install_refresh_failed",
|
|
1666
|
+
message: `Update installed the new package but \`${newBin} install\` exited ${install2.status}. Run it manually to refresh the native host manifest.`
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
const rn = spawnSync2(newBin, ["release-notes", "--since", fromVersion]);
|
|
1555
1670
|
try {
|
|
1556
1671
|
const parsed = JSON.parse(rn.stdout?.toString() ?? "");
|
|
1557
1672
|
if (Array.isArray(parsed.changes)) {
|
|
@@ -1940,8 +2055,8 @@ Notes:
|
|
|
1940
2055
|
}
|
|
1941
2056
|
if (opts.gif || opts.mp4) {
|
|
1942
2057
|
const fps = typeof opts.fps === "number" ? opts.fps : 15;
|
|
1943
|
-
const { spawnSync } = await import("child_process");
|
|
1944
|
-
const which =
|
|
2058
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
2059
|
+
const which = spawnSync2("which", ["ffmpeg"]);
|
|
1945
2060
|
if (which.status !== 0) {
|
|
1946
2061
|
if (opts.allowMissingFfmpeg) {
|
|
1947
2062
|
process.stderr.write("[chrome-relay] ffmpeg not on PATH \u2014 skipping --gif/--mp4 (allow-missing-ffmpeg).\n");
|
|
@@ -1962,7 +2077,7 @@ Notes:
|
|
|
1962
2077
|
}
|
|
1963
2078
|
if (opts.gif) {
|
|
1964
2079
|
const gifOut = `${opts.out.replace(/\/$/, "")}.gif`;
|
|
1965
|
-
const r =
|
|
2080
|
+
const r = spawnSync2("ffmpeg", [
|
|
1966
2081
|
"-y",
|
|
1967
2082
|
"-framerate",
|
|
1968
2083
|
String(fps),
|
|
@@ -1979,7 +2094,7 @@ Notes:
|
|
|
1979
2094
|
}
|
|
1980
2095
|
if (opts.mp4) {
|
|
1981
2096
|
const mp4Out = `${opts.out.replace(/\/$/, "")}.mp4`;
|
|
1982
|
-
const r =
|
|
2097
|
+
const r = spawnSync2("ffmpeg", [
|
|
1983
2098
|
"-y",
|
|
1984
2099
|
"-framerate",
|
|
1985
2100
|
String(fps),
|
package/dist/index.js
CHANGED
package/dist/native-host.js
CHANGED
|
@@ -56,7 +56,7 @@ function toBridgeError(unknownErr, fallbackTool) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/index.ts
|
|
59
|
-
var CHROME_RELAY_VERSION = true ? "0.5.
|
|
59
|
+
var CHROME_RELAY_VERSION = true ? "0.5.22" : "0.0.0-dev";
|
|
60
60
|
|
|
61
61
|
// src/release-notes.ts
|
|
62
62
|
function compareSemver(a, b) {
|