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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
nodejs 26.7.0
|
||||
pnpm 11.24.0
|
||||
bun 1.4.2
|
||||
dotnet 10
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 app’s `.../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.
|
||||
|
||||
|
||||
@@ -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 container of cyber living souls, re-creation of Neuro-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 container of cyber living souls, re-creation of Neuro-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" />
|
||||
|
||||
@@ -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,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]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
excluded:
|
||||
- ios/DerivedData
|
||||
- ios/App/CapApp-SPM/Package.swift
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
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>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 986 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
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
|
||||
@@ -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
|
||||
@@ -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" "$@"
|
||||
@@ -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")
|
||||