@ciphore/radiocli 0.2.3 → 0.2.4
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/CHANGELOG.md +77 -0
- package/CONTRIBUTING.md +36 -6
- package/README.md +54 -10
- package/dist/agent/headless-host.js +39 -17
- package/dist/agent/launcher.js +6 -67
- package/dist/agent/mcp-install.js +15 -15
- package/dist/agent/service.js +3 -3
- package/dist/agent/session.js +13 -29
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/guard.js +79 -36
- package/dist/alarms/inhibitor.js +27 -22
- package/dist/alarms/power-guard-store.js +2 -10
- package/dist/alarms/runner.js +56 -25
- package/dist/alarms/schedule.js +9 -2
- package/dist/alarms/scheduler.js +194 -52
- package/dist/alarms/setup-verification.js +1 -2
- package/dist/alarms/system-volume-ownership.js +267 -0
- package/dist/alarms/system-volume.js +155 -14
- package/dist/alarms/terminal-launcher.js +76 -136
- package/dist/alarms/tui-presence.js +2 -4
- package/dist/cli.js +103 -38
- package/dist/platform/capabilities.js +53 -0
- package/dist/platform/desktop.js +36 -0
- package/dist/{player/command.js → platform/executables.js} +43 -9
- package/dist/platform/ipc.js +14 -0
- package/dist/platform/launch-command.js +135 -0
- package/dist/platform/loopback.js +44 -0
- package/dist/platform/network.js +312 -0
- package/dist/platform/packages.js +216 -0
- package/dist/platform/paths.js +40 -0
- package/dist/platform/runtime.js +67 -0
- package/dist/platform/shell.js +24 -0
- package/dist/platform/storage.js +48 -0
- package/dist/platform/support.js +214 -0
- package/dist/platform/terminal.js +63 -0
- package/dist/platform/terminals.js +203 -0
- package/dist/player/airplay-discovery.js +4 -2
- package/dist/player/backend-install.js +13 -94
- package/dist/player/command-diagnostics.js +2 -2
- package/dist/player/mpv-ipc-client.js +2 -1
- package/dist/player/player-controller.js +203 -33
- package/dist/providers/cache.js +4 -26
- package/dist/providers/radio-browser.js +152 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +79 -151
- package/dist/storage/store.js +105 -52
- package/dist/streams/import-stream.js +163 -0
- package/dist/ui/AdaptiveContent.js +33 -24
- package/dist/ui/App.js +173 -86
- package/dist/ui/AppContent.js +3 -3
- package/dist/ui/app-state.js +8 -1
- package/dist/ui/ascii.js +11 -2
- package/dist/ui/components/AdaptiveMarquee.js +6 -3
- package/dist/ui/components/Logo.js +5 -2
- package/dist/ui/components/Menu.js +2 -2
- package/dist/ui/components/ScreenHeader.js +1 -1
- package/dist/ui/components/StationList.js +6 -4
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/display-context.js +8 -11
- package/dist/ui/help-content.js +5 -5
- package/dist/ui/layout.js +5 -2
- package/dist/ui/page-footer.js +3 -3
- package/dist/ui/screen-items.js +2 -2
- package/dist/ui/screen-meta.js +1 -1
- package/dist/ui/screens/AirPlayCodeScreen.js +6 -2
- package/dist/ui/screens/AirPlaySettingsScreen.js +7 -3
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +5 -1
- package/dist/ui/screens/MapScreen.js +1 -1
- package/dist/ui/screens/NowPlayingScreen.js +4 -4
- package/dist/ui/screens/SearchScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +14 -7
- package/dist/ui/screens/StatsScreen.js +3 -1
- package/dist/ui/system-actions.js +80 -52
- package/dist/ui/terminal-renderer.js +16 -0
- package/dist/ui/use-alarm-tui.js +36 -21
- package/dist/ui/use-app-input.js +42 -22
- package/dist/ui/use-command-executor.js +31 -7
- package/dist/update-check.js +8 -17
- package/package.json +1 -1
|
@@ -10,7 +10,11 @@ import { playbackBackendCapabilities } from '../../player/backend-install.js';
|
|
|
10
10
|
import { airPlayReceiverSettingValue } from '../airplay-settings.js';
|
|
11
11
|
import { audioOutputLabel, audioOutputSettingValue } from '../audio-output.js';
|
|
12
12
|
import { updateStatusText } from '../../update-check.js';
|
|
13
|
-
|
|
13
|
+
import { useDisplay } from '../display-context.js';
|
|
14
|
+
import { toAsciiSafe } from '../ascii.js';
|
|
15
|
+
export function SettingsScreen({ page = 'root', selected, settings, appVersion, updateCheck, storePath, playback, backends, airPlayDevices, providerHealth, theme, diagnostics, width, height, airPlaySupported = true }) {
|
|
16
|
+
const { ascii } = useDisplay();
|
|
17
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
14
18
|
const accent = themeAccent(theme);
|
|
15
19
|
const lineWidth = Math.max(32, width - 4);
|
|
16
20
|
const group = settingsGroup(page);
|
|
@@ -33,12 +37,12 @@ export function SettingsScreen({ page = 'root', selected, settings, appVersion,
|
|
|
33
37
|
const labelWidth = Math.min(34, Math.max(20, lineWidth - valueGap - 24));
|
|
34
38
|
const valueWidth = Math.max(12, Math.min(34, lineWidth - labelWidth - valueGap - 2));
|
|
35
39
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Settings", subtitle: group ? `Settings › ${group.label}` : 'Choose a category', width: width, theme: theme }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: menuWindow.items, selected: menuWindow.selectedOffset, keyFor: item => item, render: (item, _index, active) => {
|
|
36
|
-
const label = settingLabel(item, updateCheck, appVersion);
|
|
40
|
+
const label = settingLabel(item, updateCheck, appVersion, airPlaySupported);
|
|
37
41
|
const value = page === 'root'
|
|
38
42
|
? settingsRootValue(item)
|
|
39
43
|
: settingValue(item, settings, diagnostics, backends, airPlayDevices, updateCheck, appVersion);
|
|
40
|
-
return (_jsxs(Box, { width: lineWidth, children: [_jsx(Pointer, { active: active }), _jsx(Box, { width: labelWidth, children: _jsx(Text, { color: active ? accent : undefined, bold: active, children: truncate(label, labelWidth) }) }), _jsx(Box, { width: valueGap }), _jsx(Box, { width: valueWidth, children: _jsx(Text, { color: value ? accent : textMuted, children: truncate(value ?? '', valueWidth) }) })] }));
|
|
41
|
-
} }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: textMuted, bold: true, children: "Selected" }), _jsx(Text, { color: textMuted, children: truncate(detail, lineWidth) })] })] }));
|
|
44
|
+
return (_jsxs(Box, { width: lineWidth, children: [_jsx(Pointer, { active: active }), _jsx(Box, { width: labelWidth, children: _jsx(Text, { color: active ? accent : undefined, bold: active, children: a(truncate(label, labelWidth)) }) }), _jsx(Box, { width: valueGap }), _jsx(Box, { width: valueWidth, children: _jsx(Text, { color: value ? accent : textMuted, children: a(truncate(value ?? '', valueWidth)) }) })] }));
|
|
45
|
+
} }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: textMuted, bold: true, children: "Selected" }), _jsx(Text, { color: textMuted, children: a(truncate(detail, lineWidth)) })] })] }));
|
|
42
46
|
}
|
|
43
47
|
function settingsMenuWindow(items, selected, height) {
|
|
44
48
|
const reservedRows = 6;
|
|
@@ -111,13 +115,16 @@ export function settingValue(item, settings, diagnostics, backends, airPlayDevic
|
|
|
111
115
|
return `prev ${settings.mediaKeys.previous.length} · play ${settings.mediaKeys.playPause.length} · next ${settings.mediaKeys.next.length}`;
|
|
112
116
|
case 'Export preferences and library':
|
|
113
117
|
return 'JSON backup';
|
|
114
|
-
case '
|
|
118
|
+
case 'Restore preferences and library':
|
|
115
119
|
return 'restore JSON backup';
|
|
116
120
|
default:
|
|
117
121
|
return undefined;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
|
-
export function settingLabel(item, updateCheck, currentVersion) {
|
|
124
|
+
export function settingLabel(item, updateCheck, currentVersion, airPlaySupported = true) {
|
|
125
|
+
if (item === 'AirPlay receiver' && !airPlaySupported) {
|
|
126
|
+
return 'AirPlay receiver (macOS only)';
|
|
127
|
+
}
|
|
121
128
|
if (item === 'Check for updates') {
|
|
122
129
|
return updateStatusText(updateCheck, currentVersion).endsWith('available') ? 'Install update' : 'Check now';
|
|
123
130
|
}
|
|
@@ -145,7 +152,7 @@ function settingDetail(input) {
|
|
|
145
152
|
if (item === 'Automatically check for updates') {
|
|
146
153
|
return 'When enabled, RadioCLI checks the registry once whenever the app launches.';
|
|
147
154
|
}
|
|
148
|
-
if (item === 'Export preferences and library' || item === '
|
|
155
|
+
if (item === 'Export preferences and library' || item === 'Restore preferences and library') {
|
|
149
156
|
return `Library file: ${truncate(storePath, Math.max(8, lineWidth - 14))}`;
|
|
150
157
|
}
|
|
151
158
|
if (item === 'Share favorite votes with Radio Browser') {
|
|
@@ -6,6 +6,7 @@ import { panelBorder, textMuted, themeAccent, themeContributionColors } from '..
|
|
|
6
6
|
import { panelBorderStyle, useDisplay } from '../display-context.js';
|
|
7
7
|
import { ScreenHeader } from '../components/ScreenHeader.js';
|
|
8
8
|
import { truncate } from '../format.js';
|
|
9
|
+
import { toAsciiSafe } from '../ascii.js';
|
|
9
10
|
const monthLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
10
11
|
const dayLabelWidth = 5;
|
|
11
12
|
// Two terminal columns are approximately one row high in common monospace
|
|
@@ -15,6 +16,7 @@ const heatmapCell = ' ';
|
|
|
15
16
|
const heatmapGap = '';
|
|
16
17
|
export function StatsScreen({ library, theme, width, height }) {
|
|
17
18
|
const { panel: panelBackground, ascii } = useDisplay();
|
|
19
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
18
20
|
const stats = computeListeningStats(library.activity.sessions);
|
|
19
21
|
const contentWidth = Math.max(20, width - 4);
|
|
20
22
|
const graph = buildContributionGraph(stats.days, contentWidth, height);
|
|
@@ -24,7 +26,7 @@ export function StatsScreen({ library, theme, width, height }) {
|
|
|
24
26
|
const metricWidth = Math.max(28, Math.floor((contentWidth - 2) / 2));
|
|
25
27
|
const favoriteWidth = Math.max(8, metricWidth - 18);
|
|
26
28
|
const compact = height < 30;
|
|
27
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Listening stats", subtitle: "Local listening history \u00B7 never leaves this machine", width: width, theme: theme }), _jsxs(Box, { marginTop: 1, paddingBottom: 1, borderStyle: panelBorderStyle(ascii, 'single'), borderColor: panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, flexDirection: "column", children: [_jsx(Box, { children:
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Listening stats", subtitle: "Local listening history \u00B7 never leaves this machine", width: width, theme: theme }), _jsxs(Box, { marginTop: 1, paddingBottom: 1, borderStyle: panelBorderStyle(ascii, 'single'), borderColor: panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { color: themeAccent(theme), bold: true, children: a(`Activity — ${graph.year}`) }) }), _jsxs(Box, { flexDirection: "column", "aria-label": `Activity graph: ${stats.activeDays} active days in ${graph.year}`, children: [_jsxs(Text, { color: textMuted, children: [' '.repeat(dayLabelWidth), graph.months] }), graph.rows.map(row => (_jsx(React.Fragment, { children: Array.from({ length: graph.tileHeight }, (_, tileLine) => (_jsxs(Box, { children: [_jsx(Text, { color: textMuted, children: tileLine === 0 ? row.label.padEnd(dayLabelWidth) : ' '.repeat(dayLabelWidth) }), renderContributionCells(row.cells, graphColors, graph.cellGap)] }, `${row.key}-${tileLine}`))) }, row.key)))] })] }), _jsxs(Box, { marginTop: 1, borderStyle: panelBorderStyle(ascii, 'single'), borderColor: panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, flexDirection: "column", children: [_jsx(Text, { color: themeAccent(theme), bold: true, children: "Summary" }), _jsxs(Box, { marginTop: compact ? 0 : 1, flexDirection: "column", width: contentWidth, children: [metricPair('Favorite station', a(truncate(favorite, favoriteWidth)), 'Total hours listened', formatHours(totalHours), metricWidth, theme), metricPair('Sessions', stats.sessions.toLocaleString(), 'Longest streak', formatDays(stats.longestStreak), metricWidth, theme), metricPair('Current streak', formatDays(stats.currentStreak), 'Stations listened', stats.listenedStationCount.toLocaleString(), metricWidth, theme), metricPair('Active days', `${stats.activeDays}/${stats.totalTrackedDays}`, 'Stations counted after', '2 min', metricWidth, theme)] }), _jsxs(Box, { marginTop: 1, "aria-hidden": true, children: [_jsx(Text, { color: textMuted, children: a('Less · ') }), graphColors.map(color => (_jsxs(React.Fragment, { children: [renderLegendCell(color, graph.cellText, graph.cellGap), _jsx(Text, { children: " " })] }, color))), _jsx(Text, { color: textMuted, children: "More" })] })] })] }));
|
|
28
30
|
}
|
|
29
31
|
function renderContributionCells(cells, graphColors, cellGap) {
|
|
30
32
|
const runs = [];
|
|
@@ -1,65 +1,93 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { resolveCommand } from '../
|
|
2
|
+
import { resolveCommand } from '../platform/executables.js';
|
|
3
3
|
import { safeExternalHttpUrl } from '../safety.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
if (platform === 'win32') {
|
|
10
|
-
// Direct invocation avoids cmd.exe interpreting URL query characters.
|
|
11
|
-
return { command: 'explorer', args: [] };
|
|
12
|
-
}
|
|
13
|
-
return { command: 'xdg-open', args: [] };
|
|
14
|
-
}
|
|
15
|
-
// Candidate clipboard tools per platform, in priority order. pbcopy/clip ship
|
|
16
|
-
// with macOS/Windows; Linux/BSD rely on whichever of these is installed.
|
|
17
|
-
export function clipboardCommands(platform = process.platform) {
|
|
18
|
-
if (platform === 'darwin') {
|
|
19
|
-
return [{ command: 'pbcopy', args: [] }];
|
|
20
|
-
}
|
|
21
|
-
if (platform === 'win32') {
|
|
22
|
-
return [{ command: 'clip', args: [] }];
|
|
23
|
-
}
|
|
24
|
-
return [
|
|
25
|
-
{ command: 'wl-copy', args: [] },
|
|
26
|
-
{ command: 'xclip', args: ['-selection', 'clipboard'] },
|
|
27
|
-
{ command: 'xsel', args: ['--clipboard', '--input'] }
|
|
28
|
-
];
|
|
29
|
-
}
|
|
30
|
-
export function openExternal(url, platform = process.platform) {
|
|
4
|
+
import { browserCommands, clipboardCandidates, hasGraphicalSession } from '../platform/desktop.js';
|
|
5
|
+
import { identifyPlatform } from '../platform/runtime.js';
|
|
6
|
+
import { waitForLaunch } from '../platform/launch-command.js';
|
|
7
|
+
export async function openExternal(url, platform = process.platform, deps = {}) {
|
|
31
8
|
const safeUrl = safeExternalHttpUrl(url);
|
|
32
|
-
|
|
9
|
+
const env = deps.env ?? process.env;
|
|
10
|
+
const host = identifyPlatform({ platform, env });
|
|
11
|
+
if (!safeUrl || !hasGraphicalSession(host, env))
|
|
33
12
|
return false;
|
|
13
|
+
for (const { command, args } of browserCommands(host)) {
|
|
14
|
+
const executable = (deps.resolve ?? resolveCommand)(command);
|
|
15
|
+
if (!executable)
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
// xdg-open may run for the browser's entire lifetime. Observe startup
|
|
19
|
+
// failure, then let the desktop application own its detached process.
|
|
20
|
+
const child = (deps.spawn ?? spawn)(executable, [...args, safeUrl], { stdio: 'ignore', env, detached: true, windowsHide: true });
|
|
21
|
+
await waitForLaunch(child);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch { /* Try the next available opener after a launch failure. */ }
|
|
34
25
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Opening a browser is best-effort; never crash the TUI over it.
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export async function copyToClipboard(text, platform = process.platform, deps = {}) {
|
|
29
|
+
const env = deps.env ?? process.env;
|
|
30
|
+
const host = identifyPlatform({ platform, env });
|
|
31
|
+
if (!hasGraphicalSession(host, env))
|
|
44
32
|
return false;
|
|
33
|
+
for (const { command, args } of clipboardCandidates(host)) {
|
|
34
|
+
const executable = (deps.resolve ?? resolveCommand)(command);
|
|
35
|
+
if (executable && await runHelper(executable, args, text, deps))
|
|
36
|
+
return true;
|
|
45
37
|
}
|
|
38
|
+
return false;
|
|
46
39
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
function runHelper(command, args, input, deps) {
|
|
41
|
+
return new Promise(resolve => {
|
|
42
|
+
let child;
|
|
43
|
+
let settled = false;
|
|
44
|
+
let stopping = false;
|
|
45
|
+
let forceTimer;
|
|
46
|
+
let cleanupTimer;
|
|
47
|
+
const signal = (value) => {
|
|
48
|
+
try {
|
|
49
|
+
child?.kill(value);
|
|
50
|
+
}
|
|
51
|
+
catch { /* The failed action remains false; cleanup is still bounded. */ }
|
|
52
|
+
};
|
|
53
|
+
const finish = (ok) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
clearTimeout(forceTimer);
|
|
59
|
+
clearTimeout(cleanupTimer);
|
|
60
|
+
resolve(ok);
|
|
61
|
+
};
|
|
62
|
+
const stop = () => {
|
|
63
|
+
if (settled || stopping)
|
|
64
|
+
return;
|
|
65
|
+
stopping = true;
|
|
66
|
+
forceTimer = setTimeout(() => signal('SIGKILL'), 250);
|
|
67
|
+
cleanupTimer = setTimeout(() => {
|
|
68
|
+
// Even an uninterruptible OS process must not keep RadioCLI alive.
|
|
69
|
+
child?.unref();
|
|
70
|
+
finish(false);
|
|
71
|
+
}, 1_000);
|
|
72
|
+
child?.stdin?.destroy();
|
|
73
|
+
signal('SIGTERM');
|
|
74
|
+
};
|
|
75
|
+
const timer = setTimeout(stop, deps.timeoutMs ?? 5000);
|
|
53
76
|
try {
|
|
54
|
-
|
|
55
|
-
child.on('error', () =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
child = (deps.spawn ?? spawn)(command, args, { stdio: ['pipe', 'ignore', 'ignore'], env: deps.env ?? process.env, windowsHide: true });
|
|
78
|
+
child.on('error', () => { if (child?.pid)
|
|
79
|
+
stop();
|
|
80
|
+
else
|
|
81
|
+
finish(false); });
|
|
82
|
+
child.once('close', code => finish(!stopping && code === 0));
|
|
83
|
+
child.stdin?.on('error', stop);
|
|
84
|
+
child.stdin?.end(input);
|
|
59
85
|
}
|
|
60
86
|
catch {
|
|
61
|
-
|
|
87
|
+
if (child?.pid)
|
|
88
|
+
stop();
|
|
89
|
+
else
|
|
90
|
+
finish(false);
|
|
62
91
|
}
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
92
|
+
});
|
|
65
93
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { resolveTerminalCapabilities } from '../platform/terminal.js';
|
|
4
|
+
/**
|
|
5
|
+
* Ink uses Chalk's shared instance for both text and panel fills. FORCE_COLOR
|
|
6
|
+
* is a minimum in Chalk, so setting it cannot cap a Windows/CI/truecolor host
|
|
7
|
+
* at 16 or 256 colors. Set the instance's level before the first Ink render.
|
|
8
|
+
* Resolve from Ink itself so nested dependency installations work as well.
|
|
9
|
+
*/
|
|
10
|
+
export async function configureTerminalRenderer(env = process.env, evidence = {}) {
|
|
11
|
+
const terminal = resolveTerminalCapabilities(env, evidence);
|
|
12
|
+
const inkRequire = createRequire(import.meta.resolve('ink'));
|
|
13
|
+
const { default: chalk } = await import(pathToFileURL(inkRequire.resolve('chalk')).href);
|
|
14
|
+
chalk.level = terminal.colorLevel;
|
|
15
|
+
return terminal;
|
|
16
|
+
}
|
package/dist/ui/use-alarm-tui.js
CHANGED
|
@@ -180,6 +180,40 @@ export function useAlarmTui(params) {
|
|
|
180
180
|
}
|
|
181
181
|
await refreshRuntime();
|
|
182
182
|
}, [refreshRuntime, service, setMessage]);
|
|
183
|
+
const deleteAlarm = useCallback(async (alarm) => {
|
|
184
|
+
let phase = 'disable';
|
|
185
|
+
let message;
|
|
186
|
+
try {
|
|
187
|
+
// Persist the safe state before touching native jobs. A failed cleanup
|
|
188
|
+
// can then be retried without another write in the error handler.
|
|
189
|
+
const disabled = store.toggleAlarm(alarm.id, false);
|
|
190
|
+
const disabledSnapshot = store.snapshot();
|
|
191
|
+
alarmStateRef.current = disabledSnapshot.alarms;
|
|
192
|
+
setLibrary(disabledSnapshot);
|
|
193
|
+
phase = 'native';
|
|
194
|
+
await service.remove(disabled);
|
|
195
|
+
phase = 'definition';
|
|
196
|
+
store.removeAlarm(alarm.id);
|
|
197
|
+
const snapshot = store.snapshot();
|
|
198
|
+
alarmStateRef.current = snapshot.alarms;
|
|
199
|
+
setLibrary(snapshot);
|
|
200
|
+
setSelected(value => clamp(value, snapshot.alarms.length + 1));
|
|
201
|
+
message = `Deleted ${alarm.label}.`;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
message = phase === 'disable'
|
|
205
|
+
? `Delete canceled: ${messageOf(error)}. Alarm and native jobs unchanged.`
|
|
206
|
+
: phase === 'native'
|
|
207
|
+
? `Delete cleanup failed: ${messageOf(error)}. ${alarm.label} was kept disabled; press r to repair.`
|
|
208
|
+
: `Native cleanup completed. Save failed: ${messageOf(error)}. ${alarm.label} was kept disabled; retry deletion.`;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
busyAlarmIdsRef.current.delete(alarm.id);
|
|
212
|
+
setBusyAlarmIds(new Set(busyAlarmIdsRef.current));
|
|
213
|
+
}
|
|
214
|
+
await refreshRuntime();
|
|
215
|
+
setMessage(message);
|
|
216
|
+
}, [refreshRuntime, service, setLibrary, setMessage, setSelected, store]);
|
|
183
217
|
const saveDraft = useCallback(() => {
|
|
184
218
|
if (!draft || savingRef.current)
|
|
185
219
|
return;
|
|
@@ -343,26 +377,7 @@ export function useAlarmTui(params) {
|
|
|
343
377
|
setDeletingId(undefined);
|
|
344
378
|
busyAlarmIdsRef.current.add(alarm.id);
|
|
345
379
|
setBusyAlarmIds(new Set(busyAlarmIdsRef.current));
|
|
346
|
-
void
|
|
347
|
-
if (store.getAlarm(alarm.id))
|
|
348
|
-
store.removeAlarm(alarm.id);
|
|
349
|
-
const snapshot = store.snapshot();
|
|
350
|
-
alarmStateRef.current = snapshot.alarms;
|
|
351
|
-
setLibrary(snapshot);
|
|
352
|
-
setSelected(value => clamp(value, snapshot.alarms.length + 1));
|
|
353
|
-
setMessage(`Deleted ${alarm.label}.`);
|
|
354
|
-
}).catch(error => {
|
|
355
|
-
const existing = store.getAlarm(alarm.id);
|
|
356
|
-
if (existing) {
|
|
357
|
-
const disabled = store.toggleAlarm(alarm.id, false);
|
|
358
|
-
const snapshot = store.snapshot();
|
|
359
|
-
alarmStateRef.current = snapshot.alarms;
|
|
360
|
-
setLibrary(snapshot);
|
|
361
|
-
setMessage(`Delete cleanup failed; ${disabled.label} was kept disabled so Repair can retry: ${messageOf(error)}`);
|
|
362
|
-
}
|
|
363
|
-
else
|
|
364
|
-
setMessage(`Delete cleanup failed after the local alarm disappeared: ${messageOf(error)}`);
|
|
365
|
-
}).finally(() => { busyAlarmIdsRef.current.delete(alarm.id); setBusyAlarmIds(new Set(busyAlarmIdsRef.current)); void refreshRuntime(); });
|
|
380
|
+
void deleteAlarm(alarm).catch(error => setMessage(`Unexpected alarm deletion failure: ${messageOf(error)}`));
|
|
366
381
|
return true;
|
|
367
382
|
}
|
|
368
383
|
if (input !== 'x') {
|
|
@@ -677,7 +692,7 @@ export function useAlarmTui(params) {
|
|
|
677
692
|
return true;
|
|
678
693
|
}
|
|
679
694
|
return false;
|
|
680
|
-
}, [activeAlarms, deletingId, draft, editSelected, editingField, editorControl, editorField, go, library.alarms, openForStation, pickerChoices, pickerFallback, previewStation, refreshActive, refreshRuntime, runVerification, saveDraft, screen, selected, service, setLibrary, setMessage, setSelected, snoozeMinutes, store, syncPersisted, timeSegment, verification, visibleFields, weekdayIndex]);
|
|
695
|
+
}, [activeAlarms, deleteAlarm, deletingId, draft, editSelected, editingField, editorControl, editorField, go, library.alarms, openForStation, pickerChoices, pickerFallback, previewStation, refreshActive, refreshRuntime, runVerification, saveDraft, screen, selected, service, setLibrary, setMessage, setSelected, snoozeMinutes, store, syncPersisted, timeSegment, verification, visibleFields, weekdayIndex]);
|
|
681
696
|
const openActive = useCallback(() => {
|
|
682
697
|
if (!activeAlarms.length) {
|
|
683
698
|
setMessage('No alarm is currently playing.');
|
package/dist/ui/use-app-input.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
2
|
import { useInput } from 'ink';
|
|
3
3
|
import { homeItems, settingsItemsForPage, settingsPageForRootItem } from './screen-items.js';
|
|
4
|
-
import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldToggleNearbyLocationShortcut } from './app-state.js';
|
|
4
|
+
import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, isSearchFavoriteShortcut, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldTuneSearchResult, shouldToggleNearbyLocationShortcut } from './app-state.js';
|
|
5
5
|
import { parseTerminalMouseEvents, primaryMousePress, wheelScrollDelta } from './terminal-mouse.js';
|
|
6
6
|
import { completeCommand } from './help-content.js';
|
|
7
7
|
import { commitImmediateSelection } from './selection-state.js';
|
|
8
|
+
import { sanitizeTerminalText } from '../safety.js';
|
|
8
9
|
export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, cancelPendingAutoSkip, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, confirmCtrlCExit, copyStationUrl, openStationHomepage, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openSettingsPage, closeSettingsPage, openScreen, handleAlarmInput, openAlarmForStation, openActiveAlarms, playAdjacent, playStation, player, playingStation, recallSearchHistory, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, repairMcpIntegrations, setAgentIntegrationEnabled, runSearch, saveLearnedTransportKey, screen, settingsPage, searchQuery, selectedRef, selectedStationForInput, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleSetting, toggleAgentSetting, toggleNearbyLocation, toggleDirectoryVoting, toggleRadioGarden, toggleSkipBrokenStreams, updateFromSettings }) {
|
|
10
|
+
const reportInputError = useCallback((error) => {
|
|
11
|
+
setMessage(sanitizeTerminalText(error instanceof Error ? error.message : String(error)) ?? 'Could not complete this action.');
|
|
12
|
+
}, [setMessage]);
|
|
9
13
|
const commitSelection = (next) => {
|
|
10
14
|
commitImmediateSelection(selectedRef, setSelected, next, currentItemCount(screen));
|
|
11
15
|
};
|
|
@@ -15,7 +19,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
15
19
|
commitSelection(selectedRef.current + delta);
|
|
16
20
|
};
|
|
17
21
|
useEffect(() => {
|
|
18
|
-
const
|
|
22
|
+
const handleData = (data) => {
|
|
19
23
|
const rawInput = String(data);
|
|
20
24
|
if (capturingTransportAction) {
|
|
21
25
|
if (rawInput === '\u001B') {
|
|
@@ -63,6 +67,14 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
63
67
|
togglePause();
|
|
64
68
|
}
|
|
65
69
|
};
|
|
70
|
+
const onData = (data) => {
|
|
71
|
+
try {
|
|
72
|
+
handleData(data);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
reportInputError(error);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
66
78
|
stdin.on('data', onData);
|
|
67
79
|
return () => {
|
|
68
80
|
stdin.off('data', onData);
|
|
@@ -78,6 +90,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
78
90
|
lastRawTransportAtRef,
|
|
79
91
|
playAdjacent,
|
|
80
92
|
player,
|
|
93
|
+
reportInputError,
|
|
81
94
|
moveExploreCursorToCell,
|
|
82
95
|
saveLearnedTransportKey,
|
|
83
96
|
screen,
|
|
@@ -89,7 +102,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
89
102
|
stdin,
|
|
90
103
|
togglePause
|
|
91
104
|
]);
|
|
92
|
-
|
|
105
|
+
const handleInput = async (input, key) => {
|
|
93
106
|
if (key.ctrl && input === 'c') {
|
|
94
107
|
confirmCtrlCExit();
|
|
95
108
|
return;
|
|
@@ -105,9 +118,9 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
105
118
|
}
|
|
106
119
|
if (commandMode) {
|
|
107
120
|
if (key.return) {
|
|
108
|
-
void executeCommand(commandText);
|
|
109
121
|
setCommandText('');
|
|
110
122
|
setCommandMode(false);
|
|
123
|
+
await executeCommand(commandText);
|
|
111
124
|
return;
|
|
112
125
|
}
|
|
113
126
|
if (key.escape) {
|
|
@@ -128,17 +141,21 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
128
141
|
if (handleAlarmInput(input, key)) {
|
|
129
142
|
return;
|
|
130
143
|
}
|
|
131
|
-
// An active text field owns
|
|
132
|
-
//
|
|
133
|
-
//
|
|
144
|
+
// An active text field owns ordinary keystrokes before app-wide shortcuts.
|
|
145
|
+
// Dedicated modified shortcuts are handled here so plain query characters
|
|
146
|
+
// never trigger actions or leave Search.
|
|
134
147
|
if (screen === 'search' && editingSearch) {
|
|
148
|
+
if (isSearchFavoriteShortcut(input, key)) {
|
|
149
|
+
toggleFavorite(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
135
152
|
if (key.return) {
|
|
136
153
|
const inputStation = selectedStationForInput();
|
|
137
|
-
if (
|
|
138
|
-
|
|
154
|
+
if (inputStation && shouldTuneSearchResult(searchQuery, lastSubmittedSearchRef.current)) {
|
|
155
|
+
await playStation(inputStation);
|
|
139
156
|
}
|
|
140
157
|
else {
|
|
141
|
-
|
|
158
|
+
await runSearch();
|
|
142
159
|
}
|
|
143
160
|
return;
|
|
144
161
|
}
|
|
@@ -240,7 +257,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
240
257
|
if (input.startsWith(':')) {
|
|
241
258
|
const seed = input.slice(1).replace(/[\r\n]+$/g, '');
|
|
242
259
|
if (/[\r\n]/.test(input)) {
|
|
243
|
-
|
|
260
|
+
await executeCommand(seed);
|
|
244
261
|
}
|
|
245
262
|
else {
|
|
246
263
|
setCommandMode(true);
|
|
@@ -372,11 +389,11 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
372
389
|
return;
|
|
373
390
|
}
|
|
374
391
|
if (input === 'O') {
|
|
375
|
-
openStationHomepage(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
392
|
+
await openStationHomepage(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
376
393
|
return;
|
|
377
394
|
}
|
|
378
395
|
if (input === 'y') {
|
|
379
|
-
|
|
396
|
+
await copyStationUrl(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
380
397
|
return;
|
|
381
398
|
}
|
|
382
399
|
if (input === ' ') {
|
|
@@ -418,7 +435,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
418
435
|
if (screen === 'countries') {
|
|
419
436
|
const country = filteredCountries[selectedRef.current];
|
|
420
437
|
if (country) {
|
|
421
|
-
|
|
438
|
+
await loadCountry(country);
|
|
422
439
|
}
|
|
423
440
|
return;
|
|
424
441
|
}
|
|
@@ -483,10 +500,10 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
483
500
|
toggleSetting('automaticUpdateChecks');
|
|
484
501
|
}
|
|
485
502
|
else if (item === 'Allow local agent control') {
|
|
486
|
-
|
|
503
|
+
await setAgentIntegrationEnabled();
|
|
487
504
|
}
|
|
488
505
|
else if (item === 'Install or repair MCP integrations') {
|
|
489
|
-
|
|
506
|
+
await repairMcpIntegrations();
|
|
490
507
|
}
|
|
491
508
|
else if (item === 'Open TUI for agent playback') {
|
|
492
509
|
toggleAgentSetting('openUiOnPlay');
|
|
@@ -498,12 +515,12 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
498
515
|
setCommandText('export ');
|
|
499
516
|
setCommandMode(true);
|
|
500
517
|
}
|
|
501
|
-
else if (item === '
|
|
502
|
-
setCommandText('
|
|
518
|
+
else if (item === 'Restore preferences and library') {
|
|
519
|
+
setCommandText('restore ');
|
|
503
520
|
setCommandMode(true);
|
|
504
521
|
}
|
|
505
522
|
else if (item === 'Check for updates') {
|
|
506
|
-
|
|
523
|
+
await updateFromSettings();
|
|
507
524
|
}
|
|
508
525
|
else if (item === 'Refresh provider health') {
|
|
509
526
|
refreshProviderHealth();
|
|
@@ -530,15 +547,18 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
530
547
|
if (screen === 'map') {
|
|
531
548
|
const country = filteredCountries[selectedRef.current];
|
|
532
549
|
if (country) {
|
|
533
|
-
|
|
550
|
+
await loadCountry(country);
|
|
534
551
|
}
|
|
535
552
|
return;
|
|
536
553
|
}
|
|
537
554
|
const inputStation = selectedStationForInput();
|
|
538
555
|
if (inputStation) {
|
|
539
|
-
|
|
556
|
+
await playStation(inputStation);
|
|
540
557
|
}
|
|
541
558
|
}
|
|
559
|
+
};
|
|
560
|
+
useInput((input, key) => {
|
|
561
|
+
void handleInput(input, key).catch(reportInputError);
|
|
542
562
|
});
|
|
543
563
|
}
|
|
544
564
|
function shouldScrollSelectionWithWheel(screen, commandMode, editingCountryFilter) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { favoriteTarget, normalizeMediaKeyBindings, parseMediaActionName } from './app-state.js';
|
|
3
|
-
|
|
3
|
+
import { importStreamUrl } from '../streams/import-stream.js';
|
|
4
|
+
export function useCommandExecutor({ beginLearningTransportKey, countries, go, loadCountry, openAirPlaySettings, openLibrary, persistLibrary, player, playingStation, providers, resetLearnedTransportKeys, runSearch, screen, selectedStation, setCountries, setFilters, setLibrary, setMessage, setSearchQuery, setSleepUntil, setVolume, settingsRef, store, toggleFavorite, toggleMute, updateCommand, updateSettings }) {
|
|
4
5
|
return useCallback(async (rawCommand) => {
|
|
5
6
|
const trimmed = rawCommand.trim();
|
|
6
7
|
if (!trimmed) {
|
|
@@ -146,9 +147,31 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
146
147
|
}
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
|
-
if (name === 'import'
|
|
150
|
+
if (name === 'import') {
|
|
150
151
|
if (!value.trim()) {
|
|
151
|
-
setMessage('Usage: :import <
|
|
152
|
+
setMessage('Usage: :import <stream-url> [station name]');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const [url = '', ...nameParts] = rest;
|
|
157
|
+
setMessage('Reading stream metadata...');
|
|
158
|
+
const result = await importStreamUrl(url, nameParts.join(' ') || undefined);
|
|
159
|
+
const nextLibrary = persistLibrary(() => store.addImported([result.station]));
|
|
160
|
+
if (!nextLibrary)
|
|
161
|
+
return;
|
|
162
|
+
openLibrary();
|
|
163
|
+
setMessage(result.warning
|
|
164
|
+
? `Imported ${result.station.name}. ${result.warning}`
|
|
165
|
+
: `Imported ${result.station.name}${result.station.codec ? ` · ${result.station.codec}` : ''}${result.station.bitrate ? ` · ${result.station.bitrate} kbps` : ''}.`);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
setMessage(error instanceof Error ? error.message : 'Could not import the radio stream.');
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (name === 'restore') {
|
|
173
|
+
if (!value.trim()) {
|
|
174
|
+
setMessage('Usage: :restore <backup.json>');
|
|
152
175
|
return;
|
|
153
176
|
}
|
|
154
177
|
try {
|
|
@@ -156,11 +179,11 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
156
179
|
settingsRef.current = result.state.settings;
|
|
157
180
|
setLibrary(result.state);
|
|
158
181
|
setMessage(result.safetyBackupPath
|
|
159
|
-
? `Backup
|
|
160
|
-
: `Backup
|
|
182
|
+
? `Backup restored. Previous library saved to ${result.safetyBackupPath}`
|
|
183
|
+
: `Backup restored: ${result.sourcePath}`);
|
|
161
184
|
}
|
|
162
185
|
catch (error) {
|
|
163
|
-
setMessage(error instanceof Error ? error.message : 'Could not
|
|
186
|
+
setMessage(error instanceof Error ? error.message : 'Could not restore the RadioCLI backup.');
|
|
164
187
|
}
|
|
165
188
|
return;
|
|
166
189
|
}
|
|
@@ -169,7 +192,7 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
169
192
|
return;
|
|
170
193
|
}
|
|
171
194
|
if (name === 'stop') {
|
|
172
|
-
|
|
195
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
173
196
|
await player.stop();
|
|
174
197
|
return;
|
|
175
198
|
}
|
|
@@ -203,6 +226,7 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
203
226
|
loadCountry,
|
|
204
227
|
openAirPlaySettings,
|
|
205
228
|
openLibrary,
|
|
229
|
+
persistLibrary,
|
|
206
230
|
player,
|
|
207
231
|
playingStation,
|
|
208
232
|
providers,
|
package/dist/update-check.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { appVersion } from './version.js';
|
|
4
|
+
import { networkPolicy, withExternalResponse } from './platform/network.js';
|
|
4
5
|
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
5
6
|
const UPDATE_PACKAGE_NAME = '@ciphore/radiocli';
|
|
6
7
|
export async function checkForUpdate({ currentVersion = appVersion(), fetchImpl = fetch, now = new Date(), packageName = UPDATE_PACKAGE_NAME, timeoutMs = 3000 } = {}) {
|
|
7
8
|
try {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const parsed = await withExternalResponse(registryLatestUrl(packageName), { fetchImpl, timeoutMs }, async (response) => {
|
|
10
|
+
if (!response.ok)
|
|
11
|
+
throw new Error(`npm registry returned ${response.status}`);
|
|
12
|
+
return await response.json();
|
|
13
|
+
});
|
|
13
14
|
const latestVersion = typeof parsed.version === 'string' ? parsed.version : undefined;
|
|
14
15
|
if (!latestVersion) {
|
|
15
16
|
throw new Error('npm registry response did not include a version');
|
|
@@ -31,7 +32,7 @@ export async function checkForUpdate({ currentVersion = appVersion(), fetchImpl
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
export function shouldCheckForUpdate(updateCheck, now = Date.now()) {
|
|
34
|
-
if (
|
|
35
|
+
if (!automaticUpdateChecksAllowed()) {
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
37
38
|
if (!updateCheck?.checkedAt) {
|
|
@@ -41,7 +42,7 @@ export function shouldCheckForUpdate(updateCheck, now = Date.now()) {
|
|
|
41
42
|
return !Number.isFinite(checkedAt) || now - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
|
|
42
43
|
}
|
|
43
44
|
export function automaticUpdateChecksAllowed(enabled = true) {
|
|
44
|
-
return enabled && process.env.RADIOCLI_DISABLE_UPDATE_CHECK !== '1' && process.env.CI !== 'true';
|
|
45
|
+
return enabled && !networkPolicy().offline && process.env.RADIOCLI_DISABLE_UPDATE_CHECK !== '1' && process.env.CI !== 'true';
|
|
45
46
|
}
|
|
46
47
|
export function updateAvailableForVersion(updateCheck, currentVersion = updateCheck?.currentVersion) {
|
|
47
48
|
return Boolean(updateCheck?.updateAvailable &&
|
|
@@ -113,16 +114,6 @@ export function compareSemver(left, right) {
|
|
|
113
114
|
function registryLatestUrl(packageName) {
|
|
114
115
|
return `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
115
116
|
}
|
|
116
|
-
async function fetchWithTimeout(url, fetchImpl, timeoutMs) {
|
|
117
|
-
const controller = new AbortController();
|
|
118
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
119
|
-
try {
|
|
120
|
-
return await fetchImpl(url, { signal: controller.signal });
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
clearTimeout(timer);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
117
|
function semverParts(version) {
|
|
127
118
|
const normalized = version.trim().replace(/^v/i, '').split(/[+-]/)[0] ?? '';
|
|
128
119
|
const [major = '0', minor = '0', patch = '0'] = normalized.split('.');
|