@involvex/youtube-music-cli 0.0.45 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@ import { ICONS } from "../../utils/icons.js";
10
10
  const FLASH_DURATION_MS = 300;
11
11
  export default function ShortcutsBar() {
12
12
  const { theme } = useTheme();
13
- const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, } = usePlayer();
13
+ const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, toggleAutoplay, } = usePlayer();
14
14
  const [flashState, setFlashState] = useState({});
15
15
  const flash = (key) => {
16
16
  setFlashState(prev => ({ ...prev, [key]: true }));
@@ -62,6 +62,10 @@ export default function ShortcutsBar() {
62
62
  flash('repeat');
63
63
  toggleRepeat();
64
64
  });
65
+ useKeyBinding(KEYBINDINGS.AUTOPLAY_TOGGLE, () => {
66
+ flash('autoplay');
67
+ toggleAutoplay();
68
+ });
65
69
  // Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
66
70
  const shuffleColor = flashState['shuffle']
67
71
  ? theme.colors.success
@@ -76,5 +80,10 @@ export default function ShortcutsBar() {
76
80
  const volumeColor = flashState['volume']
77
81
  ? theme.colors.success
78
82
  : theme.colors.primary;
79
- return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: shortcutColor('playPause'), children: [playerState.isPlaying ? ICONS.PAUSE : ICONS.PLAY_PAUSE_ON, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: shortcutColor('prev'), children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: shortcutColor('next'), children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: shuffleColor, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: repeatColor, children: [playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL, ' ', "[R]"] }), ' ', "| ", _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: shuffleColor, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: repeatColor, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [+/-]"] }), ' ', _jsxs(Text, { color: volumeColor, children: [playerState.volume, "%"] })] })] }));
83
+ const autoplayColor = flashState['autoplay']
84
+ ? theme.colors.success
85
+ : playerState.autoplay
86
+ ? theme.colors.primary
87
+ : theme.colors.dim;
88
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: shortcutColor('playPause'), children: [playerState.isPlaying ? ICONS.PAUSE : ICONS.PLAY_PAUSE_ON, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: shortcutColor('prev'), children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: shortcutColor('next'), children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: shuffleColor, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: repeatColor, children: [playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL, ' ', "[R]"] }), ' ', "| ", _jsxs(Text, { color: autoplayColor, children: [ICONS.AUTOPLAY, " [Shift+A]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: shuffleColor, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: repeatColor, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsx(Text, { color: autoplayColor, children: ICONS.AUTOPLAY }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [+/-]"] }), ' ', _jsxs(Text, { color: volumeColor, children: [playerState.volume, "%"] })] })] }));
80
89
  }
