@ciphore/radiocli 0.2.4 → 0.2.6

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.
Files changed (145) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/CONTRIBUTING.md +28 -0
  3. package/README.md +108 -253
  4. package/SECURITY.md +23 -1
  5. package/dist/agent/alarm-service.js +10 -7
  6. package/dist/agent/cli.js +16 -1
  7. package/dist/agent/command-schema.js +47 -0
  8. package/dist/agent/headless-host.js +226 -81
  9. package/dist/agent/launcher.js +2 -1
  10. package/dist/agent/mcp-server.js +10 -5
  11. package/dist/agent/service.js +75 -40
  12. package/dist/agent/session.js +214 -24
  13. package/dist/agent/tui-session.js +213 -0
  14. package/dist/alarms/cli.js +19 -3
  15. package/dist/alarms/occurrence-cache.js +25 -0
  16. package/dist/alarms/runner.js +87 -23
  17. package/dist/alarms/schedule.js +29 -1
  18. package/dist/alarms/scheduler.js +11 -3
  19. package/dist/alarms/system-volume-linux.js +160 -0
  20. package/dist/alarms/system-volume-ownership.js +76 -8
  21. package/dist/alarms/system-volume.js +44 -23
  22. package/dist/cli/arguments.js +212 -0
  23. package/dist/cli/completion.js +44 -0
  24. package/dist/cli/doctor.js +156 -0
  25. package/dist/cli/interactive.js +79 -0
  26. package/dist/cli/listener.js +227 -0
  27. package/dist/cli/output.js +121 -0
  28. package/dist/cli/render-tui.js +34 -0
  29. package/dist/cli.js +182 -199
  30. package/dist/platform/capabilities.js +8 -1
  31. package/dist/platform/desktop-media.js +61 -0
  32. package/dist/platform/diagnostics.js +273 -0
  33. package/dist/platform/json-worker.js +111 -0
  34. package/dist/platform/launch-command.js +9 -6
  35. package/dist/platform/mpris.js +356 -0
  36. package/dist/platform/network.js +69 -1
  37. package/dist/platform/omarchy.js +147 -0
  38. package/dist/platform/packages.js +7 -1
  39. package/dist/platform/runtime.js +1 -1
  40. package/dist/platform/terminals.js +24 -4
  41. package/dist/player/airplay-discovery.js +144 -61
  42. package/dist/player/airplay-policy.js +15 -0
  43. package/dist/player/airplay-process.js +63 -0
  44. package/dist/player/airplay-readiness.js +42 -0
  45. package/dist/player/airplay-sender-health.js +49 -49
  46. package/dist/player/airplay-sender-package.js +5 -0
  47. package/dist/player/airplay-sender-patch.js +102 -4
  48. package/dist/player/airplay-setup.js +51 -0
  49. package/dist/player/airplay-worker.js +97 -41
  50. package/dist/player/backend-install.js +9 -6
  51. package/dist/player/ffmpeg-input.js +37 -0
  52. package/dist/player/mpv-desktop-media.js +194 -0
  53. package/dist/player/mpv-ipc-client.js +51 -2
  54. package/dist/player/player-controller.js +390 -73
  55. package/dist/player/radio-engine.js +282 -0
  56. package/dist/playlists/playlist.js +18 -16
  57. package/dist/providers/cache.js +291 -75
  58. package/dist/providers/discovery-session.js +111 -0
  59. package/dist/providers/provider-manager.js +106 -28
  60. package/dist/providers/radio-browser.js +272 -41
  61. package/dist/providers/radio-garden.js +4 -2
  62. package/dist/setup-playback.js +152 -0
  63. package/dist/setup.js +88 -21
  64. package/dist/storage/file-lock.js +130 -0
  65. package/dist/storage/read-settings.js +33 -0
  66. package/dist/storage/replace-file.js +20 -0
  67. package/dist/storage/repository.js +247 -0
  68. package/dist/storage/station-identity.js +21 -0
  69. package/dist/storage/station-schema.js +27 -0
  70. package/dist/storage/store.js +314 -76
  71. package/dist/types.js +22 -1
  72. package/dist/ui/AdaptiveContent.js +21 -24
  73. package/dist/ui/App.js +708 -454
  74. package/dist/ui/AppContent.js +5 -5
  75. package/dist/ui/airplay-settings.js +12 -5
  76. package/dist/ui/bidi-ranges.js +45 -0
  77. package/dist/ui/components/AdaptiveMarquee.js +16 -9
  78. package/dist/ui/components/ReceiverVisualRow.js +39 -0
  79. package/dist/ui/format.js +37 -4
  80. package/dist/ui/help-content.js +5 -2
  81. package/dist/ui/rtl-output.js +137 -0
  82. package/dist/ui/screen-items.js +10 -4
  83. package/dist/ui/screens/AirPlaySettingsScreen.js +3 -3
  84. package/dist/ui/screens/AlarmsScreen.js +5 -2
  85. package/dist/ui/screens/HelpScreen.js +3 -3
  86. package/dist/ui/screens/NowPlayingScreen.js +2 -21
  87. package/dist/ui/screens/SettingsScreen.js +16 -16
  88. package/dist/ui/screens/StatsScreen.js +1 -1
  89. package/dist/ui/text-direction.js +9 -0
  90. package/dist/ui/theme.js +46 -0
  91. package/dist/ui/use-alarm-tui.js +24 -24
  92. package/dist/ui/use-app-input.js +42 -8
  93. package/dist/ui/use-command-executor.js +21 -14
  94. package/dist/ui/visualizers/boat-lake-kernel.js +325 -0
  95. package/dist/ui/visualizers/boat-lake.js +161 -0
  96. package/dist/ui/visualizers/gallop.js +2 -2
  97. package/dist/ui/visualizers/receiver-style-registry.js +3 -1
  98. package/dist/ui/visualizers/receiver-visualizers.js +6 -108
  99. package/dist/ui/visualizers/sumi-ocean.js +183 -0
  100. package/dist/ui/visualizers/terminal-cell.js +45 -0
  101. package/dist/update-check.js +2 -1
  102. package/docs/THIRD_PARTY_NOTICES.md +6 -4
  103. package/docs/boat-lake.md +88 -0
  104. package/docs/sumi-ocean.md +80 -0
  105. package/package.json +36 -12
  106. package/scripts/build-airplay-native.mjs +22 -0
  107. package/vendor/node-airtunes2/LICENSE +208 -0
  108. package/vendor/node-airtunes2/RADIOCLI_PATCH.md +29 -0
  109. package/vendor/node-airtunes2/README.md +225 -0
  110. package/vendor/node-airtunes2/UPSTREAM.json +50 -0
  111. package/vendor/node-airtunes2/binding.gyp +41 -0
  112. package/vendor/node-airtunes2/lib/ap2_test.js +4 -0
  113. package/vendor/node-airtunes2/lib/atv.js +307 -0
  114. package/vendor/node-airtunes2/lib/atvAuthenticator.js +136 -0
  115. package/vendor/node-airtunes2/lib/audio_out.js +69 -0
  116. package/vendor/node-airtunes2/lib/circular_buffer.js +133 -0
  117. package/vendor/node-airtunes2/lib/config.js +26 -0
  118. package/vendor/node-airtunes2/lib/device_airtunes.js +532 -0
  119. package/vendor/node-airtunes2/lib/devices.js +266 -0
  120. package/vendor/node-airtunes2/lib/ed25519.js +26 -0
  121. package/vendor/node-airtunes2/lib/homekit/credentials.js +86 -0
  122. package/vendor/node-airtunes2/lib/homekit/encryption.js +69 -0
  123. package/vendor/node-airtunes2/lib/homekit/number.js +44 -0
  124. package/vendor/node-airtunes2/lib/homekit/tlv.js +95 -0
  125. package/vendor/node-airtunes2/lib/http.js +189 -0
  126. package/vendor/node-airtunes2/lib/index.d.ts +141 -0
  127. package/vendor/node-airtunes2/lib/index.js +127 -0
  128. package/vendor/node-airtunes2/lib/ntp.js +37 -0
  129. package/vendor/node-airtunes2/lib/num_util.js +21 -0
  130. package/vendor/node-airtunes2/lib/packet_pool.js +43 -0
  131. package/vendor/node-airtunes2/lib/rtsp.js +1807 -0
  132. package/vendor/node-airtunes2/lib/srp.js +158 -0
  133. package/vendor/node-airtunes2/lib/udp_servers.js +214 -0
  134. package/vendor/node-airtunes2/lib/util.js +12 -0
  135. package/vendor/node-airtunes2/package.json +31 -0
  136. package/vendor/node-airtunes2/src/CADebugMacros.h +257 -0
  137. package/vendor/node-airtunes2/src/CAHostTimeBase.cpp +105 -0
  138. package/vendor/node-airtunes2/src/CAHostTimeBase.h +224 -0
  139. package/vendor/node-airtunes2/src/aes_utils.c +864 -0
  140. package/vendor/node-airtunes2/src/aes_utils.h +24 -0
  141. package/vendor/node-airtunes2/src/base64.c +136 -0
  142. package/vendor/node-airtunes2/src/base64.h +42 -0
  143. package/vendor/node-airtunes2/src/bindings.cc +29 -0
  144. package/vendor/node-airtunes2/src/codec.cc +170 -0
  145. package/vendor/node-airtunes2/src/coreaudio.cc +332 -0
