@ciphore/radiocli 0.2.2 → 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 +124 -1
- package/CONTRIBUTING.md +36 -6
- package/README.md +84 -10
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +165 -0
- package/dist/agent/launcher.js +10 -0
- package/dist/agent/mcp-install.js +467 -0
- package/dist/agent/mcp-server.js +139 -0
- package/dist/agent/service.js +347 -0
- package/dist/agent/session.js +232 -0
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/cli.js +4 -1
- 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 +96 -48
- 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 +157 -35
- 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 +220 -31
- package/dist/providers/cache.js +4 -26
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +156 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +149 -152
- package/dist/storage/store.js +138 -53
- package/dist/streams/import-stream.js +163 -0
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +54 -30
- package/dist/ui/App.js +466 -98
- package/dist/ui/AppContent.js +4 -4
- 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 +8 -8
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/components/VersionIndicator.js +19 -0
- 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 +5 -3
- package/dist/ui/screen-items.js +42 -11
- 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/AlarmsScreen.js +2 -1
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +7 -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 +80 -56
- 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 +40 -21
- package/dist/ui/use-app-input.js +69 -23
- package/dist/ui/use-command-executor.js +31 -7
- package/dist/ui/visualizers/gallop.js +118 -0
- package/dist/ui/visualizers/horse-stride.js +20 -0
- package/dist/ui/visualizers/receiver-style-registry.js +12 -2
- package/dist/ui/visualizers/receiver-visualizers.js +3 -0
- package/dist/ui/visualizers/retro-receivers.js +4 -0
- package/dist/ui/visualizers/terminal-receivers.js +57 -0
- package/dist/update-check.js +33 -23
- package/package.json +4 -1
package/dist/setup.js
CHANGED
|
@@ -1,47 +1,86 @@
|
|
|
1
|
+
import { componentLabel, detectPackageManager, packageCommandInvocation, packageInstallCommand, packageInstallPrerequisites, packageManagerElevation, packageManagerNeedsRoot, packageManagerNotes, packageManagerProgram, packageManagers, platformLabel } from './platform/packages.js';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
3
4
|
import { createInterface } from 'node:readline/promises';
|
|
4
|
-
import { clearCommandCache, commandExists } from './
|
|
5
|
+
import { clearCommandCache, commandExists, resolveCommand } from './platform/executables.js';
|
|
5
6
|
import { detectPlaybackBackends, playbackBackendStatusLines } from './player/backend-install.js';
|
|
7
|
+
import { configureMcpIntegrations } from './agent/mcp-install.js';
|
|
8
|
+
import { JsonLibraryStore } from './storage/store.js';
|
|
9
|
+
import { defaultAgentControlSettings } from './types.js';
|
|
10
|
+
import { resolveTerminalCapabilities } from './platform/terminal.js';
|
|
11
|
+
import { identifyPlatform, readLinuxOsRelease } from './platform/runtime.js';
|
|
6
12
|
const components = ['mpv', 'ffmpeg', 'vlc'];
|
|
7
|
-
const packageManagers = ['brew', 'winget', 'scoop', 'choco', 'apt', 'dnf', 'pacman', 'apk', 'zypper'];
|
|
8
13
|
export async function runSetup(options = {}) {
|
|
9
14
|
const platform = options.platform ?? process.platform;
|
|
10
15
|
const osRelease = options.osRelease ?? readLinuxOsRelease(platform);
|
|
16
|
+
const env = options.env ?? process.env;
|
|
17
|
+
const host = identifyPlatform({ platform, osRelease, env });
|
|
11
18
|
const input = options.input ?? process.stdin;
|
|
12
19
|
const output = options.output ?? process.stdout;
|
|
13
20
|
const hasCommand = options.hasCommand ?? commandExists;
|
|
14
21
|
const parsed = parseSetupArgs(options.args ?? []);
|
|
22
|
+
if (parsed.packageManager)
|
|
23
|
+
validateNativePackageManager(parsed.packageManager, host);
|
|
15
24
|
const installed = detectInstalledComponents(hasCommand);
|
|
16
|
-
const packageManager = parsed.packageManager ?? detectPackageManager(platform, osRelease, hasCommand);
|
|
25
|
+
const packageManager = parsed.packageManager ?? detectPackageManager(platform, osRelease, hasCommand, env);
|
|
17
26
|
writeHeader(output);
|
|
18
|
-
output.write(`System ${platformLabel(platform, osRelease)}
|
|
27
|
+
output.write(`System ${platformLabel(platform, osRelease, env)} ${separator(output)} Node ${process.version}\n`);
|
|
19
28
|
output.write(`Manager ${packageManager ?? 'not detected'}\n\n`);
|
|
20
29
|
let selected = parsed.only ?? defaultComponents(platform, parsed.all);
|
|
21
30
|
if (!parsed.yes && !parsed.only && isInteractive(input, output)) {
|
|
22
31
|
selected = await promptForComponents({ platform, installed, input, output });
|
|
23
32
|
}
|
|
24
|
-
|
|
33
|
+
let mcp = parsed.mcp;
|
|
34
|
+
if (mcp === null && !parsed.yes && isInteractive(input, output)) {
|
|
35
|
+
mcp = await promptYesNo(input, output, ' Agent control via MCP (detected coding agents)', true);
|
|
36
|
+
}
|
|
37
|
+
let agentUi = parsed.agentUi;
|
|
38
|
+
if (mcp === true && agentUi === null && !parsed.yes && isInteractive(input, output)) {
|
|
39
|
+
const note = platform === 'darwin'
|
|
40
|
+
? ' (macOS will ask the agent app to control Terminal on first use)'
|
|
41
|
+
: ' (opens a separate terminal window)';
|
|
42
|
+
agentUi = await promptYesNo(input, output, ` Open the RadioCLI TUI for agent playback${note}`, true);
|
|
43
|
+
}
|
|
44
|
+
const isRoot = (options.getUid ?? process.getuid)?.() === 0;
|
|
45
|
+
const elevation = packageManagerElevation(hasCommand, isRoot);
|
|
46
|
+
const plan = createSetupPlan({ platform, osRelease, env, packageManager, installed, selected, elevation });
|
|
25
47
|
printPlan(plan, output);
|
|
48
|
+
if (packageManager && packageManagerNeedsRoot(packageManager) && !isRoot && !elevation) {
|
|
49
|
+
output.write(' Note: Run package commands as root; sudo/doas unavailable.\n');
|
|
50
|
+
}
|
|
51
|
+
for (const note of packageManagerNotes(packageManager, host))
|
|
52
|
+
output.write(` Note: ${note}\n`);
|
|
26
53
|
const missing = plan.selected.filter(component => !plan.installed[component]);
|
|
27
54
|
if (missing.length === 0) {
|
|
28
55
|
output.write(plan.selected.length === 0 ? '\nNo components selected.\n' : '\nEverything selected is already installed.\n');
|
|
56
|
+
await finishMcpSetup(mcp, agentUi, parsed.dryRun, output);
|
|
29
57
|
printVerification(output);
|
|
30
58
|
return;
|
|
31
59
|
}
|
|
32
60
|
if (!plan.packageManager) {
|
|
33
61
|
if (parsed.dryRun) {
|
|
34
62
|
output.write('\nDry run complete. Install the missing components manually; no system packages were changed.\n');
|
|
63
|
+
await finishMcpSetup(mcp, agentUi, true, output);
|
|
35
64
|
return;
|
|
36
65
|
}
|
|
37
66
|
throw new Error('No supported system package manager was found. Install mpv manually, then run radiocli doctor.');
|
|
38
67
|
}
|
|
39
68
|
if (parsed.dryRun) {
|
|
40
69
|
output.write('\nDry run complete. No system packages were changed.\n');
|
|
70
|
+
await finishMcpSetup(mcp, agentUi, true, output);
|
|
41
71
|
return;
|
|
42
72
|
}
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
const manual = missing.filter(component => !plan.commands.some(command => command.component === component));
|
|
74
|
+
if (manual.length)
|
|
75
|
+
throw new Error(`No verified automatic installation command for ${manual.join(', ')} with ${plan.packageManager}. Install these components manually or select --only=mpv.`);
|
|
76
|
+
if (plan.packageManager === 'termux-pkg' && isRoot)
|
|
77
|
+
throw new Error('Run setup as the normal Termux app user. Termux pkg refuses root execution.');
|
|
78
|
+
if (packageManagerNeedsRoot(plan.packageManager) && !isRoot && !elevation) {
|
|
79
|
+
throw new Error('System package installation requires root, sudo, or doas. Review the dry-run plan and install prerequisites with your administrator.');
|
|
80
|
+
}
|
|
81
|
+
if (parsed.packageManager && !hasCommand(packageManagerProgram(parsed.packageManager))) {
|
|
82
|
+
const program = packageManagerProgram(parsed.packageManager);
|
|
83
|
+
throw new Error(`Requested package manager is not available: ${parsed.packageManager}${program !== parsed.packageManager ? ` (${program})` : ''}.`);
|
|
45
84
|
}
|
|
46
85
|
if (!parsed.yes && isInteractive(input, output)) {
|
|
47
86
|
const confirmed = await promptYesNo(input, output, '\nInstall these components?', true);
|
|
@@ -55,59 +94,44 @@ export async function runSetup(options = {}) {
|
|
|
55
94
|
}
|
|
56
95
|
if (plan.commands.some(command => command.program === 'sudo')) {
|
|
57
96
|
output.write('\nRadioCLI needs administrator approval for the system package manager.\n');
|
|
58
|
-
await runVisibleCommand('sudo', ['-v']);
|
|
97
|
+
await runVisibleCommand(resolveCommand('sudo') ?? 'sudo', ['-v']);
|
|
59
98
|
}
|
|
60
|
-
const execute = options.runCommand ?? runInstallCommand;
|
|
99
|
+
const execute = options.runCommand ?? ((command, destination) => runInstallCommand(command, destination, platform));
|
|
61
100
|
output.write('\nInstalling\n');
|
|
62
101
|
for (const command of plan.commands) {
|
|
63
102
|
await execute(command, output);
|
|
64
103
|
}
|
|
65
104
|
clearCommandCache();
|
|
66
105
|
output.write('\nSetup complete.\n');
|
|
106
|
+
await finishMcpSetup(mcp, agentUi, false, output);
|
|
67
107
|
printVerification(output);
|
|
68
108
|
}
|
|
69
|
-
export function createSetupPlan({ platform, osRelease = '', packageManager, installed, selected }) {
|
|
109
|
+
export function createSetupPlan({ platform, osRelease = '', env = process.env, packageManager, installed, selected, elevation = 'sudo' }) {
|
|
110
|
+
if (packageManager)
|
|
111
|
+
validateNativePackageManager(packageManager, identifyPlatform({ platform, osRelease, env }));
|
|
70
112
|
const uniqueSelected = components.filter(component => selected.includes(component));
|
|
71
113
|
const missing = uniqueSelected.filter(component => !installed[component]);
|
|
72
|
-
const commands = packageManager ? missing.map(component => packageInstallCommand(packageManager, component)) : [];
|
|
73
|
-
if (packageManager
|
|
74
|
-
commands.unshift(
|
|
75
|
-
component: null,
|
|
76
|
-
label: 'Scoop extras bucket',
|
|
77
|
-
program: 'scoop',
|
|
78
|
-
args: ['bucket', 'add', 'extras'],
|
|
79
|
-
display: 'scoop bucket add extras'
|
|
80
|
-
});
|
|
81
|
-
}
|
|
114
|
+
const commands = packageManager ? missing.map(component => packageInstallCommand(packageManager, component, { elevation })).filter((command) => command !== null) : [];
|
|
115
|
+
if (packageManager)
|
|
116
|
+
commands.unshift(...packageInstallPrerequisites(packageManager, missing));
|
|
82
117
|
return {
|
|
83
118
|
platform,
|
|
84
|
-
platformLabel: platformLabel(platform, osRelease),
|
|
119
|
+
platformLabel: platformLabel(platform, osRelease, env),
|
|
85
120
|
packageManager,
|
|
86
121
|
installed,
|
|
87
122
|
selected: uniqueSelected,
|
|
88
123
|
commands
|
|
89
124
|
};
|
|
90
125
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
? ['apt']
|
|
101
|
-
: hasAny(ids, ['fedora', 'rhel', 'centos'])
|
|
102
|
-
? ['dnf']
|
|
103
|
-
: hasAny(ids, ['arch', 'manjaro'])
|
|
104
|
-
? ['pacman']
|
|
105
|
-
: hasAny(ids, ['alpine'])
|
|
106
|
-
? ['apk']
|
|
107
|
-
: hasAny(ids, ['opensuse', 'suse'])
|
|
108
|
-
? ['zypper']
|
|
109
|
-
: ['apt', 'dnf', 'pacman', 'apk', 'zypper'];
|
|
110
|
-
return firstAvailable(preferred, hasCommand);
|
|
126
|
+
function validateNativePackageManager(manager, host) {
|
|
127
|
+
const required = host.id === 'termux' ? 'termux-pkg' : host.id === 'haiku' ? 'pkgman' : host.id === 'sunos' ? 'pkgin' : null;
|
|
128
|
+
if ((required && manager !== required)
|
|
129
|
+
|| (manager === 'termux-pkg' && host.id !== 'termux')
|
|
130
|
+
|| (manager === 'pkgman' && host.id !== 'haiku')
|
|
131
|
+
|| (manager === 'pkg' && host.id !== 'freebsd')
|
|
132
|
+
|| ['aix', 'android', 'unknown'].includes(host.id)) {
|
|
133
|
+
throw new Error(`No verified ${manager} playback package recipe for ${host.id}.${required ? ` Use --package-manager=${required}.` : ' Install a native player manually and run radiocli doctor.'}`);
|
|
134
|
+
}
|
|
111
135
|
}
|
|
112
136
|
export function parseSetupArgs(args) {
|
|
113
137
|
let all = false;
|
|
@@ -115,6 +139,8 @@ export function parseSetupArgs(args) {
|
|
|
115
139
|
let yes = false;
|
|
116
140
|
let only = null;
|
|
117
141
|
let packageManager = null;
|
|
142
|
+
let mcp = null;
|
|
143
|
+
let agentUi = null;
|
|
118
144
|
for (let index = 0; index < args.length; index += 1) {
|
|
119
145
|
const arg = args[index];
|
|
120
146
|
if (arg === '--all')
|
|
@@ -123,6 +149,26 @@ export function parseSetupArgs(args) {
|
|
|
123
149
|
dryRun = true;
|
|
124
150
|
else if (arg === '--yes' || arg === '-y')
|
|
125
151
|
yes = true;
|
|
152
|
+
else if (arg === '--mcp') {
|
|
153
|
+
if (mcp === false)
|
|
154
|
+
throw new Error('Use either --mcp or --no-mcp, not both.');
|
|
155
|
+
mcp = true;
|
|
156
|
+
}
|
|
157
|
+
else if (arg === '--no-mcp') {
|
|
158
|
+
if (mcp === true)
|
|
159
|
+
throw new Error('Use either --mcp or --no-mcp, not both.');
|
|
160
|
+
mcp = false;
|
|
161
|
+
}
|
|
162
|
+
else if (arg === '--agent-ui') {
|
|
163
|
+
if (agentUi === false)
|
|
164
|
+
throw new Error('Use either --agent-ui or --headless-agent, not both.');
|
|
165
|
+
agentUi = true;
|
|
166
|
+
}
|
|
167
|
+
else if (arg === '--headless-agent') {
|
|
168
|
+
if (agentUi === true)
|
|
169
|
+
throw new Error('Use either --agent-ui or --headless-agent, not both.');
|
|
170
|
+
agentUi = false;
|
|
171
|
+
}
|
|
126
172
|
else if (arg === '--only')
|
|
127
173
|
only = parseComponents(args[++index]);
|
|
128
174
|
else if (arg.startsWith('--only='))
|
|
@@ -136,7 +182,36 @@ export function parseSetupArgs(args) {
|
|
|
136
182
|
}
|
|
137
183
|
if (all && only)
|
|
138
184
|
throw new Error('Use either --all or --only, not both.');
|
|
139
|
-
|
|
185
|
+
if (agentUi !== null && mcp !== true)
|
|
186
|
+
throw new Error('--agent-ui and --headless-agent require --mcp.');
|
|
187
|
+
return { all, dryRun, yes, only, packageManager, mcp, agentUi };
|
|
188
|
+
}
|
|
189
|
+
async function finishMcpSetup(enabled, agentUi, dryRun, output) {
|
|
190
|
+
if (enabled === null)
|
|
191
|
+
return;
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
output.write(`\nAgent integration: would be ${enabled ? 'enabled and installed for detected MCP clients' : 'disabled and removed from detected MCP clients'}.\n`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const entry = process.argv[1];
|
|
197
|
+
if (!entry)
|
|
198
|
+
throw new Error('Could not locate the RadioCLI executable for MCP setup.');
|
|
199
|
+
const results = await configureMcpIntegrations(enabled, { nodePath: process.execPath, cliPath: realpathSync(entry) }, output);
|
|
200
|
+
const failed = results.filter(result => result.status === 'failed');
|
|
201
|
+
if (failed.length > 0) {
|
|
202
|
+
throw new Error(`Playback setup finished, but ${failed.length} agent integration${failed.length === 1 ? '' : 's'} failed: ${failed.map(result => result.client).join(', ')}. Run radiocli mcp status for details.`);
|
|
203
|
+
}
|
|
204
|
+
if (enabled) {
|
|
205
|
+
const store = new JsonLibraryStore();
|
|
206
|
+
const current = store.snapshot().settings.agentControl ?? defaultAgentControlSettings;
|
|
207
|
+
const openUiOnPlay = agentUi ?? current.openUiOnPlay;
|
|
208
|
+
if (openUiOnPlay !== current.openUiOnPlay) {
|
|
209
|
+
store.updateSettings({ agentControl: { ...current, openUiOnPlay } });
|
|
210
|
+
}
|
|
211
|
+
output.write(openUiOnPlay
|
|
212
|
+
? '\nAgent playback: terminal TUI enabled (default). The host OS may request app-control permission on first use.\n'
|
|
213
|
+
: '\nAgent playback: headless; no separate terminal window or app-control permission is needed.\n');
|
|
214
|
+
}
|
|
140
215
|
}
|
|
141
216
|
function parseComponents(value) {
|
|
142
217
|
const values = value?.split(',').map(item => item.trim().toLowerCase()).filter(Boolean) ?? [];
|
|
@@ -151,51 +226,6 @@ function parsePackageManager(value) {
|
|
|
151
226
|
}
|
|
152
227
|
return value;
|
|
153
228
|
}
|
|
154
|
-
function packageInstallCommand(manager, component) {
|
|
155
|
-
const packages = {
|
|
156
|
-
brew: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
157
|
-
winget: { mpv: 'shinchiro.mpv', ffmpeg: 'Gyan.FFmpeg', vlc: 'VideoLAN.VLC' },
|
|
158
|
-
scoop: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
159
|
-
choco: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
160
|
-
apt: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
161
|
-
dnf: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
162
|
-
pacman: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
163
|
-
apk: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' },
|
|
164
|
-
zypper: { mpv: 'mpv', ffmpeg: 'ffmpeg', vlc: 'vlc' }
|
|
165
|
-
};
|
|
166
|
-
const packageName = packages[manager][component];
|
|
167
|
-
let program = manager;
|
|
168
|
-
let args;
|
|
169
|
-
if (manager === 'brew')
|
|
170
|
-
args = ['install', component === 'vlc' ? '--cask' : packageName, ...(component === 'vlc' ? [packageName] : [])];
|
|
171
|
-
else if (manager === 'winget')
|
|
172
|
-
args = ['install', '--id', packageName, '-e', '--accept-package-agreements', '--accept-source-agreements'];
|
|
173
|
-
else if (manager === 'scoop')
|
|
174
|
-
args = ['install', packageName];
|
|
175
|
-
else if (manager === 'choco')
|
|
176
|
-
args = ['install', packageName, '-y'];
|
|
177
|
-
else if (manager === 'apt') {
|
|
178
|
-
program = 'sudo';
|
|
179
|
-
args = ['apt-get', 'install', '-y', packageName];
|
|
180
|
-
}
|
|
181
|
-
else if (manager === 'dnf') {
|
|
182
|
-
program = 'sudo';
|
|
183
|
-
args = ['dnf', 'install', '-y', packageName];
|
|
184
|
-
}
|
|
185
|
-
else if (manager === 'pacman') {
|
|
186
|
-
program = 'sudo';
|
|
187
|
-
args = ['pacman', '-S', '--needed', '--noconfirm', packageName];
|
|
188
|
-
}
|
|
189
|
-
else if (manager === 'apk') {
|
|
190
|
-
program = 'sudo';
|
|
191
|
-
args = ['apk', 'add', packageName];
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
program = 'sudo';
|
|
195
|
-
args = ['zypper', '--non-interactive', 'install', packageName];
|
|
196
|
-
}
|
|
197
|
-
return { component, label: componentLabel(component), program, args, display: [program, ...args].join(' ') };
|
|
198
|
-
}
|
|
199
229
|
function defaultComponents(platform, all) {
|
|
200
230
|
if (all)
|
|
201
231
|
return [...components];
|
|
@@ -211,11 +241,11 @@ function detectInstalledComponents(hasCommand) {
|
|
|
211
241
|
async function promptForComponents({ platform, installed, input, output }) {
|
|
212
242
|
output.write('Choose components (installed items will be skipped):\n');
|
|
213
243
|
const selected = [];
|
|
214
|
-
if (await promptYesNo(input, output, ` mpv Full playback controls${installed.mpv ?
|
|
244
|
+
if (await promptYesNo(input, output, ` mpv Full playback controls${installed.mpv ? ` ${separator(output)} installed` : ''}`, true))
|
|
215
245
|
selected.push('mpv');
|
|
216
|
-
if (await promptYesNo(input, output, ` FFmpeg ${platform === 'darwin' ? 'AirPlay + ' : ''}ffplay fallback${installed.ffmpeg ?
|
|
246
|
+
if (await promptYesNo(input, output, ` FFmpeg ${platform === 'darwin' ? 'AirPlay + ' : ''}ffplay fallback${installed.ffmpeg ? ` ${separator(output)} installed` : ''}`, platform === 'darwin'))
|
|
217
247
|
selected.push('ffmpeg');
|
|
218
|
-
if (await promptYesNo(input, output, ` VLC Additional playback fallback${installed.vlc ?
|
|
248
|
+
if (await promptYesNo(input, output, ` VLC Additional playback fallback${installed.vlc ? ` ${separator(output)} installed` : ''}`, false))
|
|
219
249
|
selected.push('vlc');
|
|
220
250
|
return selected;
|
|
221
251
|
}
|
|
@@ -233,19 +263,23 @@ async function promptYesNo(input, output, question, defaultValue) {
|
|
|
233
263
|
}
|
|
234
264
|
function printPlan(plan, output) {
|
|
235
265
|
output.write('\nInstallation plan\n');
|
|
266
|
+
for (const command of plan.commands.filter(command => command.component === null)) {
|
|
267
|
+
output.write(` ${pendingMark(output)} ${command.label} ${separator(output)} ${command.display}\n`);
|
|
268
|
+
}
|
|
236
269
|
for (const component of plan.selected) {
|
|
237
270
|
if (plan.installed[component])
|
|
238
271
|
output.write(` ${successMark(output)} ${componentLabel(component)} already installed\n`);
|
|
239
272
|
else {
|
|
240
273
|
const command = plan.commands.find(candidate => candidate.component === component);
|
|
241
|
-
output.write(` ${pendingMark(output)} ${componentLabel(component)}${command ?
|
|
274
|
+
output.write(` ${pendingMark(output)} ${componentLabel(component)} ${separator(output)} ${command ? command.display : 'manual installation required'}\n`);
|
|
242
275
|
}
|
|
243
276
|
}
|
|
244
277
|
if (plan.selected.length === 0)
|
|
245
278
|
output.write(' No components selected\n');
|
|
246
279
|
}
|
|
247
|
-
async function runInstallCommand(command, output) {
|
|
248
|
-
const
|
|
280
|
+
async function runInstallCommand(command, output, platform) {
|
|
281
|
+
const terminal = setupTerminal(output);
|
|
282
|
+
const interactive = terminal.interactive && !terminal.reduceMotion;
|
|
249
283
|
const startedAt = Date.now();
|
|
250
284
|
let timer;
|
|
251
285
|
let frame = 0;
|
|
@@ -261,7 +295,8 @@ async function runInstallCommand(command, output) {
|
|
|
261
295
|
}
|
|
262
296
|
try {
|
|
263
297
|
await new Promise((resolve, reject) => {
|
|
264
|
-
const
|
|
298
|
+
const invocation = packageCommandInvocation(command, platform, resolveCommand);
|
|
299
|
+
const child = spawn(invocation.program, invocation.args, { shell: false, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
265
300
|
child.stdout?.on('data', chunk => { stdout = tail(`${stdout}${String(chunk)}`); });
|
|
266
301
|
child.stderr?.on('data', chunk => { stderr = tail(`${stderr}${String(chunk)}`); });
|
|
267
302
|
child.once('error', reject);
|
|
@@ -289,8 +324,9 @@ function progressFrame(label, frame, elapsedMs, output) {
|
|
|
289
324
|
const cycle = travel * 2;
|
|
290
325
|
const offset = frame % cycle;
|
|
291
326
|
const start = offset <= travel ? offset : cycle - offset;
|
|
292
|
-
const
|
|
293
|
-
|
|
327
|
+
const unicode = setupTerminal(output).unicode;
|
|
328
|
+
const bar = Array.from({ length: width }, (_, index) => index >= start && index < start + segment ? (unicode ? '█' : '#') : (unicode ? '░' : '.')).join('');
|
|
329
|
+
return ` ${accent(output, unicode ? '◒' : '*')} ${accent(output, `[${bar}]`)} Installing ${label} ${formatElapsed(elapsedMs)}`;
|
|
294
330
|
}
|
|
295
331
|
function printVerification(output) {
|
|
296
332
|
clearCommandCache();
|
|
@@ -303,41 +339,34 @@ function printVerification(output) {
|
|
|
303
339
|
}
|
|
304
340
|
function writeHeader(output) {
|
|
305
341
|
output.write(`${accent(output, 'RADIOCLI')} SETUP RECEIVER\n`);
|
|
306
|
-
output.write(`${accent(output, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}\n`);
|
|
307
|
-
}
|
|
308
|
-
function platformLabel(platform, osRelease) {
|
|
309
|
-
if (platform === 'darwin')
|
|
310
|
-
return `${process.arch === 'arm64' ? 'macOS Apple Silicon' : 'macOS'}`;
|
|
311
|
-
if (platform === 'win32')
|
|
312
|
-
return 'Windows';
|
|
313
|
-
if (platform === 'linux')
|
|
314
|
-
return osReleaseValue(osRelease, 'PRETTY_NAME') || 'Linux';
|
|
315
|
-
return platform;
|
|
316
|
-
}
|
|
317
|
-
function componentLabel(component) {
|
|
318
|
-
if (component === 'ffmpeg')
|
|
319
|
-
return 'FFmpeg / ffplay';
|
|
320
|
-
if (component === 'vlc')
|
|
321
|
-
return 'VLC fallback';
|
|
322
|
-
return 'mpv';
|
|
342
|
+
output.write(`${accent(output, setupTerminal(output).unicode ? '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' : '------------------------------------')}\n`);
|
|
323
343
|
}
|
|
324
344
|
function isInteractive(input, output) {
|
|
325
345
|
return Boolean(input.isTTY && output.isTTY);
|
|
326
346
|
}
|
|
327
347
|
function accent(output, value) {
|
|
328
|
-
|
|
348
|
+
const level = setupTerminal(output).colorLevel;
|
|
349
|
+
const color = level === 3 ? '38;2;116;242;138' : level === 2 ? '38;5;120' : '32';
|
|
350
|
+
return level > 0 ? `\u001b[${color}m${value}\u001b[0m` : value;
|
|
329
351
|
}
|
|
330
352
|
function successMark(output) {
|
|
331
|
-
return accent(output, '✓');
|
|
353
|
+
return accent(output, setupTerminal(output).unicode ? '✓' : '+');
|
|
332
354
|
}
|
|
333
355
|
function pendingMark(output) {
|
|
334
|
-
return accent(output, '◆');
|
|
356
|
+
return accent(output, setupTerminal(output).unicode ? '◆' : '*');
|
|
335
357
|
}
|
|
336
358
|
function failureMark(output) {
|
|
337
|
-
|
|
359
|
+
const terminal = setupTerminal(output);
|
|
360
|
+
const value = terminal.unicode ? '✗' : 'x';
|
|
361
|
+
const color = terminal.colorLevel === 3 ? '38;2;255;95;135' : terminal.colorLevel === 2 ? '38;5;204' : '31';
|
|
362
|
+
return terminal.colorLevel > 0 ? `\u001b[${color}m${value}\u001b[0m` : value;
|
|
363
|
+
}
|
|
364
|
+
function setupTerminal(output) {
|
|
365
|
+
const stream = output;
|
|
366
|
+
return resolveTerminalCapabilities(process.env, { isTTY: Boolean(stream.isTTY), colorDepth: stream.getColorDepth?.() });
|
|
338
367
|
}
|
|
339
|
-
function
|
|
340
|
-
return
|
|
368
|
+
function separator(output) {
|
|
369
|
+
return setupTerminal(output).unicode ? '·' : '-';
|
|
341
370
|
}
|
|
342
371
|
function clearLine() {
|
|
343
372
|
return '\u001b[2K';
|
|
@@ -359,35 +388,3 @@ async function runVisibleCommand(program, args) {
|
|
|
359
388
|
child.once('close', code => code === 0 ? resolve() : reject(new Error(`${program} ${args.join(' ')} exited with code ${code}.`)));
|
|
360
389
|
});
|
|
361
390
|
}
|
|
362
|
-
function firstAvailable(values, hasCommand) {
|
|
363
|
-
return values.find(hasCommand) ?? null;
|
|
364
|
-
}
|
|
365
|
-
function readLinuxOsRelease(platform) {
|
|
366
|
-
if (platform !== 'linux' || !existsSync('/etc/os-release'))
|
|
367
|
-
return '';
|
|
368
|
-
try {
|
|
369
|
-
return readFileSync('/etc/os-release', 'utf8');
|
|
370
|
-
}
|
|
371
|
-
catch {
|
|
372
|
-
return '';
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
function linuxReleaseIds(osRelease) {
|
|
376
|
-
const ids = new Set();
|
|
377
|
-
for (const line of osRelease.split('\n')) {
|
|
378
|
-
const match = /^(ID|ID_LIKE)=(.*)$/.exec(line);
|
|
379
|
-
if (!match)
|
|
380
|
-
continue;
|
|
381
|
-
for (const value of match[2].replaceAll('"', '').split(/\s+/))
|
|
382
|
-
if (value.trim())
|
|
383
|
-
ids.add(value.trim().toLowerCase());
|
|
384
|
-
}
|
|
385
|
-
return ids;
|
|
386
|
-
}
|
|
387
|
-
function osReleaseValue(osRelease, key) {
|
|
388
|
-
const line = osRelease.split('\n').find(candidate => candidate.startsWith(`${key}=`));
|
|
389
|
-
return line?.slice(key.length + 1).replace(/^"|"$/g, '') ?? '';
|
|
390
|
-
}
|
|
391
|
-
function hasAny(values, candidates) {
|
|
392
|
-
return candidates.some(candidate => values.has(candidate));
|
|
393
|
-
}
|