@@ -17,6 +17,7 @@ export declare function usePlayer(): {
17
17
  volumeFineDown: () => void;
18
18
  toggleShuffle: () => void;
19
19
  toggleRepeat: () => void;
20
+ toggleAutoplay: () => void;
20
21
  setQueue: (queue: Track[]) => void;
21
22
  addToQueue: (track: Track) => void;
22
23
  removeFromQueue: (index: number) => void;
@@ -8,6 +8,7 @@ export interface PersistedPlayerState {
8
8
  volume: number;
9
9
  shuffle: boolean;
10
10
  repeat: 'off' | 'all' | 'one';
11
+ autoplay?: boolean;
11
12
  lastUpdated: string;
12
13
  }
13
14
  /**
@@ -15,6 +15,7 @@ const defaultState = {
15
15
  volume: 70,
16
16
  shuffle: false,
17
17
  repeat: 'off',
18
+ autoplay: true,
18
19
  lastUpdated: new Date().toISOString(),
19
20
  };
20
21
  /**
@@ -21,6 +21,7 @@ class WebServerManager {
21
21
  queuePosition: 0,
22
22
  repeat: 'off',
23
23
  shuffle: false,
24
+ autoplay: true,
24
25
  isLoading: false,
25
26
  error: null,
26
27
  playRequestId: 0,
@@ -164,6 +164,7 @@ class WebStreamingService {
164
164
  'queuePosition',
165
165
  'repeat',
166
166
  'shuffle',
167
+ 'autoplay',
167
168
  'isLoading',
168
169
  'error',
169
170
  ];
@@ -186,9 +187,10 @@ class WebStreamingService {
186
187
  * Update and broadcast player state (throttled)
187
188
  */
188
189
  onStateChange(state) {
189
- this.prevState = { ...state };
190
190
  const now = Date.now();
191
+ // Compute delta against OLD prevState BEFORE updating it
191
192
  const delta = this.computeDelta(state);
193
+ this.prevState = { ...state };
192
194
  // Skip if no changes
193
195
  if (Object.keys(delta).length === 0) {
194
196
  return;
@@ -18,6 +18,7 @@ type PlayerContextValue = {
18
18
  volumeFineDown: () => void;
19
19
  toggleShuffle: () => void;
20
20
  toggleRepeat: () => void;
21
+ toggleAutoplay: () => void;
21
22
  setQueue: (queue: Track[]) => void;
22
23
  addToQueue: (track: Track) => void;
23
24
  removeFromQueue: (index: number) => void;
@@ -21,6 +21,7 @@ const initialState = {
21
21
  queuePosition: 0,
22
22
  repeat: 'off',
23
23
  shuffle: false,
24
+ autoplay: true,
24
25
  isLoading: false,
25
26
  error: null,
26
27
  playRequestId: 0,
@@ -150,6 +151,8 @@ export function playerReducer(state, action) {
150
151
  }
151
152
  case 'TOGGLE_SHUFFLE':
152
153
  return { ...state, shuffle: !state.shuffle };
154
+ case 'TOGGLE_AUTOPLAY':
155
+ return { ...state, autoplay: !state.autoplay };
153
156
  case 'TOGGLE_REPEAT':
154
157
  const repeatModes = ['off', 'all', 'one'];
155
158
  const currentIndex = repeatModes.indexOf(state.repeat);
@@ -225,6 +228,7 @@ export function playerReducer(state, action) {
225
228
  volume: action.volume,
226
229
  shuffle: action.shuffle,
227
230
  repeat: action.repeat,
231
+ autoplay: action.autoplay ?? true,
228
232
  isPlaying: false, // Don't auto-play restored state
229
233
  };
230
234
  default:
@@ -251,6 +255,7 @@ function PlayerManager() {
251
255
  }, [dispatch]);
252
256
  // Register event handler for mpv IPC events
253
257
  const eofTimestampRef = useRef(0);
258
+ const lastAutoNextRef = useRef(0);
254
259
  useEffect(() => {
255
260
  let lastProgressUpdate = 0;
256
261
  const PROGRESS_THROTTLE_MS = 1000; // Update progress max once per second
@@ -269,8 +274,10 @@ function PlayerManager() {
269
274
  if (event.eof) {
270
275
  // Track ended — record timestamp so we can suppress mpv's spurious
271
276
  // pause event that immediately follows EOF (idle state).
272
- eofTimestampRef.current = Date.now();
277
+ const now = Date.now();
278
+ eofTimestampRef.current = now;
273
279
  next();
280
+ lastAutoNextRef.current = now;
274
281
  }
275
282
  if (event.paused !== undefined) {
276
283
  // mpv sends pause=true when a track ends and it enters idle mode.
@@ -505,14 +512,101 @@ function PlayerManager() {
505
512
  config.set('volume', state.volume);
506
513
  }, [state.volume]);
507
514
  // Handle track completion
515
+ const autoAdvanceRef = useRef(false);
508
516
  useEffect(() => {
509
- if (state.duration > 0 && state.progress >= state.duration) {
510
- if (state.repeat === 'one') {
511
- dispatch({ category: 'SEEK', position: 0 });
512
- }
513
- // next() for regular track completion is handled by the eof IPC event
517
+ if (state.duration <= 0) {
518
+ autoAdvanceRef.current = false;
519
+ return;
520
+ }
521
+ if (state.progress < state.duration) {
522
+ autoAdvanceRef.current = false;
523
+ return;
524
+ }
525
+ if (state.repeat === 'one') {
526
+ dispatch({ category: 'SEEK', position: 0 });
527
+ return;
528
+ }
529
+ const hasNextTrack = state.queue.length > 0 &&
530
+ (state.repeat === 'all' ||
531
+ state.queuePosition < state.queue.length - 1 ||
532
+ (state.shuffle && state.queue.length > 1));
533
+ if (!hasNextTrack) {
534
+ return;
535
+ }
536
+ const now = Date.now();
537
+ if (now - lastAutoNextRef.current < 1500) {
538
+ return;
539
+ }
540
+ if (!autoAdvanceRef.current) {
541
+ autoAdvanceRef.current = true;
542
+ lastAutoNextRef.current = now;
543
+ dispatch({ category: 'NEXT' });
544
+ }
545
+ }, [
546
+ state.duration,
547
+ state.progress,
548
+ state.repeat,
549
+ state.queue.length,
550
+ state.queuePosition,
551
+ state.shuffle,
552
+ dispatch,
553
+ ]);
554
+ // Smart autoplay: fetch suggestions when near end of queue
555
+ const fetchedForRef = useRef(null);
556
+ const isFetchingAutoplayRef = useRef(false);
557
+ useEffect(() => {
558
+ if (!state.autoplay || !state.currentTrack || !state.isPlaying) {
559
+ return;
560
+ }
561
+ // Already looping — no need to feed the queue
562
+ if (state.repeat === 'all' || (state.shuffle && state.queue.length > 1)) {
563
+ return;
564
+ }
565
+ // Still enough tracks ahead — wait until we're close to the end
566
+ const tracksAhead = state.queue.length - state.queuePosition - 1;
567
+ if (tracksAhead > 5)
568
+ return;
569
+ // Already fetched for this track or a fetch is in flight
570
+ if (fetchedForRef.current === state.currentTrack.videoId ||
571
+ isFetchingAutoplayRef.current) {
572
+ return;
514
573
  }
515
- }, [state.progress, state.duration, state.repeat, dispatch]);
574
+ const trackId = state.currentTrack.videoId;
575
+ const trackTitle = state.currentTrack.title;
576
+ isFetchingAutoplayRef.current = true;
577
+ fetchedForRef.current = trackId;
578
+ musicService
579
+ .getSuggestions(trackId)
580
+ .then(suggestions => {
581
+ for (const track of suggestions) {
582
+ dispatch({ category: 'ADD_TO_QUEUE', track });
583
+ }
584
+ logger.info('PlayerManager', 'Autoplay: added suggestions', {
585
+ count: suggestions.length,
586
+ basedOn: trackTitle,
587
+ });
588
+ })
589
+ .catch((error) => {
590
+ isFetchingAutoplayRef.current = false;
591
+ fetchedForRef.current = null; // Allow retry on next track change
592
+ logger.warn('PlayerManager', 'Autoplay: failed to fetch suggestions', {
593
+ error: error instanceof Error ? error.message : String(error),
594
+ });
595
+ })
596
+ .finally(() => {
597
+ isFetchingAutoplayRef.current = false;
598
+ });
599
+ }, [
600
+ state.autoplay,
601
+ state.currentTrack,
602
+ state.isPlaying,
603
+ state.repeat,
604
+ state.shuffle,
605
+ state.queue.length,
606
+ state.queuePosition,
607
+ musicService,
608
+ dispatch,
609
+ ]);
516
610
  return null;
517
611
  }
518
612
  export function PlayerProvider({ children }) {
@@ -540,6 +634,7 @@ export function PlayerProvider({ children }) {
540
634
  volume: persistedState.volume,
541
635
  shuffle: persistedState.shuffle,
542
636
  repeat: persistedState.repeat,
637
+ autoplay: persistedState.autoplay ?? true,
543
638
  });
544
639
  }
545
640
  });
@@ -562,6 +657,7 @@ export function PlayerProvider({ children }) {
562
657
  volume: state.volume,
563
658
  shuffle: state.shuffle,
564
659
  repeat: state.repeat,
660
+ autoplay: state.autoplay,
565
661
  });
566
662
  },