@@ -0,0 +1,282 @@
1
+ import { stationKey } from '../storage/store.js';
2
+ import { PlaybackOutputError } from './player-controller.js';
3
+ /** Product playback rules shared by interactive and headless owners. Screen
4
+ * navigation and native alarm ownership deliberately remain in their adapters. */
5
+ export class RadioEngine {
6
+ player;
7
+ providers;
8
+ store;
9
+ settings;
10
+ onSaved;
11
+ state;
12
+ listeners = new Set();
13
+ unsubscribe;
14
+ generation = 0;
15
+ tuneAbort;
16
+ candidate;
17
+ listeningKey;
18
+ lastTrack;
19
+ history = Promise.resolve();
20
+ checkpoint;
21
+ closed = false;
22
+ recoveryTimer;
23
+ recoveryAttempts = 0;
24
+ healthyPlaybackTimer;
25
+ closing;
26
+ constructor(player, providers, store, settings, onSaved) {
27
+ this.player = player;
28
+ this.providers = providers;
29
+ this.store = store;
30
+ this.settings = settings;
31
+ this.onSaved = onSaved;
32
+ this.state = { station: null, tuningStation: null, queue: [], playback: player.getState(), nowPlaying: null };
33
+ this.unsubscribe = [
34
+ player.onChange(playback => this.onPlayback(playback)),
35
+ player.onMetadata(metadata => this.onMetadata(metadata))
36
+ ];
37
+ this.checkpoint = setInterval(() => {
38
+ if (this.listeningKey && this.state.playback.state === 'playing' && this.state.playback.ready && !this.state.playback.buffering) {
39
+ const at = new Date();
40
+ this.persist(() => store.checkpointActiveListeningSession(at));
41
+ }
42
+ }, 30_000);
43
+ this.checkpoint.unref();
44
+ }
45
+ snapshot() { return { ...this.state, playback: { ...this.state.playback }, queue: [...this.state.queue] }; }
46
+ onChange(listener) {
47
+ this.listeners.add(listener);
48
+ listener(this.snapshot());
49
+ return () => this.listeners.delete(listener);
50
+ }
51
+ clearWarning() { if (this.state.warning) {
52
+ this.state.warning = undefined;
53
+ this.emit();
54
+ } }
55
+ cancel() {
56
+ this.generation++;
57
+ clearTimeout(this.recoveryTimer);
58
+ this.recoveryTimer = undefined;
59
+ clearTimeout(this.healthyPlaybackTimer);
60
+ this.healthyPlaybackTimer = undefined;
61
+ this.tuneAbort?.abort(new DOMException('Tuning canceled.', 'AbortError'));
62
+ this.tuneAbort = undefined;
63
+ if (this.candidate && this.state.playback.ready)
64
+ this.candidate.generation = this.generation;
65
+ this.state.tuningStation = null;
66
+ this.state.message = undefined;
67
+ this.emit();
68
+ }
69
+ async tune(station, options = {}) {
70
+ if (this.closed)
71
+ throw new Error('RadioCLI playback engine is closed.');
72
+ options.signal?.throwIfAborted();
73
+ this.cancel();
74
+ this.recoveryAttempts = options.recoveryAttempt ?? 0;
75
+ const generation = this.generation;
76
+ const controller = new AbortController();
77
+ const deadline = setTimeout(() => controller.abort(new DOMException('Tuning exceeded 90 seconds.', 'TimeoutError')), 90_000);
78
+ deadline.unref();
79
+ this.tuneAbort = controller;
80
+ const abort = () => controller.abort(options.signal?.reason);
81
+ options.signal?.addEventListener('abort', abort, { once: true });
82
+ const queue = options.queue?.length ? options.queue : [station];
83
+ let next = station;
84
+ try {
85
+ while (true) {
86
+ controller.signal.throwIfAborted();
87
+ this.state.tuningStation = next;
88
+ this.state.nowPlaying = null;
89
+ this.emit();
90
+ try {
91
+ // A ready receiver can keep its connection while resolving a new URL.
92
+ // Pending startup and local processes are canceled before replacement.
93
+ const playback = this.player.getState();
94
+ if (!(playback.backend === 'airplay' && playback.ready && playback.state === 'playing'))
95
+ await this.player.stop();
96
+ controller.signal.throwIfAborted();
97
+ const resolved = await this.providers.resolve(next, { signal: controller.signal });
98
+ controller.signal.throwIfAborted();
99
+ if (generation !== this.generation)
100
+ throw new DOMException('Tuning canceled.', 'AbortError');
101
+ this.candidate = { station: next, url: resolved.url, generation };
102
+ this.lastTrack = undefined;
103
+ await this.player.play(next, resolved.url, { startPaused: options.startPaused, signal: controller.signal });
104
+ controller.signal.throwIfAborted();
105
+ this.onPlayback(this.player.getState());
106
+ this.state.station = next;
107
+ this.state.queue = [...queue];
108
+ this.state.tuningStation = null;
109
+ this.state.message = undefined;
110
+ const tuned = next;
111
+ this.persist(() => this.store.addRecent(tuned));
112
+ this.emit();
113
+ await this.flushHistory();
114
+ controller.signal.throwIfAborted();
115
+ if (generation !== this.generation)
116
+ throw new DOMException('Tuning canceled.', 'AbortError');
117
+ return next;
118
+ }
119
+ catch (error) {
120
+ if (controller.signal.aborted || generation !== this.generation)
121
+ throw controller.signal.reason ?? error;
122
+ const index = queue.findIndex(item => stationKey(item) === stationKey(next));
123
+ const following = index >= 0 ? queue[index + 1] : undefined;
124
+ if (!following || options.startPaused || options.skipBroken === false || !this.settings().skipBrokenStreams || error instanceof PlaybackOutputError)
125
+ throw error;
126
+ this.state.message = `${error instanceof Error ? error.message : 'Could not tune station.'} Skipping to ${following.name}.`;
127
+ this.state.tuningStation = following;
128
+ this.emit();
129
+ await abortableDelay(1000, controller.signal);
130
+ next = following;
131
+ }
132
+ }
133
+ }
134
+ finally {
135
+ clearTimeout(deadline);
136
+ options.signal?.removeEventListener('abort', abort);
137
+ if (generation === this.generation) {
138
+ this.tuneAbort = undefined;
139
+ this.state.tuningStation = null;
140
+ this.emit();
141
+ }
142
+ }
143
+ }
144
+ async stop(options = {}) {
145
+ this.cancel();
146
+ this.candidate = undefined;
147
+ this.finishListening();
148
+ this.state.nowPlaying = null;
149
+ if (!options.retainSelection) {
150
+ this.state.station = null;
151
+ this.state.queue = [];
152
+ }
153
+ await this.player.stop();
154
+ this.onPlayback(this.player.getState());
155
+ this.emit();
156
+ await this.flushHistory();
157
+ }
158
+ async pause() { const result = await this.player.pause(); this.onPlayback(this.player.getState()); await this.flushHistory(); return result; }
159
+ async resume() { const result = await this.player.resume(); this.onPlayback(this.player.getState()); await this.flushHistory(); return result; }
160
+ select(station, queue = this.state.queue) { this.state.station = station; this.state.queue = [...queue]; this.emit(); }
161
+ async flushHistory() { await this.history; }
162
+ close() {
163
+ return this.closing ??= (async () => {
164
+ this.closed = true;
165
+ clearInterval(this.checkpoint);
166
+ try {
167
+ await this.stop();
168
+ }
169
+ finally {
170
+ for (const unsubscribe of this.unsubscribe)
171
+ unsubscribe();
172
+ this.listeners.clear();
173
+ }
174
+ })();
175
+ }
176
+ onPlayback(playback) {
177
+ this.state.playback = playback;
178
+ const active = this.candidate;
179
+ const belongs = active && active.generation === this.generation && (!playback.streamUrl || playback.streamUrl === active.url);
180
+ if (belongs && playback.ready && playback.state === 'playing' && !playback.buffering) {
181
+ if (this.recoveryAttempts > 0 && !this.healthyPlaybackTimer) {
182
+ const generation = this.generation;
183
+ this.healthyPlaybackTimer = setTimeout(() => {
184
+ this.healthyPlaybackTimer = undefined;
185
+ if (generation === this.generation && !this.closed)
186
+ this.recoveryAttempts = 0;
187
+ }, 60_000);
188
+ this.healthyPlaybackTimer.unref();
189
+ }
190
+ const key = stationKey(active.station);
191
+ if (this.listeningKey !== key) {
192
+ this.finishListening();
193
+ this.listeningKey = key;
194
+ const at = new Date();
195
+ this.persist(() => this.store.startListeningSession(active.station, at));
196
+ }
197
+ }
198
+ else {
199
+ clearTimeout(this.healthyPlaybackTimer);
200
+ this.healthyPlaybackTimer = undefined;
201
+ if (!playback.ready || playback.state !== 'playing' || playback.buffering)
202
+ this.finishListening();
203
+ }
204
+ if (belongs && playback.state === 'error' && !this.state.tuningStation && !this.closed && this.player.recoverableFailure())
205
+ this.scheduleRecovery(active.station);
206
+ this.emit();
207
+ }
208
+ onMetadata(metadata) {
209
+ const active = this.candidate;
210
+ if (!active || active.generation !== this.generation || this.state.playback.streamUrl && this.state.playback.streamUrl !== active.url)
211
+ return;
212
+ this.state.nowPlaying = metadata;
213
+ const key = `${stationKey(active.station)}:${metadata.title}`;
214
+ if (metadata.title && key !== this.lastTrack) {
215
+ this.lastTrack = key;
216
+ this.persist(() => this.store.recordTrack(active.station, metadata.title));
217
+ }
218
+ this.emit();
219
+ }
220
+ finishListening() {
221
+ if (!this.listeningKey)
222
+ return;
223
+ this.listeningKey = undefined;
224
+ const at = new Date();
225
+ this.persist(() => this.store.finishActiveListeningSession(at));
226
+ }
227
+ scheduleRecovery(station) {
228
+ if (this.recoveryTimer)
229
+ return;
230
+ if (this.recoveryAttempts >= 3) {
231
+ this.state.message = 'Reconnection stopped after 3 attempts. Select the station to try again.';
232
+ return;
233
+ }
234
+ const attempt = ++this.recoveryAttempts;
235
+ const generation = this.generation;
236
+ this.state.message = `Stream connection interrupted. Reconnecting ${station.name} (${attempt}/3).`;
237
+ this.recoveryTimer = setTimeout(() => {
238
+ this.recoveryTimer = undefined;
239
+ if (this.closed || generation !== this.generation)
240
+ return;
241
+ void this.tune(station, { queue: this.state.queue, recoveryAttempt: attempt, skipBroken: false }).catch(error => {
242
+ if (this.closed || error.name === 'AbortError')
243
+ return;
244
+ if (this.player.recoverableFailure() && this.recoveryAttempts < 3)
245
+ this.scheduleRecovery(station);
246
+ else {
247
+ this.state.message = `Could not reconnect ${station.name}: ${error instanceof Error ? error.message : String(error)}`;
248
+ this.emit();
249
+ }
250
+ });
251
+ }, 1000 * 2 ** (attempt - 1));
252
+ this.recoveryTimer.unref();
253
+ }
254
+ persist(write) {
255
+ this.history = this.history.then(async () => {
256
+ try {
257
+ const saved = await write();
258
+ this.onSaved?.(saved);
259
+ }
260
+ catch (error) {
261
+ const code = error.code;
262
+ this.state.warning = `Listening history was not saved${code ? ` (${code})` : ''}. Check storage permissions or set RADIOCLI_HOME to a private writable directory.`;
263
+ this.emit();
264
+ }
265
+ });
266
+ }
267
+ emit() { for (const listener of this.listeners) {
268
+ try {
269
+ listener(this.snapshot());
270
+ }
271
+ catch { /* Isolate adapters. */ }
272
+ } }
273
+ }
274
+ function abortableDelay(ms, signal) {
275
+ signal.throwIfAborted();
276
+ return new Promise((resolve, reject) => {
277
+ const finish = () => { signal.removeEventListener('abort', abort); resolve(); };
278
+ const timer = setTimeout(finish, ms);
279
+ const abort = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); reject(signal.reason); };
280
+ signal.addEventListener('abort', abort, { once: true });
281
+ });
282
+ }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { customStationId } from '../storage/station-identity.js';
3
4
  export function parsePlaylistFile(filePath, depth = 0) {
4
5
  if (!existsSync(filePath)) {
5
6
  throw new Error(`Playlist not found: ${filePath}`);
@@ -34,7 +35,7 @@ export function writeM3u(filePath, stations) {
34
35
  export function stationFromUrl(url, name = url) {
35
36
  const cleanedName = cleanName(name) || url;
36
37
  return {
37
- id: stableId(url),
38
+ id: customStationId(url),
38
39
  provider: 'playlist',
39
40
  name: cleanedName,
40
41
  tags: ['custom'],
@@ -110,27 +111,28 @@ function firstTag(text, tag) {
110
111
  return new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i').exec(text)?.[1]?.trim();
111
112
  }
112
113
  function decodeXml(value) {
113
- return value
114
- ?.replace(/&amp;/g, '&')
115
- .replace(/&lt;/g, '<')
116
- .replace(/&gt;/g, '>')
117
- .replace(/&quot;/g, '"')
118
- .replace(/&#39;/g, "'")
119
- .trim();
120
- }
121
- function stableId(value) {
122
- let hash = 0;
123
- for (let index = 0; index < value.length; index += 1) {
124
- hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
125
- }
126
- return `custom-${hash.toString(16)}`;
114
+ // Decode in one pass: CDATA is literal text, and &amp;lt; represents &lt;,
115
+ // not a second reference to expand. No DTD or external entity is evaluated.
116
+ const entities = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
117
+ return value?.replace(/<!\[CDATA\[([\s\S]*?)\]\]>|&(amp|lt|gt|quot|apos|#\d+|#x[\da-fA-F]+);/g, (reference, cdata, entity) => {
118
+ if (cdata !== undefined)
119
+ return cdata;
120
+ if (!entity)
121
+ return reference;
122
+ if (!entity.startsWith('#'))
123
+ return entities[entity] ?? reference;
124
+ const point = entity.startsWith('#x') ? Number.parseInt(entity.slice(2), 16) : Number(entity.slice(1));
125
+ return Number.isInteger(point) && point > 0 && point <= 0x10ffff && !(point >= 0xd800 && point <= 0xdfff)
126
+ ? String.fromCodePoint(point) : reference;
127
+ }).trim();
127
128
  }
128
129
  function normalizePlaylistTarget(target, root) {
129
130
  const trimmed = target.trim();
130
131
  if (isSupportedTarget(trimmed)) {
131
132
  return trimmed;
132
133
  }
133
- if (!/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
134
+ // On Windows a native drive path has a colon but is not a URL scheme.
135
+ if (isAbsolute(trimmed) || !/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
134
136
  const filePath = isAbsolute(trimmed) ? trimmed : resolve(root, trimmed);
135
137
  if (existsSync(filePath)) {
136
138
  return filePath;