From 325b485ef685cecdd6ee472fb01c4d389dd92909 Mon Sep 17 00:00:00 2001 From: Weathercold Date: Wed, 28 Oct 2026 09:05:01 -0400 Subject: [PATCH 1/7] chore(nix): update pnpmDeps hash (#3185) Auto-generated by CI to keep Nix pnpmDeps hash up-to-date. Co-authored-by: github-actions[bot] --- nix/pnpm-deps-hash.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/pnpm-deps-hash.txt b/nix/pnpm-deps-hash.txt index e5f8b93ff3..20545efc08 100644 --- a/nix/pnpm-deps-hash.txt +++ b/nix/pnpm-deps-hash.txt @@ -1 +1 @@ -sha256-f/u7/wIlSBHUpOhZlD4zoSQuI0ENofreBe/H8oZNag4= +sha256-ynePLbeSYhTg8eWw8p86x7P9lJuEyw85NzsqwAQ6rIw= From a19cdf05fb8fa880d310c49f0df289aea59a030a Mon Sep 17 00:00:00 2001 From: Neko Date: Wed, 28 Oct 2026 21:11:14 +0800 Subject: [PATCH 2/7] fix(stage-tamagotchi): write the updater log in the user data directory (#3187) --- .../main/services/electron/auto-updater.test.ts | 15 +++++++++++++++ .../src/main/services/electron/auto-updater.ts | 12 +++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts index 91b7e8bf68..37c52ee887 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts @@ -293,6 +293,21 @@ describe('setupAutoUpdater', () => { expect(service.state.status).toBe('not-available') }) + // An install with its own user data directory, for example a test build, must not write into the log of another install. + it('writes the updater log in the user data directory and reports that path', async () => { + const { appendFile } = await import('node:fs/promises') + const { join } = await import('node:path') + const fake = createFakeUpdater() + const { setupAutoUpdater } = await import('./auto-updater') + const service = setupAutoUpdater({ updater: fake.updater }) + const logFile = join('/tmp/userData', 'logs', 'updater-log.txt') + + fake.setStatus({ status: 'error', error: { code: 'network-error', message: 'Offline.' } }) + + await vi.waitFor(() => expect(appendFile).toHaveBeenCalledWith(logFile, expect.stringContaining('network-error'))) + expect(service.state.diagnostics?.logFilePath).toBe(logFile) + }) + it('does not touch the updater when the launcher turned it off', async () => { const fake = createFakeUpdater({ status: 'disabled', reason: 'store-build' }) const { setupAutoUpdater } = await import('./auto-updater') diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts index 85407b0358..b8690208ea 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts @@ -28,14 +28,12 @@ import { channelOfVersion, updateChannels } from '../../../shared/update-channel const UPDATE_CHANNEL_ENV_KEY = 'AIRI_UPDATE_CHANNEL' -function getCacheRoot() { - // NOTICE: Electron resolves the cache directory per platform/app, but the - // shipped type definitions here do not expose `cache`, so we cast the key. - return app.getPath('cache' as Parameters[0]) -} - +/** + * The updater log is next to the session logs of `app/file-logger.ts`, in the user data directory. So an install with + * its own user data directory, for example a test build, writes its own log. + */ function updaterLogFile() { - return join(getCacheRoot(), 'stage-tamagotchi-updater', 'updater-log.txt') + return join(app.getPath('userData'), 'logs', 'updater-log.txt') } async function logToFile(level: 'INFO' | 'WARN' | 'ERROR', message: string) { From a2bef067be925a34c8ab6ae66904ea970ede1a39 Mon Sep 17 00:00:00 2001 From: CyberSprout Date: Wed, 28 Oct 2026 21:12:16 +0800 Subject: [PATCH 3/7] fix(stage-ui): keep wake word preparation when an equal catalog arrives during load (#3104) --- .../src/libs/voice/wake-word-detector.test.ts | 21 +++++++++++++++++++ .../src/libs/voice/wake-word-detector.ts | 5 +++-- packages/testing-audio/src/describe.ts | 7 ------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts b/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts index ad8b59debe..f555ac1f71 100644 --- a/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts +++ b/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts @@ -252,6 +252,27 @@ describe('wakeWordDetector', () => { expect(harness.spotter.setKeywords).not.toHaveBeenCalled() }) + it('becomes ready when the same catalog arrives again while the spotter is loading', async () => { + let finishLoad!: () => void + const loaded = new Promise(resolve => finishLoad = resolve) + const harness = createHarness() + harness.createSpotter.mockImplementationOnce(async () => { + await loaded + return harness.spotter + }) + const catalog = () => pronunciations([{ characterId: 'alice', text: 'Alice', tokens: [['AE1', 'L', 'IH0', 'S']] }]) + + // A reactive catalog can emit an equal list again, for example after the card store hydrates. + const first = harness.detector.setPronunciations(catalog()) + const second = harness.detector.setPronunciations(catalog()) + finishLoad() + await Promise.all([first, second]) + + expect(harness.createSpotter).toHaveBeenCalledTimes(1) + expect(harness.detector.ready).toBe(true) + expect(harness.onPreparationChange).toHaveBeenLastCalledWith('ready') + }) + it('does not load a model for pronunciations of another model', async () => { const harness = createHarness() diff --git a/packages/stage-ui/src/libs/voice/wake-word-detector.ts b/packages/stage-ui/src/libs/voice/wake-word-detector.ts index 218497a3a8..bd42a3e9dd 100644 --- a/packages/stage-ui/src/libs/voice/wake-word-detector.ts +++ b/packages/stage-ui/src/libs/voice/wake-word-detector.ts @@ -152,7 +152,8 @@ export class WakeWordDetector { const keywords = this.keywordsOf([...next.values()]) const update = this.updates.then(async () => { // A newer list or a stop replaced this one while an earlier update ran. - if (generation !== this.generation || next !== this.pronunciations) + // An equal list replaces `pronunciations` without a new generation, so only the generation tells a newer list. + if (generation !== this.generation) return if (keywords.length === 0) { @@ -184,7 +185,7 @@ export class WakeWordDetector { // A keyword update starts a new stream in the Worker. this.streamSampleRate = undefined this.resetStream = false - if (generation !== this.generation || next !== this.pronunciations) + if (generation !== this.generation) return this.keywordsReady = true this.options.onPreparationChange?.('ready') diff --git a/packages/testing-audio/src/describe.ts b/packages/testing-audio/src/describe.ts index 02b28243a2..359f0613f8 100644 --- a/packages/testing-audio/src/describe.ts +++ b/packages/testing-audio/src/describe.ts @@ -37,13 +37,6 @@ const audioTestAPI = createAudioTestAPI< }] }, async execute({ plan, task, invokeHandler, runPreflight }) { - // NOTICE: - // CI wake word cases time out while preparation stays `preparing`. The root cause is not confirmed. - // Context: audio-web wake word cases time out in the CI Unit Test job. - // Remove this skip after the preparation failure has a verified fix on CI. - if (env.CI && plan.definition.pipelines?.includes('kws')) - task.context.skip(true, 'Wake word preparation times out on CI. The root cause is not confirmed.') - // Electron bundles the wake word model. Web downloads it, so the download must finish before the recording starts. if (plan.definition.pipelines?.includes('kws') && inject('fakemicRuntime').kind === 'web') await prepareWakeWordModel() From 3a6c65b7985e2e8f9b94d793fa81ffc9446b4b86 Mon Sep 17 00:00:00 2001 From: Neko Date: Wed, 28 Oct 2026 21:36:29 +0800 Subject: [PATCH 4/7] refactor: move hand-made tints to chromatic colours (#3191) --- DESIGN.md | 2 +- apps/stage-pocket/src/App.vue | 23 ++++----------- apps/stage-pocket/src/styles/main.css | 21 ++++++++++++-- apps/stage-web/src/App.vue | 23 ++++----------- apps/stage-web/src/styles/main.css | 21 ++++++++++++-- apps/ui-server-auth/src/styles/main.css | 4 +-- .../components/Backgrounds/default/index.vue | 21 ++++++++------ .../Backgrounds/default/pattern-cross.vue | 4 +-- .../Backgrounds/default/preview.vue | 21 ++++++++------ .../motion/live2d/components/workbench.vue | 2 +- .../ui/src/components/form/range/range.vue | 12 ++++---- packages/ui/src/fallback.css | 20 ------------- pnpm-lock.yaml | 28 +++++++++---------- pnpm-workspace.yaml | 4 +-- 14 files changed, 102 insertions(+), 104 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 8e8876131a..b49777a6be 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -23,7 +23,7 @@ API details belong in the [component reference](./docs/ai/context/ui-components. ## Colors - [UnoCSS configuration](./uno.config.ts) provides Chromatic palettes, fonts, icons, and shared animations. -- [UI fallback variables](./packages/ui/src/fallback.css) define `--chromatic-hue` and shade-specific chroma variables. +- [UI fallback variables](./packages/ui/src/fallback.css) define the `--chromatic-hue` fallback. - [Theme settings](./packages/stage-ui/src/stores/settings/theme.ts) store the selected hue. The default is `220.44`. - [The UI stylesheet entry](./packages/ui/src/main.css) includes fallback variables, lamp animation, and component styles. diff --git a/apps/stage-pocket/src/App.vue b/apps/stage-pocket/src/App.vue index f2bd864a5d..7e6d63434d 100644 --- a/apps/stage-pocket/src/App.vue +++ b/apps/stage-pocket/src/App.vue @@ -82,26 +82,13 @@ function registerAuthenticatedSetup() { stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration) } -const primaryColor = computed(() => { - return isDark.value - ? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360))` - : `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))` -}) - -const secondaryColor = computed(() => { - return isDark.value - ? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 70%, oklch(50% 0 360))` - : `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 90%, oklch(90% 0 360))` -}) - -const tertiaryColor = computed(() => { - return isDark.value - ? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 70%, oklch(50% 0 360))` - : `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 90%, oklch(90% 0 360))` -}) +// Defined in `styles/main.css`, so they follow the theme hue (including `dynamic-hue`) and dark mode. +const primaryColor = 'var(--stage-transition-primary)' +const secondaryColor = 'var(--stage-transition-secondary)' +const tertiaryColor = 'var(--stage-transition-tertiary)' const colors = computed(() => { - return [primaryColor.value, secondaryColor.value, tertiaryColor.value, isDark.value ? '#121212' : '#FFFFFF'] + return [primaryColor, secondaryColor, tertiaryColor, isDark.value ? '#121212' : '#FFFFFF'] }) watch(settings.language, () => { diff --git a/apps/stage-pocket/src/styles/main.css b/apps/stage-pocket/src/styles/main.css index 8d884843e0..9804ac5e56 100755 --- a/apps/stage-pocket/src/styles/main.css +++ b/apps/stage-pocket/src/styles/main.css @@ -43,12 +43,29 @@ html.dark { color-scheme: dark; } +/* + * Stage transition colours (StageTransitionGroup in App.vue): pale tints of the theme. They live in CSS so they follow + * `--chromatic-hue`, including the `dynamic-hue` animation, and dark mode. + */ +:root { + --stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360)); + --stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 90%, oklch(90% 0 360)); + /* primary + 60°: a third of the way from primary (h) to complementary (h + 180°), going up in hue */ + --stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 90%, oklch(90% 0 360)); +} + +.dark { + --stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360)); + --stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 70%, oklch(50% 0 360)); + --stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 70%, oklch(50% 0 360)); +} + #nprogress { pointer-events: none; } #nprogress .bar { - background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360)); + background: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360)); opacity: 0.75; position: fixed; z-index: 1031; @@ -59,7 +76,7 @@ html.dark { } .dark #nprogress .bar { - background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`; + background: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360)); } @media (display-mode: standalone) { diff --git a/apps/stage-web/src/App.vue b/apps/stage-web/src/App.vue index 4d2a4bcfff..6b412a5938 100644 --- a/apps/stage-web/src/App.vue +++ b/apps/stage-web/src/App.vue @@ -90,26 +90,13 @@ watch(showingSetup, (visible) => { startupOnboarding.value = false }) -const primaryColor = computed(() => { - return isDark.value - ? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360))` - : `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))` -}) - -const secondaryColor = computed(() => { - return isDark.value - ? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 70%, oklch(50% 0 360))` - : `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 90%, oklch(90% 0 360))` -}) - -const tertiaryColor = computed(() => { - return isDark.value - ? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 70%, oklch(50% 0 360))` - : `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 90%, oklch(90% 0 360))` -}) +// Defined in `styles/main.css`, so they follow the theme hue (including `dynamic-hue`) and dark mode. +const primaryColor = 'var(--stage-transition-primary)' +const secondaryColor = 'var(--stage-transition-secondary)' +const tertiaryColor = 'var(--stage-transition-tertiary)' const colors = computed(() => { - return [primaryColor.value, secondaryColor.value, tertiaryColor.value, isDark.value ? '#121212' : '#FFFFFF'] + return [primaryColor, secondaryColor, tertiaryColor, isDark.value ? '#121212' : '#FFFFFF'] }) router.beforeEach((to, from) => { diff --git a/apps/stage-web/src/styles/main.css b/apps/stage-web/src/styles/main.css index 79f4498d49..ee6dd286a3 100755 --- a/apps/stage-web/src/styles/main.css +++ b/apps/stage-web/src/styles/main.css @@ -37,12 +37,29 @@ html.dark { color-scheme: dark; } +/* + * Stage transition colours (StageTransitionGroup in App.vue): pale tints of the theme. They live in CSS so they follow + * `--chromatic-hue`, including the `dynamic-hue` animation, and dark mode. + */ +:root { + --stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360)); + --stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 90%, oklch(90% 0 360)); + /* primary + 60°: a third of the way from primary (h) to complementary (h + 180°), going up in hue */ + --stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 90%, oklch(90% 0 360)); +} + +.dark { + --stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360)); + --stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 70%, oklch(50% 0 360)); + --stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 70%, oklch(50% 0 360)); +} + #nprogress { pointer-events: none; } #nprogress .bar { - background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360)); + background: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360)); opacity: 0.75; position: fixed; z-index: 1031; @@ -53,7 +70,7 @@ html.dark { } .dark #nprogress .bar { - background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`; + background: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360)); } @media (display-mode: standalone) { diff --git a/apps/ui-server-auth/src/styles/main.css b/apps/ui-server-auth/src/styles/main.css index 79f4498d49..0012081235 100755 --- a/apps/ui-server-auth/src/styles/main.css +++ b/apps/ui-server-auth/src/styles/main.css @@ -42,7 +42,7 @@ html.dark { } #nprogress .bar { - background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360)); + background: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360)); opacity: 0.75; position: fixed; z-index: 1031; @@ -53,7 +53,7 @@ html.dark { } .dark #nprogress .bar { - background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`; + background: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360)); } @media (display-mode: standalone) { diff --git a/packages/stage-layouts/src/components/Backgrounds/default/index.vue b/packages/stage-layouts/src/components/Backgrounds/default/index.vue index 0adf4b682f..c259f01ea2 100644 --- a/packages/stage-layouts/src/components/Backgrounds/default/index.vue +++ b/packages/stage-layouts/src/components/Backgrounds/default/index.vue @@ -1,20 +1,25 @@ + + diff --git a/packages/stage-layouts/src/components/Backgrounds/default/pattern-cross.vue b/packages/stage-layouts/src/components/Backgrounds/default/pattern-cross.vue index 4e58505985..4a49bc0449 100644 --- a/packages/stage-layouts/src/components/Backgrounds/default/pattern-cross.vue +++ b/packages/stage-layouts/src/components/Backgrounds/default/pattern-cross.vue @@ -11,7 +11,7 @@ https://www.magicpattern.design/tools/css-backgrounds */ .cross-background-container { background-color: #ffffff; - --cross-color: oklch(0.85 calc(var(--chromatic-chroma) * 0.3) var(--chromatic-hue) / 0.2); + --cross-color: theme('colors.primary.200 / 20%'); background: radial-gradient(circle, transparent 20%, #ffffff 20%, #ffffff 80%, transparent 80%, transparent), radial-gradient(circle, transparent 20%, #ffffff 20%, #ffffff 80%, transparent 80%, transparent) 25px 25px, @@ -26,7 +26,7 @@ https://www.magicpattern.design/tools/css-backgrounds .dark .cross-background-container { background-color: #121212; - --cross-color: oklch(0.2 calc(var(--chromatic-chroma) * 0.3) var(--chromatic-hue)); + --cross-color: theme('colors.primary.950'); background: radial-gradient(circle, transparent 20%, #121212 20%, #121212 80%, transparent 80%, transparent), radial-gradient(circle, transparent 20%, #121212 20%, #121212 80%, transparent 80%, transparent) 25px 25px, diff --git a/packages/stage-layouts/src/components/Backgrounds/default/preview.vue b/packages/stage-layouts/src/components/Backgrounds/default/preview.vue index a0f5cca144..34701faa86 100644 --- a/packages/stage-layouts/src/components/Backgrounds/default/preview.vue +++ b/packages/stage-layouts/src/components/Backgrounds/default/preview.vue @@ -1,20 +1,25 @@ + + diff --git a/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.vue b/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.vue index f6f595fd6c..549e13f2f8 100644 --- a/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.vue +++ b/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.vue @@ -105,7 +105,7 @@ function handleReady({ api }: DockviewReadyEvent) { @import 'dockview-vue/dist/styles/dockview.css'; .dockview-theme-airi { - --motion-workbench-accent: oklch(clamp(0%, calc(74% * var(--chromatic-bri, 1)), 100%) calc(var(--chromatic-chroma-400) * var(--chromatic-sat, 1)) calc(var(--chromatic-hue) + 0)); + --motion-workbench-accent: theme('colors.primary.400'); --dv-tabs-and-actions-container-height: 2.25rem; --dv-tabs-and-actions-container-background-color: rgb(250 250 250 / 0.92); --dv-group-view-background-color: rgb(255 255 255 / 0.68); diff --git a/packages/ui/src/components/form/range/range.vue b/packages/ui/src/components/form/range/range.vue index c2347a1e38..640f8cfd2e 100644 --- a/packages/ui/src/components/form/range/range.vue +++ b/packages/ui/src/components/form/range/range.vue @@ -84,9 +84,9 @@ https://toughengineer.github.io/demo/slider-styler*/ --thumb-box-shadow: 0 0 0px #e6e6e6; --thumb-border: none; --thumb-border-radius: 999px; - --thumb-background: oklch(80% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0)); - --thumb-background-hover: oklch(90% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0)); - --thumb-background-active: oklch(70% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0)); + --thumb-background: theme('colors.primary.300'); + --thumb-background-hover: theme('colors.primary.200'); + --thumb-background-active: theme('colors.primary.400'); --track-height: calc(var(--height) - var(--track-value-padding) * 2); --track-box-shadow: none; @@ -103,9 +103,9 @@ https://toughengineer.github.io/demo/slider-styler*/ } .dark .form_input-range { - --thumb-background: oklch(70% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0)); - --thumb-background-hover: oklch(90% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0)); - --thumb-background-active: oklch(80% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0)); + --thumb-background: theme('colors.primary.400'); + --thumb-background-hover: theme('colors.primary.200'); + --thumb-background-active: theme('colors.primary.300'); --track-border: solid 2px rgb(44, 44, 44); --track-background: rgba(44, 44, 44, 0.7); diff --git a/packages/ui/src/fallback.css b/packages/ui/src/fallback.css index d2fb80ef12..3405cb1668 100644 --- a/packages/ui/src/fallback.css +++ b/packages/ui/src/fallback.css @@ -1,25 +1,5 @@ -/* - * Chroma variables in the shape of @proj-airi/unocss-preset-chromatic 1.x (`0.18 + 0.04 · cos(hue)`, scaled per shade). - * - * NOTICE: since chromatic 2 the preset no longer emits these (its default `v2` model uses per-colour variables), so - * this file is now their only source. They back the hand-made tints that pick their own lightness (stage transition - * colours, nprogress, default backgrounds, range thumbs, the motion workbench). Keep them until those sites move to - * chromatic utilities (`~` / `*` modifiers) or to `chromaticColorFrom()`. - */ :root { --chromatic-hue: 220.44; - --chromatic-chroma: calc(0.18 + (cos(var(--chromatic-hue) * 3.14159265 / 180) * 0.04)); - --chromatic-chroma-50: calc(var(--chromatic-chroma) * 0.3); - --chromatic-chroma-100: calc(var(--chromatic-chroma) * 0.5); - --chromatic-chroma-200: calc(var(--chromatic-chroma) * 0.6); - --chromatic-chroma-300: calc(var(--chromatic-chroma) * 0.75); - --chromatic-chroma-400: calc(var(--chromatic-chroma) * 0.85); - --chromatic-chroma-500: var(--chromatic-chroma); - --chromatic-chroma-600: calc(var(--chromatic-chroma) * 1.15); - --chromatic-chroma-700: calc(var(--chromatic-chroma) * 1.1); - --chromatic-chroma-800: calc(var(--chromatic-chroma) * 0.85); - --chromatic-chroma-900: calc(var(--chromatic-chroma) * 0.7); - --chromatic-chroma-950: calc(var(--chromatic-chroma) * 0.5); } .is-interacting { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b826f08636..f47d2792b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -403,8 +403,8 @@ catalogs: specifier: ^1000.1.5 version: 1000.1.5 '@proj-airi/chromatic': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.0.1 + version: 2.0.1 '@proj-airi/drizzle-duckdb-wasm': specifier: ^0.6.0 version: 0.6.0 @@ -415,8 +415,8 @@ catalogs: specifier: ^1.0.19 version: 1.0.19 '@proj-airi/unocss-preset-chromatic': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.0.1 + version: 2.0.1 '@proj-airi/unplugin-fetch': specifier: ^0.2.3 version: 0.2.3 @@ -1307,7 +1307,7 @@ importers: version: 0.1.0-beta.19(@antfu/eslint-config@8.2.0(@typescript-eslint/typescript-estree@8.67.0(supports-color@10.2.2)(typescript@6.0.3))(@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@unocss/eslint-plugin@66.7.5(@typescript-eslint/types@8.67.0)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint-plugin-format@2.0.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.79.0)(supports-color@10.2.2)(typescript@6.0.3)(vitest@4.1.11))(eslint-plugin-oxlint@1.79.0(oxlint@1.79.0))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@proj-airi/unocss-preset-chromatic': specifier: 'catalog:' - version: 2.0.0 + version: 2.0.1 '@types/node': specifier: 'catalog:' version: 26.2.0 @@ -5097,7 +5097,7 @@ importers: version: link:../ccc '@proj-airi/chromatic': specifier: 'catalog:' - version: 2.0.0 + version: 2.0.1 '@proj-airi/cloud-client': specifier: workspace:^ version: link:../cloud-client @@ -6205,7 +6205,7 @@ importers: version: link:../../packages/stage-ui '@proj-airi/unocss-preset-chromatic': specifier: 'catalog:' - version: 2.0.0 + version: 2.0.1 '@unocss/reset': specifier: 'catalog:' version: 66.7.5 @@ -11555,8 +11555,8 @@ packages: prisma: optional: true - '@proj-airi/chromatic@2.0.0': - resolution: {integrity: sha512-QAu6aqg/ZacEADRvnevakt5ksUneqt748IkRXn6sj0kQqU7EruFfmbv2O52lIK/ysiw5MNFs4qUuyYjg2z7GwQ==} + '@proj-airi/chromatic@2.0.1': + resolution: {integrity: sha512-CTZMkkqYEpUdWM1eFqQcJE2FdFKWY7NyrNYfzD4908GYcSVQaJAliVTEGVajxUfOJP0aD13BSyN7vo1PmsuW/g==} '@proj-airi/drizzle-duckdb-wasm@0.6.0': resolution: {integrity: sha512-iW7u6eGg7uHXQ/9MsyVfgqcoa07wcdaOp5v63crJnQx8XXdF2uus3yVtx7NXzHXWF6TF9IeybMNBM4CHLhjDGQ==} @@ -11582,8 +11582,8 @@ packages: '@proj-airi/lobe-icons@1.0.19': resolution: {integrity: sha512-hBB/MUYqzckzMXYDLZ7/4HRuldqEvLKhMzTcVPLUOdXR0sn9y4vk+k14tynwgeePCrUKSUTP/bnPdETqvWJzqw==} - '@proj-airi/unocss-preset-chromatic@2.0.0': - resolution: {integrity: sha512-Msnsu3JFi0kxFnsf45LT2ZL5eT1bd9/inibv9/r826lScnZvQ1i8GD2RMVJUwoM9IiHqnEUVZn3ycKhxRcEqzQ==} + '@proj-airi/unocss-preset-chromatic@2.0.1': + resolution: {integrity: sha512-mXzCMxkO5113nhlG4dILCnh3WZgWBn45TEG5tQJzcOSpgqVmJNM9nLbLzB+JDDZ2sMXAo77WrRqSbvWoOlCWbQ==} '@proj-airi/unplugin-fetch@0.2.3': resolution: {integrity: sha512-90I6CfbC7xMSm9/0gl+DJvHmg2Hw9E/7L5rtfXpqMmrRQqd/jhfD1Sb5LjywqTZ3rlFfnb39d051yFGeYigi9A==} @@ -26252,7 +26252,7 @@ snapshots: '@prisma/client@5.22.0': {} - '@proj-airi/chromatic@2.0.0': + '@proj-airi/chromatic@2.0.1': dependencies: culori: 4.0.2 @@ -26290,9 +26290,9 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@proj-airi/unocss-preset-chromatic@2.0.0': + '@proj-airi/unocss-preset-chromatic@2.0.1': dependencies: - '@proj-airi/chromatic': 2.0.0 + '@proj-airi/chromatic': 2.0.1 '@proj-airi/unplugin-fetch@0.2.3(vite@8.3.4(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@10.2.2))(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.1))': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ba2c1b172..dfe42b415d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -196,11 +196,11 @@ catalog: '@pixiv/three-vrm-animation': ^3.5.5 '@pixiv/three-vrm-core': ^3.5.5 '@pnpm/find-workspace-dir': ^1000.1.5 - '@proj-airi/chromatic': ^2.0.0 + '@proj-airi/chromatic': ^2.0.1 '@proj-airi/drizzle-duckdb-wasm': ^0.6.0 '@proj-airi/iconify-meteocons': ^0.1.5 '@proj-airi/lobe-icons': ^1.0.19 - '@proj-airi/unocss-preset-chromatic': ^2.0.0 + '@proj-airi/unocss-preset-chromatic': ^2.0.1 '@proj-airi/unplugin-fetch': ^0.2.3 '@proj-airi/unplugin-live2d-sdk': ^0.1.7 '@revenuecat/purchases-js': ^1.15.0 From 68869042082d27035dd5d5f49e3a8e46f69cc6fd Mon Sep 17 00:00:00 2001 From: Neko Date: Wed, 28 Oct 2026 21:50:13 +0800 Subject: [PATCH 5/7] fix(ui): keep range thumbs buildable without the chromatic preset (#3194) --- packages/ui/src/components/form/range/range.vue | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/form/range/range.vue b/packages/ui/src/components/form/range/range.vue index 640f8cfd2e..d209833ef2 100644 --- a/packages/ui/src/components/form/range/range.vue +++ b/packages/ui/src/components/form/range/range.vue @@ -84,9 +84,10 @@ https://toughengineer.github.io/demo/slider-styler*/ --thumb-box-shadow: 0 0 0px #e6e6e6; --thumb-border: none; --thumb-border-radius: 999px; - --thumb-background: theme('colors.primary.300'); - --thumb-background-hover: theme('colors.primary.200'); - --thumb-background-active: theme('colors.primary.400'); + /* The defaults keep @proj-airi/ui buildable where the chromatic preset is not configured (e.g. ui-loading-screens). */ + --thumb-background: theme('colors.primary.300', oklch(85% 0.09 var(--chromatic-hue))); + --thumb-background-hover: theme('colors.primary.200', oklch(90% 0.07 var(--chromatic-hue))); + --thumb-background-active: theme('colors.primary.400', oklch(77% 0.12 var(--chromatic-hue))); --track-height: calc(var(--height) - var(--track-value-padding) * 2); --track-box-shadow: none; @@ -103,9 +104,9 @@ https://toughengineer.github.io/demo/slider-styler*/ } .dark .form_input-range { - --thumb-background: theme('colors.primary.400'); - --thumb-background-hover: theme('colors.primary.200'); - --thumb-background-active: theme('colors.primary.300'); + --thumb-background: theme('colors.primary.400', oklch(77% 0.12 var(--chromatic-hue))); + --thumb-background-hover: theme('colors.primary.200', oklch(90% 0.07 var(--chromatic-hue))); + --thumb-background-active: theme('colors.primary.300', oklch(85% 0.09 var(--chromatic-hue))); --track-border: solid 2px rgb(44, 44, 44); --track-background: rgba(44, 44, 44, 0.7); From 48a8b0b7dc25e92ea736b8efb33a87844855d20c Mon Sep 17 00:00:00 2001 From: Neko Date: Wed, 28 Oct 2026 23:07:21 +0800 Subject: [PATCH 6/7] test(stage-ui): load the UnoCSS preflight in the motion workbench test (#3199) --- .../motion/live2d/components/workbench.browser.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.browser.test.ts b/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.browser.test.ts index 480a256601..117181b0df 100644 --- a/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.browser.test.ts +++ b/packages/stage-ui/src/features/devtools/motion/live2d/components/workbench.browser.test.ts @@ -5,6 +5,7 @@ import { createI18n } from 'vue-i18n' import Workbench from './workbench.vue' import '@proj-airi/ui/main.css' +import 'virtual:uno.css' function createTestI18n() { return createI18n({ @@ -24,6 +25,10 @@ describe('live2d motion workbench', () => { // // The workbench used an undefined --color-primary-400 variable. Dockview // created its drop target, but the browser resolved its background to transparent. + // + // The accent is now `theme('colors.primary.400')`, which reads the chromatic preset's + // custom properties from the UnoCSS preflight, so the test loads `virtual:uno.css` + // like the apps do. await render(Workbench, { attrs: { style: 'width: 1200px; height: 800px' }, global: { plugins: [createTestI18n()] }, From 0586ed5fe90bdb6f1a89471f0a64f763f1bdbaf7 Mon Sep 17 00:00:00 2001 From: Neko Date: Wed, 28 Oct 2026 23:08:15 +0800 Subject: [PATCH 7/7] feat(stage-tamagotchi): build AppImage and pacman packages for Linux (#3188) --- .../workflows/release-tamagotchi-steam.yml | 3 +- .github/workflows/release-tamagotchi.yml | 43 +++ .../electron-builder.config.ts | 35 +++ apps/stage-tamagotchi/scripts/utils.test.ts | 53 ++++ apps/stage-tamagotchi/scripts/utils.ts | 256 ++++-------------- 5 files changed, 186 insertions(+), 204 deletions(-) create mode 100644 apps/stage-tamagotchi/scripts/utils.test.ts diff --git a/.github/workflows/release-tamagotchi-steam.yml b/.github/workflows/release-tamagotchi-steam.yml index af7af64320..f9ef588130 100644 --- a/.github/workflows/release-tamagotchi-steam.yml +++ b/.github/workflows/release-tamagotchi-steam.yml @@ -57,7 +57,8 @@ jobs: - depot: linux os: ubuntu-latest - builder-args: --linux --x64 + # The depot ships `dist/linux-unpacked`, so electron-builder builds no Linux package. + builder-args: --linux dir --x64 arch: x64 steps: diff --git a/.github/workflows/release-tamagotchi.yml b/.github/workflows/release-tamagotchi.yml index 260605c3c5..2998bfbc7f 100644 --- a/.github/workflows/release-tamagotchi.yml +++ b/.github/workflows/release-tamagotchi.yml @@ -296,6 +296,13 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_DEVELOPER_APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }} + # fpm writes the `.MTREE` file of the pacman package with bsdtar. + - name: Install Linux Packaging Dependencies (Linux Only) + if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y libarchive-tools + - name: Build (Linux Only) # Linux if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' run: pnpm run -F @proj-airi/stage-tamagotchi build && pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=${{ (inputs.build_only || inputs.artifacts_only) && 'never' || 'onTagOrDraft' }} @@ -355,6 +362,8 @@ jobs: run: | echo "DEB_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename deb)" >> $GITHUB_ENV echo "RPM_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename rpm)" >> $GITHUB_ENV + echo "PACMAN_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename pacman)" >> $GITHUB_ENV + echo "APPIMAGE_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename AppImage)" >> $GITHUB_ENV echo "FLATPAK_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename flatpak)" >> $GITHUB_ENV - name: Upload Artifacts (Nightly + Non-Linux) @@ -379,6 +388,20 @@ jobs: name: ${{ env.RPM_BUNDLE_NAME }} path: apps/stage-tamagotchi/bundle/${{ env.RPM_BUNDLE_NAME }} + - name: Upload Artifacts (Nightly + Linux pacman) + if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }} + uses: actions/upload-artifact@v7 + with: + name: ${{ env.PACMAN_BUNDLE_NAME }} + path: apps/stage-tamagotchi/bundle/${{ env.PACMAN_BUNDLE_NAME }} + + - name: Upload Artifacts (Nightly + Linux AppImage) + if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }} + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APPIMAGE_BUNDLE_NAME }} + path: apps/stage-tamagotchi/bundle/${{ env.APPIMAGE_BUNDLE_NAME }} + - name: Upload Flatpak Artifact (Nightly + Linux Only) if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }} uses: actions/upload-artifact@v7 @@ -486,6 +509,8 @@ jobs: run: | echo "DEB_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename deb --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV echo "RPM_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename rpm --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV + echo "PACMAN_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename pacman --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV + echo "APPIMAGE_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename AppImage --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV echo "FLATPAK_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename flatpak --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV - name: Upload Artifacts (Manual + Non-Release + Non-Linux) @@ -510,6 +535,20 @@ jobs: name: ${{ env.RPM_BUNDLE_NAME }} path: apps/stage-tamagotchi/bundle/${{ env.RPM_BUNDLE_NAME }} + - name: Upload Artifacts (Manual + Non-Release + Linux pacman) + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }} + uses: actions/upload-artifact@v7 + with: + name: ${{ env.PACMAN_BUNDLE_NAME }} + path: apps/stage-tamagotchi/bundle/${{ env.PACMAN_BUNDLE_NAME }} + + - name: Upload Artifacts (Manual + Non-Release + Linux AppImage) + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }} + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APPIMAGE_BUNDLE_NAME }} + path: apps/stage-tamagotchi/bundle/${{ env.APPIMAGE_BUNDLE_NAME }} + - name: Upload Flatpak Artifact (Manual + Non-Release + Linux) if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }} uses: actions/upload-artifact@v7 @@ -570,6 +609,8 @@ jobs: apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.dmg apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.deb apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.rpm + apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.pacman + apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.AppImage apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.flatpak apps/stage-tamagotchi/bundle/latest-*.yml append_body: true @@ -683,6 +724,8 @@ jobs: apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.dmg apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.deb apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.rpm + apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.pacman + apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.AppImage apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.flatpak apps/stage-tamagotchi/bundle/latest-*.yml append_body: true diff --git a/apps/stage-tamagotchi/electron-builder.config.ts b/apps/stage-tamagotchi/electron-builder.config.ts index f0cab549dc..c8fdbf729f 100644 --- a/apps/stage-tamagotchi/electron-builder.config.ts +++ b/apps/stage-tamagotchi/electron-builder.config.ts @@ -248,9 +248,12 @@ export default { artifactName: '${productName}-${version}-darwin-${arch}.${ext}', }, linux: { + // AUR packages and Arch users install the `pacman` package. Other distributions can run the AppImage. target: [ 'deb', 'rpm', + 'pacman', + 'AppImage', ], // NOTICE: Same channel rule as Windows/macOS. Keep `${arch}` to avoid x64/arm64 feed collisions on Linux. publish: { @@ -269,6 +272,38 @@ export default { appImage: { artifactName: '${productName}-${version}-linux-${arch}.${ext}', }, + pacman: { + // NOTICE: + // The default depends name `http-parser` and `libappindicator-gtk3`. Arch removed both, so `pacman -U` fails. + // Source: getDefaultDepends in app-builder-lib/out/targets/FpmTarget.js. + // Remove it when electron-builder updates its pacman defaults. + // This list comes from `ldd`, plus the libraries that Electron loads at run time. + depends: [ + 'alsa-lib', + 'at-spi2-core', + 'gtk3', + 'libcups', + 'libnotify', + 'libsecret', + 'libxkbcommon', + 'libxss', + 'libxt', + 'libxtst', + 'mesa', + 'nspr', + 'nss', + 'util-linux-libs', + 'xdg-utils', + ], + }, + rpm: { + // NOTICE: + // Two Electron rpm packages conflict on the same `/usr/lib/.build-id` links, so one of them fails to install. + // rpmbuild creates these links for each ELF file. Electron ships the same Chromium binaries in each app. + // https://github.com/jordansissel/fpm/issues/1503, https://github.com/jgraph/drawio-desktop/issues/259 + // Remove it when fpm stops the creation of build-id links by default. + fpm: ['--rpm-rpmbuild-define', '_build_id_links none'], + }, npmRebuild: false, } satisfies Configuration diff --git a/apps/stage-tamagotchi/scripts/utils.test.ts b/apps/stage-tamagotchi/scripts/utils.test.ts new file mode 100644 index 0000000000..c33352005d --- /dev/null +++ b/apps/stage-tamagotchi/scripts/utils.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import packageJSON from '../package.json' with { type: 'json' } + +import { getFilenames } from './utils' + +const release = { release: true, autoTag: false, tag: ['v0.13.1'] } + +describe('getFilenames', () => { + // electron-builder names the architecture differently for each package format. + // See `getArtifactArchName` in `builder-util/out/arch.js`. + it.each([ + { + target: 'x86_64-unknown-linux-gnu', + expected: { + 'deb': 'AIRI-0.13.1-linux-amd64.deb', + 'rpm': 'AIRI-0.13.1-linux-x86_64.rpm', + 'pacman': 'AIRI-0.13.1-linux-x64.pacman', + 'AppImage': 'AIRI-0.13.1-linux-x86_64.AppImage', + 'flatpak': 'AIRI-0.13.1-linux-x64.flatpak', + 'latest-x64-linux.yml': 'latest-x64-linux.yml', + }, + }, + { + target: 'aarch64-unknown-linux-gnu', + expected: { + 'deb': 'AIRI-0.13.1-linux-arm64.deb', + 'rpm': 'AIRI-0.13.1-linux-aarch64.rpm', + 'pacman': 'AIRI-0.13.1-linux-aarch64.pacman', + 'AppImage': 'AIRI-0.13.1-linux-arm64.AppImage', + 'flatpak': 'AIRI-0.13.1-linux-arm64.flatpak', + 'latest-arm64-linux-arm64.yml': 'latest-arm64-linux-arm64.yml', + }, + }, + ])('names each Linux package of $target as electron-builder does', async ({ target, expected }) => { + const filenames = await getFilenames(target, release) + + expect(Object.fromEntries(filenames.map(filename => [filename.extension, filename.releaseArtifactFilename]))).toEqual(expected) + }) + + it('finds each Linux build output under the version of package.json', async () => { + const filenames = await getFilenames('x86_64-unknown-linux-gnu', release) + + expect(filenames.find(filename => filename.extension === 'AppImage')?.outputFilename) + .toBe(`AIRI-${packageJSON.version}-linux-x86_64.AppImage`) + }) + + it('marks only the update feed as optional', async () => { + const filenames = await getFilenames('x86_64-unknown-linux-gnu', release) + + expect(filenames.filter(filename => filename.optional).map(filename => filename.extension)).toEqual(['latest-x64-linux.yml']) + }) +}) diff --git a/apps/stage-tamagotchi/scripts/utils.ts b/apps/stage-tamagotchi/scripts/utils.ts index 50b95cb9ad..c205a9b883 100644 --- a/apps/stage-tamagotchi/scripts/utils.ts +++ b/apps/stage-tamagotchi/scripts/utils.ts @@ -84,22 +84,24 @@ interface FilenameOutputEntry { optional?: boolean } +/** + * Returns the architecture name that electron-builder writes into the artifact name of `ext`. + * + * The names differ by package format. See `getArtifactArchName` in `builder-util/out/arch.js`. + */ export function mapArchFor( target: string, ext: string, ): string { switch (true) { case target === 'aarch64-unknown-linux-gnu': - if (ext === 'rpm') { + if (ext === 'rpm' || ext === 'pacman') { return 'aarch64' } - if (ext === 'deb') { - return 'arm64' - } return 'arm64' case target === 'x86_64-unknown-linux-gnu': - if (ext === 'rpm') { + if (ext === 'rpm' || ext === 'AppImage') { return 'x86_64' } if (ext === 'deb') { @@ -140,6 +142,51 @@ function getMacZipFilename(productName: string, version: string, target: string) return `${productName}-${version}-${archPrefix}mac.zip` } +/** The Linux package formats that electron-builder builds, in the order of `linux.target`. */ +const LINUX_ELECTRON_BUILDER_EXTENSIONS = ['deb', 'rpm', 'pacman', 'AppImage'] + +function getLinuxFilenames( + target: string, + electronBuilder: Configuration, + productName: string, + beforeVersion: string, + version: string, +): FilenameOutputEntry[] { + const linux = electronBuilder.linux + const artifacts: FilenameOutputEntry[] = [] + if (linux?.artifactName) { + const targets = [linux.target ?? []].flat().map(entry => typeof entry === 'string' ? entry : entry.target) + // Flatpak is built outside electron-builder, but it follows the Linux name template. + const extensions = [...LINUX_ELECTRON_BUILDER_EXTENSIONS.filter(extension => targets.includes(extension)), 'flatpak'] + + for (const extension of extensions) { + artifacts.push({ + target, + extension, + outputFilename: applyTemplateOfArtifactName(linux.artifactName, productName, beforeVersion, mapArchFor(target, extension), extension), + releaseArtifactFilename: applyTemplateOfArtifactName(linux.artifactName, productName, version, mapArchFor(target, extension), extension), + productName, + version, + }) + } + } + + const latestUpdateFilename = getLatestUpdateFilename(target) + if (latestUpdateFilename) { + artifacts.push({ + target, + extension: latestUpdateFilename, + outputFilename: latestUpdateFilename, + releaseArtifactFilename: latestUpdateFilename, + productName, + version, + optional: true, + }) + } + + return artifacts +} + export async function getFilenames(target: string, options: { release: boolean, autoTag: boolean, tag: string[] }): Promise { const electronBuilder = await getElectronBuilderConfig() const version = await getVersion(options) @@ -187,205 +234,8 @@ export async function getFilenames(target: string, options: { release: boolean, }, ] case 'x86_64-unknown-linux-gnu': - { - const artifacts: FilenameOutputEntry[] = [] - if (electronBuilder.linux?.artifactName) { - if ( - (Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('deb')) - || electronBuilder.linux.target === 'deb' - ) { - artifacts.push( - { - target: 'x86_64-unknown-linux-gnu', - extension: 'deb', - outputFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - beforeVersion, - mapArchFor(target, 'deb'), - 'deb', - ), - releaseArtifactFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - version, - mapArchFor(target, 'deb'), - 'deb', - ), - productName, - version, - }, - ) - } - - if ( - (Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('rpm')) - || electronBuilder.linux.target === 'rpm' - ) { - artifacts.push( - { - target: 'x86_64-unknown-linux-gnu', - extension: 'rpm', - outputFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - beforeVersion, - mapArchFor(target, 'rpm'), - 'rpm', - ), - releaseArtifactFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - version, - mapArchFor(target, 'rpm'), - 'rpm', - ), - productName, - version, - }, - ) - } - - // Flatpak artifact (built outside electron-builder, but we follow linux template) - artifacts.push( - { - target: 'x86_64-unknown-linux-gnu', - extension: 'flatpak', - outputFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - beforeVersion, - mapArchFor(target, 'flatpak'), - 'flatpak', - ), - releaseArtifactFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - version, - mapArchFor(target, 'flatpak'), - 'flatpak', - ), - productName, - version, - }, - ) - } - - const latestUpdateFilename = getLatestUpdateFilename(target) - if (latestUpdateFilename) { - artifacts.push({ - target: 'x86_64-unknown-linux-gnu', - extension: latestUpdateFilename, - outputFilename: latestUpdateFilename, - releaseArtifactFilename: latestUpdateFilename, - productName, - version, - optional: true, - }) - } - - return artifacts - } case 'aarch64-unknown-linux-gnu': - { - const artifacts: FilenameOutputEntry[] = [] - if (electronBuilder.linux?.artifactName) { - if ( - (Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('deb')) - || electronBuilder.linux.target === 'deb' - ) { - artifacts.push( - { - target: 'aarch64-unknown-linux-gnu', - extension: 'deb', - outputFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - beforeVersion, - mapArchFor(target, 'deb'), - 'deb', - ), - releaseArtifactFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - version, - mapArchFor(target, 'deb'), - 'deb', - ), - productName, - version, - }, - ) - } - - if ( - (Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('rpm')) - || electronBuilder.linux.target === 'rpm' - ) { - artifacts.push( - { - target: 'aarch64-unknown-linux-gnu', - extension: 'rpm', - outputFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - beforeVersion, - mapArchFor(target, 'rpm'), - 'rpm', - ), - releaseArtifactFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - version, - mapArchFor(target, 'rpm'), - 'rpm', - ), - productName, - version, - }, - ) - } - - // Flatpak artifact (built outside electron-builder, but we follow linux template) - artifacts.push( - { - target: 'aarch64-unknown-linux-gnu', - extension: 'flatpak', - outputFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - beforeVersion, - mapArchFor(target, 'flatpak'), - 'flatpak', - ), - releaseArtifactFilename: applyTemplateOfArtifactName( - electronBuilder.linux.artifactName!, - productName, - version, - mapArchFor(target, 'flatpak'), - 'flatpak', - ), - productName, - version, - }, - ) - } - - const latestUpdateFilename = getLatestUpdateFilename(target) - if (latestUpdateFilename) { - artifacts.push({ - target: 'aarch64-unknown-linux-gnu', - extension: latestUpdateFilename, - outputFilename: latestUpdateFilename, - releaseArtifactFilename: latestUpdateFilename, - productName, - version, - optional: true, - }) - } - - return artifacts - } + return getLinuxFilenames(target, electronBuilder, productName, beforeVersion, version) case 'aarch64-apple-darwin': { const artifacts: FilenameOutputEntry[] = [