feat: add auto update support

This commit is contained in:
2026-05-15 01:47:56 -07:00
parent d1ec678d7a
commit 094bcce0dc
101 changed files with 4978 additions and 163 deletions
+99 -4
View File
@@ -9,6 +9,7 @@ import {
getDefaultConfigDir,
getDefaultConfigFilePaths,
getSetupStatePath,
normalizeSetupState,
readSetupState,
resolveDefaultMpvInstallPaths,
writeSetupState,
@@ -102,7 +103,28 @@ test('readSetupState ignores invalid files and round-trips valid state', () => {
});
});
test('readSetupState migrates v1 state to v3 windows shortcut defaults', () => {
test('createDefaultSetupState includes v4 command-line launcher defaults', () => {
assert.deepEqual(createDefaultSetupState(), {
version: 4,
status: 'incomplete',
completedAt: null,
completionSource: null,
yomitanSetupMode: null,
lastSeenYomitanDictionaryCount: 0,
pluginInstallStatus: 'unknown',
pluginInstallPathSummary: null,
windowsMpvShortcutPreferences: {
startMenuEnabled: true,
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
bunInstallStatus: 'unknown',
launcherInstallStatus: 'unknown',
launcherInstallPath: null,
});
});
test('readSetupState migrates v1 state to v4 launcher defaults', () => {
withTempDir((root) => {
const statePath = getSetupStatePath(root);
fs.writeFileSync(
@@ -119,7 +141,7 @@ test('readSetupState migrates v1 state to v3 windows shortcut defaults', () => {
);
assert.deepEqual(readSetupState(statePath), {
version: 3,
version: 4,
status: 'incomplete',
completedAt: null,
completionSource: null,
@@ -132,11 +154,14 @@ test('readSetupState migrates v1 state to v3 windows shortcut defaults', () => {
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
bunInstallStatus: 'unknown',
launcherInstallStatus: 'unknown',
launcherInstallPath: null,
});
});
});
test('readSetupState migrates completed v2 state to internal yomitan setup mode', () => {
test('readSetupState migrates completed v2 state to v4 launcher defaults', () => {
withTempDir((root) => {
const statePath = getSetupStatePath(root);
fs.writeFileSync(
@@ -158,7 +183,7 @@ test('readSetupState migrates completed v2 state to internal yomitan setup mode'
);
assert.deepEqual(readSetupState(statePath), {
version: 3,
version: 4,
status: 'completed',
completedAt: '2026-03-12T00:00:00.000Z',
completionSource: 'user',
@@ -171,10 +196,80 @@ test('readSetupState migrates completed v2 state to internal yomitan setup mode'
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
bunInstallStatus: 'unknown',
launcherInstallStatus: 'unknown',
launcherInstallPath: null,
});
});
});
test('readSetupState migrates v3 state to v4 without requiring optional launcher fields', () => {
withTempDir((root) => {
const statePath = getSetupStatePath(root);
fs.writeFileSync(
statePath,
JSON.stringify({
version: 3,
status: 'completed',
completedAt: '2026-04-01T00:00:00.000Z',
completionSource: 'user',
yomitanSetupMode: 'external',
lastSeenYomitanDictionaryCount: 0,
pluginInstallStatus: 'installed',
pluginInstallPathSummary: '/tmp/mpv',
windowsMpvShortcutPreferences: {
startMenuEnabled: false,
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'installed',
}),
);
assert.deepEqual(readSetupState(statePath), {
version: 4,
status: 'completed',
completedAt: '2026-04-01T00:00:00.000Z',
completionSource: 'user',
yomitanSetupMode: 'external',
lastSeenYomitanDictionaryCount: 0,
pluginInstallStatus: 'installed',
pluginInstallPathSummary: '/tmp/mpv',
windowsMpvShortcutPreferences: {
startMenuEnabled: false,
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'installed',
bunInstallStatus: 'unknown',
launcherInstallStatus: 'unknown',
launcherInstallPath: null,
});
});
});
test('normalizeSetupState rejects invalid v4 Bun and launcher statuses', () => {
const valid = {
version: 4,
status: 'incomplete',
completedAt: null,
completionSource: null,
yomitanSetupMode: null,
lastSeenYomitanDictionaryCount: 0,
pluginInstallStatus: 'unknown',
pluginInstallPathSummary: null,
windowsMpvShortcutPreferences: {
startMenuEnabled: true,
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
bunInstallStatus: 'unknown',
launcherInstallStatus: 'unknown',
launcherInstallPath: null,
};
assert.equal(normalizeSetupState({ ...valid, bunInstallStatus: 'bogus' }), null);
assert.equal(normalizeSetupState({ ...valid, launcherInstallStatus: 'bogus' }), null);
});
test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults', () => {
const linuxHomeDir = path.join(path.sep, 'tmp', 'home');
const xdgConfigHome = path.join(path.sep, 'tmp', 'xdg');
+54 -11
View File
@@ -8,6 +8,8 @@ export type SetupCompletionSource = 'user' | 'legacy_auto_detected' | null;
export type SetupYomitanMode = 'internal' | 'external' | null;
export type SetupPluginInstallStatus = 'unknown' | 'installed' | 'skipped' | 'failed';
export type SetupWindowsMpvShortcutInstallStatus = 'unknown' | 'installed' | 'skipped' | 'failed';
export type SetupBunInstallStatus = 'unknown' | 'installed' | 'skipped' | 'failed';
export type SetupLauncherInstallStatus = 'unknown' | 'installed' | 'skipped' | 'failed';
export interface SetupWindowsMpvShortcutPreferences {
startMenuEnabled: boolean;
@@ -15,7 +17,7 @@ export interface SetupWindowsMpvShortcutPreferences {
}
export interface SetupState {
version: 3;
version: 4;
status: SetupStateStatus;
completedAt: string | null;
completionSource: SetupCompletionSource;
@@ -25,6 +27,9 @@ export interface SetupState {
pluginInstallPathSummary: string | null;
windowsMpvShortcutPreferences: SetupWindowsMpvShortcutPreferences;
windowsMpvShortcutLastStatus: SetupWindowsMpvShortcutInstallStatus;
bunInstallStatus: SetupBunInstallStatus;
launcherInstallStatus: SetupLauncherInstallStatus;
launcherInstallPath: string | null;
}
export interface ConfigFilePaths {
@@ -54,7 +59,7 @@ function asObject(value: unknown): Record<string, unknown> | null {
export function createDefaultSetupState(): SetupState {
return {
version: 3,
version: 4,
status: 'incomplete',
completedAt: null,
completionSource: null,
@@ -67,6 +72,9 @@ export function createDefaultSetupState(): SetupState {
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
bunInstallStatus: 'unknown',
launcherInstallStatus: 'unknown',
launcherInstallPath: null,
};
}
@@ -80,9 +88,11 @@ export function normalizeSetupState(value: unknown): SetupState | null {
const yomitanSetupMode = record.yomitanSetupMode;
const windowsPrefs = asObject(record.windowsMpvShortcutPreferences);
const windowsMpvShortcutLastStatus = record.windowsMpvShortcutLastStatus;
const bunInstallStatus = record.bunInstallStatus;
const launcherInstallStatus = record.launcherInstallStatus;
if (
(version !== 1 && version !== 2 && version !== 3) ||
(version !== 1 && version !== 2 && version !== 3 && version !== 4) ||
(status !== 'incomplete' &&
status !== 'in_progress' &&
status !== 'completed' &&
@@ -91,7 +101,7 @@ export function normalizeSetupState(value: unknown): SetupState | null {
pluginInstallStatus !== 'installed' &&
pluginInstallStatus !== 'skipped' &&
pluginInstallStatus !== 'failed') ||
(version === 2 &&
((version === 2 || version === 3 || version === 4) &&
windowsMpvShortcutLastStatus !== 'unknown' &&
windowsMpvShortcutLastStatus !== 'installed' &&
windowsMpvShortcutLastStatus !== 'skipped' &&
@@ -99,21 +109,32 @@ export function normalizeSetupState(value: unknown): SetupState | null {
(completionSource !== null &&
completionSource !== 'user' &&
completionSource !== 'legacy_auto_detected') ||
(version === 3 &&
((version === 3 || version === 4) &&
yomitanSetupMode !== null &&
yomitanSetupMode !== 'internal' &&
yomitanSetupMode !== 'external')
yomitanSetupMode !== 'external') ||
(version === 4 &&
bunInstallStatus !== 'unknown' &&
bunInstallStatus !== 'installed' &&
bunInstallStatus !== 'skipped' &&
bunInstallStatus !== 'failed') ||
(version === 4 &&
launcherInstallStatus !== 'unknown' &&
launcherInstallStatus !== 'installed' &&
launcherInstallStatus !== 'skipped' &&
launcherInstallStatus !== 'failed')
) {
return null;
}
return {
version: 3,
version: 4,
status,
completedAt: typeof record.completedAt === 'string' ? record.completedAt : null,
completionSource,
yomitanSetupMode:
version === 3 && (yomitanSetupMode === 'internal' || yomitanSetupMode === 'external')
(version === 3 || version === 4) &&
(yomitanSetupMode === 'internal' || yomitanSetupMode === 'external')
? yomitanSetupMode
: status === 'completed'
? 'internal'
@@ -129,22 +150,44 @@ export function normalizeSetupState(value: unknown): SetupState | null {
typeof record.pluginInstallPathSummary === 'string' ? record.pluginInstallPathSummary : null,
windowsMpvShortcutPreferences: {
startMenuEnabled:
version === 2 && typeof windowsPrefs?.startMenuEnabled === 'boolean'
(version === 2 || version === 3 || version === 4) &&
typeof windowsPrefs?.startMenuEnabled === 'boolean'
? windowsPrefs.startMenuEnabled
: true,
desktopEnabled:
version === 2 && typeof windowsPrefs?.desktopEnabled === 'boolean'
(version === 2 || version === 3 || version === 4) &&
typeof windowsPrefs?.desktopEnabled === 'boolean'
? windowsPrefs.desktopEnabled
: true,
},
windowsMpvShortcutLastStatus:
version === 2 &&
(version === 2 || version === 3 || version === 4) &&
(windowsMpvShortcutLastStatus === 'unknown' ||
windowsMpvShortcutLastStatus === 'installed' ||
windowsMpvShortcutLastStatus === 'skipped' ||
windowsMpvShortcutLastStatus === 'failed')
? windowsMpvShortcutLastStatus
: 'unknown',
bunInstallStatus:
version === 4 &&
(bunInstallStatus === 'unknown' ||
bunInstallStatus === 'installed' ||
bunInstallStatus === 'skipped' ||
bunInstallStatus === 'failed')
? bunInstallStatus
: 'unknown',
launcherInstallStatus:
version === 4 &&
(launcherInstallStatus === 'unknown' ||
launcherInstallStatus === 'installed' ||
launcherInstallStatus === 'skipped' ||
launcherInstallStatus === 'failed')
? launcherInstallStatus
: 'unknown',
launcherInstallPath:
version === 4 && typeof record.launcherInstallPath === 'string'
? record.launcherInstallPath
: null,
};
}