567
663
  // Debounce progress updates (5s), immediate for track/queue changes
@@ -579,6 +675,7 @@ export function PlayerProvider({ children }) {
579
675
  state.volume,
580
676
  state.shuffle,
581
677
  state.repeat,
678
+ state.autoplay,
582
679
  ]);
583
680
  // Save immediately on unmount/quit
584
681
  useEffect(() => {
@@ -593,6 +690,7 @@ export function PlayerProvider({ children }) {
593
690
  volume: currentState.volume,
594
691
  shuffle: currentState.shuffle,
595
692
  repeat: currentState.repeat,
693
+ autoplay: currentState.autoplay,
596
694
  });
597
695
  };
598
696
  process.on('beforeExit', handleExit);
@@ -661,6 +759,7 @@ export function PlayerProvider({ children }) {
661
759
  },
662
760
  toggleShuffle: () => dispatch({ category: 'TOGGLE_SHUFFLE' }),
663
761
  toggleRepeat: () => dispatch({ category: 'TOGGLE_REPEAT' }),
762
+ toggleAutoplay: () => dispatch({ category: 'TOGGLE_AUTOPLAY' }),
664
763
  setQueue: (queue) => dispatch({ category: 'SET_QUEUE', queue }),
665
764
  addToQueue: (track) => dispatch({ category: 'ADD_TO_QUEUE', track }),
