@ciphore/radiocli 0.2.3 → 0.2.5
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 +147 -2
- package/CONTRIBUTING.md +46 -6
- package/README.md +108 -209
- package/SECURITY.md +23 -1
- package/dist/agent/alarm-service.js +10 -7
- package/dist/agent/cli.js +16 -1
- package/dist/agent/command-schema.js +47 -0
- package/dist/agent/headless-host.js +234 -67
- package/dist/agent/launcher.js +7 -67
- package/dist/agent/mcp-install.js +15 -15
- package/dist/agent/mcp-server.js +10 -5
- package/dist/agent/service.js +74 -39
- package/dist/agent/session.js +227 -53
- package/dist/agent/tui-session.js +213 -0
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/cli.js +19 -3
- package/dist/alarms/guard.js +79 -36
- package/dist/alarms/inhibitor.js +27 -22
- package/dist/alarms/occurrence-cache.js +25 -0
- package/dist/alarms/power-guard-store.js +2 -10
- package/dist/alarms/runner.js +140 -45
- package/dist/alarms/schedule.js +37 -2
- package/dist/alarms/scheduler.js +202 -52
- package/dist/alarms/setup-verification.js +1 -2
- package/dist/alarms/system-volume-linux.js +160 -0
- package/dist/alarms/system-volume-ownership.js +335 -0
- package/dist/alarms/system-volume.js +188 -26
- package/dist/alarms/terminal-launcher.js +76 -136
- package/dist/alarms/tui-presence.js +2 -4
- package/dist/cli/arguments.js +212 -0
- package/dist/cli/completion.js +44 -0
- package/dist/cli/doctor.js +156 -0
- package/dist/cli/interactive.js +79 -0
- package/dist/cli/listener.js +227 -0
- package/dist/cli/output.js +121 -0
- package/dist/cli/render-tui.js +34 -0
- package/dist/cli.js +198 -154
- package/dist/platform/capabilities.js +60 -0
- package/dist/platform/desktop-media.js +61 -0
- package/dist/platform/desktop.js +36 -0
- package/dist/platform/diagnostics.js +273 -0
- package/dist/{player/command.js → platform/executables.js} +43 -9
- package/dist/platform/ipc.js +14 -0
- package/dist/platform/json-worker.js +111 -0
- package/dist/platform/launch-command.js +138 -0
- package/dist/platform/loopback.js +44 -0
- package/dist/platform/mpris.js +356 -0
- package/dist/platform/network.js +380 -0
- package/dist/platform/omarchy.js +147 -0
- package/dist/platform/packages.js +222 -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 +223 -0
- package/dist/player/airplay-discovery.js +145 -60
- package/dist/player/airplay-policy.js +15 -0
- package/dist/player/airplay-process.js +63 -0
- package/dist/player/airplay-readiness.js +42 -0
- package/dist/player/airplay-sender-health.js +49 -49
- package/dist/player/airplay-sender-package.js +5 -0
- package/dist/player/airplay-sender-patch.js +102 -4
- package/dist/player/airplay-setup.js +51 -0
- package/dist/player/airplay-worker.js +97 -41
- package/dist/player/backend-install.js +17 -95
- package/dist/player/command-diagnostics.js +2 -2
- package/dist/player/ffmpeg-input.js +37 -0
- package/dist/player/mpv-desktop-media.js +194 -0
- package/dist/player/mpv-ipc-client.js +52 -2
- package/dist/player/player-controller.js +569 -82
- package/dist/player/radio-engine.js +282 -0
- package/dist/playlists/playlist.js +18 -16
- package/dist/providers/cache.js +293 -99
- package/dist/providers/discovery-session.js +111 -0
- package/dist/providers/provider-manager.js +106 -28
- package/dist/providers/radio-browser.js +418 -89
- package/dist/providers/radio-garden.js +18 -23
- package/dist/setup-playback.js +152 -0
- package/dist/setup.js +163 -168
- package/dist/storage/file-lock.js +113 -0
- package/dist/storage/read-settings.js +33 -0
- package/dist/storage/replace-file.js +20 -0
- package/dist/storage/repository.js +247 -0
- package/dist/storage/station-identity.js +21 -0
- package/dist/storage/station-schema.js +27 -0
- package/dist/storage/store.js +333 -107
- package/dist/streams/import-stream.js +163 -0
- package/dist/ui/AdaptiveContent.js +49 -32
- package/dist/ui/App.js +822 -481
- package/dist/ui/AppContent.js +5 -5
- package/dist/ui/airplay-settings.js +12 -5
- package/dist/ui/app-state.js +8 -1
- package/dist/ui/ascii.js +11 -2
- package/dist/ui/bidi-ranges.js +45 -0
- package/dist/ui/components/AdaptiveMarquee.js +20 -10
- 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/format.js +37 -4
- package/dist/ui/help-content.js +9 -6
- package/dist/ui/layout.js +5 -2
- package/dist/ui/page-footer.js +3 -3
- package/dist/ui/rtl-output.js +137 -0
- package/dist/ui/screen-items.js +12 -6
- package/dist/ui/screen-meta.js +1 -1
- package/dist/ui/screens/AirPlayCodeScreen.js +6 -2
- package/dist/ui/screens/AirPlaySettingsScreen.js +10 -6
- package/dist/ui/screens/AlarmsScreen.js +5 -2
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +8 -4
- 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 +28 -21
- 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/text-direction.js +9 -0
- package/dist/ui/use-alarm-tui.js +57 -42
- package/dist/ui/use-app-input.js +84 -30
- package/dist/ui/use-command-executor.js +50 -19
- package/dist/ui/visualizers/gallop.js +1 -1
- package/dist/ui/visualizers/receiver-style-registry.js +2 -1
- package/dist/update-check.js +10 -18
- package/docs/THIRD_PARTY_NOTICES.md +6 -4
- package/package.json +34 -11
- package/scripts/build-airplay-native.mjs +22 -0
- package/vendor/node-airtunes2/LICENSE +208 -0
- package/vendor/node-airtunes2/RADIOCLI_PATCH.md +29 -0
- package/vendor/node-airtunes2/README.md +225 -0
- package/vendor/node-airtunes2/UPSTREAM.json +50 -0
- package/vendor/node-airtunes2/binding.gyp +41 -0
- package/vendor/node-airtunes2/lib/ap2_test.js +4 -0
- package/vendor/node-airtunes2/lib/atv.js +307 -0
- package/vendor/node-airtunes2/lib/atvAuthenticator.js +136 -0
- package/vendor/node-airtunes2/lib/audio_out.js +69 -0
- package/vendor/node-airtunes2/lib/circular_buffer.js +133 -0
- package/vendor/node-airtunes2/lib/config.js +26 -0
- package/vendor/node-airtunes2/lib/device_airtunes.js +532 -0
- package/vendor/node-airtunes2/lib/devices.js +266 -0
- package/vendor/node-airtunes2/lib/ed25519.js +26 -0
- package/vendor/node-airtunes2/lib/homekit/credentials.js +86 -0
- package/vendor/node-airtunes2/lib/homekit/encryption.js +69 -0
- package/vendor/node-airtunes2/lib/homekit/number.js +44 -0
- package/vendor/node-airtunes2/lib/homekit/tlv.js +95 -0
- package/vendor/node-airtunes2/lib/http.js +189 -0
- package/vendor/node-airtunes2/lib/index.d.ts +141 -0
- package/vendor/node-airtunes2/lib/index.js +127 -0
- package/vendor/node-airtunes2/lib/ntp.js +37 -0
- package/vendor/node-airtunes2/lib/num_util.js +21 -0
- package/vendor/node-airtunes2/lib/packet_pool.js +43 -0
- package/vendor/node-airtunes2/lib/rtsp.js +1807 -0
- package/vendor/node-airtunes2/lib/srp.js +158 -0
- package/vendor/node-airtunes2/lib/udp_servers.js +214 -0
- package/vendor/node-airtunes2/lib/util.js +12 -0
- package/vendor/node-airtunes2/package.json +31 -0
- package/vendor/node-airtunes2/src/CADebugMacros.h +257 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.cpp +105 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.h +224 -0
- package/vendor/node-airtunes2/src/aes_utils.c +864 -0
- package/vendor/node-airtunes2/src/aes_utils.h +24 -0
- package/vendor/node-airtunes2/src/base64.c +136 -0
- package/vendor/node-airtunes2/src/base64.h +42 -0
- package/vendor/node-airtunes2/src/bindings.cc +29 -0
- package/vendor/node-airtunes2/src/codec.cc +170 -0
- package/vendor/node-airtunes2/src/coreaudio.cc +332 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AlarmOccurrenceCache } from '../alarms/occurrence-cache.js';
|
|
2
|
+
import { canonicalizeAlarmTime, canonicalizeIsoWeekdays, canonicalizeTimeZone } from '../alarms/schedule.js';
|
|
2
3
|
import { connectActiveAlarms } from '../alarms/active-session.js';
|
|
3
4
|
import { createSchedulerService } from '../alarms/scheduler.js';
|
|
4
5
|
export class AgentAlarmService {
|
|
@@ -6,6 +7,7 @@ export class AgentAlarmService {
|
|
|
6
7
|
resolveStation;
|
|
7
8
|
scheduler;
|
|
8
9
|
handoffToInteractive;
|
|
10
|
+
occurrences = new AlarmOccurrenceCache();
|
|
9
11
|
constructor(store, resolveStation, scheduler = createSchedulerService(), handoffToInteractive) {
|
|
10
12
|
this.store = store;
|
|
11
13
|
this.resolveStation = resolveStation;
|
|
@@ -45,7 +47,7 @@ export class AgentAlarmService {
|
|
|
45
47
|
keepAwakeUntilAlarm: input.keepAwakeUntilAlarm ?? false
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
|
-
const alarm = this.store.addAlarm(alarmInput);
|
|
50
|
+
const alarm = await this.store.addAlarm(alarmInput);
|
|
49
51
|
await this.syncSaved(alarm);
|
|
50
52
|
return this.describe(alarm);
|
|
51
53
|
}
|
|
@@ -55,7 +57,7 @@ export class AgentAlarmService {
|
|
|
55
57
|
const fallbackStation = input.clearFallback
|
|
56
58
|
? undefined
|
|
57
59
|
: input.fallbackStationId ? await this.requiredStation(input.fallbackStationId) : alarm.playback.fallbackStation;
|
|
58
|
-
const updated = this.store.updateAlarm(alarm.id, {
|
|
60
|
+
const updated = await this.store.updateAlarm(alarm.id, {
|
|
59
61
|
label: input.label === undefined ? alarm.label : cleanLabel(input.label),
|
|
60
62
|
enabled: input.enabled ?? alarm.enabled,
|
|
61
63
|
station,
|
|
@@ -76,7 +78,7 @@ export class AgentAlarmService {
|
|
|
76
78
|
return this.describe(updated);
|
|
77
79
|
}
|
|
78
80
|
async setEnabled(id, enabled) {
|
|
79
|
-
const alarm = this.store.toggleAlarm(this.requiredAlarm(id).id, enabled);
|
|
81
|
+
const alarm = await this.store.toggleAlarm(this.requiredAlarm(id).id, enabled);
|
|
80
82
|
await this.syncSaved(alarm);
|
|
81
83
|
return this.describe(alarm);
|
|
82
84
|
}
|
|
@@ -84,8 +86,9 @@ export class AgentAlarmService {
|
|
|
84
86
|
if (!confirm)
|
|
85
87
|
throw new Error('Alarm removal requires confirm=true. List alarms first and confirm the exact alarm ID with the user.');
|
|
86
88
|
const alarm = this.requiredAlarm(id);
|
|
87
|
-
await this.
|
|
88
|
-
|
|
89
|
+
const disabled = await this.store.toggleAlarm(alarm.id, false);
|
|
90
|
+
await this.scheduler.remove(disabled.id);
|
|
91
|
+
if (!await this.store.removeAlarm(alarm.id))
|
|
89
92
|
throw new Error(`Alarm not found: ${id}`);
|
|
90
93
|
return { ok: true, removed: alarm.id };
|
|
91
94
|
}
|
|
@@ -117,7 +120,7 @@ export class AgentAlarmService {
|
|
|
117
120
|
enabled: alarm.enabled,
|
|
118
121
|
station: { id: `${alarm.station.provider}:${alarm.station.id}`, name: alarm.station.name },
|
|
119
122
|
schedule: alarm.schedule,
|
|
120
|
-
nextOccurrence:
|
|
123
|
+
nextOccurrence: this.occurrences.next(alarm, new Date())?.toISOString() ?? null,
|
|
121
124
|
playback: {
|
|
122
125
|
volume: alarm.playback.volume,
|
|
123
126
|
fadeSeconds: alarm.playback.fadeSeconds,
|
package/dist/agent/cli.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { airPlayPolicy } from '../player/airplay-policy.js';
|
|
2
|
+
import { readSettingsWithoutMigration } from '../storage/read-settings.js';
|
|
3
|
+
import { defaultLibraryPath } from '../storage/store.js';
|
|
1
4
|
import { AgentRadioService, stationForAgent } from './service.js';
|
|
2
5
|
import { configureMcpIntegrations, mcpIntegrationReport, portableMcpConfig } from './mcp-install.js';
|
|
3
6
|
import { runMcpServer } from './mcp-server.js';
|
|
@@ -64,7 +67,7 @@ export async function runAgentCliCommand(args, runtime) {
|
|
|
64
67
|
else if (command === 'done')
|
|
65
68
|
value = await service.runCompletionPreset();
|
|
66
69
|
else
|
|
67
|
-
throw new Error(
|
|
70
|
+
throw new Error(agentCommandUsage());
|
|
68
71
|
console.log(JSON.stringify(value, null, 2));
|
|
69
72
|
}
|
|
70
73
|
function parseBrowseKind(value) {
|
|
@@ -191,3 +194,15 @@ function assertMcpInstallSucceeded(results, action) {
|
|
|
191
194
|
return;
|
|
192
195
|
throw new Error(`RadioCLI was ${action} where possible, but ${failed.length} integration${failed.length === 1 ? '' : 's'} failed: ${failed.map(result => `${result.client} (${result.detail})`).join('; ')}`);
|
|
193
196
|
}
|
|
197
|
+
export function agentCommandUsage() {
|
|
198
|
+
let enabled = airPlayPolicy().enabled;
|
|
199
|
+
if (process.platform === 'linux') {
|
|
200
|
+
try {
|
|
201
|
+
enabled = airPlayPolicy({ linuxAirPlayEnabled: readSettingsWithoutMigration(defaultLibraryPath())?.linuxAirPlayEnabled === true }).enabled;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
enabled = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return `Usage: radiocli agent <status|search|browse|play|pause|resume|stop|next|previous|volume|mute|unmute|favorite|unfavorite|stats|appearance${enabled ? '|airplay' : ''}|preset|done>`;
|
|
208
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { receiverStyleNames, themeNames } from '../types.js';
|
|
3
|
+
import { stationSchema } from '../storage/station-schema.js';
|
|
4
|
+
const empty = (type) => z.strictObject({ type: z.literal(type) });
|
|
5
|
+
const adjacent = { preservePause: z.boolean().optional(), preserveStop: z.boolean().optional() };
|
|
6
|
+
// A TUI list can contain many discovery pages, beyond one provider page.
|
|
7
|
+
const stationQueue = z.array(stationSchema).max(10_000);
|
|
8
|
+
const agentControl = z.strictObject({
|
|
9
|
+
enabled: z.boolean(), openUiOnPlay: z.boolean(), focusNowPlaying: z.boolean(),
|
|
10
|
+
completionPreset: z.strictObject({
|
|
11
|
+
action: z.enum(['play', 'pause', 'resume', 'stop']), source: z.enum(['recent', 'favorite', 'popular', 'country']),
|
|
12
|
+
countryCode: z.string().length(2).optional(), ifPlaying: z.enum(['keep', 'replace']), openUi: z.boolean().optional()
|
|
13
|
+
})
|
|
14
|
+
});
|
|
15
|
+
const commandSchema = z.discriminatedUnion('type', [
|
|
16
|
+
empty('status'), empty('pause'), empty('alarm-preempt'), empty('airplay-list'), empty('airplay-local'), empty('tui-cancel'), empty('tui-refresh'), empty('tui-toggle-muted'), empty('tui-toggle-pause'),
|
|
17
|
+
z.strictObject({ type: z.literal('tui-sleep'), until: z.number().int().nonnegative().max(8.64e15).nullable() }),
|
|
18
|
+
z.strictObject({ type: z.literal('tui-adjust-volume'), delta: z.number().min(-100).max(100) }),
|
|
19
|
+
z.strictObject({ type: z.literal('desktop-transport'), action: z.enum(['play', 'pause', 'toggle', 'stop', 'next', 'previous']) }),
|
|
20
|
+
z.strictObject({ type: z.literal('play'), station: stationSchema, queue: stationQueue.optional(), openNowPlaying: z.boolean().optional(), ifPlaying: z.enum(['keep', 'replace']).optional(), startPaused: z.boolean().optional() }),
|
|
21
|
+
z.strictObject({ type: z.literal('tui-attach'), pid: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), clientId: z.string().uuid() }),
|
|
22
|
+
z.strictObject({ type: z.literal('tui-detach'), clientId: z.string().uuid() }),
|
|
23
|
+
z.strictObject({ type: z.literal('tui-select'), station: stationSchema, queue: stationQueue }),
|
|
24
|
+
z.strictObject({ type: z.literal('tui-output'), deviceId: z.string().min(1).max(4096).nullable() }),
|
|
25
|
+
z.strictObject({ type: z.literal('resume'), restartStopped: z.boolean().optional() }),
|
|
26
|
+
z.strictObject({ type: z.literal('stop'), retainSelection: z.boolean().optional() }),
|
|
27
|
+
z.strictObject({ type: z.literal('next'), ...adjacent }), z.strictObject({ type: z.literal('previous'), ...adjacent }),
|
|
28
|
+
z.strictObject({ type: z.literal('set-volume'), volume: z.number().min(0).max(100), unmute: z.boolean().optional(), allowStopped: z.boolean().optional() }),
|
|
29
|
+
z.strictObject({ type: z.literal('set-muted'), muted: z.boolean(), allowStopped: z.boolean().optional() }),
|
|
30
|
+
z.strictObject({ type: z.literal('set-favorite'), favorite: z.boolean(), station: stationSchema.optional() }),
|
|
31
|
+
z.strictObject({ type: z.literal('airplay-select'), deviceId: z.string().min(1).max(4096) }),
|
|
32
|
+
z.strictObject({ type: z.literal('airplay-passcode'), code: z.string().min(1).max(128) }),
|
|
33
|
+
z.strictObject({ type: z.literal('update-settings'), settings: z.strictObject({ theme: z.enum(themeNames).optional(), receiverStyle: z.enum(receiverStyleNames).optional(), agentControl: agentControl.optional() }) })
|
|
34
|
+
]);
|
|
35
|
+
export function parseRadioCommand(value) {
|
|
36
|
+
const parsed = commandSchema.safeParse(value);
|
|
37
|
+
if (!parsed.success)
|
|
38
|
+
throw new Error(`Invalid radio command: ${parsed.error.issues.map(issue => `${issue.path.join('.') || 'command'}: ${issue.message}`).slice(0, 3).join('; ')}`);
|
|
39
|
+
return parsed.data;
|
|
40
|
+
}
|
|
41
|
+
export function parseRadioRequest(value) {
|
|
42
|
+
if (value && typeof value === 'object' && 'version' in value) {
|
|
43
|
+
const envelope = z.strictObject({ version: z.literal(2), requestId: z.string().uuid(), command: z.unknown() }).parse(value);
|
|
44
|
+
return { command: parseRadioCommand(envelope.command), requestId: envelope.requestId };
|
|
45
|
+
}
|
|
46
|
+
return { command: parseRadioCommand(value) };
|
|
47
|
+
}
|
|
@@ -1,74 +1,196 @@
|
|
|
1
1
|
import { PlayerController } from '../player/player-controller.js';
|
|
2
2
|
import { ProviderManager } from '../providers/provider-manager.js';
|
|
3
3
|
import { JsonLibraryStore, stationKey } from '../storage/store.js';
|
|
4
|
-
import {
|
|
4
|
+
import { LibraryRepository } from '../storage/repository.js';
|
|
5
|
+
import { RadioEngine } from '../player/radio-engine.js';
|
|
6
|
+
import { allowedRadioSessionSettings, resolveDesktopTransport, serializeRadioCommands, startRadioSession } from './session.js';
|
|
7
|
+
import { desktopMediaPolicy, startDesktopMediaControls } from '../platform/desktop-media.js';
|
|
5
8
|
import { detectPlaybackBackends } from '../player/backend-install.js';
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
import { networkPolicy } from '../platform/network.js';
|
|
10
|
+
export async function runHeadlessAgentHost(options = {}) {
|
|
11
|
+
const store = new LibraryRepository(options.store ?? new JsonLibraryStore(), { worker: options.worker });
|
|
8
12
|
const providers = new ProviderManager();
|
|
9
|
-
const player = new PlayerController(() => store.
|
|
13
|
+
const player = new PlayerController(() => store.settings());
|
|
10
14
|
player.refreshDetectedBackends();
|
|
11
|
-
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const engine = new RadioEngine(player, providers, store, () => store.settings());
|
|
16
|
+
let persistenceWarning;
|
|
17
|
+
let desktopSuspended = false;
|
|
18
|
+
let shuttingDown = false;
|
|
19
|
+
let media;
|
|
20
|
+
let tui;
|
|
21
|
+
let terminalProtocol = false;
|
|
22
|
+
let focusNowPlaying = 0;
|
|
23
|
+
let sleepUntil = null;
|
|
24
|
+
let sleepTimer;
|
|
25
|
+
const scheduleSleep = () => {
|
|
26
|
+
if (sleepUntil === null)
|
|
27
|
+
return;
|
|
28
|
+
const remaining = sleepUntil - Date.now();
|
|
29
|
+
if (remaining <= 0) {
|
|
30
|
+
sleepUntil = null;
|
|
31
|
+
void dispatch({ type: 'stop', retainSelection: true }).catch(() => undefined);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
sleepTimer = setTimeout(scheduleSleep, Math.min(2_147_483_647, remaining));
|
|
35
|
+
sleepTimer.unref();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const tuiPresent = () => {
|
|
39
|
+
if (!tui)
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
process.kill(tui.pid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return error.code !== 'ESRCH';
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const saveLibrary = async (write) => {
|
|
50
|
+
await write();
|
|
51
|
+
persistenceWarning = undefined;
|
|
52
|
+
engine.clearWarning();
|
|
53
|
+
};
|
|
54
|
+
const status = () => {
|
|
55
|
+
const current = engine.snapshot();
|
|
56
|
+
const settings = store.settings();
|
|
57
|
+
return { owner: tuiPresent() ? 'tui' : 'headless', playback: player.getState(), station: current.station, nowPlaying: current.nowPlaying,
|
|
58
|
+
queue: current.queue, persistenceWarning: persistenceWarning ?? current.warning, output: {
|
|
59
|
+
preferredBackend: settings.preferredBackend === 'airplay' && player.airPlaySelectionRequired() ? 'auto' : settings.preferredBackend,
|
|
60
|
+
preferredAirPlayDevice: settings.preferredAirPlayDevice
|
|
61
|
+
}, ...(terminalProtocol ? { tui: { tuningStation: current.tuningStation, message: current.message, focusNowPlaying, sleepUntil, diagnostics: player.diagnostics(),
|
|
62
|
+
devices: player.detectedAirPlayDevices(), selectionRequired: player.airPlaySelectionRequired(), desktopMediaCompatible: player.desktopMediaCompatible() } } : {}) };
|
|
63
|
+
};
|
|
17
64
|
const result = (message, ok = true, data) => ({ ok, message, status: status(), ...(data ? { data } : {}) });
|
|
18
|
-
const play = async (next, nextQueue = [next]) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
65
|
+
const play = async (next, nextQueue = [next], startPaused = false, signal) => {
|
|
66
|
+
signal?.throwIfAborted();
|
|
67
|
+
// Earlier accepted output saves must reach their durable acknowledgement
|
|
68
|
+
// before this tune chooses a backend. Stop can still cancel the wait.
|
|
69
|
+
await store.readFresh();
|
|
70
|
+
signal?.throwIfAborted();
|
|
71
|
+
desktopSuspended = false;
|
|
72
|
+
const tuned = await engine.tune(next, { queue: nextQueue, startPaused, signal });
|
|
73
|
+
if (shuttingDown)
|
|
74
|
+
return result('RadioCLI is shutting down.', false);
|
|
75
|
+
media?.refresh();
|
|
76
|
+
return result(`${startPaused ? 'Selected' : 'Playing'} ${tuned.name}.`);
|
|
28
77
|
};
|
|
29
|
-
const handle = async (command) => {
|
|
78
|
+
const handle = async (command, signal) => {
|
|
79
|
+
if (shuttingDown)
|
|
80
|
+
return result('RadioCLI is shutting down.', false);
|
|
81
|
+
signal?.throwIfAborted();
|
|
82
|
+
const { station, queue } = engine.snapshot();
|
|
83
|
+
if (command.type === 'tui-attach') {
|
|
84
|
+
if (tuiPresent() && tui?.clientId !== command.clientId)
|
|
85
|
+
return result('RadioCLI is already open in another terminal.', false);
|
|
86
|
+
tui = { pid: command.pid, clientId: command.clientId };
|
|
87
|
+
terminalProtocol = true;
|
|
88
|
+
return result(station ? `Reconnected to ${station.name}.` : 'RadioCLI connected.');
|
|
89
|
+
}
|
|
90
|
+
if (command.type === 'tui-detach') {
|
|
91
|
+
if (tui?.clientId !== command.clientId)
|
|
92
|
+
return result('This terminal no longer owns the RadioCLI view.', false);
|
|
93
|
+
tui = undefined;
|
|
94
|
+
shuttingDown = true;
|
|
95
|
+
await engine.stop();
|
|
96
|
+
// Let the command response finish, then close our own resources directly.
|
|
97
|
+
// Sending SIGTERM to ourselves terminates Windows without running cleanup.
|
|
98
|
+
setTimeout(onShutdown, 50).unref();
|
|
99
|
+
return result('Closed RadioCLI.');
|
|
100
|
+
}
|
|
101
|
+
if (command.type === 'tui-cancel') {
|
|
102
|
+
engine.cancel();
|
|
103
|
+
return result('Canceled pending tune.');
|
|
104
|
+
}
|
|
105
|
+
if (command.type === 'tui-sleep') {
|
|
106
|
+
clearTimeout(sleepTimer);
|
|
107
|
+
sleepUntil = command.until;
|
|
108
|
+
scheduleSleep();
|
|
109
|
+
return result('Sleep timer updated.');
|
|
110
|
+
}
|
|
111
|
+
if (command.type === 'tui-adjust-volume')
|
|
112
|
+
return handle({ type: 'set-volume', volume: Math.max(0, Math.min(100, player.getState().volume + command.delta)) }, signal);
|
|
113
|
+
if (command.type === 'tui-toggle-muted')
|
|
114
|
+
return handle({ type: 'set-muted', muted: !player.getState().muted }, signal);
|
|
115
|
+
if (command.type === 'tui-toggle-pause') {
|
|
116
|
+
const control = await player.togglePause();
|
|
117
|
+
await engine.flushHistory();
|
|
118
|
+
return result(control.message ?? 'Playback updated.', control.ok);
|
|
119
|
+
}
|
|
120
|
+
if (command.type === 'tui-select') {
|
|
121
|
+
engine.select(command.station, command.queue);
|
|
122
|
+
media?.refresh();
|
|
123
|
+
return result(`Selected ${command.station.name}.`);
|
|
124
|
+
}
|
|
125
|
+
if (command.type === 'tui-refresh') {
|
|
126
|
+
await store.readFresh();
|
|
127
|
+
player.refreshDetectedBackends();
|
|
128
|
+
return result('Playback settings refreshed.');
|
|
129
|
+
}
|
|
130
|
+
if (command.type === 'tui-output') {
|
|
131
|
+
await store.readFresh();
|
|
132
|
+
const selected = await player.selectAirPlayDevice(command.deviceId);
|
|
133
|
+
return result(selected ? 'Audio output selected.' : 'Audio output is unavailable.', selected);
|
|
134
|
+
}
|
|
135
|
+
if (command.type === 'desktop-transport') {
|
|
136
|
+
const resolved = resolveDesktopTransport(command.action, status());
|
|
137
|
+
return resolved ? handle(resolved, signal) : result('Playback state unchanged.');
|
|
138
|
+
}
|
|
139
|
+
if (['airplay-list', 'airplay-select', 'airplay-passcode'].includes(command.type) && !player.airPlayPolicy().enabled) {
|
|
140
|
+
return result(player.airPlayPolicy().message, false, command.type === 'airplay-list' ? [] : undefined);
|
|
141
|
+
}
|
|
142
|
+
if (['airplay-list', 'airplay-select'].includes(command.type) && networkPolicy().offline)
|
|
143
|
+
return result('AirPlay discovery is disabled by RADIOCLI_OFFLINE=1.', false, []);
|
|
30
144
|
if (command.type === 'status')
|
|
31
145
|
return result(station ? `${player.getState().state}: ${station.name}` : 'RadioCLI is idle.');
|
|
32
146
|
if (command.type === 'play') {
|
|
33
147
|
if (command.ifPlaying === 'keep' && ['playing', 'paused', 'loading'].includes(player.getState().state))
|
|
34
148
|
return result(`Kept current station ${station?.name ?? ''}.`);
|
|
35
|
-
|
|
149
|
+
if (command.openNowPlaying)
|
|
150
|
+
focusNowPlaying++;
|
|
151
|
+
return play(command.station, command.queue, command.startPaused, signal);
|
|
36
152
|
}
|
|
37
153
|
if (command.type === 'pause') {
|
|
38
|
-
const control = await
|
|
39
|
-
if (control.ok)
|
|
40
|
-
store.finishActiveListeningSession();
|
|
154
|
+
const control = await engine.pause();
|
|
41
155
|
return result(control.message ?? 'Paused.', control.ok);
|
|
42
156
|
}
|
|
43
157
|
if (command.type === 'resume') {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
158
|
+
if (command.restartStopped && station && ['idle', 'stopped'].includes(player.getState().state))
|
|
159
|
+
return play(station, queue, false, signal);
|
|
160
|
+
const control = await engine.resume();
|
|
47
161
|
return result(control.message ?? 'Resumed.', control.ok);
|
|
48
162
|
}
|
|
49
163
|
if (command.type === 'stop') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
164
|
+
await engine.stop({ retainSelection: command.retainSelection });
|
|
165
|
+
if (command.retainSelection) {
|
|
166
|
+
media?.refresh();
|
|
167
|
+
return result('Stopped playback.');
|
|
168
|
+
}
|
|
169
|
+
if (!tuiPresent()) {
|
|
170
|
+
shuttingDown = true;
|
|
171
|
+
setTimeout(onShutdown, 50).unref();
|
|
172
|
+
}
|
|
55
173
|
return result('Stopped RadioCLI.');
|
|
56
174
|
}
|
|
57
175
|
if (command.type === 'alarm-preempt') {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
queue = [];
|
|
176
|
+
desktopSuspended = true;
|
|
177
|
+
media?.refresh();
|
|
178
|
+
await engine.stop();
|
|
62
179
|
return result('Interactive playback yielded to the alarm.');
|
|
63
180
|
}
|
|
64
181
|
if (command.type === 'set-volume') {
|
|
65
|
-
const control = await player.setVolume(command.volume);
|
|
182
|
+
const control = command.unmute || command.allowStopped ? await player.setVolume(command.volume, { allowStopped: true }) : await player.setVolume(command.volume);
|
|
66
183
|
if (control.ok)
|
|
67
|
-
store.updateSettings({ volume: player.getState().volume });
|
|
184
|
+
await saveLibrary(() => store.updateSettings({ volume: player.getState().volume }));
|
|
185
|
+
if (control.ok && command.unmute && command.volume > 0) {
|
|
186
|
+
const unmuted = await player.setMuted(false, { allowStopped: true });
|
|
187
|
+
if (!unmuted.ok)
|
|
188
|
+
return result(unmuted.message ?? 'The output could not be unmuted.', false);
|
|
189
|
+
}
|
|
68
190
|
return result(control.message ?? `Volume ${player.getState().volume}.`, control.ok);
|
|
69
191
|
}
|
|
70
192
|
if (command.type === 'set-muted') {
|
|
71
|
-
const control = await player.setMuted(command.muted);
|
|
193
|
+
const control = await player.setMuted(command.muted, { allowStopped: command.allowStopped });
|
|
72
194
|
return result(control.message ?? (command.muted ? 'Muted.' : 'Unmuted.'), control.ok);
|
|
73
195
|
}
|
|
74
196
|
if (command.type === 'set-favorite') {
|
|
@@ -77,8 +199,8 @@ export async function runHeadlessAgentHost() {
|
|
|
77
199
|
return result('No active station to favorite.', false);
|
|
78
200
|
const current = store.isFavorite(target);
|
|
79
201
|
if (current !== command.favorite)
|
|
80
|
-
store.toggleFavorite(target);
|
|
81
|
-
if (command.favorite && !current && store.
|
|
202
|
+
await saveLibrary(() => store.toggleFavorite(target));
|
|
203
|
+
if (command.favorite && !current && store.settings().shareDirectoryVotes)
|
|
82
204
|
void providers.vote(target);
|
|
83
205
|
return result(`${command.favorite ? 'Favorited' : 'Removed favorite'}: ${target.name}.`);
|
|
84
206
|
}
|
|
@@ -88,56 +210,101 @@ export async function runHeadlessAgentHost() {
|
|
|
88
210
|
}
|
|
89
211
|
if (command.type === 'airplay-select') {
|
|
90
212
|
const devices = await player.refreshAirPlayDevices();
|
|
213
|
+
signal?.throwIfAborted();
|
|
91
214
|
const device = devices.find(item => item.id === command.deviceId);
|
|
92
215
|
if (!device)
|
|
93
216
|
return result('AirPlay receiver not found. Refresh and use an exact receiver ID.', false, devices);
|
|
94
217
|
if (device.local)
|
|
95
|
-
return switchToLocal();
|
|
96
|
-
if (!
|
|
218
|
+
return switchToLocal(signal);
|
|
219
|
+
if (!player.refreshDetectedBackends().includes('airplay'))
|
|
97
220
|
return result('AirPlay playback is unavailable; run radiocli doctor.', false);
|
|
98
|
-
store.updateSettings({ preferredAirPlayDevice: device.id });
|
|
99
|
-
|
|
221
|
+
await saveLibrary(() => store.updateSettings({ preferredAirPlayDevice: device.id, preferredBackend: 'airplay' }));
|
|
222
|
+
await player.selectAirPlayDevice(device.id);
|
|
223
|
+
signal?.throwIfAborted();
|
|
100
224
|
if (station && ['playing', 'paused', 'loading'].includes(player.getState().state))
|
|
101
|
-
return play(station, queue);
|
|
225
|
+
return play(station, queue, false, signal);
|
|
102
226
|
return result(`Audio output set to AirPlay receiver ${device.name}.`);
|
|
103
227
|
}
|
|
104
228
|
if (command.type === 'airplay-local')
|
|
105
|
-
return switchToLocal();
|
|
229
|
+
return switchToLocal(signal);
|
|
106
230
|
if (command.type === 'airplay-passcode') {
|
|
107
|
-
const control = player.submitAirPlayPasscode(command.code);
|
|
231
|
+
const control = await player.submitAirPlayPasscode(command.code);
|
|
108
232
|
return result(control.message ?? 'AirPlay code sent.', control.ok);
|
|
109
233
|
}
|
|
110
234
|
if (command.type === 'update-settings') {
|
|
111
|
-
|
|
235
|
+
if (!allowedRadioSessionSettings(command.settings))
|
|
236
|
+
return result('Session settings only accept appearance and agent control. Use radiocli setup --airplay to enable Linux AirPlay.', false);
|
|
237
|
+
await saveLibrary(() => store.updateSettings(command.settings));
|
|
112
238
|
return result('RadioCLI settings updated.');
|
|
113
239
|
}
|
|
240
|
+
if (command.type !== 'next' && command.type !== 'previous')
|
|
241
|
+
throw new Error('Unsupported radio command.');
|
|
114
242
|
const currentIndex = station ? queue.findIndex(item => stationKey(item) === stationKey(station)) : -1;
|
|
115
243
|
const delta = command.type === 'next' ? 1 : -1;
|
|
116
244
|
const next = queue.length ? queue[(Math.max(0, currentIndex) + delta + queue.length) % queue.length] : undefined;
|
|
117
|
-
|
|
245
|
+
if (next && command.preserveStop && ['idle', 'stopped'].includes(player.getState().state)) {
|
|
246
|
+
engine.select(next, queue);
|
|
247
|
+
media?.refresh();
|
|
248
|
+
return result(`Selected ${next.name}.`);
|
|
249
|
+
}
|
|
250
|
+
return next ? play(next, queue, Boolean(command.preservePause && player.getState().state === 'paused'), signal) : result('No playback queue is available.', false);
|
|
118
251
|
};
|
|
119
|
-
const switchToLocal = async () => {
|
|
252
|
+
const switchToLocal = async (signal) => {
|
|
253
|
+
signal?.throwIfAborted();
|
|
120
254
|
const backends = detectPlaybackBackends();
|
|
121
255
|
const backend = backends.includes('mpv') ? 'mpv' : backends.includes('ffplay') ? 'ffplay' : backends.includes('vlc') ? 'vlc' : null;
|
|
122
256
|
if (!backend)
|
|
123
257
|
return result('No local playback backend is available. Run radiocli setup to install mpv.', false);
|
|
124
|
-
store.updateSettings({ preferredBackend: backend });
|
|
258
|
+
await saveLibrary(() => store.updateSettings({ preferredBackend: backend }));
|
|
259
|
+
await player.selectAirPlayDevice(null);
|
|
260
|
+
signal?.throwIfAborted();
|
|
261
|
+
const { station, queue } = engine.snapshot();
|
|
125
262
|
if (station && ['playing', 'paused', 'loading'].includes(player.getState().state))
|
|
126
|
-
return play(station, queue);
|
|
263
|
+
return play(station, queue, false, signal);
|
|
127
264
|
return result(`Audio output set to this device (${backend}).`);
|
|
128
265
|
};
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
266
|
+
const dispatch = serializeRadioCommands(handle, { responsive: true });
|
|
267
|
+
const session = await startRadioSession((command, source) => dispatch(command, async () => {
|
|
268
|
+
if (command.type.startsWith('tui-') && source !== 'cli')
|
|
269
|
+
throw new Error('Terminal attachment requires a local CLI client.');
|
|
270
|
+
if (source !== 'cli' && command.type !== 'status' && command.type !== 'alarm-preempt') {
|
|
271
|
+
await store.readFresh();
|
|
272
|
+
if (!store.settings().agentControl?.enabled)
|
|
273
|
+
throw new Error('Agent controls are disabled.');
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}), undefined, { queue: 'owner' }).catch(async (error) => {
|
|
277
|
+
try {
|
|
278
|
+
await engine.close();
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await store.close();
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
});
|
|
285
|
+
media = startDesktopMediaControls({ player, identity: store.filePath, policy: desktopMediaPolicy(store.settings()), status,
|
|
286
|
+
command: (command, permitted) => dispatch(command, () => permitted?.() !== false && !desktopSuspended), suspended: () => desktopSuspended });
|
|
287
|
+
const unsubscribe = engine.onChange(() => media?.refresh());
|
|
288
|
+
let shutdownPromise;
|
|
289
|
+
const signals = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
290
|
+
const shutdown = () => shutdownPromise ??= (async () => {
|
|
291
|
+
shuttingDown = true;
|
|
292
|
+
clearTimeout(sleepTimer);
|
|
293
|
+
desktopSuspended = true;
|
|
294
|
+
for (const signal of signals)
|
|
295
|
+
process.removeListener(signal, onShutdown);
|
|
296
|
+
media?.close();
|
|
297
|
+
unsubscribe();
|
|
298
|
+
try {
|
|
299
|
+
await engine.close();
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
await session.close();
|
|
303
|
+
await store.close();
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
const onShutdown = () => { void shutdown().finally(() => process.exit(0)); };
|
|
307
|
+
for (const signal of signals)
|
|
308
|
+
process.once(signal, onShutdown);
|
|
142
309
|
await new Promise(() => undefined);
|
|
143
310
|
}
|
package/dist/agent/launcher.js
CHANGED
|
@@ -1,71 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const terminal = detectAlarmTerminal();
|
|
8
|
-
const direct = environmentCommand(nodePath, cliPath, ['agent-ui', encodedCommand]);
|
|
9
|
-
const command = `${direct.map(shellQuote).join(' ')}`;
|
|
10
|
-
if (terminal === 'darwin:apple-terminal')
|
|
11
|
-
await launched(spawnDetached('/usr/bin/osascript', appleTerminalScript(command)));
|
|
12
|
-
else if (terminal === 'darwin:iterm')
|
|
13
|
-
await launched(spawnDetached('/usr/bin/osascript', iTermScript(command)));
|
|
14
|
-
else if (terminal === 'darwin:wezterm')
|
|
15
|
-
await launched(spawnDetached('/usr/bin/open', ['-na', 'WezTerm', '--args', 'start', '--always-new-process', '--', ...direct]));
|
|
16
|
-
else if (terminal === 'darwin:ghostty')
|
|
17
|
-
await launched(spawnDetached('/usr/bin/open', ['-na', 'Ghostty', '--args', '-e', ...direct]));
|
|
18
|
-
else if (terminal === 'darwin:kitty')
|
|
19
|
-
await launched(spawnDetached('/usr/bin/open', ['-na', 'kitty', '--args', '--detach', ...direct]));
|
|
20
|
-
else if (terminal === 'win32:windows-terminal')
|
|
21
|
-
await launched(spawnDetached('wt.exe', ['-w', 'new', 'new-tab', '--title', 'RadioCLI', ...direct]));
|
|
22
|
-
else if (terminal === 'win32:console')
|
|
23
|
-
await launched(spawnDetached('cmd.exe', ['/d', '/c', 'start', 'RadioCLI', 'cmd.exe', '/k', windowsCommand(direct)]));
|
|
24
|
-
else if (terminal.startsWith('linux:')) {
|
|
25
|
-
const executable = terminal.slice('linux:'.length);
|
|
26
|
-
if (!linuxTerminals.has(basename(executable)))
|
|
27
|
-
throw new Error('Saved Linux terminal is not supported.');
|
|
28
|
-
const name = basename(executable);
|
|
29
|
-
const prefix = name === 'gnome-terminal' || name === 'mate-terminal' || name === 'xfce4-terminal'
|
|
30
|
-
? ['--']
|
|
31
|
-
: name === 'wezterm' ? ['start', '--always-new-process', '--'] : ['-e'];
|
|
32
|
-
await launched(spawnDetached(executable, [...prefix, ...direct]));
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
throw new Error('No supported graphical terminal was found. Set agentControl.openUiOnPlay to false or open radiocli manually.');
|
|
36
|
-
}
|
|
2
|
+
import { launchTerminalCommand } from '../platform/terminals.js';
|
|
3
|
+
import { waitForLaunch } from '../platform/launch-command.js';
|
|
4
|
+
/** The agent service verifies the authenticated session after this request. */
|
|
5
|
+
export async function launchRadioTui(nodePath, cliPath, encodedCommand, options = {}) {
|
|
6
|
+
await launchTerminalCommand({ ...options, nodePath, args: [cliPath, 'agent-ui', encodedCommand], title: 'RadioCLI' });
|
|
37
7
|
}
|
|
38
8
|
export async function launchHeadlessHost(nodePath, cliPath) {
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
function environmentCommand(nodePath, cliPath, args) {
|
|
42
|
-
const command = [nodePath, cliPath, ...args];
|
|
43
|
-
const radioCliHome = process.env.RADIOCLI_HOME;
|
|
44
|
-
if (!radioCliHome)
|
|
45
|
-
return command;
|
|
46
|
-
if (process.platform === 'win32') {
|
|
47
|
-
return ['cmd.exe', '/d', '/c', `set "RADIOCLI_HOME=${cmdEscape(radioCliHome)}" && ${windowsCommand(command)}`];
|
|
48
|
-
}
|
|
49
|
-
return ['/usr/bin/env', `RADIOCLI_HOME=${radioCliHome}`, ...command];
|
|
50
|
-
}
|
|
51
|
-
function shellQuote(value) { return `'${value.replaceAll("'", `'\\''`)}'`; }
|
|
52
|
-
function windowsCommand(values) { return values.map(value => `"${value.replaceAll('"', '""')}"`).join(' '); }
|
|
53
|
-
function cmdEscape(value) { return value.replaceAll('%', '%%').replaceAll('"', '""').replaceAll('^', '^^').replaceAll('&', '^&').replaceAll('|', '^|').replaceAll('<', '^<').replaceAll('>', '^>'); }
|
|
54
|
-
function appleTerminalScript(command) { return ['-e', 'on run argv', '-e', 'tell application "Terminal"', '-e', 'activate', '-e', 'do script (item 1 of argv)', '-e', 'end tell', '-e', 'end run', command]; }
|
|
55
|
-
function iTermScript(command) { return ['-e', 'on run argv', '-e', 'tell application "iTerm"', '-e', 'activate', '-e', 'set w to (create window with default profile)', '-e', 'tell current session of w to write text (item 1 of argv)', '-e', 'end tell', '-e', 'end run', command]; }
|
|
56
|
-
function spawnDetached(command, args) { return spawn(command, [...args], { detached: true, stdio: 'ignore', windowsHide: false }); }
|
|
57
|
-
function launched(child) { return new Promise((resolve, reject) => { child.once('error', reject); child.once('spawn', () => { child.unref(); resolve(); }); }); }
|
|
58
|
-
export function resolveExecutable(input, env = process.env, platform = process.platform) {
|
|
59
|
-
if ((input.includes('/') || input.includes('\\')) && existsSync(input))
|
|
60
|
-
return input;
|
|
61
|
-
const suffixes = platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
62
|
-
const pathDelimiter = platform === 'win32' ? ';' : ':';
|
|
63
|
-
for (const directory of (env.PATH ?? '').split(pathDelimiter)) {
|
|
64
|
-
for (const suffix of suffixes) {
|
|
65
|
-
const path = join(directory, `${input}${suffix}`);
|
|
66
|
-
if (existsSync(path))
|
|
67
|
-
return path;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
9
|
+
const loader = cliPath.endsWith('.tsx') ? ['--import', import.meta.resolve('tsx')] : [];
|
|
10
|
+
await waitForLaunch(spawn(nodePath, [...loader, cliPath, 'agent-host'], { detached: true, stdio: 'ignore', windowsHide: true }));
|
|
71
11
|
}
|