diff --git a/changes/launcher-version-flag.md b/changes/launcher-version-flag.md new file mode 100644 index 00000000..0507d52e --- /dev/null +++ b/changes/launcher-version-flag.md @@ -0,0 +1,4 @@ +type: added +area: launcher + +- Added `subminer --version` and `subminer -v` to print the installed SubMiner app version. diff --git a/docs-site/launcher-script.md b/docs-site/launcher-script.md index 01576ec7..f1b71bd1 100644 --- a/docs-site/launcher-script.md +++ b/docs-site/launcher-script.md @@ -99,6 +99,7 @@ Use `subminer -h` for command-specific help. | `-r, --recursive` | Search directories recursively | | `-R, --rofi` | Use rofi instead of fzf | | `--setup` | Open first-run setup popup manually | +| `-v, --version` | Print installed SubMiner version | | `-u, --update` | Check for SubMiner updates and update the app/launcher when possible | | `--start` | Explicitly start overlay after mpv launches | | `-S, --start-overlay` | Explicitly start overlay after mpv launches | diff --git a/docs-site/usage.md b/docs-site/usage.md index 646b7f16..531a1aa0 100644 --- a/docs-site/usage.md +++ b/docs-site/usage.md @@ -78,6 +78,8 @@ subminer -S video.mkv # Same as above via --start-overlay subminer https://youtu.be/... # Play a YouTube URL subminer ytsearch:"jp news" # Play first YouTube search result subminer --setup # Open first-run setup popup +subminer --version # Print installed SubMiner version +subminer -v # Same as above subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging subminer --log-level warn video.mkv # Set logging level explicitly subminer --args '--fs=opengl-hq --ytdl-format=bestvideo*+bestaudio/best' video.mkv # Pass extra mpv args diff --git a/launcher/commands/playback-command.test.ts b/launcher/commands/playback-command.test.ts index 4c351aca..fac501fa 100644 --- a/launcher/commands/playback-command.test.ts +++ b/launcher/commands/playback-command.test.ts @@ -52,6 +52,7 @@ function createContext(): LauncherCommandContext { stats: false, doctor: false, doctorRefreshKnownWords: false, + version: false, configPath: false, configShow: false, mpvIdle: false, diff --git a/launcher/config/args-normalizer.ts b/launcher/config/args-normalizer.ts index 14183dae..3e08bc5e 100644 --- a/launcher/config/args-normalizer.ts +++ b/launcher/config/args-normalizer.ts @@ -156,6 +156,7 @@ export function createDefaultArgs( statsCleanupLifetime: false, doctor: false, doctorRefreshKnownWords: false, + version: false, update: false, configPath: false, configShow: false, @@ -219,6 +220,7 @@ export function applyRootOptionsToArgs( if (typeof options.passwordStore === 'string') parsed.passwordStore = options.passwordStore; if (options.rofi === true) parsed.useRofi = true; if (options.update === true) parsed.update = true; + if (options.version === true) parsed.version = true; if (options.startOverlay === true) parsed.autoStartOverlay = true; if (options.texthooker === false) parsed.useTexthooker = false; if (typeof options.args === 'string') parsed.mpvArgs = options.args; diff --git a/launcher/config/cli-parser-builder.ts b/launcher/config/cli-parser-builder.ts index 684a3add..62548d9f 100644 --- a/launcher/config/cli-parser-builder.ts +++ b/launcher/config/cli-parser-builder.ts @@ -57,6 +57,7 @@ function applyRootOptions(program: Command): void { .option('-p, --profile ', 'MPV profile') .option('--start', 'Explicitly start overlay') .option('--log-level ', 'Log level') + .option('-v, --version', 'Show SubMiner version') .option('-u, --update', 'Check for updates') .option('-R, --rofi', 'Use rofi picker') .option('-S, --start-overlay', 'Auto-start overlay') diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 4fc6d7a7..a80db49e 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -99,6 +99,30 @@ test('config discovery ignores lowercase subminer candidate', () => { assert.equal(resolved, expected); }); +test('version flag prints installed app version without requiring app binary', () => { + withTempDir((root) => { + const homeDir = path.join(root, 'home'); + const xdgConfigHome = path.join(root, 'xdg'); + const result = runLauncher(['--version'], makeTestEnv(homeDir, xdgConfigHome)); + + assert.equal(result.status, 0); + assert.match(result.stdout.trim(), /^SubMiner \d+\.\d+\.\d+/); + assert.equal(result.stderr, ''); + }); +}); + +test('short version flag prints installed app version without requiring app binary', () => { + withTempDir((root) => { + const homeDir = path.join(root, 'home'); + const xdgConfigHome = path.join(root, 'xdg'); + const result = runLauncher(['-v'], makeTestEnv(homeDir, xdgConfigHome)); + + assert.equal(result.status, 0); + assert.match(result.stdout.trim(), /^SubMiner \d+\.\d+\.\d+/); + assert.equal(result.stderr, ''); + }); +}); + test('config path prefers jsonc over json for same directory', () => { withTempDir((root) => { const homeDir = path.join(root, 'home'); diff --git a/launcher/main.ts b/launcher/main.ts index 33892b6f..c3a1efac 100644 --- a/launcher/main.ts +++ b/launcher/main.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import packageJson from '../package.json'; import { loadLauncherJellyfinConfig, loadLauncherMpvConfig, @@ -20,6 +21,11 @@ import { runJellyfinCommand } from './commands/jellyfin-command.js'; import { runPlaybackCommand } from './commands/playback-command.js'; import { runUpdateCommand } from './commands/update-command.js'; +const APP_VERSION = + typeof packageJson.version === 'string' && packageJson.version.trim() + ? packageJson.version + : 'unknown'; + function createCommandContext( args: ReturnType, scriptPath: string, @@ -56,6 +62,12 @@ async function main(): Promise { const launcherConfig = loadLauncherYoutubeSubgenConfig(); const launcherMpvConfig = loadLauncherMpvConfig(); const args = parseArgs(process.argv.slice(2), scriptName, launcherConfig, launcherMpvConfig); + + if (args.version) { + console.log(`SubMiner ${APP_VERSION}`); + return; + } + const pluginRuntimeConfig = readPluginRuntimeConfig(args.logLevel); const appPath = findAppBinary(scriptPath); diff --git a/launcher/mpv.test.ts b/launcher/mpv.test.ts index 8350d4de..4c59201b 100644 --- a/launcher/mpv.test.ts +++ b/launcher/mpv.test.ts @@ -529,6 +529,7 @@ function makeArgs(overrides: Partial = {}): Args { stats: false, doctor: false, doctorRefreshKnownWords: false, + version: false, configPath: false, configShow: false, mpvIdle: false, diff --git a/launcher/parse-args.test.ts b/launcher/parse-args.test.ts index d43232c1..e4b40cc9 100644 --- a/launcher/parse-args.test.ts +++ b/launcher/parse-args.test.ts @@ -69,6 +69,17 @@ test('parseArgs maps root update flags without conflicting with jellyfin usernam assert.equal(jellyfinParsed.jellyfinUsername, 'kyle'); }); +test('parseArgs maps root version flags without conflicting with stats vocab flag', () => { + const shortParsed = parseArgs(['-v'], 'subminer', {}); + const longParsed = parseArgs(['--version'], 'subminer', {}); + const statsParsed = parseArgs(['stats', 'cleanup', '-v'], 'subminer', {}); + + assert.equal(shortParsed.version, true); + assert.equal(longParsed.version, true); + assert.equal(statsParsed.version, false); + assert.equal(statsParsed.statsCleanupVocab, true); +}); + test('parseArgs maps jellyfin play action and log-level override', () => { const parsed = parseArgs(['jellyfin', 'play', '--log-level', 'debug'], 'subminer', {}); diff --git a/launcher/types.ts b/launcher/types.ts index 7ce1cabf..d2cc320e 100644 --- a/launcher/types.ts +++ b/launcher/types.ts @@ -134,6 +134,7 @@ export interface Args { dictionaryTarget?: string; doctor: boolean; doctorRefreshKnownWords: boolean; + version: boolean; update?: boolean; configPath: boolean; configShow: boolean;