666
765
  removeFromQueue: (index) => dispatch({ category: 'REMOVE_FROM_QUEUE', index }),
@@ -44,6 +44,9 @@ export interface ToggleShuffleAction {
44
44
  export interface ToggleRepeatAction {
45
45
  readonly category: 'TOGGLE_REPEAT';
46
46
  }
47
+ export interface ToggleAutoplayAction {
48
+ readonly category: 'TOGGLE_AUTOPLAY';
49
+ }
47
50
  export interface SetQueueAction {
48
51
  readonly category: 'SET_QUEUE';
49
52
  queue: Track[];
@@ -91,6 +94,7 @@ export interface RestoreStateAction {
91
94
  volume: number;
92
95
  shuffle: boolean;
93
96
  repeat: 'off' | 'all' | 'one';
97
+ autoplay?: boolean;
94
98
  }
95
99
  export interface SetSpeedAction {
96
100
  readonly category: 'SET_SPEED';
@@ -1,4 +1,4 @@
1
- import type { PlayAction, PauseAction, ResumeAction, StopAction, NextAction, PreviousAction, SeekAction, SetVolumeAction, VolumeUpAction, VolumeDownAction, VolumeFineUpAction, VolumeFineDownAction, ToggleShuffleAction, ToggleRepeatAction, SetQueueAction, AddToQueueAction, RemoveFromQueueAction, ClearQueueAction, SetQueuePositionAction, UpdateProgressAction, SetDurationAction, TickAction, SetLoadingAction, SetErrorAction, RestoreStateAction, SetSpeedAction } from './actions.ts';
1
+ import type { PlayAction, PauseAction, ResumeAction, StopAction, NextAction, PreviousAction, SeekAction, SetVolumeAction, VolumeUpAction, VolumeDownAction, VolumeFineUpAction, VolumeFineDownAction, ToggleShuffleAction, ToggleRepeatAction, ToggleAutoplayAction, SetQueueAction, AddToQueueAction, RemoveFromQueueAction, ClearQueueAction, SetQueuePositionAction, UpdateProgressAction, SetDurationAction, TickAction, SetLoadingAction, SetErrorAction, RestoreStateAction, SetSpeedAction } from './actions.ts';
2
2
  import type { Track } from './youtube-music.types.ts';
3
3
  export interface PlayerState {
4
4
  currentTrack: Track | null;
@@ -11,8 +11,9 @@ export interface PlayerState {
11
11
  queuePosition: number;
12
12
  repeat: 'off' | 'all' | 'one';
13
13
  shuffle: boolean;
14
+ autoplay: boolean;
14
15
  isLoading: boolean;
15
16
  error: string | null;
16
17
  playRequestId: number;
17
18
  }
18
- export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | VolumeFineUpAction | VolumeFineDownAction | ToggleShuffleAction | ToggleRepeatAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction | SetSpeedAction;
19
+ export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | VolumeFineUpAction | VolumeFineDownAction | ToggleShuffleAction | ToggleRepeatAction | ToggleAutoplayAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction | SetSpeedAction;
@@ -49,6 +49,7 @@ export declare const KEYBINDINGS: {
49
49
  readonly VOLUME_FINE_DOWN: readonly ["shift+-"];
50
50
  readonly SHUFFLE: readonly ["shift+s"];
51
51
  readonly REPEAT: readonly ["r"];
52
+ readonly AUTOPLAY_TOGGLE: readonly ["shift+a"];
52
53
  readonly GAPLESS_TOGGLE: readonly ["shift+g"];
53
54
  readonly CROSSFADE_CYCLE: readonly ["shift+c"];
54
55
  readonly EQUALIZER_CYCLE: readonly ["shift+e"];
@@ -85,6 +85,7 @@ export const KEYBINDINGS = {
85
85
  VOLUME_FINE_DOWN: ['shift+-'], // Fine-grained -1 step
86
86
  SHUFFLE: ['shift+s'],
87
87
  REPEAT: ['r'],
88
+ AUTOPLAY_TOGGLE: ['shift+a'],
88
89
  GAPLESS_TOGGLE: ['shift+g'],
89
90
  CROSSFADE_CYCLE: ['shift+c'],
90
91
  EQUALIZER_CYCLE: ['shift+e'],
@@ -16,4 +16,5 @@ export declare const ICONS: {
16
16
  readonly RESUME: "⟳";
17
17
  readonly BG_PLAY: "○";
18
18
  readonly VOLUME: "♪";
19
+ readonly AUTOPLAY: "∞";
19
20
  };
@@ -23,4 +23,6 @@ export const ICONS = {
23
23
  BG_PLAY: '○', // U+25CB
24
24
  // Status
25
25
  VOLUME: '♪', // U+266A
26
+ // Autoplay / radio
27
+ AUTOPLAY: '∞', // U+221E
26
28
  };
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
package/readme.md CHANGED
@@ -27,6 +27,7 @@ A powerful Terminal User Interface (TUI) music player for YouTube Music
27
27
  - 🖥️ **Headless Mode** - Run without TUI for scripting
28
28
  - 💾 **Downloads** - Save tracks/playlists/artists with `Shift+D`
29
29
  - 🏷️ **Metadata Tagging** - Auto-tag title/artist/album with optional cover art
30
+ - ⚡️ **Shell Completions** - `ymc completions <bash|zsh|powershell|fish>` emits scripts you can source or save so the CLI (also available as `ymc`) tab-completes subcommands and flags
30
31
 
31
32
  ## Roadmap
32
33
 
@@ -161,6 +162,28 @@ youtube-music-cli skip
161
162
  youtube-music-cli back
162
163
  ```
163
164
 
165
+ ### Shell completions
166
+
167
+ Generate shell completion helpers through the lightweight `ymc` alias that ships with the CLI. Run `ymc completions <bash|zsh|powershell|fish>` to print the completion script for your shell, then source it or persist it in your profile:
168
+
169
+ ```bash
170
+ # Bash
171
+ source <(ymc completions bash)
172
+ ymc completions bash >> ~/.bash_completion
173
+
174
+ # Zsh
175
+ source <(ymc completions zsh)
176
+
177
+ # PowerShell
178
+ ymc completions powershell | Out-File -Encoding utf8 $PROFILE
179
+ Invoke-Expression (ymc completions powershell)
180
+
181
+ # Fish
182
+ ymc completions fish > ~/.config/fish/completions/ymc.fish
183
+ ```
184
+
185
+ If you installed the CLI globally with an alias or script name, make sure `ymc` points at the same binary before generating completions so that the script matches your install path.
186
+
164
187
  ### Options
165
188
 
166
189
  | Flag | Short | Description |