chore: prune unused apps and services for the moeka fork
CI / Lint (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Unit Test (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Sync Labels / sync-labels (push) Waiting to run

- Remove stage-tamagotchi, stage-pocket, server, and engine workspaces with their workflows, addons, and docs
- Switch the toolchain from pnpm to bun, replace lockfiles with bun.lock
- Rewrite remaining product references to Moeka
This commit is contained in:
2026-09-14 16:40:49 +07:00
parent 00c6867b7f
commit dc26878386
1981 changed files with 10974 additions and 260677 deletions
+27 -87
View File
@@ -1,4 +1,4 @@
# Start contributing to [Project AIRI](https://github.com/moeru-ai/airi)
# Start contributing to [Project Moeka](https://github.com/cuwayo/moeka)
Hello! Thank you for your interest in contributing to this project. This guide will help you get started.
@@ -6,44 +6,31 @@ Hello! Thank you for your interest in contributing to this project. This guide w
- [Git](https://git-scm.com/downloads)
- [Node.js 23+](https://nodejs.org/en/download/)
- [corepack](https://github.com/nodejs/corepack)
- [pnpm](https://pnpm.io/installation)
- [Bun 1.4+](https://bun.sh/docs/installation)
<details>
<summary>Windows setup</summary>
0. Download [Visual Studio](https://visualstudio.microsoft.com/downloads/) and follow the instructions here: https://rust-lang.github.io/rustup/installation/windows-msvc.html#walkthrough-installing-visual-studio-2022
> Make sure to install Windows SDK and C++ build tools when installing Visual Studio.
1. Open PowerShell
2. Install [`scoop`](https://scoop.sh/)
0. Open PowerShell
1. Install [`scoop`](https://scoop.sh/)
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
```
3. Install `git`, Node.js, `rustup`, `msvc` through `scoop`
2. Install `git` and Node.js through `scoop`
```powershell
scoop install git nodejs rustup
# For Rust dependencies
# Not required if you are not going to develop on either crates or apps/tamagotchi
scoop install main/rust-msvc
# Rust & Windows specific
rustup toolchain install stable-x86_64-pc-windows-msvc
rustup default stable-x86_64-pc-windows-msvc
scoop install git nodejs
```
> https://stackoverflow.com/a/64121601
4. Install `pnpm` through `corepack`
3. Install `bun` through the official script
```powershell
corepack enable
corepack prepare pnpm@latest --activate
powershell -c "irm bun.sh/install.ps1 | iex"
```
</details>
@@ -58,11 +45,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
brew install git node
```
2. Install `pnpm` through `corepack`
2. Install `bun` through `brew`
```shell
corepack enable
corepack prepare pnpm@latest --activate
brew install oven-sh/bun/bun
```
</details>
@@ -73,22 +59,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
0. Open terminal
1. Follow [nodesource/distributions: NodeSource Node.js Binary Distributions](https://github.com/nodesource/distributions?tab=readme-ov-file#table-of-contents) to install `node`
2. Follow [Git](https://git-scm.com/downloads/linux) to install `git`
3. Install `pnpm` through `corepack`
3. Install `bun` through the official script
```shell
corepack enable
corepack prepare pnpm@latest --activate
```
4. If you would love to help to develop the desktop version, you will need those dependencies:
```shell
sudo apt install \
libssl-dev \
libglib2.0-dev \
libgtk-3-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev
curl -fsSL https://bun.sh/install | bash
```
</details>
@@ -121,8 +95,8 @@ Click on the **Fork** button on the top right corner of the [moeru-ai/airi](http
## Clone
```shell
git clone https://github.com/<your-github-username>/airi.git
cd airi
git clone https://github.com/<your-github-username>/moeka.git
cd moeka
```
## Create your working branch
@@ -134,12 +108,7 @@ git checkout -b <your-branch-name>
## Install dependencies
```shell
corepack enable
pnpm install
# For Rust dependencies
# Not required if you are not going to develop on either crates or apps/tamagotchi
cargo fetch
bun install
```
> [!NOTE]
@@ -147,51 +116,22 @@ cargo fetch
> We would recommend to install [@antfu/ni](https://github.com/antfu-collective/ni) to make your script simpler.
>
> ```shell
> corepack enable
> npm i -g @antfu/ni
> ```
>
> Once installed, you can
>
> - use `ni` for `pnpm install`, `npm install` and `yarn install`.
> - use `nr` for `pnpm run`, `npm run` and `yarn run`.
> - use `ni` for `bun install`, `npm install` and `yarn install`.
> - use `nr` for `bun run`, `npm run` and `yarn run`.
>
> You don't need to care about the package manager, `ni` will help you choose the right one.
## Choose the application you want to develop on
### Stage Tamagotchi (Desktop version)
### Stage Web
```shell
pnpm dev:tamagotchi
```
> [!NOTE]
>
> For [@antfu/ni](https://github.com/antfu-collective/ni) users, you can
>
> ```shell
> nr dev:tamagotchi
> ```
> [!NOTE]
>
> The `dev` and `start` scripts run `install-electron` before `electron-vite`.
>
> Electron 42 removed the `postinstall` script. The `electron` package now downloads its binary
> when you first run its `bin` entry. `electron-vite` reads `node_modules/electron/path.txt`
> directly, so it never starts that download. A fresh install therefore fails with
> `Error: Electron uninstall`.
>
> `install-electron` runs the same code as the removed `postinstall` script. It returns
> immediately when the binary is already present.
>
> Remove this step after `electron-vite` supports the lazy download.
### Stage Web (Browser version for [airi.moeru.ai](https://airi.moeru.ai))
```shell
pnpm dev
bun run dev
```
> [!NOTE]
@@ -209,7 +149,7 @@ Browse the live UI component storyboard at [airi.moeru.ai/ui](https://airi.moeru
### Documentation site
```shell
pnpm dev:docs
bun run dev:docs
```
> [!NOTE]
@@ -240,14 +180,14 @@ Edit the credentials in `.env.local`.
Migrate the database
```shell
pnpm -F @proj-airi/telegram-bot db:generate
pnpm -F @proj-airi/telegram-bot db:push
bun run --filter @proj-airi/telegram-bot db:generate
bun run --filter @proj-airi/telegram-bot db:push
```
Run the bot
```shell
pnpm -F @proj-airi/telegram-bot start
bun run --filter @proj-airi/telegram-bot start
```
> [!NOTE]
@@ -275,7 +215,7 @@ Edit the credentials in `.env.local`.
Run the bot
```shell
pnpm -F @proj-airi/discord-bot start
bun run --filter @proj-airi/discord-bot start
```
> [!NOTE]
@@ -305,7 +245,7 @@ Edit the credentials in `.env.local`.
Run the bot
```shell
pnpm -F @proj-airi/minecraft-bot start
bun run --filter @proj-airi/minecraft-bot start
```
> [!NOTE]
@@ -323,13 +263,13 @@ pnpm -F @proj-airi/minecraft-bot start
Please make sure lint (static checkers) and TypeScript compilers are satisfied:
```shell
pnpm lint && pnpm typecheck
bun run lint && bun run typecheck
```
If you are committing images, consider using AVIF format instead of PNG, JPG etc. You can convert existing images to AVIF by running:
```shell
pnpm to-avif <PATH_TO_IMAGE_OR_DIRECTORY1> <PATH_2> <PATH_3> ...
bun run to-avif <PATH_TO_IMAGE_OR_DIRECTORY1> <PATH_2> <PATH_3> ...
```
> [!NOTE]
+2 -2
View File
@@ -30,13 +30,13 @@ Describe the expected behavior or evidence target.
Run the narrowest relevant command first:
```bash
pnpm -F <package-name> exec vitest run <path/to/test>
bun run --filter <package-name> exec vitest run <path/to/test>
```
If runtime contracts changed, also run:
```bash
pnpm -F <package-name> typecheck
bun run --filter <package-name> typecheck
```
## Output required
+2 -2
View File
@@ -1,6 +1,6 @@
# Copilot repository instructions
This is the `moeru-ai/airi` pnpm monorepo. Prefer the smallest safe change and keep every task inside its stated scope.
This is the `cuwayo/moeka` Bun monorepo. Prefer the smallest safe change and keep every task inside its stated scope.
## General rules
@@ -14,7 +14,7 @@ This is the `moeru-ai/airi` pnpm monorepo. Prefer the smallest safe change and k
## Validation
- Use package-scoped pnpm commands where possible.
- Use package-scoped Bun commands where possible.
- Run the narrowest relevant test first.
- Run typecheck if runtime contracts or exported types changed.
- Run affected package tests if shared logic changed.
+1 -7
View File
@@ -1,9 +1,3 @@
- name: 'apps/stage-pocket'
description: 'Mobile App: iOS / Android'
color: 'b8d5ff'
- name: 'apps/stage-tamagotchi'
description: 'Desktop App: Windows & macOS & Linux'
color: 'a1f7ef'
- name: 'apps/stage-web'
description: 'Web App: PWA & Browser'
color: 'abedff'
@@ -137,7 +131,7 @@
description: 'Scope related to providers we support'
color: '4468eb'
- name: 'scope/server-api'
description: 'Scope related to the server api we maintained, the public service of AIRI'
description: 'Scope related to the server api we maintained, the public service of Moeka'
color: '4468eb'
- name: 'scope/ui'
description: 'Scope related to UI/UX, or interface improve, perf, and bugs'
+3 -3
View File
@@ -123,16 +123,16 @@ write_expected_github_asset_metadata() {
gh api "repos/${GITHUB_REPOSITORY_NAME}/releases/tags/${encoded_tag}" \
> "${release_assets_json}"
jq -r '.assets[].name | select(test("^(AIRI-.*|latest-.*\\.yml)$"))' "${release_assets_json}" \
jq -r '.assets[].name | select(test("^(Moeka-.*|latest-.*\\.yml)$"))' "${release_assets_json}" \
| sort \
> "${WORK_DIR}/expected-asset-names.txt"
jq -r '.assets[] | select(.name | test("^(AIRI-.*|latest-.*\\.yml)$")) | [.name, (.digest // "")] | @tsv' "${release_assets_json}" \
jq -r '.assets[] | select(.name | test("^(Moeka-.*|latest-.*\\.yml)$")) | [.name, (.digest // "")] | @tsv' "${release_assets_json}" \
| sort \
> "${WORK_DIR}/expected-asset-digests.tsv"
if [[ ! -s "${WORK_DIR}/expected-asset-names.txt" ]]; then
echo "::error::No GitHub release assets matched AIRI-* or latest-*.yml."
echo "::error::No GitHub release assets matched Moeka-* or latest-*.yml."
exit 1
fi
+10 -10
View File
@@ -16,20 +16,20 @@ jobs:
- uses: actions/checkout@v6
# NOTICE:
# Pin Node.js because 26.8.0 reports 26.8.0-alpha.0.0.0.
# sharp 0.29.3 rejects this prerelease version during pnpm install.
# sharp 0.29.3 rejects this prerelease version during bun install.
# Source: https://github.com/nodejs/node/blob/v26.8.0/src/node_version.h
# Remove this pin when Node.js 26 reports a stable version.
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
- run: pnpm install
- run: pnpm prune && pnpm dedupe
- run: pnpm run lint:fix
- run: docker run -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest --fix
working-directory: ./apps/stage-pocket
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- run: bun install
- run: bun prune && bun dedupe
- run: bun run lint:fix
# - name: AutoCorrect
# uses: huacnlee/autocorrect-action@main
+33 -84
View File
@@ -18,33 +18,19 @@ jobs:
- uses: actions/checkout@v6
# NOTICE:
# Pin Node.js because 26.8.0 reports 26.8.0-alpha.0.0.0.
# sharp 0.29.3 rejects this prerelease version during pnpm install.
# sharp 0.29.3 rejects this prerelease version during bun install.
# Source: https://github.com/nodejs/node/blob/v26.8.0/src/node_version.h
# Remove this pin when Node.js 26 reports a stable version.
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
# Setup .NET for Godot C# linting
- name: Setup .NET
uses: actions/setup-dotnet@v4
- uses: oven-sh/setup-bun@v2
with:
dotnet-version: '10.0.x'
bun-version: 1.4.2
# Lint Godot C# code
- name: Lint Godot C#
working-directory: ./engines/stage-tamagotchi-godot
run: dotnet format stage-tamagotchi-godot.slnx --verify-no-changes
- run: docker run -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest
working-directory: ./apps/stage-pocket
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: docker run -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest
working-directory: ./apps/stage-pocket
- run: bun install --frozen-lockfile
- run: bun run lint
build-test:
name: Build Test (${{ matrix.app_name }})
@@ -53,19 +39,13 @@ jobs:
matrix:
include:
- app_name: stage-web
command: pnpm -F @proj-airi/stage-web run build && pnpm -F @proj-airi/docs run build:base && mv ./docs/.vitepress/dist ./apps/stage-web/dist/docs && pnpm -F @proj-airi/stage-ui run story:build && mv ./packages/stage-ui/.histoire/dist ./apps/stage-web/dist/ui
- app_name: stage-tamagotchi
command: pnpm -F @proj-airi/stage-tamagotchi run build
- app_name: stage-tamagotchi-godot
command: cd engines/stage-tamagotchi-godot && dotnet restore && dotnet build -c ExportRelease
command: bun run --filter @proj-airi/stage-web build && bun run --filter @proj-airi/docs build:base && mv ./docs/.vitepress/dist ./apps/stage-web/dist/docs && bun run --filter @proj-airi/stage-ui story:build && mv ./packages/stage-ui/.histoire/dist ./apps/stage-web/dist/ui
- app_name: ui-transitions
command: pnpm -F @proj-airi/ui-transitions run play:build
command: bun run --filter @proj-airi/ui-transitions play:build
- app_name: ui-loading-screens
command: pnpm -F @proj-airi/ui-loading-screens run play:build
command: bun run --filter @proj-airi/ui-loading-screens play:build
runs-on: ubuntu-latest
steps:
@@ -80,40 +60,20 @@ jobs:
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
# Setup .NET and Godot
- name: Setup .NET
if: matrix.app_name == 'stage-tamagotchi-godot'
uses: actions/setup-dotnet@v4
- uses: oven-sh/setup-bun@v2
with:
dotnet-version: '10.0.x'
bun-version: 1.4.2
- name: Setup Godot
if: matrix.app_name == 'stage-tamagotchi-godot'
uses: chickensoft-games/setup-godot@v2
with:
version: 4.7.1
use-dotnet: true
include-templates: true
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- run: bun install --frozen-lockfile
- run: bun run build:packages
- name: Build App
run: ${{ matrix.command }}
- name: Export Godot Linux sidecar
if: matrix.app_name == 'stage-tamagotchi-godot'
working-directory: ./engines/stage-tamagotchi-godot
run: |
mkdir -p out/linux
godot --headless --export-release "Linux x64" out/linux/godot-stage
unit-test:
name: Unit Test
runs-on: ubuntu-latest
@@ -129,34 +89,21 @@ jobs:
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
# # Setup .NET and Godot
# - name: Setup .NET
# if: matrix.app_name == 'stage-tamagotchi-godot'
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: '10.0.x'
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
# - name: Setup Godot
# if: matrix.app_name == 'stage-tamagotchi-godot'
# uses: chickensoft-games/setup-godot@v2
# with:
# version: 4.7.1
# use-dotnet: true
# include-templates: true
- run: pnpm install --frozen-lockfile
- run: bun install --frozen-lockfile
- name: Setup Playwright
run: npx playwright install chromium
- run: pnpm run build:packages
- run: pnpm run test:run
- run: bun run build:packages
- run: bun run test:run
env:
NODE_OPTIONS: --no-experimental-webstorage
@@ -168,14 +115,16 @@ jobs:
- uses: actions/checkout@v6
# Node.js
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun run typecheck
check-provenance:
name: Check Provenance
@@ -188,7 +137,7 @@ jobs:
id: check
with:
fail-on-provenance-change: true # optional, default: false
# lockfile: pnpm-lock.yaml # optional
# lockfile: bun.lock # optional
# base-ref: origin/main # optional, default: origin/main
# fail-on-downgrade: true # optional, default: true
- name: Print result
@@ -1,60 +0,0 @@
name: Cloudflare Pages (Auth UI)
on:
push:
branches:
- 'main'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
name: Deploy - ui-server-auth
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
environment:
name: Auth Production
url: https://accounts.airi.build/ui/
steps:
- uses: actions/checkout@v6
# Turborepo
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
# NOTICE:
#
# Keep the wrangler setup consistent with the stage-web deployment
# workflow. cloudflare/wrangler-action expects a usable wrangler binary
# when running repository-local pnpm workspaces.
- run: pnpm i -g wrangler@4
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Build ui-server-auth
run: pnpm -F @proj-airi/ui-server-auth run build
env:
VITE_ENABLE_ANALYTICS: 'true'
VITE_SERVER_URL: 'https://api.airi.build'
- uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ./apps/ui-server-auth/dist --project-name=airi-accounts-ui --branch=main
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
@@ -1,132 +0,0 @@
name: Cloudflare Workers (server-dev)
on:
push:
branches:
- 'server-dev'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-stage-web:
name: Deploy - stage-web (server-dev)
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
environment:
name: ServerDev
url: ${{ steps.preview-urls.outputs.preview_url }}
steps:
- uses: actions/checkout@v6
# Turborepo
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
- run: pnpm i -g wrangler@4
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Build stage-web
run: |
pnpm -F @proj-airi/stage-web run build
pnpm -F @proj-airi/docs run build:base
mv ./docs/.vitepress/dist ./apps/stage-web/dist/docs
cp ./apps/stage-web/dist/docs/sitemap.xml ./apps/stage-web/dist/sitemap.xml
pnpm -F @proj-airi/stage-ui run story:build
mv ./packages/stage-ui/.histoire/dist ./apps/stage-web/dist/ui
env:
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_REGION: ${{ secrets.S3_REGION }}
WARP_DRIVE_PUBLIC_BASE: ${{ secrets.WARP_DRIVE_PUBLIC_BASE }}
VITE_SERVER_URL: 'https://airi-server-dev.up.railway.app'
- name: Wrangler Upload
id: wrangler-versions-upload
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions upload -c ./apps/stage-web/wrangler.toml --preview-alias server-dev --message "GitHub Actions uploaded server-dev branch preview" --tag v0.0.1-server-dev
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare preview URL
id: preview-urls
run: |
deployment_url="${{ steps.wrangler-versions-upload.outputs.deployment-url }}"
if [ -z "$deployment_url" ]; then
echo "Deployment URL from wrangler upload is empty." >&2
exit 1
fi
host_without_scheme="${deployment_url#https://}"
host_without_scheme="${host_without_scheme#http://}"
preview_url="https://server-dev-${host_without_scheme#*-}"
echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT"
echo "Preview URL: $preview_url"
deploy-auth-ui:
name: Deploy - ui-server-auth (server-dev)
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
environment:
name: AuthServerDev
url: https://server-dev.airi-server-auth.pages.dev/ui/
steps:
- uses: actions/checkout@v6
# Turborepo
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-auth-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-auth-
${{ runner.os }}-turbo-
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
- run: pnpm i -g wrangler@4
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Build ui-server-auth
run: pnpm -F @proj-airi/ui-server-auth run build
env:
VITE_SERVER_URL: 'https://airi-server-dev.up.railway.app'
- name: Wrangler Pages Deploy
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ./apps/ui-server-auth/dist --project-name=airi-accounts-ui --branch=server-dev
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Print preview URL
run: |
echo "Preview URL: https://server-dev.airi-server-auth.pages.dev/ui/"
@@ -1,260 +0,0 @@
name: Cloudflare Workers (Preview) Deploy
on:
workflow_run:
workflows:
- Cloudflare Workers (Preview) Prepare
types:
- completed
jobs:
ask-for-approval:
name: Ask for approval to deploy - ${{ matrix.app_name }}
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
strategy:
matrix:
include:
- app_name: stage-web
steps:
- name: Download artifact - metadata
uses: dawidd6/action-download-artifact@v19
with:
workflow_conclusion: success
run_id: ${{ github.event.workflow_run.id }}
name: preview-meta
path: preview-meta
allow_forks: true
- name: Parse metadata
id: meta
run: |
node --input-type=module <<'NODE'
import { appendFile, readFile } from 'node:fs/promises'
const metadata = JSON.parse(await readFile('preview-meta/preview-meta.json', 'utf8'))
if (
!/^\d+$/.test(metadata.prNumber)
|| !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(metadata.repositoryFullName)
|| !/^[a-f0-9]{40}$/i.test(metadata.headSha)
) {
throw new Error('Preview metadata has an invalid shape')
}
await appendFile(process.env.GITHUB_OUTPUT, [
`PR_NUM=${metadata.prNumber}`,
`REPO_FULL_NAME=${metadata.repositoryFullName}`,
`HEAD_SHA=${metadata.headSha}`,
'',
].join('\n'))
NODE
- name: Find Comment
if: ${{ always() }}
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ steps.meta.outputs.PR_NUM }}
comment-author: 'github-actions[bot]'
body-includes: to Cloudflare Workers (Preview) for *${{ matrix.app_name }}*
- name: Comment on require approval
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ steps.meta.outputs.PR_NUM }}
edit-mode: replace
body: |
## ⏳ Approval required for deploying to Cloudflare Workers (Preview) for *${{ matrix.app_name }}*.
| Name | Link |
|:------------------------|:---------------------------------------------------------------------------------------|
| 🔭 Waiting for approval | For maintainers, approve [here](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
Hey, maintainers, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏
on-success:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: Deploy - ${{ matrix.app_name }}
runs-on: ubuntu-latest
environment: Preview
permissions:
contents: read
pull-requests: write
strategy:
matrix:
include:
- app_name: stage-web
wrangler_config_path: ./apps/stage-web/wrangler.toml
build_directory: ./apps/stage-web/dist
build_command: |
pnpm -F @proj-airi/stage-web run build
pnpm -F @proj-airi/docs run build:base
mv ./docs/.vitepress/dist ./apps/stage-web/dist/docs
cp ./apps/stage-web/dist/docs/sitemap.xml ./apps/stage-web/dist/sitemap.xml
pnpm -F @proj-airi/stage-ui run story:build
mv ./packages/stage-ui/.histoire/dist ./apps/stage-web/dist/ui
steps:
- name: Download artifact - metadata
uses: dawidd6/action-download-artifact@v19
with:
workflow_conclusion: success
run_id: ${{ github.event.workflow_run.id }}
name: preview-meta
path: preview-meta
allow_forks: true
- name: Parse metadata
id: meta
run: |
node --input-type=module <<'NODE'
import { appendFile, readFile } from 'node:fs/promises'
const metadata = JSON.parse(await readFile('preview-meta/preview-meta.json', 'utf8'))
if (
!/^\d+$/.test(metadata.prNumber)
|| !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(metadata.repositoryFullName)
|| !/^[a-f0-9]{40}$/i.test(metadata.headSha)
) {
throw new Error('Preview metadata has an invalid shape')
}
await appendFile(process.env.GITHUB_OUTPUT, [
`PR_NUM=${metadata.prNumber}`,
`REPO_FULL_NAME=${metadata.repositoryFullName}`,
`HEAD_SHA=${metadata.headSha}`,
'',
].join('\n'))
NODE
- name: Checkout repository
uses: actions/checkout@v6
with:
repository: ${{ steps.meta.outputs.REPO_FULL_NAME }}
ref: ${{ steps.meta.outputs.HEAD_SHA }}
fetch-depth: 1
persist-credentials: false
# https://gh.io/securely-using-pull_request_target
allow-unsafe-pr-checkout: true
- name: Setup pnpm
uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
# NOTICE:
#
# Here installing wrangler to global is required, or otherwise:
# ERR_PNPM_ADDING_TO_ROOT Running this command will add the dependency to the workspace root...
# error occurs.
#
# Since https://github.com/cloudflare/wrangler-action/pull/339#issuecomment-2667622947 rejected the -g support
# by saying un-reasonable 'I'm not sure if it's common ... to install packages to the global scope, ... might be introducing some unintended side effects.'
#
# Clearly I think installing with `<packageManager> install` brings more unintended side effects...
#
# As suggested by https://github.com/cloudflare/wrangler-action/issues/181#issuecomment-2127990708, we should pre-install
# with our package manager and then use it in the action.
- run: pnpm i -g wrangler@4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm run build:packages
- name: Build ${{ matrix.app_name }}
id: build
run: ${{ matrix.build_command }}
continue-on-error: true
env:
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_REGION: ${{ secrets.S3_REGION }}
WARP_DRIVE_PUBLIC_BASE: ${{ secrets.WARP_DRIVE_PUBLIC_BASE }}
STAGE_WEB_WARP_DRIVE_PREFIX: proj-airi/stage-web/pr-${{ steps.meta.outputs.PR_NUM }}/
STAGE_UI_WARP_DRIVE_PREFIX: proj-airi/stage-ui/pr-${{ steps.meta.outputs.PR_NUM }}/
- name: Find Comment
if: ${{ always() }}
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ steps.meta.outputs.PR_NUM }}
comment-author: 'github-actions[bot]'
body-includes: to Cloudflare Workers (Preview) for *${{ matrix.app_name }}*
- name: Comment on build failure
if: ${{ steps.build.outcome != 'success' }}
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ steps.meta.outputs.PR_NUM }}
edit-mode: replace
body: |
## ❌ Deploy to Cloudflare Workers (Preview) for *${{ matrix.app_name }}* failed.
| Name | Link |
|:---------------------|:---------------------------------------------------------------------------------------|
| 🔍 Latest deploy log | https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} |
- name: Fail if build failed
if: ${{ steps.build.outcome != 'success' }}
run: |
echo "Build step failed; marking workflow as failed."
exit 1
- name: Wrangler Upload
if: ${{ steps.build.outcome == 'success' }}
id: wrangler-versions-upload
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions upload -c ${{ matrix.wrangler_config_path }} --preview-alias pr-${{ steps.meta.outputs.PR_NUM }} --message "GitHub Actions uploaded preview for Pull Request ${{ steps.meta.outputs.PR_NUM }}" --tag v0.0.1-pr.${{ steps.meta.outputs.PR_NUM }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare preview URLs
if: ${{ steps.build.outcome == 'success' }}
id: preview-urls
run: |
deployment_url="${{ steps.wrangler-versions-upload.outputs.deployment-url }}"
if [ -z "$deployment_url" ]; then
echo "Deployment URL from wrangler upload is empty." >&2
exit 1
fi
host_without_scheme="${deployment_url#https://}"
host_without_scheme="${host_without_scheme#http://}"
preview_alias_url="https://pr-${{ steps.meta.outputs.PR_NUM }}-${host_without_scheme#*-}"
echo "deploy_preview_url=$deployment_url" >> "$GITHUB_OUTPUT"
echo "pull_request_preview_url=$preview_alias_url" >> "$GITHUB_OUTPUT"
- name: Create or update comment
if: ${{ steps.build.outcome == 'success' }}
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ steps.meta.outputs.PR_NUM }}
edit-mode: replace
body: |
## ✅ Deploy to Cloudflare Workers (Preview) for *${{ matrix.app_name }}* ready!
| Name | Link |
|:------------------------|:--------------------------------------------------------------------------------------------|
| 🔍 Latest deploy log | https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| 😎 Deploy Preview | ${{ steps.preview-urls.outputs.deploy_preview_url }} |
| 🚀 Pull Request Preview | ${{ steps.preview-urls.outputs.pull_request_preview_url }} |
@@ -1,37 +0,0 @@
name: Cloudflare Workers (Preview) Prepare
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
prepare:
name: Prepare preview sources
runs-on: ubuntu-latest
steps:
- name: Persist checkout metadata
env:
PR_NUM: ${{ github.event.number }}
REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
node --input-type=module <<'NODE'
import { writeFile } from 'node:fs/promises'
await writeFile('preview-meta.json', JSON.stringify({
prNumber: process.env.PR_NUM,
repositoryFullName: process.env.REPO_FULL_NAME,
headSha: process.env.HEAD_SHA,
}))
NODE
- name: Upload metadata artifact
uses: actions/upload-artifact@v7
with:
name: preview-meta
path: ./preview-meta.json
overwrite: true
@@ -1,81 +0,0 @@
name: Cloudflare Workers
on:
push:
branches:
- 'main'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
name: Deploy - stage-web
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
environment:
name: Production
url: https://airi.moeru.ai/docs/
steps:
- uses: actions/checkout@v6
# Turborepo
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
# NOTICE:
#
# Here installing wrangler to global is required, or otherwise:
# ERR_PNPM_ADDING_TO_ROOT Running this command will add the dependency to the workspace root...
# error occurs.
#
# Since https://github.com/cloudflare/wrangler-action/pull/339#issuecomment-2667622947 rejected the -g support
# by saying un-reasonable 'I'm not sure if it's common ... to install packages to the global scope, ... might be introducing some unintended side effects.'
#
# Clearly I think installing with `<packageManager> install` brings more unintended side effects...
#
# As suggested by https://github.com/cloudflare/wrangler-action/issues/181#issuecomment-2127990708, we should pre-install
# with our package manager and then use it in the action.
- run: pnpm i -g wrangler@4
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Build stage-web
run: |
pnpm -F @proj-airi/stage-web run build
pnpm -F @proj-airi/docs run build:base
mv ./docs/.vitepress/dist ./apps/stage-web/dist/docs
cp ./apps/stage-web/dist/docs/sitemap.xml ./apps/stage-web/dist/sitemap.xml
pnpm -F @proj-airi/stage-ui run story:build
mv ./packages/stage-ui/.histoire/dist ./apps/stage-web/dist/ui
env:
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_REGION: ${{ secrets.S3_REGION }}
VITE_ENABLE_ANALYTICS: 'true'
VITE_SERVER_URL: 'https://api.airi.build'
WARP_DRIVE_PUBLIC_BASE: ${{ secrets.WARP_DRIVE_PUBLIC_BASE }}
- uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy -c ./apps/stage-web/wrangler.toml
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
@@ -22,14 +22,16 @@ jobs:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_USERNAME: ${{ secrets.HF_USERNAME }}
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun run build
env:
TARGET_HUGGINGFACE_SPACE: 'true'
VITE_APP_TARGET_HUGGINGFACE_SPACE: 'true'
+4 -4
View File
@@ -351,15 +351,15 @@ jobs:
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7cd577a1928d35f4_EOF'
{"add_labels":{"allowed":["bug","feature","pending triage","apps/stage-pocket","apps/stage-tamagotchi","apps/stage-web","env/os-all","env/os-linux","env/os-macos","env/os-windows","priority/general","priority/nice-to-have","priority/urgent","scope/agent","scope/audio-input","scope/audio-output","scope/avatar","scope/avatar/live2d","scope/avatar/vrm","scope/documentation","scope/engineering","scope/extension","scope/game-playing-ai","scope/i18n","scope/providers","scope/server-api","scope/ui"],"max":12,"target":"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["bug","feature","pending triage","apps/stage-pocket","apps/stage-tamagotchi","apps/stage-web","env/os-all","env/os-linux","env/os-macos","env/os-windows","priority/general","priority/nice-to-have","priority/urgent","scope/agent","scope/audio-input","scope/audio-output","scope/avatar","scope/avatar/live2d","scope/avatar/vrm","scope/documentation","scope/engineering","scope/extension","scope/game-playing-ai","scope/i18n","scope/providers","scope/server-api","scope/ui"],"max":24,"target":"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}"},"report_incomplete":{}}
{"add_labels":{"allowed":["bug","feature","pending triage","apps/stage-web","env/os-all","env/os-linux","env/os-macos","env/os-windows","priority/general","priority/nice-to-have","priority/urgent","scope/agent","scope/audio-input","scope/audio-output","scope/avatar","scope/avatar/live2d","scope/avatar/vrm","scope/documentation","scope/engineering","scope/extension","scope/game-playing-ai","scope/i18n","scope/providers","scope/server-api","scope/ui"],"max":12,"target":"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["bug","feature","pending triage","apps/stage-web","env/os-all","env/os-linux","env/os-macos","env/os-windows","priority/general","priority/nice-to-have","priority/urgent","scope/agent","scope/audio-input","scope/audio-output","scope/avatar","scope/avatar/live2d","scope/avatar/vrm","scope/documentation","scope/engineering","scope/extension","scope/game-playing-ai","scope/i18n","scope/providers","scope/server-api","scope/ui"],"max":24,"target":"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_7cd577a1928d35f4_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
{
"description_suffixes": {
"add_labels": " CONSTRAINTS: Maximum 12 label(s) can be added. Only these labels are allowed: [\"bug\" \"feature\" \"pending triage\" \"apps/stage-pocket\" \"apps/stage-tamagotchi\" \"apps/stage-web\" \"env/os-all\" \"env/os-linux\" \"env/os-macos\" \"env/os-windows\" \"priority/general\" \"priority/nice-to-have\" \"priority/urgent\" \"scope/agent\" \"scope/audio-input\" \"scope/audio-output\" \"scope/avatar\" \"scope/avatar/live2d\" \"scope/avatar/vrm\" \"scope/documentation\" \"scope/engineering\" \"scope/extension\" \"scope/game-playing-ai\" \"scope/i18n\" \"scope/providers\" \"scope/server-api\" \"scope/ui\"]. Target: ${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}.",
"remove_labels": " CONSTRAINTS: Maximum 24 label(s) can be removed. Only these labels can be removed: [bug feature pending triage apps/stage-pocket apps/stage-tamagotchi apps/stage-web env/os-all env/os-linux env/os-macos env/os-windows priority/general priority/nice-to-have priority/urgent scope/agent scope/audio-input scope/audio-output scope/avatar scope/avatar/live2d scope/avatar/vrm scope/documentation scope/engineering scope/extension scope/game-playing-ai scope/i18n scope/providers scope/server-api scope/ui]. Target: ${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}."
"add_labels": " CONSTRAINTS: Maximum 12 label(s) can be added. Only these labels are allowed: [\"bug\" \"feature\" \"pending triage\" \"apps/stage-web\" \"env/os-all\" \"env/os-linux\" \"env/os-macos\" \"env/os-windows\" \"priority/general\" \"priority/nice-to-have\" \"priority/urgent\" \"scope/agent\" \"scope/audio-input\" \"scope/audio-output\" \"scope/avatar\" \"scope/avatar/live2d\" \"scope/avatar/vrm\" \"scope/documentation\" \"scope/engineering\" \"scope/extension\" \"scope/game-playing-ai\" \"scope/i18n\" \"scope/providers\" \"scope/server-api\" \"scope/ui\"]. Target: ${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}.",
"remove_labels": " CONSTRAINTS: Maximum 24 label(s) can be removed. Only these labels can be removed: [bug feature pending triage apps/stage-web env/os-all env/os-linux env/os-macos env/os-windows priority/general priority/nice-to-have priority/urgent scope/agent scope/audio-input scope/audio-output scope/avatar scope/avatar/live2d scope/avatar/vrm scope/documentation scope/engineering scope/extension scope/game-playing-ai scope/i18n scope/providers scope/server-api scope/ui]. Target: ${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}."
},
"repo_params": {},
"dynamic_tools": []
@@ -1112,7 +1112,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"bug\",\"feature\",\"pending triage\",\"apps/stage-pocket\",\"apps/stage-tamagotchi\",\"apps/stage-web\",\"env/os-all\",\"env/os-linux\",\"env/os-macos\",\"env/os-windows\",\"priority/general\",\"priority/nice-to-have\",\"priority/urgent\",\"scope/agent\",\"scope/audio-input\",\"scope/audio-output\",\"scope/avatar\",\"scope/avatar/live2d\",\"scope/avatar/vrm\",\"scope/documentation\",\"scope/engineering\",\"scope/extension\",\"scope/game-playing-ai\",\"scope/i18n\",\"scope/providers\",\"scope/server-api\",\"scope/ui\"],\"max\":12,\"target\":\"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"bug\",\"feature\",\"pending triage\",\"apps/stage-pocket\",\"apps/stage-tamagotchi\",\"apps/stage-web\",\"env/os-all\",\"env/os-linux\",\"env/os-macos\",\"env/os-windows\",\"priority/general\",\"priority/nice-to-have\",\"priority/urgent\",\"scope/agent\",\"scope/audio-input\",\"scope/audio-output\",\"scope/avatar\",\"scope/avatar/live2d\",\"scope/avatar/vrm\",\"scope/documentation\",\"scope/engineering\",\"scope/extension\",\"scope/game-playing-ai\",\"scope/i18n\",\"scope/providers\",\"scope/server-api\",\"scope/ui\"],\"max\":24,\"target\":\"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"bug\",\"feature\",\"pending triage\",\"apps/stage-web\",\"env/os-all\",\"env/os-linux\",\"env/os-macos\",\"env/os-windows\",\"priority/general\",\"priority/nice-to-have\",\"priority/urgent\",\"scope/agent\",\"scope/audio-input\",\"scope/audio-output\",\"scope/avatar\",\"scope/avatar/live2d\",\"scope/avatar/vrm\",\"scope/documentation\",\"scope/engineering\",\"scope/extension\",\"scope/game-playing-ai\",\"scope/i18n\",\"scope/providers\",\"scope/server-api\",\"scope/ui\"],\"max\":12,\"target\":\"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"bug\",\"feature\",\"pending triage\",\"apps/stage-web\",\"env/os-all\",\"env/os-linux\",\"env/os-macos\",\"env/os-windows\",\"priority/general\",\"priority/nice-to-have\",\"priority/urgent\",\"scope/agent\",\"scope/audio-input\",\"scope/audio-output\",\"scope/avatar\",\"scope/avatar/live2d\",\"scope/avatar/vrm\",\"scope/documentation\",\"scope/engineering\",\"scope/extension\",\"scope/game-playing-ai\",\"scope/i18n\",\"scope/providers\",\"scope/server-api\",\"scope/ui\"],\"max\":24,\"target\":\"${{ github.event.pull_request.number || github.event.inputs.pull_request_number }}\"},\"report_incomplete\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
+2 -8
View File
@@ -33,8 +33,6 @@ safe-outputs:
- bug
- feature
- pending triage
- apps/stage-pocket
- apps/stage-tamagotchi
- apps/stage-web
- env/os-all
- env/os-linux
@@ -64,8 +62,6 @@ safe-outputs:
- bug
- feature
- pending triage
- apps/stage-pocket
- apps/stage-tamagotchi
- apps/stage-web
- env/os-all
- env/os-linux
@@ -105,7 +101,7 @@ The target pull request is:
Managed labels:
- Type: `bug`, `feature`
- App surface: `apps/stage-pocket`, `apps/stage-tamagotchi`, `apps/stage-web`
- App surface: `apps/stage-web`
- Environment: `env/os-all`, `env/os-linux`, `env/os-macos`, `env/os-windows`
- Scope: `scope/agent`, `scope/audio-input`, `scope/audio-output`, `scope/avatar`, `scope/avatar/live2d`, `scope/avatar/vrm`, `scope/documentation`, `scope/engineering`, `scope/extension`, `scope/game-playing-ai`, `scope/i18n`, `scope/providers`, `scope/server-api`, `scope/ui`
- Priority: `priority/general`, `priority/nice-to-have`, `priority/urgent`
@@ -160,9 +156,7 @@ Do not use web search. Do not use bash to modify files, create branches, post co
### App labels
- Apply `apps/stage-web` when files under `apps/stage-web/` change, or the PR text explicitly says the change is for the web app or PWA/browser surface.
- Apply `apps/stage-tamagotchi` when files under `apps/stage-tamagotchi/` change, or the PR text explicitly says desktop/Electron/Windows/macOS/Linux app.
- Apply `apps/stage-pocket` when files under `apps/stage-pocket/` change, or the PR text explicitly says mobile/iOS/Android app.
- Multiple app labels are allowed when the evidence is explicit.
- Only `apps/stage-web` exists in this fork. Never apply a desktop or mobile app label.
### Environment labels
+10 -8
View File
@@ -21,11 +21,13 @@ jobs:
with:
fetch-depth: 0
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- name: Configure npm registry
uses: actions/setup-node@v6
@@ -39,9 +41,9 @@ jobs:
# https://github.com/e18e/ecosystem-issues/issues/201
- run: npm install -g npm@latest
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- run: bun install --frozen-lockfile
- run: bun run build:packages
- run: pnpm publish -r --access public --no-git-checks --dry-run
- run: bun run publish:packages -- --dry-run
- run: pnpm publish -r --access public --no-git-checks
- run: bun run publish:packages
@@ -1,136 +0,0 @@
name: Release Pocket (Android)
permissions:
contents: write
actions: read
on:
release:
types:
- prereleased
workflow_dispatch:
inputs:
build_only:
description: Build only (no upload to release)
required: false
default: false
type: boolean
tag:
description: Specific tag for the release upload
required: false
type: string
schedule:
- cron: '0 1 * * *'
jobs:
build:
name: Build Android APK
runs-on: ubuntu-latest
env:
PRODUCT_NAME: 'AIRI'
# Every current trigger path produces an installable APK artifact, so ship the analytics-enabled build consistently.
VITE_ENABLE_ANALYTICS: 'true'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
- name: Install Android build tools
run: |
sdkmanager "build-tools;36.1.0"
echo "${ANDROID_SDK_ROOT:-$ANDROID_HOME}/build-tools/36.1.0" >> "$GITHUB_PATH"
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Validate workflow dispatch release tag
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.tag == '' }}
run: |
echo "workflow_dispatch release uploads require the tag input"
exit 1
- name: Build web assets
run: pnpm -F @proj-airi/stage-pocket run build
- name: Capacitor sync
run: pnpm -F @proj-airi/stage-pocket exec cap sync android
- name: Extract android keystore
run: echo "${{ secrets.CAPACITOR_ANDROID_KEYSTORE_BASE64 }}" | base64 -d > ${{ github.workspace }}/apps/stage-pocket/airi-pocket.jks
- name: Build APK
working-directory: apps/stage-pocket/android
run: pnpm -F @proj-airi/stage-pocket exec cap build android
env:
CAPACITOR_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/apps/stage-pocket/airi-pocket.jks
CAPACITOR_ANDROID_KEYSTORE_ALIAS: ${{ secrets.CAPACITOR_ANDROID_KEYSTORE_ALIAS }}
CAPACITOR_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.CAPACITOR_ANDROID_KEYSTORE_PASSWORD }}
CAPACITOR_ANDROID_KEYSTORE_ALIAS_PASSWORD: ${{ secrets.CAPACITOR_ANDROID_KEYSTORE_ALIAS_PASSWORD }}
- name: Rename APK for distribution
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name || inputs.tag }}
run: |
VERSION="${RELEASE_TAG_NAME#v}"
if [ -z "$VERSION" ]; then
VERSION="$(sed -n 's/^AIRI_VERSION_NAME=//p' apps/stage-pocket/android/app-version.properties)"
fi
APK_NAME="${PRODUCT_NAME}-${VERSION}-android.apk"
APK_SOURCE="apps/stage-pocket/android/app/build/outputs/apk/release/app-release-signed.apk"
APK_BUNDLE_DIR="apps/stage-pocket/android/bundle"
mkdir -p "$APK_BUNDLE_DIR"
mv "$APK_SOURCE" "$APK_BUNDLE_DIR/$APK_NAME"
echo "APK_NAME=$APK_NAME" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_NAME }}
path: apps/stage-pocket/android/bundle/${{ env.APK_NAME }}
if-no-files-found: error
- name: Upload to GitHub Releases
id: upload-github-release
if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.build_only) }}
uses: softprops/action-gh-release@v2
with:
files: apps/stage-pocket/android/bundle/${{ env.APK_NAME }}
append_body: true
tag_name: ${{ github.event.release.tag_name || inputs.tag }}
- name: Setup butler
uses: remarkablegames/setup-butler@v2
# https://itch.io/docs/butler/pushing.html
- name: Upload Android to itch.io
if: ${{ github.event_name == 'release' }}
run: butler push apps/stage-pocket/android/bundle/${{ env.APK_NAME }} nekomeowww/airi:android --userversion ${{ env.VERSION }}
env:
BUTLER_API_KEY: ${{ secrets.GAME_PUBLISHING_ITCHIO }}
- name: Publish release assets to GitCode
if: ${{ always() && github.repository == 'moeru-ai/airi' && steps.upload-github-release.outcome == 'success' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.build_only)) }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
GITCODE_OWNER: ${{ secrets.GITCODE_OWNER }}
GITCODE_REPO: ${{ secrets.GITCODE_REPO }}
GITCODE_RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: bash .github/scripts/publish-gitcode-release.sh
-180
View File
@@ -1,180 +0,0 @@
name: Release Pocket (iOS)
permissions:
contents: write
actions: read
on:
release:
types:
- prereleased
workflow_dispatch:
inputs:
testflight:
description: Upload to TestFlight
type: boolean
default: false
tag:
description: GitHub Release tag (empty to skip)
type: string
required: false
default: ''
jobs:
build:
name: Build iOS
runs-on: macos-26
env:
PRODUCT_NAME: 'AIRI'
VITE_ENABLE_ANALYTICS: 'true'
UPLOAD_TESTFLIGHT: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.testflight) }}
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Build assets
run: pnpm -F @proj-airi/stage-pocket run build
- name: Sync assets to iOS
run: pnpm -F @proj-airi/stage-pocket exec cap sync ios
- name: Install Apple certificate
env:
CERT_BASE64: ${{ secrets.APPLE_DISTRIBUTION_CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.APPLE_DISTRIBUTION_CERT_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"
echo "$CERT_BASE64" | base64 --decode -o "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 3600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$CERT_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Install provisioning profile
env:
PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
run: |
PROFILE_PATH="$RUNNER_TEMP/profile.mobileprovision"
echo "$PROFILE_BASE64" | base64 --decode -o "$PROFILE_PATH"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Read version from Xcode project
run: |
PBXPROJ="apps/stage-pocket/ios/App/App.xcodeproj/project.pbxproj"
VERSION="$(sed -n 's/.*MARKETING_VERSION = \(.*\);/\1/p' "$PBXPROJ" | head -1 | tr -d ' ')"
BUILD_NUMBER="$(sed -n 's/.*CURRENT_PROJECT_VERSION = \(.*\);/\1/p' "$PBXPROJ" | head -1 | tr -d ' ')"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
- name: Archive iOS
run: |
xcodebuild archive \
-project apps/stage-pocket/ios/App/App.xcodeproj \
-scheme App \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$RUNNER_TEMP/App.xcarchive" \
CURRENT_PROJECT_VERSION=${{ env.BUILD_NUMBER }}
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/App.xcarchive" \
-exportPath "$RUNNER_TEMP/output" \
-exportOptionsPlist apps/stage-pocket/ios/ExportOptions.plist
- name: Rename IPA for distribution
run: |
IPA_SOURCE="$(find "$RUNNER_TEMP/output" -name '*.ipa' -print -quit)"
IPA_NAME="${PRODUCT_NAME}-${VERSION}_(${BUILD_NUMBER})-ios.ipa"
mv "$IPA_SOURCE" "$RUNNER_TEMP/output/$IPA_NAME"
echo "IPA_NAME=$IPA_NAME" >> "$GITHUB_ENV"
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.IPA_NAME }}
path: ${{ runner.temp }}/output/${{ env.IPA_NAME }}
if-no-files-found: error
- name: Install Transporter
if: ${{ env.UPLOAD_TESTFLIGHT == 'true' }}
run: |
curl -fsSL "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/resources/download/public/Transporter__OSX/bin/" -o "$RUNNER_TEMP/itmstransporter.pkg"
sudo installer -pkg "$RUNNER_TEMP/itmstransporter.pkg" -target /
rm -f "$RUNNER_TEMP/itmstransporter.pkg"
- name: Upload to TestFlight
if: ${{ env.UPLOAD_TESTFLIGHT == 'true' }}
uses: apple-actions/upload-testflight-build@v4
with:
app-path: ${{ runner.temp }}/output/${{ env.IPA_NAME }}
issuer-id: ${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_CONNECT_API_KEY }}
- name: Upload to GitHub Releases
if: ${{ env.RELEASE_TAG != '' }}
uses: softprops/action-gh-release@v2
with:
files: ${{ runner.temp }}/output/${{ env.IPA_NAME }}
append_body: true
fail_on_unmatched_files: true
tag_name: ${{ env.RELEASE_TAG }}
- name: Clean up signing
if: always()
run: |
if [ -n "$KEYCHAIN_PATH" ]; then
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
fi
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision
publish-gitcode-release:
name: Publish GitCode Release Mirror
needs: build
if: ${{ always() && github.repository == 'moeru-ai/airi' && needs.build.result == 'success' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.tag != '')) }}
continue-on-error: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Validate release mirror tools
run: |
gh --version
jq --version
curl --version
- name: Publish release assets to GitCode
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
GITCODE_OWNER: ${{ secrets.GITCODE_OWNER }}
GITCODE_REPO: ${{ secrets.GITCODE_REPO }}
GITCODE_RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: bash .github/scripts/publish-gitcode-release.sh
@@ -1,278 +0,0 @@
name: Release Tamagotchi to Steam
permissions:
contents: read
actions: read
on:
release:
types:
- prereleased
workflow_dispatch:
inputs:
upload_to_steam:
description: Upload the completed build to Steam
required: true
default: false
type: boolean
release_branch:
description: Steam branch to set live
required: true
default: internal-test
type: string
build_description:
description: Optional Steam build description
required: false
type: string
concurrency:
group: steam-deploy-tamagotchi-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || 'internal-test' }}
cancel-in-progress: false
jobs:
build:
name: Build ${{ matrix.depot }} depot
runs-on: ${{ matrix.os }}
env:
VITE_DISTRIBUTION: steam
VITE_DISABLE_CUSTOM_PROVIDERS: 'true'
VITE_DISABLE_FLUX_PURCHASE: 'true'
VITE_ENABLE_ANALYTICS: 'false'
strategy:
fail-fast: false
matrix:
include:
- depot: windows
os: windows-latest
builder-args: --windows --x64
arch: x64
- depot: macos
os: macos-26
builder-args: --macos --arm64
arch: arm64
- depot: linux
os: ubuntu-latest
builder-args: --linux --x64
arch: x64
steps:
# The Linux desktop build needs more space than the default hosted runner provides.
- name: Free Linux disk space
if: matrix.depot == 'linux'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
- uses: actions/checkout@v6
- name: Select Xcode 26.2
if: matrix.depot == 'macos'
run: |
sudo xcode-select -s /Applications/Xcode_26.2.app
xcodebuild -version
- name: Show macOS toolchain
if: matrix.depot == 'macos'
run: |
xcodebuild -version
actool --version
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
- name: Install macOS system dependencies for node-canvas
if: matrix.depot == 'macos'
run: brew install pkg-config cairo pango jpeg giflib librsvg
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Setup Godot
uses: chickensoft-games/setup-godot@v2
with:
version: '4.7.1'
use-dotnet: true
include-templates: true
- name: Export Godot Stage for Windows
if: matrix.depot == 'windows'
working-directory: ./engines/stage-tamagotchi-godot
shell: bash
run: |
mkdir -p out/win
godot --headless --export-release "Windows Desktop" out/win/godot-stage.exe
- name: Export Godot Stage for macOS
if: matrix.depot == 'macos'
working-directory: ./engines/stage-tamagotchi-godot
run: |
mkdir -p out/mac
godot --headless --export-release "macOS" out/mac/godot-stage.app
- name: Clean Godot Stage for Steam on macOS
if: matrix.depot == 'macos'
working-directory: ./engines/stage-tamagotchi-godot
run: |
# NOTICE:
# SteamPipe excludes *.pdb files after Electron signs the parent app.
# Removing Godot debug symbols and AppleDouble files before signing keeps macOS sealed resources aligned with the installed bundle.
# Source/context: Steam internal-test BuildID 24160350 and the game-ci SteamPipe FileExclusion "*.pdb" behavior.
# Removal condition: SteamPipe preserves these resources or the embedded Godot bundle no longer contains them.
find out/mac/godot-stage.app -name '*.pdb' -delete
find out/mac/godot-stage.app -name '._*' -delete
- name: Export Godot Stage for Linux
if: matrix.depot == 'linux'
working-directory: ./engines/stage-tamagotchi-godot
run: |
mkdir -p out/linux
godot --headless --export-release "Linux ${{ matrix.arch }}" out/linux/godot-stage
- name: Build Windows app
if: matrix.depot == 'windows'
run: pnpm run -F @proj-airi/stage-tamagotchi build && pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build macOS app
if: matrix.depot == 'macos'
run: |
echo "$CSC_CONTENT" | base64 --decode > apps/stage-tamagotchi/apple-developer-code-signing.p12
export CSC_LINK="./apple-developer-code-signing.p12"
pnpm run -F @proj-airi/stage-tamagotchi build
pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_CONTENT: ${{ secrets.CSC_CONTENT }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_DEVELOPER_APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_DEVELOPER_APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }}
- name: Build Linux app
if: matrix.depot == 'linux'
run: pnpm run -F @proj-airi/stage-tamagotchi build && pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Package Windows depot
if: matrix.depot == 'windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force steam-content/windows | Out-Null
Copy-Item apps/stage-tamagotchi/dist/win-unpacked/* steam-content/windows -Recurse -Force
tar -cf steam-content-windows.tar -C steam-content windows
- name: Package macOS depot
if: matrix.depot == 'macos'
run: |
app_path="apps/stage-tamagotchi/dist/mac-arm64/AIRI.app"
if [[ ! -d "$app_path" ]]; then
echo "::error::No macOS app bundle was produced."
exit 1
fi
mkdir -p steam-content/macos
ditto --norsrc "$app_path" steam-content/macos/AIRI.app
find steam-content/macos -name '._*' -delete
COPYFILE_DISABLE=1 tar -cf steam-content-macos.tar -C steam-content macos
- name: Package Linux depot
if: matrix.depot == 'linux'
run: |
mkdir -p steam-content/linux
cp -a apps/stage-tamagotchi/dist/linux-unpacked/. steam-content/linux/
tar -cf steam-content-linux.tar -C steam-content linux
- name: Upload depot artifact
uses: actions/upload-artifact@v7
with:
name: steam-content-${{ matrix.depot }}
path: steam-content-${{ matrix.depot }}.tar
if-no-files-found: error
overwrite: true
deploy:
name: Upload to Steam
if: ${{ github.event_name == 'release' || inputs.upload_to_steam }}
needs: build
runs-on: ubuntu-latest
env:
STEAM_APP_ID: '3885340'
STEAM_DEBUG_BRANCH: 'false'
STEAM_RELEASE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || 'internal-test' }}
STEAM_WINDOWS_DEPOT_ID: '3885342'
steps:
- name: Validate Steam branch
env:
RELEASE_BRANCH: ${{ env.STEAM_RELEASE_BRANCH }}
run: |
if [[ ! "$RELEASE_BRANCH" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "::error::Steam release branch may contain only letters, numbers, dots, underscores, and hyphens."
exit 1
fi
if [[ "$RELEASE_BRANCH" == "default" ]]; then
echo '::error::Steam release branch "default" must be promoted manually.'
exit 1
fi
- name: Normalize Steam build description
id: metadata
uses: actions/github-script@v9
env:
INPUT_BUILD_DESCRIPTION: ${{ inputs.build_description }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
with:
result-encoding: string
script: |
const source = process.env.RELEASE_TAG || process.env.GITHUB_SHA
const fallback = `AIRI ${source} Steam candidate`
const normalizedDescription = (process.env.INPUT_BUILD_DESCRIPTION || fallback)
.replace(/[\r\n\t]+/g, ' ')
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
.replaceAll('\\', '/')
.replaceAll('"', "'")
.replace(/ {2,}/g, ' ')
.trim()
.slice(0, 512)
return normalizedDescription || fallback
- name: Download Steam depots
uses: actions/download-artifact@v5
with:
pattern: steam-content-*
path: steam-artifacts
merge-multiple: true
- name: Prepare Steam upload root
run: |
mkdir -p steam-build
tar -xf steam-artifacts/steam-content-windows.tar -C steam-build
tar -xf steam-artifacts/steam-content-macos.tar -C steam-build
tar -xf steam-artifacts/steam-content-linux.tar -C steam-build
- name: Deploy desktop builds to Steam
uses: game-ci/steam-deploy@v3.2.0
with:
username: ${{ secrets.STEAM_USERNAME }}
configVdf: ${{ secrets.STEAM_CONFIG_VDF }}
appId: ${{ env.STEAM_APP_ID }}
firstDepotIdOverride: ${{ env.STEAM_WINDOWS_DEPOT_ID }}
buildDescription: ${{ steps.metadata.outputs.result }}
rootPath: steam-build
depot1Path: windows
depot2Path: macos
depot3Path: linux
releaseBranch: ${{ env.STEAM_RELEASE_BRANCH }}
debugBranch: ${{ env.STEAM_DEBUG_BRANCH }}
-665
View File
@@ -1,665 +0,0 @@
name: Release Tamagotchi
permissions:
contents: write
actions: read
env:
BUNDLE_NAME: ''
DEB_BUNDLE_NAME: ''
RPM_BUNDLE_NAME: ''
FLATPAK_BUNDLE_NAME: ''
PRODUCT_NAME: 'AIRI'
VERSION: ''
on:
release:
types:
- prereleased
workflow_dispatch:
inputs:
build_only:
description: Build only
required: false
default: false
type: boolean
artifacts_only:
description: Build and upload artifacts only
required: false
default: false
type: boolean
tag:
description: Specific tag/commit for the release (leave empty to auto-detect latest tag)
required: false
type: string
platform:
description: Platform
type: choice
options:
- all
- windows
- macos
- linux
schedule:
- cron: '0 0 * * *'
jobs:
build:
name: Build
continue-on-error: ${{ matrix.skip }} # WORKAROUND: Skipping subsequent steps without failing the whole workflow
env:
VITE_ENABLE_ANALYTICS: ${{ ((github.event_name == 'release') || (github.event_name == 'workflow_dispatch' && !inputs.build_only)) && 'true' || 'false' }}
strategy:
matrix:
include:
- os: macos-15-intel
artifact: darwin-x64
target: x86_64-apple-darwin
arch: x64
builder-args: --macos --x64
skip: ${{ inputs.platform != '' && inputs.platform != 'all' && inputs.platform != 'macos' }}
- os: macos-26
artifact: darwin-arm64
target: aarch64-apple-darwin
builder-args: --macos --arm64
arch: arm64
skip: ${{ inputs.platform != '' && inputs.platform != 'all' && inputs.platform != 'macos' }}
- os: ubuntu-latest
artifact: linux-x64
target: x86_64-unknown-linux-gnu
builder-args: --linux --x64
arch: x64
skip: ${{ inputs.platform != '' && inputs.platform != 'all' && inputs.platform != 'linux' }}
- os: ubuntu-24.04-arm
artifact: linux-arm64
target: aarch64-unknown-linux-gnu
builder-args: --linux --arm64
arch: arm64
skip: ${{ inputs.platform != '' && inputs.platform != 'all' && inputs.platform != 'linux' }}
- os: windows-latest
artifact: windows-x64-setup
target: x86_64-pc-windows-msvc
builder-args: --windows --x64
arch: x64
skip: ${{ inputs.platform != '' && inputs.platform != 'all' && inputs.platform != 'windows' }}
runs-on: ${{ matrix.os }}
steps:
- name: Skip the build (fail fast)
run: |
echo "Skipping build for ${{ matrix.os }}..."
echo "This will fail the job, but don't panic—this is expected when not building for all."
echo "This will be improved in the future."
exit ${{ matrix.skip && '1' || '0' }}
# Why?
#
# failed to build archive at `/home/runner/work/airi/airi/target/x86_64-unknown-linux-gnu/release/deps/libapp_lib.rlib`:
# No space left on device (os error 28)
- name: Free Disk Space
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
- uses: actions/checkout@v6
- name: macOS Select Xcode 26.2
if: matrix.os == 'macos-15-intel' || matrix.os == 'macos-26'
run: |
sudo xcode-select -s /Applications/Xcode_26.2.app
xcodebuild -version
- name: macOS Show Toolchain
if: matrix.os == 'macos-15-intel' || matrix.os == 'macos-26'
run: |
xcodebuild -version
actool --version
# NOTICE:
# pnpm 11 does not ship a working standalone executable for Intel macOS.
# Install pnpm through npm so it uses the Node.js runtime from setup-node.
# https://github.com/pnpm/pnpm/issues/11423
# Remove this branch when the project uses pnpm 12 or later.
- uses: actions/setup-node@v6
if: matrix.os == 'macos-15-intel'
with:
node-version: 26.7.0
- name: Install pnpm with npm
if: matrix.os == 'macos-15-intel'
run: npm install --global pnpm@11.24.0
- name: Get pnpm store path
if: matrix.os == 'macos-15-intel'
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
if: matrix.os == 'macos-15-intel'
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-pnpm-store-
- uses: pnpm/setup@v2
if: matrix.os != 'macos-15-intel'
with:
runtime: node@26.7.0
cache: true
install: false
- name: Install macOS System Dependencies for node-canvas
if: runner.os == 'macOS'
run: |
brew install pkg-config cairo pango jpeg giflib librsvg
- run: pnpm install --frozen-lockfile
# ---------
# Build
# ---------
- run: pnpm run build:packages
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Setup Godot
uses: chickensoft-games/setup-godot@v2
with:
version: '4.7.1'
use-dotnet: true
include-templates: true
- name: Export Godot Stage (Windows)
if: matrix.os == 'windows-latest'
working-directory: ./engines/stage-tamagotchi-godot
shell: bash
run: |
mkdir -p out/win
godot --headless --export-release "Windows Desktop" out/win/godot-stage.exe
- name: Export Godot Stage (macOS)
if: matrix.os == 'macos-15-intel' || matrix.os == 'macos-26'
working-directory: ./engines/stage-tamagotchi-godot
shell: bash
run: |
mkdir -p out/mac
godot --headless --export-release "macOS" out/mac/godot-stage.app
- name: Export Godot Stage (Linux)
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm'
working-directory: ./engines/stage-tamagotchi-godot
shell: bash
run: |
mkdir -p out/linux
godot --headless --export-release "Linux ${{ matrix.arch }}" out/linux/godot-stage
- name: Build (Windows Only) # Windows
if: matrix.os == 'windows-latest'
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' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (macOS Only) # macOS
if: matrix.os == 'macos-15-intel' || matrix.os == 'macos-26'
run: |
echo "$CSC_CONTENT" | base64 --decode > apps/stage-tamagotchi/apple-developer-code-signing.p12
export CSC_LINK="./apple-developer-code-signing.p12"
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' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_CONTENT: ${{ secrets.CSC_CONTENT }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_DEVELOPER_APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_DEVELOPER_APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }}
- 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' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Flatpak (Linux Only)
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }}
run: |
sudo apt update
sudo apt install -y flatpak flatpak-builder elfutils
flatpak --version
flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Build Flatpak (Linux Only) # Flatpak
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }}
working-directory: ./apps/stage-tamagotchi
run: |
mkdir -p dist
# Auto-install required SDK/Platform/BaseApp from Flathub in user scope
flatpak-builder --user --install-deps-from=flathub ./flatpak ai.moeru.airi.flatpak.yml --force-clean
flatpak build-export ./flatpak-repo ./flatpak
export FLATPAK_OUTPUT_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-output-filename flatpak)
flatpak build-bundle ./flatpak-repo dist/${FLATPAK_OUTPUT_NAME} ai.moeru.airi
# ---------
# Nightly (schedule) builds only
# ---------
- name: Get Artifacts Envs (Nightly + Windows Only)
if: ${{ github.event_name == 'schedule' && matrix.os == 'windows-latest' }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-bundle-name)" >> $env:GITHUB_ENV
echo "VERSION=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-version)" >> $env:GITHUB_ENV
- name: Get Artifacts Envs (Nightly + Non-Windows)
if: ${{ github.event_name == 'schedule' && matrix.os != 'windows-latest' }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "VERSION=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-version)" >> $GITHUB_ENV
- name: Get Artifacts Envs (Nightly + macOS Only)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'macos-26' || matrix.os == 'macos-15-intel') }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-bundle-name)" >> $GITHUB_ENV
- name: Rename Artifacts (Nightly)
if: ${{ github.event_name == 'schedule' }}
run:
pnpm run -F @proj-airi/stage-tamagotchi rename-artifacts ${{ matrix.target }}
- name: Get Linux Artifact Names (Nightly + Linux Only)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
working-directory: ./apps/stage-tamagotchi
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 "FLATPAK_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename flatpak)" >> $GITHUB_ENV
- name: Upload Artifacts (Nightly + Non-Linux)
if: ${{ github.event_name == 'schedule' && (matrix.os != 'ubuntu-latest' && matrix.os != 'ubuntu-24.04-arm') }}
id: unsigned-artifacts-nightly
uses: actions/upload-artifact@v7
with:
name: ${{ env.BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload Artifacts (Nightly + Linux deb)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.DEB_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.DEB_BUNDLE_NAME }}
- name: Upload Artifacts (Nightly + Linux rpm)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.RPM_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.RPM_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
with:
name: ${{ env.FLATPAK_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.FLATPAK_BUNDLE_NAME }}
- name: Sign Windows Artifacts with SignPath (Nightly + Windows Only)
id: signpath-nightly-windows
continue-on-error: true
uses: signpath/github-action-submit-signing-request@v2
if: ${{ github.event_name == 'schedule' && (matrix.os == 'windows-latest' && github.repository == 'moeru-ai/airi') }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANIZATION_ID }}'
project-slug: 'airi'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: ci-github-actions-artifacts-windows
github-artifact-id: '${{ steps.unsigned-artifacts-nightly.outputs.artifact-id }}'
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 900 # 15 minutes
download-signed-artifact-timeout-in-seconds: 900 # 15 minutes
output-artifact-directory: apps/stage-tamagotchi/bundle/signed/windows/
- name: Warn When Windows Signing Fails (Nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'windows-latest' && steps.signpath-nightly-windows.outcome != 'success' }}
run: |
echo "::warning::SignPath Windows signing failed for nightly build. Continuing with unsigned artifact at apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}."
- name: Upload Signed Artifacts (Nightly + Non-Linux)
if: ${{ github.event_name == 'schedule' && matrix.os == 'windows-latest' && steps.signpath-nightly-windows.outcome == 'success' }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/signed/windows/${{ env.BUNDLE_NAME }}
overwrite: true
- name: Upload Unsigned Windows Artifacts Fallback (Nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'windows-latest' && steps.signpath-nightly-windows.outcome != 'success' }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
overwrite: true
# NOTICE: Electron Builder generates latest-x64.yml during the build step, before this workflow
# submits the Windows installer to SignPath. SignPath re-signs the .exe and changes its bytes,
# which changes the updater hashes too. Regenerating latest-x64.yml from the signed installer keeps
# the published Windows update metadata aligned with the final released artifact.
- name: Regenerate Windows latest-x64.yml (Nightly + Windows Only)
if: ${{ github.event_name == 'schedule' && matrix.os == 'windows-latest' && steps.signpath-nightly-windows.outcome == 'success' }}
run: |
pnpm -F @proj-airi/stage-tamagotchi run regenerate-windows-latest --input apps/stage-tamagotchi/bundle/signed/windows/${{ env.BUNDLE_NAME }} --output apps/stage-tamagotchi/bundle/latest-x64.yml --version ${{ env.VERSION }}
- name: Regenerate Windows latest-x64.yml From Unsigned Artifact (Nightly Fallback)
if: ${{ github.event_name == 'schedule' && matrix.os == 'windows-latest' && steps.signpath-nightly-windows.outcome != 'success' }}
run: |
pnpm -F @proj-airi/stage-tamagotchi run regenerate-windows-latest --input apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }} --output apps/stage-tamagotchi/bundle/latest-x64.yml --version ${{ env.VERSION }}
# ---------
# Workflow Dispatch only
# ---------
- name: Get Artifacts Envs (Manual + Windows Only)
if: ${{ github.event_name == 'workflow_dispatch' && matrix.os == 'windows-latest' }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-bundle-name --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $env:GITHUB_ENV
echo "VERSION=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-version --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $env:GITHUB_ENV
- name: Get Artifacts Envs (Manual + Non-Windows)
if: ${{ github.event_name == 'workflow_dispatch' && matrix.os != 'windows-latest' }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "VERSION=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-version --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
- name: Get Artifacts Envs (Manual + macOS Only)
if: ${{ github.event_name == 'workflow_dispatch' && (matrix.os == 'macos-26' || matrix.os == 'macos-15-intel') }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-bundle-name --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
- name: Rename Artifacts (Manual)
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
pnpm run -F @proj-airi/stage-tamagotchi rename-artifacts ${{ matrix.target }} --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }}
- name: Get Linux Artifact Names (Manual + Non-Release + Linux Only)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
working-directory: ./apps/stage-tamagotchi
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 "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)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os != 'ubuntu-latest' && matrix.os != 'ubuntu-24.04-arm') }}
id: unsigned-artifacts-workflow-dispatch
uses: actions/upload-artifact@v7
with:
name: ${{ env.BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload Artifacts (Manual + Non-Release + Linux deb)
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.DEB_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.DEB_BUNDLE_NAME }}
- name: Upload Artifacts (Manual + Non-Release + Linux rpm)
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.RPM_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.RPM_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
with:
name: ${{ env.FLATPAK_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.FLATPAK_BUNDLE_NAME }}
- name: Sign Windows Artifacts with SignPath (Manual + Windows Only)
id: signpath-manual-windows
continue-on-error: true
uses: signpath/github-action-submit-signing-request@v2
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'windows-latest' && github.repository == 'moeru-ai/airi') }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANIZATION_ID }}'
project-slug: 'airi'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: ci-github-actions-artifacts-windows
github-artifact-id: '${{ steps.unsigned-artifacts-workflow-dispatch.outputs.artifact-id }}'
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 900 # 15 minutes
download-signed-artifact-timeout-in-seconds: 900 # 15 minutes
output-artifact-directory: apps/stage-tamagotchi/bundle/signed/windows/
- name: Warn When Windows Signing Fails (Manual)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && matrix.os == 'windows-latest' && steps.signpath-manual-windows.outcome != 'success' }}
run: |
echo "::warning::SignPath Windows signing failed for workflow_dispatch artifact build. Continuing with unsigned artifact at apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}."
- name: Move Signed Artifacts (Manual + Release + Windows Only)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && matrix.os == 'windows-latest' && steps.signpath-manual-windows.outcome == 'success' }}
run: |
Move-Item -Force apps/stage-tamagotchi/bundle/signed/windows/${{ env.BUNDLE_NAME }} apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload Signed Artifacts (Manual + Windows Only)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'windows-latest') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
overwrite: true
# NOTICE: Electron Builder already wrote latest-x64.yml before SignPath signed the installer.
# After the signed .exe replaces the unsigned one, we must rebuild latest-x64.yml so its hashes
# describe the final Windows artifact that users will actually download.
- name: Regenerate Windows latest-x64.yml (Manual + Windows Only)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && matrix.os == 'windows-latest' }}
run: |
pnpm -F @proj-airi/stage-tamagotchi run regenerate-windows-latest --input apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }} --output apps/stage-tamagotchi/bundle/latest-x64.yml --version ${{ env.VERSION }}
- name: Upload To GitHub Releases (Manual + Release + Overwrite Release)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && !inputs.artifacts_only }}
uses: softprops/action-gh-release@v2
with:
files: |
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.exe
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.zip
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 }}-*.flatpak
apps/stage-tamagotchi/bundle/latest-*.yml
append_body: true
tag_name: ${{ inputs.tag }}
# ---------
# Version push
# ---------
- name: Rename Artifacts (Automatic)
if: ${{ github.event_name == 'release' }}
run: |
pnpm run -F @proj-airi/stage-tamagotchi rename-artifacts ${{ matrix.target }} --release --auto-tag
- name: Get Artifacts Envs (Automatic + Windows Only)
if: ${{ github.event_name == 'release' && matrix.os == 'windows-latest' }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-bundle-name --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $env:GITHUB_ENV
echo "VERSION=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-version --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $env:GITHUB_ENV
- name: Get Artifacts Envs (Automatic + Non-Windows)
if: ${{ github.event_name == 'release' && matrix.os != 'windows-latest' }}
working-directory: ./apps/stage-tamagotchi
run: |
echo "BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-bundle-name --release --auto-tag)" >> $GITHUB_ENV
- name: Upload Artifacts (Automatic + Windows Only)
if: ${{ github.event_name == 'release' && matrix.os == 'windows-latest' }}
id: unsigned-artifacts-release
uses: actions/upload-artifact@v7
with:
name: ${{ env.BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Sign Windows Artifacts with SignPath (Nightly + Windows Only)
id: signpath-release-windows
continue-on-error: true
uses: signpath/github-action-submit-signing-request@v2
if: ${{ github.event_name == 'release' && (matrix.os == 'windows-latest' && github.repository == 'moeru-ai/airi') }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANIZATION_ID }}'
project-slug: 'airi'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: ci-github-actions-artifacts-windows
github-artifact-id: '${{ steps.unsigned-artifacts-release.outputs.artifact-id }}'
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 900 # 15 minutes
download-signed-artifact-timeout-in-seconds: 900 # 15 minutes
output-artifact-directory: apps/stage-tamagotchi/bundle/signed/windows/
- name: Warn When Windows Signing Fails (Release)
if: ${{ github.event_name == 'release' && matrix.os == 'windows-latest' && steps.signpath-release-windows.outcome != 'success' }}
run: |
echo "::warning::SignPath Windows signing failed for release build. Continuing with unsigned artifact at apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}."
- name: Move Signed Artifacts (Automatic + Windows Only)
if: ${{ github.event_name == 'release' && matrix.os == 'windows-latest' && steps.signpath-release-windows.outcome == 'success' }}
run: |
Move-Item -Force apps/stage-tamagotchi/bundle/signed/windows/${{ env.BUNDLE_NAME }} apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
# NOTICE: The root cause here is the build/signing order: latest-x64.yml is produced by Electron
# Builder before SignPath re-signs the installer. That post-build signing step mutates the .exe,
# so the updater metadata becomes stale. Recomputing latest-x64.yml after the signed file is moved
# into its final bundle path makes the release upload self-consistent again.
- name: Regenerate Windows latest manifest (Automatic + Windows Only)
if: ${{ github.event_name == 'release' && matrix.os == 'windows-latest' }}
run: |
pnpm -F @proj-airi/stage-tamagotchi run regenerate-windows-latest --input apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }} --output apps/stage-tamagotchi/bundle/latest-x64.yml --version ${{ env.VERSION }}
- name: Upload Distribution Artifact (Automatic + macOS arm64 Only)
if: ${{ github.event_name == 'release' && matrix.os == 'macos-26' }}
uses: actions/upload-artifact@v7
with:
name: distribution-osx
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload Distribution Artifact (Automatic + Windows Only)
if: ${{ github.event_name == 'release' && matrix.os == 'windows-latest' }}
uses: actions/upload-artifact@v7
with:
name: distribution-win
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload Distribution Artifact (Automatic + Linux x64 Only)
if: ${{ github.event_name == 'release' && matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@v7
with:
name: distribution-linux-x64
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload Distribution Artifact (Automatic + Linux arm64 Only)
if: ${{ github.event_name == 'release' && matrix.os == 'ubuntu-24.04-arm' }}
uses: actions/upload-artifact@v7
with:
name: distribution-linux-arm64
path: apps/stage-tamagotchi/bundle/${{ env.BUNDLE_NAME }}
- name: Upload To GitHub Releases (Automatic)
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
# Possible auto updater files:
# Windows: latest-x64.yml
# macOS: latest-arm64-mac.yml, latest-x64-mac.yml
# Linux: latest-arm64-linux.yml (arm64), latest-x64-linux.yml (x64)
files: |
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.exe
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.zip
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 }}-*.flatpak
apps/stage-tamagotchi/bundle/latest-*.yml
append_body: true
publish-gitcode-release:
name: Publish GitCode Release Mirror
needs: build
if: ${{ always() && github.repository == 'moeru-ai/airi' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.build_only && !inputs.artifacts_only && inputs.tag != '')) }}
continue-on-error: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Validate release mirror tools
run: |
gh --version
jq --version
curl --version
- name: Publish release assets to GitCode
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
GITCODE_OWNER: ${{ secrets.GITCODE_OWNER }}
GITCODE_REPO: ${{ secrets.GITCODE_REPO }}
GITCODE_RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: bash .github/scripts/publish-gitcode-release.sh
publish-distribution:
name: Publish Distribution
needs: build
if: ${{ github.event_name == 'release' }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- artifact_name: distribution-osx
platform: osx
- artifact_name: distribution-win
platform: win
- artifact_name: distribution-linux-x64
platform: linux
- artifact_name: distribution-linux-arm64
platform: linux
steps:
- name: Resolve release version
run: |
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: Download distribution artifact
uses: actions/download-artifact@v5
with:
name: ${{ matrix.artifact_name }}
path: dist/${{ matrix.artifact_name }}
- name: Resolve artifact path
run: |
echo "ARTIFACT_PATH=$(find dist/${{ matrix.artifact_name }} -maxdepth 1 -mindepth 1 | head -n 1)" >> $GITHUB_ENV
- name: Setup butler
uses: remarkablegames/setup-butler@v2
# https://itch.io/docs/butler/pushing.html
- name: Publish distribution artifact to itch.io
run: butler push "${{ env.ARTIFACT_PATH }}" nekomeowww/airi:${{ matrix.platform }} --userversion "${{ env.VERSION }}"
env:
BUTLER_API_KEY: ${{ secrets.GAME_PUBLISHING_ITCHIO }}
+11 -9
View File
@@ -16,11 +16,13 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- name: Configure npm registry
uses: actions/setup-node@v6
@@ -29,11 +31,11 @@ jobs:
# https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- run: pnpm run build:packages
- run: pnpm run -F @proj-airi/airi-plugin-vscode build
- run: pnpm run -F vscode-airi build
- run: pnpm run publish
- run: bun install --frozen-lockfile
- run: bun run build:packages
- run: bun run --filter @proj-airi/airi-plugin-vscode build
- run: bun run --filter vscode-airi build
- run: bun run publish
working-directory: ./integrations/vscode/vscode-airi
env:
VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
+8 -6
View File
@@ -21,18 +21,20 @@ jobs:
with:
ref: main
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
- run: pnpm install --frozen-lockfile
node-version: 26.7.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- run: bun install --frozen-lockfile
- name: Pull with rebase
run: git pull origin main --rebase --autostash
- name: Generate sponsors svg
run: pnpm run sponsors:generate
run: bun run sponsors:generate
- name: Remove private SponsorKit cache
run: rm -f docs/content/public/assets/sponsors/.cache.json
@@ -1,75 +0,0 @@
name: Update Nix assets Hash
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'apps/stage-web/package.json'
- 'apps/stage-web/vite.config.ts'
permissions:
contents: write
pull-requests: write
jobs:
update:
if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork
runs-on: ubuntu-latest
steps:
# Why?
#
# failed to
# $ nix build .#airi-pnpm-deps
# > airi-pnpm-deps> Running phase: fixupPhase
# > error: writing to file: No space left on device
- name: Free Disk Space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
- uses: actions/checkout@v6
with:
ref: main # Use main regardless of workflow_dispatch branch
# Authenticate git with PAT so that pushing to head retriggers CI
token: ${{ secrets.HASH_UPDATE_TOKEN }}
- uses: cachix/install-nix-action@v31
with:
extra_nix_config: experimental-features = nix-command flakes
- name: Update Hash
run: nix/update-assets-hash.sh
- name: Check for changes
id: changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Create PR
if: steps.changes.outputs.has_changes == 'true'
env:
# Create PR with PAT to trigger CI
GH_TOKEN: ${{ secrets.HASH_UPDATE_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add nix/assets-hash.txt
git commit -m 'chore(nix): update assets hash'
git push -f origin HEAD:chore/update-nix-assets-hash
# Create PR if it doesn't already exist
existing_pr=$(gh pr list --head chore/update-nix-assets-hash --json number -q '.[0].number' 2>/dev/null || true)
if [ -z "$existing_pr" ]; then
gh pr create \
--base main \
--head chore/update-nix-assets-hash \
--title 'chore(nix): update assets hash' \
--body 'Auto-generated by CI to keep Nix asset hash up-to-date.'
fi
# Enable auto-merge so PR merges once checks pass
gh pr merge --squash --auto --delete-branch chore/update-nix-assets-hash
@@ -1,74 +0,0 @@
name: Update Nix pnpmDeps Hash
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'pnpm-lock.yaml'
permissions:
contents: write
pull-requests: write
jobs:
update:
if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork
runs-on: ubuntu-latest
steps:
# Why?
#
# failed to
# $ nix build .#airi-pnpm-deps
# > airi-pnpm-deps> Running phase: fixupPhase
# > error: writing to file: No space left on device
- name: Free Disk Space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
- uses: actions/checkout@v6
with:
ref: main # Use main regardless of workflow_dispatch branch
# Authenticate git with PAT so that pushing to head retriggers CI
token: ${{ secrets.HASH_UPDATE_TOKEN }}
- uses: cachix/install-nix-action@v31
with:
extra_nix_config: experimental-features = nix-command flakes
- name: Update Hash
run: nix/update-pnpm-deps-hash.sh
- name: Check for changes
id: changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Create PR
if: steps.changes.outputs.has_changes == 'true'
env:
# Create PR with PAT to trigger CI
GH_TOKEN: ${{ secrets.HASH_UPDATE_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add nix/pnpm-deps-hash.txt
git commit -m 'chore(nix): update pnpmDeps hash'
git push -f origin HEAD:chore/update-nix-pnpm-deps-hash
# Create PR if it doesn't already exist
existing_pr=$(gh pr list --head chore/update-nix-pnpm-deps-hash --json number -q '.[0].number' 2>/dev/null || true)
if [ -z "$existing_pr" ]; then
gh pr create \
--base main \
--head chore/update-nix-pnpm-deps-hash \
--title 'chore(nix): update pnpmDeps hash' \
--body 'Auto-generated by CI to keep Nix pnpmDeps hash up-to-date.'
fi
# Enable auto-merge so PR merges once checks pass
gh pr merge --squash --auto --delete-branch chore/update-nix-pnpm-deps-hash
@@ -1,165 +0,0 @@
name: Update (Post Release) README URLs
on:
release:
types:
- published
- released
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v0.10.2)'
required: true
type: string
permissions:
contents: read
concurrency:
group: update-post-release-readme-urls
cancel-in-progress: true
jobs:
update:
# Keep prereleases from replacing the stable README download buttons.
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
runs-on: ubuntu-latest
env:
README_BRANCH: automation/update-readme-release-links
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
steps:
- uses: actions/checkout@v6
with:
ref: main
persist-credentials: false
- uses: pnpm/setup@v2
with:
runtime: node@26.7.0
cache: true
install: false
- run: pnpm install --frozen-lockfile
- name: Update README download links
run: pnpm -F @proj-airi/stage-tamagotchi exec tsx scripts/update-readme-download-links.ts "$RELEASE_TAG"
- name: Pull with rebase
run: git pull origin main --rebase --autostash
- name: Prepare README release update
id: prepare
env:
GH_TOKEN: ${{ secrets.PAT_SLASH_COMMAND_DISPATCH }}
run: |
# The updater is only allowed to publish the generated README link changes.
if ! git diff --quiet -- . ':(exclude)README.md' ':(exclude)docs/README*.md'; then
echo "::error::Unexpected non-README changes were generated."
git status --short
exit 1
fi
git add README.md 'docs/README*.md'
if git diff --cached --quiet; then
echo "No README release links changed."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "$GH_TOKEN" ]; then
echo "::error::PAT_SLASH_COMMAND_DISPATCH is required so the README update PR triggers required checks."
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
REMOTE_BRANCH="$(git ls-remote --heads origin "refs/heads/${README_BRANCH}")"
if [ -n "$REMOTE_BRANCH" ]; then
git fetch origin "${README_BRANCH}:refs/remotes/origin/${README_BRANCH}"
fi
git checkout -B "$README_BRANCH"
PR_TITLE="chore: update README release artifacts to ${RELEASE_TAG}"
git commit -m "$PR_TITLE"
git push --force-with-lease origin "$README_BRANCH"
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "pr_title=$PR_TITLE" >> "$GITHUB_OUTPUT"
- name: Create or update README release PR
if: steps.prepare.outputs.changed == 'true'
uses: actions/github-script@v8
env:
PR_TITLE: ${{ steps.prepare.outputs.pr_title }}
with:
github-token: ${{ secrets.PAT_SLASH_COMMAND_DISPATCH }}
script: |
const { owner, repo } = context.repo;
const head = `${owner}:${process.env.README_BRANCH}`;
const body = [
'## Description',
'',
`Updates stable desktop download links after the ${process.env.RELEASE_TAG} release.`,
'',
'## Linked Issues',
'',
'None.',
'',
'## Additional Context',
'',
'Generated by the post-release README workflow.',
].join('\n');
const { data: openPullRequests } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
base: 'main',
head,
per_page: 1,
});
let pullRequest;
if (openPullRequests.length > 0) {
const { data } = await github.rest.pulls.update({
owner,
repo,
pull_number: openPullRequests[0].number,
title: process.env.PR_TITLE,
body,
});
pullRequest = data;
core.info(`Updated pull request #${pullRequest.number}.`);
}
else {
const { data } = await github.rest.pulls.create({
owner,
repo,
base: 'main',
head: process.env.README_BRANCH,
title: process.env.PR_TITLE,
body,
});
pullRequest = data;
core.info(`Created pull request #${pullRequest.number}.`);
}
if (!pullRequest.auto_merge) {
await github.graphql(
`mutation EnableAutoMerge($pullRequestId: ID!) {
enablePullRequestAutoMerge(input: {
pullRequestId: $pullRequestId
mergeMethod: SQUASH
}) {
pullRequest {
number
}
}
}`,
{
pullRequestId: pullRequest.node_id,
},
);
core.info(`Enabled squash auto-merge for pull request #${pullRequest.number}.`);
}
@@ -19,18 +19,20 @@ jobs:
fi
- uses: actions/checkout@v6
- uses: pnpm/setup@v2
- uses: actions/setup-node@v6
with:
runtime: node@26.7.0
cache: true
install: false
node-version: 26.7.0
- run: pnpm install --frozen-lockfile
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- run: bun install --frozen-lockfile
# The TBX file is generated, never committed, so it cannot drift from terms.yaml.
# The schema also rejects a malformed entry here, before anything reaches Crowdin.
- name: Generate TBX File
run: pnpm -F @proj-airi/i18n glossary:build
run: bun run --filter @proj-airi/i18n glossary:build
# NOTICE: Crowdin does not document whether an import merges into the existing
# concepts or adds duplicates beside them, and a merge cannot be undone. This
-3
View File
@@ -133,9 +133,6 @@ plugins/development
plugins-local
plugins-development
apps/stage-pocket/ios/buildServer.json
apps/stage-pocket/airi-pocket.jks
apps/stage-tamagotchi/electron.vite.config.*.mjs
docs/superpowers
docs/research
+1 -1
View File
@@ -1,3 +1,3 @@
nodejs 26.7.0
pnpm 11.24.0
bun 1.4.2
dotnet 10
+5 -41
View File
@@ -1,30 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Electron",
"type": "node",
"request": "launch",
// Where to run the command
"runtimeExecutable": "pnpm",
"runtimeArgs": ["-F", "@proj-airi/stage-tamagotchi", "exec", "electron-vite", "--sourcemap"],
// Open terminal when debugging starts (Optional)
// Useful to see console.logs
// "console": "integratedTerminal",
// "internalConsoleOptions": "neverOpen",
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Minecraft Service",
"type": "node",
"request": "launch",
// Where to run the command
// Where to run the command
"runtimeExecutable": "pnpm",
"runtimeArgs": ["-F", "@proj-airi/minecraft-bot", "exec", "tsx"],
"args": ["--env-file=.env", "--env-file-if-exists=.env.local", "src/main.ts"],
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "start"],
"cwd": "${workspaceFolder}/integrations/minecraft",
"envFile": "${workspaceFolder}/integrations/minecraft/.env",
// Open terminal when debugging starts (Optional)
// Useful to see console.logs
@@ -34,36 +18,16 @@
"<node_internals>/**"
]
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
},
{
"name": "Debug VSCode Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/plugins/airi-plugin-vscode"
"--extensionDevelopmentPath=${workspaceFolder}/integrations/vscode/airi-plugin-vscode"
],
"outFiles": [
"${workspaceFolder}/plugins/airi-plugin-vscode/dist/**/*.cjs"
"${workspaceFolder}/integrations/vscode/airi-plugin-vscode/dist/**/*.cjs"
]
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Electron", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}
+37 -41
View File
@@ -1,21 +1,18 @@
# Project AIRI Agent Guide
# Project Moeka Agent Guide
Concise but detailed reference for contributors working across the `moeru-ai/airi` monorepo. Improve code when you touch it; avoid one-off patterns.
## Tech Stack (by surface)
- **Desktop (stage-tamagotchi)**: Electron, Vue, Vite, TypeScript, Pinia, VueUse, Eventa (IPC/RPC), UnoCSS, Vitest, ESLint.
- **Web (stage-web)**: Vue 3 + Vue Router, Vite, TypeScript, Pinia, VueUse, UnoCSS, Vitest, ESLint. Backend: WIP.
- **Mobile (stage-pocket)**: Vue 3 + Vue Router, Vite, TypeScript, Pinia, VueUse, UnoCSS, Vitest, ESLint, Kotlin, Swift, Capacitor.
- **Other apps**: `apps/ui-server-auth` (auth UI for the hosted auth service), `apps/component-calling` (realtime audio demo).
- **UI/Shared Packages**:
- `packages/stage-ui`: Core business components, composables, stores shared by stage-web & stage-tamagotchi (heart of stage work).
- `packages/stage-ui`: Core business components, composables, stores shared by the stage apps (heart of stage work).
- `packages/stage-ui-three`: Three.js bindings + Vue components.
- `packages/stage-ui-pixi`: Planned Pixi bindings.
- `packages/stage-shared`: Shared logic across stage-ui, stage-ui-three, stage-web, stage-tamagotchi.
- `packages/stage-shared`: Shared logic across stage-ui, stage-ui-three, and stage-web.
- `packages/ui`: Standardized primitives (inputs, textarea, buttons, layout) built on reka-ui; minimal business logic.
- `packages/i18n`: Central translations.
- Server channel: `packages/server-runtime`, `packages/server-sdk`, `packages/server-shared` (power `services/` and `plugins/`).
- Legacy: `crates/` (old Tauri desktop; current desktop is Electron).
## Structure & Responsibilities
@@ -27,18 +24,18 @@ Concise but detailed reference for contributors working across the `moeru-ai/air
- `server/docker-compose.yaml`: complete local backend stack.
- **Apps**
- `apps/stage-web`: Web app; composables/stores in `src/composables`, `src/stores`; pages in `src/pages`; devtools in `src/pages/devtools`; router config via `vite.config.ts`.
- `apps/stage-tamagotchi`: Electron app; renderer pages in `src/renderer/pages`; devtools in `src/renderer/pages/devtools`; settings layout at `src/renderer/layouts/settings.vue`; router config via `electron.vite.config.ts`.
- Settings/devtools routes rely on `<route lang="yaml"> meta: layout: settings </route>`; ensure routes/icons are registered accordingly (`apps/stage-tamagotchi/src/renderer/layouts/settings.vue`, `apps/stage-web/src/layouts/settings.vue`).
- `apps/ui-server-auth`: Auth UI served by the hosted auth service; pages in `src/pages`.
- Settings/devtools routes rely on `<route lang="yaml"> meta: layout: settings </route>`; ensure routes/icons are registered accordingly (`apps/stage-web/src/layouts/settings.vue`).
- Shared page bases: `packages/stage-pages`.
- Stage pages: `apps/stage-web/src/pages`, `apps/stage-tamagotchi/src/renderer/pages` (plus devtools folders).
- Stage pages: `apps/stage-web/src/pages` (plus the devtools folder).
- **Stage UI internals** (`packages/stage-ui/src`)
- Providers: `stores/providers.ts` and `stores/providers/` (standardized provider definitions).
- Modules: `stores/modules/` (AIRI orchestration building blocks).
- Modules: `stores/modules/` (Moeka orchestration building blocks).
- Composables: `composables/` (business-oriented Vue helpers).
- Components: `components/`; scenarios in `components/scenarios/` for page/use-case-specific pieces.
- Stories: `packages/stage-ui/stories`, `packages/stage-ui/histoire.config.ts` (e.g. `components/misc/Button.story.vue`).
- **IPC/Eventa**: Always use `@moeru/eventa` for type-safe, framework/runtime-agnostic IPC/RPC. Define contracts centrally (e.g., `apps/stage-tamagotchi/src/shared`) and follow usage patterns in `apps/stage-tamagotchi/src/main/services/electron` for main/renderer integration.
- **Dependency Injection**: Use `injeca` for services/electron modules/plugins/frontend; see `apps/stage-tamagotchi/src/main/index.ts` for composition patterns.
- **IPC/Eventa**: Always use `@moeru/eventa` for type-safe, framework/runtime-agnostic event and RPC contracts. Define contracts next to the module that owns them (e.g., `packages/stage-shared/src/beat-sync/eventa.ts`, `packages/plugin-protocol/src/types/events.ts`, `server/packages/server-sdk-shared/src/v2.ts`).
- **Dependency Injection**: Use `injeca` for services and server/plugin composition; see `server/apps/api/src/app.ts` and `server/apps/auth/src/server.ts` for composition patterns.
- **Build/CI/Lint**: `.github/workflows` for pipelines; `eslint.config.js` for lint rules.
- **Styles**: UnoCSS config at `uno.config.ts`; check `apps/stage-web/src/styles` for existing animations; prefer UnoCSS over Tailwind.
@@ -46,48 +43,47 @@ Concise but detailed reference for contributors working across the `moeru-ai/air
- `packages/stage-ui`: Core stage business components/composables/stores.
- `src/stores/providers.ts` and `src/stores/providers/`: provider definitions (standardized).
- `src/stores/modules/`: AIRI orchestration modules.
- `src/stores/modules/`: Moeka orchestration modules.
- `src/composables/`: reusable Vue composables (business-oriented).
- `src/components/`: business components; `src/components/scenarios/` for page/use-case-specific pieces.
- Stories: `packages/stage-ui/stories`, `packages/stage-ui/histoire.config.ts` (e.g. `components/misc/Button.story.vue`).
- `packages/stage-ui-three`: Three.js bindings + Vue components.
- `packages/stage-ui-pixi`: Planned Pixi bindings.
- `packages/stage-shared`: Shared logic across stage-ui, stage-ui-three, stage-web, stage-tamagotchi.
- `packages/stage-shared`: Shared logic across stage-ui, stage-ui-three, and stage-web.
- `packages/ui`: Standardized primitives (inputs/textarea/buttons/layout) built on reka-ui.
- `packages/i18n`: All translations.
- Hosted backend: `server/apps/api`, `server/apps/auth`, `server/packages`, and local tooling under `server/dev`.
- Server channel: `packages/server-runtime`, `packages/server-sdk`, `packages/server-shared` (power `services/` and `plugins/`).
- Legacy desktop: `crates/` (old Tauri; Electron is current).
- Pages: `packages/stage-pages` (shared bases); `apps/stage-web/src/pages` and `apps/stage-tamagotchi/src/renderer/pages` for app-specific pages; devtools live in each apps `.../pages/devtools`.
- Router configs: `apps/stage-web/vite.config.ts`, `apps/stage-tamagotchi/electron.vite.config.ts`.
- Devtools/layouts: `apps/stage-tamagotchi/src/renderer/layouts/settings.vue`, `apps/stage-web/src/layouts/settings.vue`.
- IPC/Eventa contracts/examples: `apps/stage-tamagotchi/src/shared`, `apps/stage-tamagotchi/src/main/services/electron`.
- DI examples: `apps/stage-tamagotchi/src/main/index.ts` (injeca).
- Pages: `packages/stage-pages` (shared bases); `apps/stage-web/src/pages` and `apps/ui-server-auth/src/pages` for app-specific pages; devtools live in `apps/stage-web/src/pages/devtools`.
- Router config: `apps/stage-web/vite.config.ts`.
- Devtools/layouts: `apps/stage-web/src/layouts/settings.vue`.
- IPC/Eventa contracts/examples: `packages/stage-shared/src/beat-sync/eventa.ts`, `packages/plugin-protocol/src/types/events.ts`.
- DI examples: `server/apps/api/src/app.ts`, `server/apps/auth/src/server.ts` (injeca).
- Styles: `uno.config.ts` (UnoCSS), `apps/stage-web/src/styles` (animations/reference).
- Build pipeline refs: `.github/workflows`; lint rules in `eslint.config.js`.
- Documented solutions: `docs/solutions/` records past fixes and workflow learnings, organized by category with YAML frontmatter (`module`, `tags`, `problem_type`); relevant when implementing, debugging, or verifying in documented areas.
- Tailwind/UnoCSS: prefer UnoCSS; if standardizing styles, add shortcuts/rules/plugins in `uno.config.ts`.
## Commands (pnpm with filters)
## Commands (Bun with filters)
> Use pnpm workspace filters to scope tasks. Examples below are generic; replace the filter with the target workspace name (e.g. `@proj-airi/stage-tamagotchi`, `@proj-airi/stage-web`, `@proj-airi/stage-ui`, etc.).
> Use Bun workspace filters to scope tasks. Examples below are generic; replace the filter with the target workspace name (e.g. `@proj-airi/stage-web`, `@proj-airi/stage-ui`, etc.).
- **Typecheck**
- `pnpm -F <package.json name> typecheck`
- Example: `pnpm -F @proj-airi/stage-tamagotchi typecheck` (runs `tsc` + `vue-tsc`).
- `bun run --filter <package.json name> typecheck`
- Example: `bun run --filter @proj-airi/stage-web typecheck` (runs `tsc` + `vue-tsc`).
- **Unit tests (Vitest)**
- Targeted: `pnpm exec vitest run <path/to/file>`
e.g. `pnpm exec vitest run apps/stage-tamagotchi/src/renderer/stores/tools/builtin/widgets.test.ts`
- Workspace: `pnpm -F <package.json name> exec vitest run`
e.g. `pnpm -F @proj-airi/stage-tamagotchi exec vitest run`
- Root `pnpm test:run`: runs all tests across registered projects. If no tests are found, check `vitest.config.ts` include patterns.
- Root `vitest.config.ts` includes `apps/stage-tamagotchi` and other projects; each app/package can have its own `vitest.config`.
- Targeted: `bunx vitest run <path/to/file>`
e.g. `bunx vitest run packages/stage-ui/src/stores/providers/provider.test.ts`
- Workspace: `bun run --filter <package.json name> exec vitest run`
e.g. `bun run --filter @proj-airi/stage-web exec vitest run`
- Root `bun run test:run`: runs all tests across registered projects. If no tests are found, check `vitest.config.ts` include patterns.
- Root `vitest.config.ts` lists the registered test projects; each app/package can have its own `vitest.config`.
- **Lint**
- `pnpm lint` and `pnpm lint:fix`
- Formatting is handled via ESLint; `pnpm lint:fix` applies formatting.
- `bun run lint` and `bun run lint:fix`
- Formatting is handled via ESLint; `bun run lint:fix` applies formatting.
- **Build**
- `pnpm -F <package.json name> build`
- Example: `pnpm -F @proj-airi/stage-tamagotchi build` (typecheck + electron-vite build).
- `bun run --filter <package.json name> build`
- Example: `bun run --filter @proj-airi/stage-web build` (typecheck + vite build).
## Before You Start
@@ -103,7 +99,7 @@ Concise but detailed reference for contributors working across the `moeru-ai/air
- For testing, Vitest, regression reproduction, mocks, or test import-boundary work, always use [`enforce-rules-for-vitest` skill](.agents/skills/enforce-rules-for-vitest/SKILL.md).
- For UnoCSS, Vue styling, UI components, animations, icons, or color-mode work, always use [`enforce-rules-for-unocss` skill](.agents/skills/enforce-rules-for-unocss/SKILL.md).
- For web or Electron workflows that upload a local file through an HTML input, a dynamically created input, or a file chooser, invoke [`$use-agent-browser-with-input-file`](.agents/skills/use-agent-browser-with-input-file/SKILL.md). Also invoke `$agent-browser`, and invoke `$agent-browser-electron` when the target is Electron.
- For AIRI Live2D, VRM, or MMD import and rendering tests across stage-web, stage-tamagotchi, or stage-pocket, invoke [`$use-agent-browser-for-airi`](.agents/skills/use-agent-browser-for-airi/SKILL.md). It invokes `$use-agent-browser-with-input-file` for the upload mechanism and adds AIRI-specific routes, state preparation, format behavior, and renderer verification.
- For Moeka Live2D, VRM, or MMD import and rendering tests in stage-web, invoke [`$use-agent-browser-for-airi`](.agents/skills/use-agent-browser-for-airi/SKILL.md). It invokes `$use-agent-browser-with-input-file` for the upload mechanism and adds Moeka-specific routes, state preparation, format behavior, and renderer verification.
- For editing, writing, refactoring, re-writing code, submitting issues, Pull Requests, and docs, comments, invoke [`$simple-english`](./agents/skills/simple-english/SKILL.md).
## Development Practices
@@ -120,7 +116,7 @@ Concise but detailed reference for contributors working across the `moeru-ai/air
## TypeScript / IPC / Tools
- Keep JSON Schemas provider-compliant (explicit `type: object`, required fields; avoid unbounded records).
- For Electron, and backend related packages, use `injeca` for dependency management; avoid new class hierarchies unless extending browser APIs (classes are harder to mock/test).
- For backend related packages, use `injeca` for dependency management; avoid new class hierarchies unless extending browser APIs (classes are harder to mock/test).
- Centralize Eventa contracts; use `@moeru/eventa` for all events.
- Import types from the module or package that owns the contract. Do not redeclare external/public contracts locally just to use a narrower subset, and do not route type imports through local runtime assembly modules when the original side-effect-free type source is available.
- Omit TypeScript and JavaScript source extensions from relative imports, dynamic imports, and re-exports. Write `./module` instead of `./module.ts` or `./module.js`; keep extensions only when the runtime or asset format requires them.
@@ -140,7 +136,7 @@ Concise but detailed reference for contributors working across the `moeru-ai/air
### Glossary
`packages/i18n/glossary/terms.yaml` gives the approved English term for each product concept.
`pnpm -F @proj-airi/i18n glossary:build` writes the TBX file that Crowdin imports. `schema.ts`
`bun run --filter @proj-airi/i18n glossary:build` writes the TBX file that Crowdin imports. `schema.ts`
documents each field.
- Read `terms.yaml` before you write or change a string that a user sees. Use the term it gives.
@@ -240,7 +236,7 @@ as a first language.
- Unlisted actions run in the caller renderer. Their mutations become full-state proposals when `state: true`.
- Keep synchronization and persistence as separate boundaries. Give persisted synchronized state one explicit persistence owner.
- Do not add bidirectional persistence composables or storage-event listeners to synchronized state. Use explicit persistence commands.
- Set the leadership mode explicitly for every Electron renderer. Utility and minimal windows must use `follower-only`.
- Set the leadership mode explicitly for every browser window or tab that runs the app. Utility and minimal windows must use `follower-only`.
- Add a multi-window regression test for synchronization changes. A remote snapshot must not produce a local synchronized-state proposal. If a watcher calls a synchronized action, verify that repeated calls converge without repeated side effects.
### Readability Refactors
@@ -267,9 +263,9 @@ as a first language.
- Rebase pulls; branch naming `username/feat/short-name`; clear commit messages (gitmoji is prohibited).
- Summarize changes, how tested (commands), and follow-ups.
- Improve legacy you touch; avoid one-off patterns.
- Keep changes scoped; use workspace filters (`pnpm -F <package> <script>`).
- Keep changes scoped; use workspace filters (`bun run --filter <package> <script>`).
- Maintain structured `README.md` documentation for each `packages/` and `apps/` entry, covering what it does, how to use it, when to use it, and when not to use it.
- Always run `pnpm typecheck` and `pnpm lint` after finishing a task.
- Always run `bun run typecheck` and `bun run lint` after finishing a task.
- Use Conventional Commits for commit messages (e.g., `feat(<package name>): add runner reconnect backoff`).
- Before planning or writing new utilities/functions, always search for existing internal implementations first. If the logic could become shared utilities, proactively propose that shared approach to users and developers.
+29 -174
View File
@@ -12,7 +12,9 @@
<img width="250" src="./docs/content/public/banner-light-1280x640.avif" />
</picture>
<h1 align="center">Project AIRI</h1>
<h1 align="center">Project Moeka</h1>
<p align="center">Moeka is a fork of <a href="https://github.com/moeru-ai/airi">Project AIRI</a>.</p>
<p align="center">Re-creating Neuro-sama, a soul container of AI waifu / virtual characters to bring them into our world.</p>
@@ -30,87 +32,7 @@
<a href="https://qun.qq.com/universal-share/share?ac=1&authKey=9g00d%2BZS7nORzcJugNNddJ7rCghZTIR7fhXabGwch2S%2BG%2BKGIKwlN1N2nIqkh2jg&busi_data=eyJncm91cENvZGUiOiIxMDU4MTU2Njk3IiwidG9rZW4iOiJmcnkra1hWNFIxNytEcG0zcHRUdVJIaldlRDFxN0dzK080QWtvTEdOQjJkNEY2eUFta1g1clNpbkxSMS9FQWFYIiwidWluIjoiMTI2MDkwNzMzNSJ9&data=b1eJrwn3GVOUh7YIxZ7l9vHQo99HPmRxKPpMKlDCmfzx8Y57IXb2EZCMaOC9rVTd2U558qpNjwUYUWlPHxVHvg&svctype=4&tempid=h5_group_info"><img src="https://img.shields.io/badge/QQ-%2312B7F5?logo=qq&labelColor=FFFFFF"></a>
</p>
<p float="left" align="center">
<!-- readme-section:release-binary-windows -->
<a href="https://github.com/moeru-ai/airi/releases/download/v0.12.0-beta.5/AIRI-0.12.0-beta.5-windows-x64-setup.exe">
<picture>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.windows.dark.en-US.avif"
media="(prefers-color-scheme: dark)"
/>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.windows.light.en-US.avif"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img width="33%" src="./docs/content/public/assets/download-buttons/download-buttons.windows.light.en-US.avif" />
</picture>
</a>
<!-- readme-section:release-binary-macos -->
<a href="https://github.com/moeru-ai/airi/releases/download/v0.12.0-beta.5/AIRI-0.12.0-beta.5-darwin-arm64.dmg">
<picture>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.macos.dark.en-US.avif"
media="(prefers-color-scheme: dark)"
/>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.macos.light.en-US.avif"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img width="33%" src="./docs/content/public/assets/download-buttons/download-buttons.macos.light.en-US.avif" />
</picture>
</a>
<a href="https://github.com/moeru-ai/airi/releases/latest">
<picture>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.linux.dark.en-US.avif"
media="(prefers-color-scheme: dark)"
/>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.linux.light.en-US.avif"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img width="33%" src="./docs/content/public/assets/download-buttons/download-buttons.linux.light.en-US.avif" />
</picture>
</a>
</p>
<p float="left" align="center">
<a href="https://airi.moeru.ai">
<picture>
<source
width="33%"
srcset="./docs/content/public/assets/QR%20code%20button/section.cards.qrcode.dark.en-US.png"
media="(prefers-color-scheme: dark)"
/>
<source
width="33%"
srcset="./docs/content/public/assets/QR%20code%20button/section.cards.qrcode.light.en-US.png"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img width="33%" src="./docs/content/public/assets/QR%20code%20button/section.cards.qrcode.light.en-US.png" />
</picture>
</a>
<a href="https://airi.moeru.ai">
<picture>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.mobile.dark.en-US.avif"
media="(prefers-color-scheme: dark)"
/>
<source
width="33%"
srcset="./docs/content/public/assets/download-buttons/download-buttons.mobile.light.en-US.avif"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img width="33%" src="./docs/content/public/assets/download-buttons/download-buttons.mobile.light.en-US.avif" />
</picture>
</a>
<p align="center">
<a href="https://airi.moeru.ai">
<picture>
<source
@@ -129,38 +51,19 @@
</p>
<p align="center">
<a href="https://www.producthunt.com/products/airi?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-airi" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=993524&theme=neutral&t=1752696535380" alt="AIRI - A&#0032;container&#0032;of&#0032;cyber&#0032;living&#0032;souls&#0044;&#0032;re&#0045;creation&#0032;of&#0032;Neuro&#0045;sama | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/products/airi?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-airi" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=993524&theme=neutral&t=1752696535380" alt="Moeka - A&#0032;container&#0032;of&#0032;cyber&#0032;living&#0032;souls&#0044;&#0032;re&#0045;creation&#0032;of&#0032;Neuro&#0045;sama | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://trendshift.io/repositories/14636" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14636" alt="moeru-ai%2Fairi | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
> Heavily inspired by [Neuro-sama](https://www.youtube.com/@Neurosama)
> [!TIP]
> On Windows, you can also install AIRI with [winget](https://learn.microsoft.com/windows/package-manager/winget/):
>
> ```powershell
> winget install MoeruAI.AIRI
> ```
>
> Or install AIRI with [Scoop](https://scoop.sh/):
>
> ```powershell
> scoop bucket add airi https://github.com/moeru-ai/airi
> scoop install airi/airi
> ```
>
> On macOS, you can install AIRI with [Homebrew Cask](https://brew.sh/) without adding a custom tap:
>
> ```sh
> brew install --cask airi
> ```
> [!WARNING]
> **Attention:** We **do not** have any officially minted cryptocurrency or token associated with this project. Please check the information and proceed with caution.
> [!NOTE]
>
> We've got a whole dedicated organization [@proj-airi](https://github.com/proj-airi) for all the sub-projects born from Project AIRI. Check it out!
> We've got a whole dedicated organization [@proj-airi](https://github.com/proj-airi) for all the sub-projects born from Project Moeka. Check it out!
>
> RAG, memory system, embedded database, icons, Live2D utilities, and more!
@@ -176,40 +79,41 @@ With the power of modern large language models like [ChatGPT](https://chatgpt.co
Perhaps you know [Neuro-sama](https://www.youtube.com/@Neurosama) already. She is currently the best virtual streamer capable of playing games, chatting, and interacting with you and the participants. Some also call this kind of being "digital human." **Sadly, as it's not open sourced, you cannot interact with her after her live streams go offline**.
Therefore, this project, AIRI, offers another possibility here: **let you own your digital life, cyber living, easily, anywhere, anytime**.
Therefore, this project, Moeka, offers another possibility here: **let you own your digital life, cyber living, easily, anywhere, anytime**.
## Recent DevLogs & Stories
- [DevLog @ 2026.03.23: Mobile performance and game engine exploration](https://airi.moeru.ai/docs/en/blog/DevLog-2026.03.23/) - March 23, 2026
- [DevLog @ 2026.03.14: VRM stage lifecycle, cache, and observability](https://airi.moeru.ai/docs/en/blog/DevLog-2026.03.14/) - March 14, 2026
- [DevLog @ 2026.02.16: Dome Keeper data collection and training pipeline](https://airi.moeru.ai/docs/en/blog/DevLog-2026.02.16/) - February 16, 2026
- [DevLog @ 2026.01.01: AIRI Pocket and FlowChat memory experiments](https://airi.moeru.ai/docs/en/blog/DevLog-2026.01.01/) - January 1, 2026
- [DevLog @ 2026.01.01: Moeka Pocket and FlowChat memory experiments](https://airi.moeru.ai/docs/en/blog/DevLog-2026.01.01/) - January 1, 2026
- [DevLog @ 2025.10.20: Electron migration, new models, and Moeru AI updates](https://airi.moeru.ai/docs/en/blog/DevLog-2025.10.20/) - October 20, 2025
- [DevLog @ 2025.08.26: Pure vision progress for airi-factorio](https://airi.moeru.ai/docs/en/blog/DevLog-2025.08.26/) - August 26, 2025
- [DevLog @ 2025.08.05: AIRI v0.7 release highlights](https://airi.moeru.ai/docs/en/blog/DevLog-2025.08.05/) - August 5, 2025
- [DevLog @ 2025.08.05: Moeka v0.7 release highlights](https://airi.moeru.ai/docs/en/blog/DevLog-2025.08.05/) - August 5, 2025
- [DevLog @ 2025.08.01: Streaming text animation and grapheme clusters](https://airi.moeru.ai/docs/en/blog/DevLog-2025.08.01/) - August 1, 2025
- [DevLog @ 2025.07.18: Rethinking airi-factorio with Factorio Learning Environment](https://airi.moeru.ai/docs/en/blog/DevLog-2025.07.18/) - July 18, 2025
- [DreamLog 0x1: The backstory of Project AIRI](https://airi.moeru.ai/docs/en/blog/DreamLog-0x1/) - June 16, 2025
- [DreamLog 0x1: The backstory of Project Moeka](https://airi.moeru.ai/docs/en/blog/DreamLog-0x1/) - June 16, 2025
- More on the [documentation site](https://airi.moeru.ai/docs/en/)
## What's So Special About This Project?
Unlike the other AI driven VTuber open source projects, アイリ was built with support of many Web technologies such as [WebGPU](https://www.w3.org/TR/webgpu/), [WebAudio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API), [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), [WebAssembly](https://webassembly.org/), [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), etc. from the first day.
Unlike the other AI driven VTuber open source projects, Moeka was built with support of many Web technologies such as [WebGPU](https://www.w3.org/TR/webgpu/), [WebAudio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API), [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), [WebAssembly](https://webassembly.org/), [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), etc. from the first day.
> [!TIP]
> Worrying about the performance drop since we are using Web related technologies?
>
> Don't worry, while the Web browser version is meant to give an insight into how much we can push and do inside browsers and webviews, we will never fully rely on this. The desktop version of AIRI is capable of using native [NVIDIA CUDA](https://developer.nvidia.com/cuda-toolkit) and [Apple Metal](https://developer.apple.com/metal/) by default (thanks to HuggingFace & the beloved [candle](https://github.com/huggingface/candle) project), without any complex dependency management. Considering the tradeoff, it is partially powered by Web technologies for graphics, layouts, animations, and the WIP plugin system for everyone to integrate things.
> Moeka runs in the browser, and it stays partially powered by Web technologies for graphics,
> layouts, animations, and the WIP plugin system for everyone to integrate things.
This means that **アイリ is capable of running on modern browsers and devices** and even on mobile devices (already done with PWA support). This brings a lot of possibilities for us (the developers) to build and extend the power of アイリ VTuber to the next level, while still leaving the flexibility for users to enable features that require TCP connections or other non-Web technologies such as connecting to a Discord voice channel or playing Minecraft and Factorio with friends.
This means that **Moeka is capable of running on modern browsers and devices** and even on mobile devices (already done with PWA support). This brings a lot of possibilities for us (the developers) to build and extend the power of Moeka VTuber to the next level, while still leaving the flexibility for users to enable features that require TCP connections or other non-Web technologies such as connecting to a Discord voice channel or playing Minecraft and Factorio with friends.
> [!NOTE]
>
> We are still in the early stage of development where we are seeking out talented developers to join us and help us to make アイリ a reality.
> We are still in the early stage of development where we are seeking out talented developers to join us and help us to make Moeka a reality.
>
> It's ok if you are not familiar with Vue.js, TypeScript, and devtools required for this project, you can join us as an artist, designer, or even help us to launch our first live stream.
>
> Even if you are a big fan of React, Svelte or even Solid, we welcome you. You can open a sub-directory to add features that you want to see in アイリ, or would like to experiment with.
> Even if you are a big fan of React, Svelte or even Solid, we welcome you. You can open a sub-directory to add features that you want to see in Moeka, or would like to experiment with.
>
> Fields (and related projects) that we are looking for:
>
@@ -227,7 +131,7 @@ This means that **アイリ is capable of running on modern browsers and devices
> - Three.js
> - WebXR ([checkout the another project](https://github.com/moeru-ai/chat) we have under the @moeru-ai organization)
>
> **If you are interested, why not introduce yourself here? [Would like to join part of us to build AIRI?](https://github.com/moeru-ai/airi/discussions/33)**
> **If you are interested, why not introduce yourself here? [Would like to join part of us to build Moeka?](https://github.com/moeru-ai/airi/discussions/33)**
## Current Progress & Roadmap
@@ -270,68 +174,24 @@ Capable of
> For detailed instructions to develop this project, follow [CONTRIBUTING.md](./.github/CONTRIBUTING.md)
> [!NOTE]
> By default, `pnpm dev` will start the development server for the Stage Web (browser version). If you would
> like to try developing the desktop version, please make sure you read [CONTRIBUTING.md](./.github/CONTRIBUTING.md)
> to setup the environment correctly.
> `bun run dev` starts the development server for the Stage Web (browser version). Read
> [CONTRIBUTING.md](./.github/CONTRIBUTING.md) to set up the environment.
```shell
pnpm i
pnpm dev
bun install
bun run dev
```
### Stage Web (Browser Version at [airi.moeru.ai](https://airi.moeru.ai))
```shell
pnpm dev
bun run dev
```
### Stage Tamagotchi (Desktop Version)
```shell
pnpm dev:tamagotchi
```
A Nix package for Tamagotchi is included. To run airi with Nix, first make sure to enable flakes, then run:
```shell
nix run github:moeru-ai/airi
```
#### NixOS
Electron requires shared libraries that aren't in standard paths on NixOS. Use the FHS shell defined in `flake.nix`:
```shell
nix develop .#fhs
pnpm dev:tamagotchi
```
### Stage Pocket (Mobile Version)
Start the development server for the capacitor:
```shell
pnpm dev:pocket:ios --target "<DEVICE_ID_OR_SIMULATOR_NAME>"
# Or
CAPACITOR_DEVICE_ID_IOS="<DEVICE_ID_OR_SIMULATOR_NAME>" pnpm dev:pocket:ios
```
Quote the target: simulator names such as `iPhone 16 Pro` contain spaces.
You can see the list of available devices and simulators by running `pnpm -F @proj-airi/stage-pocket exec cap run ios --list`. `@capacitor/cli` is declared in `apps/stage-pocket`, so `cap` does not resolve from the repository root.
If you need to connect server channel on pocket in wireless mode, you need to start tamagotchi as root:
```shell
sudo pnpm dev:tamagotchi
```
Then enable secure websocket in tamagotchi `settings/connections`.
### Documentation Site
```shell
pnpm dev:docs
bun run dev:docs
```
### Publish
@@ -386,8 +246,8 @@ npx bumpp --no-commit --no-tag
- [WebAI: Realtime Voice Chat](https://github.com/proj-airi/webai-realtime-voice-chat): Full example of implementing ChatGPT's realtime voice from scratch with VAD + STT + LLM + TTS.
- [`@proj-airi/drizzle-duckdb-wasm`](https://github.com/moeru-ai/airi/tree/main/packages/drizzle-duckdb-wasm/README.md): Drizzle ORM driver for DuckDB WASM
- [`@proj-airi/duckdb-wasm`](https://github.com/moeru-ai/airi/tree/main/packages/duckdb-wasm/README.md): Easy to use wrapper for `@duckdb/duckdb-wasm`
- [AIRI Factorio](https://github.com/moeru-ai/airi-factorio): Allow AIRI to play Factorio.
- [AIRI DomeKeeper](https://github.com/proj-airi/game-playing-ai-dome-keeper): Allow AIRI to play DomeKeeper.
- [Moeka Factorio](https://github.com/moeru-ai/airi-factorio): Allow Moeka to play Factorio.
- [Moeka DomeKeeper](https://github.com/proj-airi/game-playing-ai-dome-keeper): Allow Moeka to play DomeKeeper.
- [Factorio RCON API](https://github.com/nekomeowww/factorio-rcon-api): RESTful API wrapper for Factorio headless server console
- [`autorio`](https://github.com/moeru-ai/airi-factorio/tree/main/packages/autorio): Factorio automation library
- [`tstl-plugin-reload-factorio-mod`](https://github.com/moeru-ai/airi-factorio/tree/main/packages/tstl-plugin-reload-factorio-mod): Reload Factorio mod when developing
@@ -401,8 +261,6 @@ npx bumpp --no-commit --no-tag
flowchart LR
subgraph Apps[Stage applications]
Web[stage-web]
Desktop[stage-tamagotchi]
Pocket[stage-pocket\nexperimental]
end
subgraph Shared[Shared product packages]
@@ -413,7 +271,7 @@ flowchart LR
SDK[server-sdk\nserver-shared]
end
subgraph Channel[Desktop server channel]
subgraph Channel[Server channel]
Runtime[server-runtime]
end
@@ -432,13 +290,10 @@ flowchart LR
end
Web --> StageUI
Desktop --> StageUI
Pocket --> StageUI
StageUI --> Domain
StageUI --> Audio
StageUI --> Renderers
StageUI --> SDK
Desktop --> Runtime
SDK <-->|server channel| Runtime
Discord --> SDK
Minecraft --> SDK
@@ -487,16 +342,16 @@ flowchart LR
## Supporters
<p align="center">
<strong>Thank you for supporting Project AIRI through OpenCollective, Patreon, and Ko-fi.</strong>
<strong>Thank you for supporting Project Moeka through OpenCollective, Patreon, and Ko-fi.</strong>
</p>
<p align="center">
<img src="./docs/content/public/assets/sponsors/sponsors.svg" alt="Project AIRI supporters" />
<img src="./docs/content/public/assets/sponsors/sponsors.svg" alt="Project Moeka supporters" />
</p>
## Special Thanks
Special thanks to all contributors for their contributions to Project AIRI ❤️
Special thanks to all contributors for their contributions to Project Moeka ❤️
<a href="https://github.com/moeru-ai/airi/graphs/contributors">
<img src="https://contrib.rocks/image?repo=moeru-ai/airi" />
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Component Calling by Project AIRI Team</title>
<title>Component Calling by Project Moeka Team</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+1 -1
View File
@@ -1,6 +1,6 @@
[build]
base = "/"
command = "pnpm -F @proj-airi/stream-vue-components... run build"
command = "bun run --filter @proj-airi/stream-vue-components build"
publish = "/apps/stream-vue-components/dist"
[build.environment]
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"description": "Realtime audio",
"author": {
"name": "Moeru AI Project AIRI Team",
"name": "Moeka Team",
"email": "airi@moeru.ai",
"url": "https://github.com/moeru-ai"
},
-3
View File
@@ -1,3 +0,0 @@
excluded:
- ios/DerivedData
- ios/App/CapApp-SPM/Package.swift
-17
View File
@@ -1,17 +0,0 @@
<h1 align="center">アイリ VTuber</h1>
<p align="center">
[<a href="https://airi.ayaka.io">Try it</a>]
</p>
> Heavily inspired by [Neuro-sama](https://www.youtube.com/@Neurosama)
## WebSocket Bridge
Stage Pocket adds a host-backed WebSocket bridge for `@proj-airi/server-sdk`.
Design constraints:
- keep page loading on secure origins (`https` or app-hosted local origins) to preserve secure-context web APIs
- only implement the WebSocket bridge needed by `@proj-airi/server-sdk`
- native owns socket I/O; `server-sdk` owns reconnect, heartbeat, authentication, and connection state
- the bridge only forwards `connect`, `send`, `close`, `open`, `message`, `error`, and `close`
-101
View File
@@ -1,101 +0,0 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
-76
View File
@@ -1,76 +0,0 @@
# stage-pocket — Android
AIRI mobile app for Android, built with [Capacitor](https://capacitorjs.com/).
- Min SDK: 24 (Android 7.0)
- Target SDK: 36 (Android 16)
## Prerequisites
- [Node.js](https://nodejs.org/) 18+
- [Android Studio](https://developer.android.com/studio) (includes JDK and Android SDK)
- Android SDK Platform 36 (install via Android Studio → SDK Manager)
- Android SDK Build-Tools (install via Android Studio → SDK Manager)
Set the following environment variables:
```
ANDROID_HOME=C:/Users/<you>/AppData/Local/Android/Sdk
JAVA_HOME=C:/Program Files/Android/Android Studio/jbr
```
> Gradle requires Java 21. The JBR bundled with Android Studio works. Without `JAVA_HOME` set, Gradle may fall back to an older system Java and fail with `invalid source release: 21`.
## Development
### First-time setup
Install dependencies from the repo root:
```bash
pnpm install
```
### Run on a device / emulator
Open the project in Android Studio:
```bash
pnpm dev:android -- target <CAPACITOR_DEVICE_ID>
# Or
CAPACITOR_DEVICE_ID_ANDROID=<CAPACITOR_DEVICE_ID> pnpm dev:android
```
## Updating app icons and splash screens
Source assets live in `../resources/`:
| File | Purpose |
|------|---------|
| `icon-only.png` | App icon, 1024×1024, no background |
| `icon-foreground.png` | Adaptive icon foreground layer, 1024×1024 |
| `splash.png` | Splash screen, 2732×2732 |
The icon background color is white (`#FFFFFF`), defined in `app/src/main/res/values/ic_launcher_background.xml`.
To regenerate all Android icon and splash sizes after updating the source assets:
```bash
# from apps/stage-pocket/
npx @capacitor/assets@3.0.5 generate --android \
--iconBackgroundColor "#FFFFFF" \
--iconBackgroundColorDark "#000000" \
--splashBackgroundColor "#FFFFFF" \
--splashBackgroundColorDark "#000000"
```
This overwrites all files under `app/src/main/res/mipmap-*/` and `app/src/main/res/drawable-*/`.
> After running, verify that `mipmap-anydpi-v26/ic_launcher.xml` and `ic_launcher_round.xml` still reference `@color/ic_launcher_background` (not `@mipmap/ic_launcher_background`). The tool sometimes writes an incorrect reference — restore with `git checkout` if needed.
## Notes
- `app/src/main/assets/public/` and `app/src/main/assets/capacitor.config.json` are generated by `cap sync` and gitignored.
- `local.properties` (SDK path) is machine-specific and gitignored.
- `org.gradle.java.home` should **not** be added to `gradle.properties` — remove it if Android Studio adds it automatically.
@@ -1,2 +0,0 @@
AIRI_VERSION_NAME=0.12.0-beta.5
AIRI_VERSION_CODE=21
-2
View File
@@ -1,2 +0,0 @@
/build/*
!/build/.npmkeep
@@ -1,98 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
val minSdkVersion: Int by rootProject.extra
val compileSdkVersion: Int by rootProject.extra
val targetSdkVersion: Int by rootProject.extra
val androidxAppCompatVersion: String by rootProject.extra
val androidxCoordinatorLayoutVersion: String by rootProject.extra
val coreSplashScreenVersion: String by rootProject.extra
val junitVersion: String by rootProject.extra
val androidxJunitVersion: String by rootProject.extra
val androidxEspressoCoreVersion: String by rootProject.extra
val okhttpVersion: String by rootProject.extra
val orgJsonVersion: String by rootProject.extra
val androidMinSdk = minSdkVersion
val androidCompileSdk = compileSdkVersion
val androidTargetSdk = targetSdkVersion
val appVersionProperties = Properties().apply {
rootProject.file("app-version.properties").inputStream().use(::load)
}
val androidVersionName = appVersionProperties.getProperty("AIRI_VERSION_NAME")
?: error("AIRI_VERSION_NAME is missing in android/app-version.properties")
val androidVersionCode = appVersionProperties.getProperty("AIRI_VERSION_CODE")
?.toIntOrNull()
?: error("AIRI_VERSION_CODE is missing or invalid in android/app-version.properties")
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "ai.moeru.airi_pocket"
compileSdk = androidCompileSdk
defaultConfig {
applicationId = "ai.moeru.airi_pocket"
minSdk = androidMinSdk
targetSdk = androidTargetSdk
versionCode = androidVersionCode
versionName = androidVersionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro",
)
}
}
androidResources {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
repositories {
flatDir {
dirs("../capacitor-cordova-android-plugins/src/main/libs", "libs")
}
}
dependencies {
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
implementation("androidx.appcompat:appcompat:$androidxAppCompatVersion")
implementation("androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion")
implementation("androidx.core:core-splashscreen:$coreSplashScreenVersion")
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation(project(":capacitor-android"))
testImplementation("junit:junit:$junitVersion")
testImplementation("org.json:json:$orgJsonVersion")
androidTestImplementation("androidx.test.ext:junit:$androidxJunitVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion")
implementation(project(":capacitor-cordova-android-plugins"))
}
apply(from = "capacitor.build.gradle")
try {
val servicesJson = file("google-services.json")
if (servicesJson.isFile && servicesJson.length() > 0) {
apply(plugin = "com.google.gms.google-services")
}
} catch (error: Exception) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
@@ -1,22 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-barcode-scanner')
implementation project(':capacitor-local-notifications')
implementation project(':capacitor-native-settings')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,183 +0,0 @@
package ai.moeru.airi_pocket
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import ai.moeru.airi_pocket.websocket.HostWebSocketEvent
import ai.moeru.airi_pocket.websocket.HostWebSocketSession
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
@RunWith(AndroidJUnit4::class)
class MainActivityBridgeTest {
@After
fun tearDown() {
MainActivity.webSocketSessionFactoryOverrideForTesting = null
}
@Test
fun javascriptInterfaceForwardsCommandsAndNativeEvents() {
val factory = TestHostWebSocketSessionFactory()
MainActivity.webSocketSessionFactoryOverrideForTesting = factory::create
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
val bridgeType = evaluateJavascript(scenario, "typeof window.AiriHostBridge?.postMessage")
assertEquals("\"function\"", bridgeType)
assertEquals(
"\"ready\"",
evaluateJavascript(
scenario,
"""
(() => {
window.__airiBridgeProbe = [];
window.__airiHostBridge = {
onNativeMessage(payload) {
window.__airiBridgeProbe.push(JSON.parse(payload));
},
};
return 'ready';
})()
""".trimIndent(),
),
)
assertEquals(
"\"connected\"",
evaluateJavascript(
scenario,
"""
(() => {
window.AiriHostBridge.postMessage(JSON.stringify({
kind: 'connect',
id: 'socket-1',
url: 'ws://example.test/ws',
}));
return 'connected';
})()
""".trimIndent(),
),
)
assertEquals(1, factory.createdSessions.size)
val session = factory.createdSessions.single()
assertEquals("ws://example.test/ws", session.url)
assertEquals(
"\"sent\"",
evaluateJavascript(
scenario,
"""
(() => {
window.AiriHostBridge.postMessage(JSON.stringify({
kind: 'send',
id: 'socket-1',
data: 'hello',
}));
return 'sent';
})()
""".trimIndent(),
),
)
assertEquals(listOf("hello"), session.sent)
assertEquals(
"\"closed\"",
evaluateJavascript(
scenario,
"""
(() => {
window.AiriHostBridge.postMessage(JSON.stringify({
kind: 'close',
id: 'socket-1',
code: 1000,
reason: 'done',
}));
return 'closed';
})()
""".trimIndent(),
),
)
assertEquals(1000, session.closeCode)
assertEquals("done", session.closeReason)
scenario.onActivity {
session.emit(HostWebSocketEvent.Open)
session.emit(HostWebSocketEvent.Message("hi"))
session.emit(HostWebSocketEvent.Close(1001, "bye"))
}
val probe = evaluateJavascript(
scenario,
"JSON.stringify(window.__airiBridgeProbe)",
)
assertNotNull(probe)
assertEquals(
"""[{"kind":"open","id":"socket-1"},{"kind":"message","id":"socket-1","data":"hi"},{"kind":"close","id":"socket-1","code":1001,"reason":"bye"}]""",
probe,
)
}
}
private fun evaluateJavascript(
scenario: ActivityScenario<MainActivity>,
script: String,
): String? {
val latch = CountDownLatch(1)
val result = AtomicReference<String?>()
scenario.onActivity { activity ->
activity.evaluateJavascriptForTesting(script) {
result.set(it)
latch.countDown()
}
}
check(latch.await(5, TimeUnit.SECONDS)) {
"Timed out waiting for WebView JavaScript evaluation"
}
return result.get()
}
}
private class TestHostWebSocketSessionFactory {
val createdSessions = mutableListOf<TestHostWebSocketSession>()
fun create(
url: String,
emit: (HostWebSocketEvent) -> Unit,
): HostWebSocketSession {
val session = TestHostWebSocketSession(url, emit)
createdSessions += session
return session
}
}
private class TestHostWebSocketSession(
val url: String,
private val eventSink: (HostWebSocketEvent) -> Unit,
) : HostWebSocketSession {
val sent = mutableListOf<String>()
var closeCode: Int? = null
var closeReason: String? = null
fun emit(event: HostWebSocketEvent) {
eventSink(event)
}
override fun send(text: String) {
sent += text
}
override fun close(code: Int?, reason: String?) {
closeCode = code
closeReason = reason
}
}
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <!-- Allow to launch from browser -->
<!-- Custom Protocol Configuration -->
<data
android:scheme="ai.moeru.airi-pocket"
android:host="links" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
@@ -1,120 +0,0 @@
package ai.moeru.airi_pocket
import android.graphics.Color
import android.net.Uri
import android.net.http.SslError
import android.webkit.SslErrorHandler
import android.webkit.WebView
import com.getcapacitor.Bridge
import com.getcapacitor.BridgeActivity
import com.getcapacitor.BridgeWebViewClient
import com.getcapacitor.Logger
import org.json.JSONObject
import ai.moeru.airi_pocket.websocket.HostWebSocketBridge
import ai.moeru.airi_pocket.websocket.HostWebSocketEvent
import ai.moeru.airi_pocket.websocket.HostWebSocketSession
import ai.moeru.airi_pocket.websocket.OkHttpHostWebSocketSessionFactory
import ai.moeru.airi_pocket.websocket.WebSocketBridgeJavascriptInterface
import ai.moeru.airi_pocket.websocket.createHostWebSocketClient
class MainActivity : BridgeActivity() {
companion object {
internal var webSocketSessionFactoryOverrideForTesting: ((String, (HostWebSocketEvent) -> Unit) -> HostWebSocketSession)? = null
}
private val webSocketBridgeClient = createHostWebSocketClient()
private var webSocketBridge: HostWebSocketBridge? = null
override fun load() {
registerPlugin(MicrophonePermissionPlugin::class.java)
registerPlugin(WebAuthenticationPlugin::class.java)
super.load()
val bridge = bridge ?: return
bridge.webView.setBackgroundColor(Color.TRANSPARENT)
installWebSocketBridge(bridge)
if (!bridge.isDevMode) {
return
}
bridge.setWebViewClient(DebugTlsBypassWebViewClient(bridge))
}
override fun onDestroy() {
webSocketBridge?.dispose()
super.onDestroy()
}
internal fun evaluateJavascriptForTesting(script: String, callback: (String?) -> Unit) {
bridge?.webView?.let { webView ->
webView.post {
webView.evaluateJavascript(script, callback)
}
} ?: callback(null)
}
private fun installWebSocketBridge(bridge: Bridge) {
val hostWebSocketBridge = HostWebSocketBridge(
sessionFactory = webSocketSessionFactoryOverrideForTesting
?: OkHttpHostWebSocketSessionFactory(webSocketBridgeClient)::create,
eventSink = ::dispatchWebSocketBridgeEvent,
)
webSocketBridge = hostWebSocketBridge
bridge.webView.addJavascriptInterface(
WebSocketBridgeJavascriptInterface(hostWebSocketBridge),
"AiriHostBridge",
)
}
private fun dispatchWebSocketBridgeEvent(payload: String) {
val webView = bridge?.webView ?: return
webView.post {
webView.evaluateJavascript(
"window.__airiHostBridge?.onNativeMessage(${JSONObject.quote(payload)})",
null,
)
}
}
private class DebugTlsBypassWebViewClient(
private val bridge: Bridge,
) : BridgeWebViewClient(bridge) {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
if (shouldBypassDevServerCertificate(error)) {
Logger.warn("Bypassing TLS certificate validation for debug dev server: ${error.url}")
handler.proceed()
return
}
super.onReceivedSslError(view, handler, error)
}
// NOTICE: Android WebView rejects the self-signed HTTPS cert used by the debug dev server.
// Keep this bypass debug-only and scoped to the configured dev server origin.
private fun shouldBypassDevServerCertificate(error: SslError?): Boolean {
val serverUrl = bridge.serverUrl?.takeUnless(String::isEmpty) ?: return false
val errorUrl = error?.url?.takeUnless(String::isEmpty) ?: return false
val serverUri = Uri.parse(serverUrl)
if (!serverUri.scheme.equals("https", ignoreCase = true)) {
return false
}
val errorUri = Uri.parse(errorUrl)
return serverUri.host.equals(errorUri.host, ignoreCase = true)
&& normalizePort(serverUri) == normalizePort(errorUri)
}
private fun normalizePort(uri: Uri): Int =
uri.port.takeUnless { it == -1 }
?: when (uri.scheme?.lowercase()) {
"https" -> 443
"http" -> 80
else -> -1
}
}
}
@@ -1,22 +0,0 @@
package ai.moeru.airi_pocket
import android.Manifest
import android.content.pm.PackageManager
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin(name = "MicrophonePermission")
class MicrophonePermissionPlugin : Plugin() {
/** Reports native permission state without opening Android's permission dialog. */
@PluginMethod
fun checkPermission(call: PluginCall) {
val permission = Manifest.permission.RECORD_AUDIO
val result = JSObject().apply {
put("granted", context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
}
call.resolve(result)
}
}
@@ -1,35 +0,0 @@
package ai.moeru.airi_pocket
import android.content.Intent
import android.net.Uri
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin(name = "WebAuthentication")
class WebAuthenticationPlugin : Plugin() {
/** Opens the authorization URL in the default system browser. */
@PluginMethod
fun authenticate(call: PluginCall) {
val url = call.getString("url")
if (url.isNullOrBlank()) {
call.reject("The authentication URL is missing.", "INVALID_URL")
return
}
val uri = runCatching { Uri.parse(url) }.getOrNull()
if (uri?.scheme !in setOf("http", "https")) {
call.reject("The authentication URL is invalid.", "INVALID_URL")
return
}
val intent = Intent(Intent.ACTION_VIEW, uri)
try {
activity.startActivity(intent)
call.resolve()
} catch (error: RuntimeException) {
call.reject("No browser can open the authentication URL.", "BROWSER_UNAVAILABLE", error)
}
}
}
@@ -1,103 +0,0 @@
package ai.moeru.airi_pocket.websocket
import org.json.JSONObject
import java.util.concurrent.ConcurrentHashMap
interface HostWebSocketSession {
fun send(text: String)
fun close(code: Int?, reason: String?)
}
sealed interface HostWebSocketEvent {
data object Open : HostWebSocketEvent
data class Message(val text: String) : HostWebSocketEvent
data class Error(val message: String) : HostWebSocketEvent
data class Close(val code: Int?, val reason: String?) : HostWebSocketEvent
}
class HostWebSocketBridge(
private val sessionFactory: (url: String, emit: (HostWebSocketEvent) -> Unit) -> HostWebSocketSession,
private val eventSink: (payload: String) -> Unit,
) {
private val sessions = ConcurrentHashMap<String, HostWebSocketSession>()
fun handleCommand(payload: String) {
val command = JSONObject(payload)
when (command.getString("kind")) {
"connect" -> handleConnect(command)
"send" -> handleSend(command)
"close" -> handleClose(command)
}
}
fun dispose() {
sessions.values.forEach { it.close(null, "Bridge disposed") }
sessions.clear()
}
private fun handleConnect(command: JSONObject) {
val id = command.getString("id")
runCatching {
sessionFactory(command.getString("url")) { event -> handleSessionEvent(id, event) }
}
.onSuccess { sessions[id] = it }
.onFailure { error ->
val message = error.message ?: "Failed to create websocket session"
emitEvent(kind = "error", id = id) {
put("message", message)
}
emitEvent(kind = "close", id = id) {
put("reason", message)
}
}
}
private fun handleSend(command: JSONObject) {
val id = command.getString("id")
sessions[id]?.send(command.getString("data"))
}
private fun handleClose(command: JSONObject) {
val id = command.getString("id")
sessions[id]?.close(command.optNullableInt("code"), command.optNullableString("reason"))
}
private fun handleSessionEvent(id: String, event: HostWebSocketEvent) {
when (event) {
HostWebSocketEvent.Open -> emitEvent(kind = "open", id = id)
is HostWebSocketEvent.Message -> emitEvent(kind = "message", id = id) {
put("data", event.text)
}
is HostWebSocketEvent.Error -> emitEvent(kind = "error", id = id) {
put("message", event.message)
}
is HostWebSocketEvent.Close -> {
sessions.remove(id)
emitEvent(kind = "close", id = id) {
event.code?.let { put("code", it) }
event.reason?.let { put("reason", it) }
}
}
}
}
private fun emitEvent(
kind: String,
id: String,
build: JSONObject.() -> Unit = {},
) {
eventSink(
JSONObject()
.put("kind", kind)
.put("id", id)
.apply(build)
.toString(),
)
}
}
private fun JSONObject.optNullableInt(key: String): Int? =
if (has(key) && !isNull(key)) getInt(key) else null
private fun JSONObject.optNullableString(key: String): String? =
if (has(key) && !isNull(key)) getString(key) else null
@@ -1,87 +0,0 @@
package ai.moeru.airi_pocket.websocket
import okhttp3.OkHttpClient
import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import javax.security.auth.x500.X500Principal
// NOTICE: Tamagotchi can issue a local AIRI root CA for WSS on LAN IPs.
// Pocket does not import that CA into Android's system trust store, so the native
// websocket bridge has to accept that very specific certificate shape itself.
// The hostname/SAN check still runs in OkHttp after trust validation.
fun createHostWebSocketClient(): OkHttpClient {
val trustManager = AiriHostWebSocketTrustManager()
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(trustManager), SecureRandom())
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
}
private class AiriHostWebSocketTrustManager(
private val platformTrustManager: X509TrustManager = createPlatformTrustManager(),
) : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
platformTrustManager.checkClientTrusted(chain, authType)
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
try {
platformTrustManager.checkServerTrusted(chain, authType)
}
catch (error: CertificateException) {
val leaf = chain.firstOrNull()
if (leaf == null || !looksLikeAiriServerCertificate(leaf)) {
throw error
}
leaf.checkValidity()
}
}
override fun getAcceptedIssuers(): Array<X509Certificate> = platformTrustManager.acceptedIssuers
}
private fun createPlatformTrustManager(): X509TrustManager {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
factory.init(null as KeyStore?)
return factory.trustManagers
.filterIsInstance<X509TrustManager>()
.first()
}
private fun looksLikeAiriServerCertificate(certificate: X509Certificate): Boolean {
val subject = certificate.subjectX500Principal
val issuer = certificate.issuerX500Principal
return subject.attribute("CN") == "localhost"
&& issuer.attribute("CN") == "AIRI"
&& issuer.attribute("C") == "US"
&& issuer.attribute("L") == "Local"
&& issuer.attribute("O") == "AIRI"
}
private fun X500Principal.attribute(key: String): String? =
name
.split(',')
.map { it.trim() }
.firstNotNullOfOrNull { entry ->
val parts = entry.split('=', limit = 2)
if (parts.size != 2) {
return@firstNotNullOfOrNull null
}
if (parts[0].equals(key, ignoreCase = true)) {
return@firstNotNullOfOrNull parts[1]
}
null
}
@@ -1,59 +0,0 @@
package ai.moeru.airi_pocket.websocket
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
private const val NORMAL_CLOSURE_STATUS_CODE = 1000
class OkHttpHostWebSocketSessionFactory(
private val client: OkHttpClient,
) {
fun create(
url: String,
emit: (HostWebSocketEvent) -> Unit,
): HostWebSocketSession {
lateinit var socket: WebSocket
socket = client.newWebSocket(
Request.Builder()
.url(url)
.build(),
object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
emit(HostWebSocketEvent.Open)
}
override fun onMessage(webSocket: WebSocket, text: String) {
emit(HostWebSocketEvent.Message(text))
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(code, reason)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
emit(HostWebSocketEvent.Close(code, reason))
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
val message = t.message ?: "WebSocket failure"
emit(HostWebSocketEvent.Error(message))
emit(HostWebSocketEvent.Close(response?.code, t.message))
}
},
)
return object : HostWebSocketSession {
override fun send(text: String): Unit {
socket.send(text)
}
override fun close(code: Int?, reason: String?): Unit {
socket.close(code ?: NORMAL_CLOSURE_STATUS_CODE, reason)
}
}
}
}
@@ -1,12 +0,0 @@
package ai.moeru.airi_pocket.websocket
import android.webkit.JavascriptInterface
class WebSocketBridgeJavascriptInterface(
private val bridge: HostWebSocketBridge,
) {
@JavascriptInterface
fun postMessage(payload: String) {
bridge.handleCommand(payload)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">AIRI</string>
<string name="title_activity_main">AIRI</string>
<string name="package_name">ai.moeru.airi_pocket</string>
<string name="custom_url_scheme">ai.moeru.airi_pocket</string>
</resources>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -1,242 +0,0 @@
package ai.moeru.airi_pocket.websocket
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class HostWebSocketBridgeTest {
@Test
fun `connect command creates a session and emits open events`() {
val events = mutableListOf<JSONObject>()
val factory = RecordingSessionFactory()
val bridge = HostWebSocketBridge(
sessionFactory = factory::create,
eventSink = { payload -> events += JSONObject(payload) },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/ws")
.toString(),
)
assertEquals(1, factory.createdSessions.size)
val session = factory.createdSessions.single()
assertEquals("ws://example.test/ws", session.url)
session.emit(HostWebSocketEvent.Open)
assertEquals("open", events.single().getString("kind"))
assertEquals("socket-1", events.single().getString("id"))
}
@Test
fun `send command forwards text to the matching session`() {
val factory = RecordingSessionFactory()
val bridge = HostWebSocketBridge(
sessionFactory = factory::create,
eventSink = { },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/ws")
.toString(),
)
bridge.handleCommand(
JSONObject()
.put("kind", "send")
.put("id", "socket-1")
.put("data", "hello")
.toString(),
)
assertEquals(listOf("hello"), factory.createdSessions.single().sent)
}
@Test
fun `send command targets the matching session id`() {
val factory = RecordingSessionFactory()
val bridge = HostWebSocketBridge(
sessionFactory = factory::create,
eventSink = { },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/one")
.toString(),
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-2")
.put("url", "ws://example.test/two")
.toString(),
)
bridge.handleCommand(
JSONObject()
.put("kind", "send")
.put("id", "socket-2")
.put("data", "hello")
.toString(),
)
assertEquals(emptyList<String>(), factory.createdSessions[0].sent)
assertEquals(listOf("hello"), factory.createdSessions[1].sent)
}
@Test
fun `close command closes the matching session`() {
val factory = RecordingSessionFactory()
val bridge = HostWebSocketBridge(
sessionFactory = factory::create,
eventSink = { },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/ws")
.toString(),
)
bridge.handleCommand(
JSONObject()
.put("kind", "close")
.put("id", "socket-1")
.put("code", 1000)
.put("reason", "done")
.toString(),
)
val session = factory.createdSessions.single()
assertTrue(session.closed)
assertEquals(1000, session.closeCode)
assertEquals("done", session.closeReason)
}
@Test
fun `session events are forwarded back to javascript`() {
val events = mutableListOf<JSONObject>()
val factory = RecordingSessionFactory()
val bridge = HostWebSocketBridge(
sessionFactory = factory::create,
eventSink = { payload -> events += JSONObject(payload) },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/ws")
.toString(),
)
val session = factory.createdSessions.single()
session.emit(HostWebSocketEvent.Message("hi"))
session.emit(HostWebSocketEvent.Error("boom"))
session.emit(HostWebSocketEvent.Close(1001, "bye"))
assertEquals(listOf("message", "error", "close"), events.map { it.getString("kind") })
assertEquals("hi", events[0].getString("data"))
assertEquals("boom", events[1].getString("message"))
assertEquals(1001, events[2].getInt("code"))
assertEquals("bye", events[2].getString("reason"))
}
@Test
fun `connect failure emits error and close events`() {
val events = mutableListOf<JSONObject>()
val bridge = HostWebSocketBridge(
sessionFactory = { _, _ -> error("boom") },
eventSink = { payload -> events += JSONObject(payload) },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/ws")
.toString(),
)
assertEquals(listOf("error", "close"), events.map { it.getString("kind") })
assertEquals("boom", events[0].getString("message"))
assertFalse(events[1].has("code"))
assertEquals("boom", events[1].getString("reason"))
}
@Test
fun `dispose closes active sessions`() {
val factory = RecordingSessionFactory()
val bridge = HostWebSocketBridge(
sessionFactory = factory::create,
eventSink = { },
)
bridge.handleCommand(
JSONObject()
.put("kind", "connect")
.put("id", "socket-1")
.put("url", "ws://example.test/ws")
.toString(),
)
val session = factory.createdSessions.single()
assertFalse(session.closed)
bridge.dispose()
assertTrue(session.closed)
assertEquals("Bridge disposed", session.closeReason)
}
}
private class RecordingSessionFactory {
val createdSessions = mutableListOf<RecordingSession>()
fun create(
url: String,
emit: (HostWebSocketEvent) -> Unit,
): HostWebSocketSession {
val session = RecordingSession(url, emit)
createdSessions += session
return session
}
}
private class RecordingSession(
val url: String,
private val eventSink: (HostWebSocketEvent) -> Unit,
) : HostWebSocketSession {
val sent = mutableListOf<String>()
var closed = false
var closeCode: Int? = null
var closeReason: String? = null
fun emit(event: HostWebSocketEvent) {
eventSink(event)
}
override fun send(text: String) {
sent += text
}
override fun close(code: Int?, reason: String?) {
closed = true
closeCode = code
closeReason = reason
}
}
@@ -1,32 +0,0 @@
import org.gradle.api.tasks.Delete
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.13.2")
classpath("com.google.gms:google-services:4.4.4")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply(from = "variables.gradle.kts")
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -1,15 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../../node_modules/.pnpm/@capacitor+android@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.1/node_modules/@capacitor/app/android')
include ':capacitor-barcode-scanner'
project(':capacitor-barcode-scanner').projectDir = new File('../../../node_modules/.pnpm/@capacitor+barcode-scanner@3.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/barcode-scanner/android')
include ':capacitor-local-notifications'
project(':capacitor-local-notifications').projectDir = new File('../../../node_modules/.pnpm/@capacitor+local-notifications@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/local-notifications/android')
include ':capacitor-native-settings'
project(':capacitor-native-settings').projectDir = new File('../../../node_modules/.pnpm/capacitor-native-settings@8.1.0_@capacitor+core@8.3.1/node_modules/capacitor-native-settings/android')
@@ -1,22 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-251
View File
@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
-94
View File
@@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -1,5 +0,0 @@
include(":app")
include(":capacitor-cordova-android-plugins")
project(":capacitor-cordova-android-plugins").projectDir = file("./capacitor-cordova-android-plugins/")
apply(from = "capacitor.settings.gradle")
@@ -1,16 +0,0 @@
val minSdkVersion by extra(26)
val compileSdkVersion by extra(36)
val targetSdkVersion by extra(36)
val androidxActivityVersion by extra("1.11.0")
val androidxAppCompatVersion by extra("1.7.1")
val androidxCoordinatorLayoutVersion by extra("1.3.0")
val androidxCoreVersion by extra("1.17.0")
val androidxFragmentVersion by extra("1.8.9")
val coreSplashScreenVersion by extra("1.2.0")
val androidxWebkitVersion by extra("1.14.0")
val junitVersion by extra("4.13.2")
val androidxJunitVersion by extra("1.3.0")
val androidxEspressoCoreVersion by extra("3.7.0")
val okhttpVersion by extra("4.12.0")
val orgJsonVersion by extra("20240303")
val cordovaAndroidVersion by extra("14.0.1")

Some files were not shown because too many files have changed in this diff Show More