merge: integrate main seahorse context changes

This commit is contained in:
Hoshina 2026-04-13 23:02:38 +08:00
commit 69ff6909e1
28 changed files with 445 additions and 80 deletions

View file

@ -16,5 +16,5 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Build - name: Build core binaries
run: make build-all run: make build-all

View file

@ -17,29 +17,35 @@ jobs:
with: with:
ref: main ref: main
# 1. 安装指定版本的 Go (可选,但推荐) # 1. Install Go from go.mod
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
# 2. 安装 pnpm - name: Setup Node.js
- name: Install pnpm uses: actions/setup-node@v6
run: brew install pnpm with:
node-version: 22
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
# 3. 运行你的 Makefile 编译二进制文件 - name: Setup pnpm
run: corepack enable && corepack install
# 3. Build the application bundle
- name: Build with Make - name: Build with Make
run: make build ARCH=${{ matrix.arch }} && make build-macos-app ARCH=${{ matrix.arch }} run: make build ARCH=${{ matrix.arch }} && make build-macos-app ARCH=${{ matrix.arch }}
# 4. 签名 # 4. Apply ad-hoc signing
- name: Ad-hoc Sign - name: Ad-hoc Sign
run: codesign --force --deep --sign - "build/PicoClaw Launcher.app" run: codesign --force --deep --sign - "build/PicoClaw Launcher.app"
# 5. 安装打包工具 # 5. Install the DMG packaging tool
- name: Install create-dmg - name: Install create-dmg
run: brew install create-dmg run: brew install create-dmg
# 6. 执行打包命令 # 6. Create the DMG
- name: Create DMG - name: Create DMG
run: | run: |
mkdir -p dist mkdir -p dist
@ -54,7 +60,7 @@ jobs:
"dist/picoclaw-${{ matrix.arch }}.dmg" \ "dist/picoclaw-${{ matrix.arch }}.dmg" \
"build/PicoClaw Launcher.app" "build/PicoClaw Launcher.app"
# 7. 上传文件到 GitHub Artifacts (供你下载) # 7. Upload the DMG as a GitHub artifact
- name: Upload DMG - name: Upload DMG
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:

View file

@ -51,9 +51,11 @@ jobs:
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 22
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
- name: Setup pnpm - name: Setup pnpm
run: corepack enable && corepack prepare pnpm@latest --activate run: corepack enable && corepack install
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
@ -75,6 +77,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install zip
run: sudo apt-get install -y zip
- name: Create local tag for GoReleaser - name: Create local tag for GoReleaser
run: git tag "${{ steps.version.outputs.version }}" run: git tag "${{ steps.version.outputs.version }}"
@ -90,6 +95,7 @@ jobs:
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }} GOVERSION: ${{ steps.setup-go.outputs.go-version }}
GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }} GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }}
INCLUDE_ANDROID_BUNDLE: "true"
NIGHTLY_BUILD: "true" NIGHTLY_BUILD: "true"
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
@ -123,7 +129,7 @@ jobs:
# Collect release artifacts from goreleaser dist/ # Collect release artifacts from goreleaser dist/
ASSETS=() ASSETS=()
for f in dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/checksums.txt; do for f in dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/checksums.txt build/picoclaw-android-universal.zip; do
[ -f "$f" ] && ASSETS+=("$f") [ -f "$f" ] && ASSETS+=("$f")
done done
@ -135,4 +141,3 @@ jobs:
--prerelease \ --prerelease \
--latest=false \ --latest=false \
"${ASSETS[@]}" "${ASSETS[@]}"

View file

@ -69,9 +69,11 @@ jobs:
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 22
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
- name: Setup pnpm - name: Setup pnpm
run: corepack enable && corepack prepare pnpm@latest --activate run: corepack enable && corepack install
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
@ -93,6 +95,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install zip
run: sudo apt-get install -y zip
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7 uses: goreleaser/goreleaser-action@v7
with: with:
@ -104,23 +109,13 @@ jobs:
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }} GOVERSION: ${{ steps.setup-go.outputs.go-version }}
INCLUDE_ANDROID_BUNDLE: "true"
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
- name: Build and upload Android arm64
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sudo apt-get install -y zip
make build-android-bundle
gh release upload "${{ inputs.tag }}" \
build/picoclaw-android-universal.zip \
--clobber
- name: Apply release flags - name: Apply release flags
shell: bash shell: bash
env: env:

View file

@ -9,11 +9,10 @@ git:
before: before:
hooks: hooks:
- go mod tidy
- go generate ./... - go generate ./...
- sh -c 'cd web/frontend && pnpm install && pnpm build:backend' - sh -c 'cd web/frontend && CI=true pnpm install --frozen-lockfile && pnpm build:backend'
- go install github.com/tc-hib/go-winres@latest - sh -c 'GOBIN="$(go env GOPATH)/bin"; mkdir -p "$GOBIN"; go install github.com/tc-hib/go-winres@v0.3.3 && "$GOBIN/go-winres" make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }}'
- go-winres make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }} - sh -c 'if [ "${INCLUDE_ANDROID_BUNDLE:-}" = "true" ]; then make build-android-bundle; fi'
builds: builds:
- id: picoclaw - id: picoclaw
@ -27,7 +26,7 @@ builds:
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }} - -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }} - -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }} - -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ .Env.GOVERSION }} - -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos: goos:
- linux - linux
- windows - windows
@ -67,6 +66,10 @@ builds:
- stdjson - stdjson
ldflags: ldflags:
- -s -w - -s -w
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos: goos:
- linux - linux
- windows - windows
@ -106,6 +109,10 @@ builds:
- stdjson - stdjson
ldflags: ldflags:
- -s -w - -s -w
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos: goos:
- linux - linux
- windows - windows
@ -245,6 +252,8 @@ changelog:
release: release:
disable: '{{ isEnvSet "NIGHTLY_BUILD" }}' disable: '{{ isEnvSet "NIGHTLY_BUILD" }}'
extra_files:
- glob: ./build/picoclaw-android-universal.zip
footer: >- footer: >-
--- ---

View file

@ -1,4 +1,4 @@
.PHONY: all build install uninstall clean help test .PHONY: all build install uninstall clean help test build-all
# Build variables # Build variables
BINARY_NAME=picoclaw BINARY_NAME=picoclaw
@ -217,7 +217,9 @@ build-launcher-android-arm64:
@echo "Building picoclaw-launcher for android/arm64..." @echo "Building picoclaw-launcher for android/arm64..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@$(MAKE) -C web build-android-arm64 \ @$(MAKE) -C web build-android-arm64 \
OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-android-arm64" OUTPUT_ANDROID_ARM64="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-android-arm64" \
GO='$(GO)' \
LDFLAGS='$(LDFLAGS)'
@echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-android-arm64" @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-android-arm64"
## build-android-bundle: Build core and launcher for all Android architectures and package as universal zip ## build-android-bundle: Build core and launcher for all Android architectures and package as universal zip
@ -240,7 +242,7 @@ build-android-bundle: generate
build-pi-zero: build-linux-arm build-linux-arm64 build-pi-zero: build-linux-arm build-linux-arm64
@echo "Pi Zero 2 W builds: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm (32-bit), $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 (64-bit)" @echo "Pi Zero 2 W builds: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm (32-bit), $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 (64-bit)"
## build-all: Build picoclaw for all platforms ## build-all: Build the picoclaw core binary for all Makefile-managed platforms
build-all: generate build-all: generate
@echo "Building for multiple platforms..." @echo "Building for multiple platforms..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@ -257,8 +259,7 @@ build-all: generate
GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR) GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
GOOS=netbsd GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 ./$(CMD_DIR) GOOS=netbsd GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 ./$(CMD_DIR)
GOOS=netbsd GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 ./$(CMD_DIR) GOOS=netbsd GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 ./$(CMD_DIR)
@$(MAKE) build-android-bundle @echo "Core builds complete"
@echo "All builds complete"
## install: Install picoclaw to system and copy builtin skills ## install: Install picoclaw to system and copy builtin skills
install: build install: build

View file

@ -167,19 +167,27 @@ Vous pouvez aussi télécharger le binaire pour votre plateforme depuis la page
### Compiler depuis les sources (pour le développement) ### Compiler depuis les sources (pour le développement)
Prérequis :
- Go 1.25+
- Node.js 22+ avec Corepack activé pour les builds Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Installer le gestionnaire de paquets frontend déclaré par le dépôt
(cd web/frontend && corepack install)
# Compiler le binaire principal # Compiler le binaire principal
make build make build
# Compiler le Web UI Launcher (requis pour le mode WebUI) # Compiler le Web UI Launcher (requis pour le mode WebUI)
make build-launcher make build-launcher
# Compiler pour plusieurs plateformes # Compiler les binaires core pour toutes les plateformes gérées par le Makefile
make build-all make build-all
# Compiler pour Raspberry Pi Zero 2 W (32 bits : make build-linux-arm ; 64 bits : make build-linux-arm64) # Compiler pour Raspberry Pi Zero 2 W (32 bits : make build-linux-arm ; 64 bits : make build-linux-arm64)
@ -620,5 +628,3 @@ Discord : <https://discord.gg/V4sAZ9XWpN>
WeChat : WeChat :
<img src="assets/wechat.png" alt="WeChat group QR code" width="512"> <img src="assets/wechat.png" alt="WeChat group QR code" width="512">

View file

@ -164,19 +164,27 @@ Atau, unduh binary untuk platform Anda dari halaman [GitHub Releases](https://gi
### Build dari source (untuk pengembangan) ### Build dari source (untuk pengembangan)
Prasyarat:
- Go 1.25+
- Node.js 22+ dengan Corepack aktif untuk build Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Instal package manager frontend yang dideklarasikan repo
(cd web/frontend && corepack install)
# Build binary inti # Build binary inti
make build make build
# Build Web UI Launcher (diperlukan untuk mode WebUI) # Build Web UI Launcher (diperlukan untuk mode WebUI)
make build-launcher make build-launcher
# Build untuk berbagai platform # Build binary inti untuk semua platform yang dikelola Makefile
make build-all make build-all
# Build untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Build untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ In alternativa, scarica il binario per la tua piattaforma dalla pagina delle [Gi
### Compila dai sorgenti (per lo sviluppo) ### Compila dai sorgenti (per lo sviluppo)
Prerequisiti:
- Go 1.25+
- Node.js 22+ con Corepack abilitato per le build Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Installa il package manager frontend dichiarato dal repository
(cd web/frontend && corepack install)
# Compila il binario core # Compila il binario core
make build make build
# Compila il Web UI Launcher (necessario per la modalità WebUI) # Compila il Web UI Launcher (necessario per la modalità WebUI)
make build-launcher make build-launcher
# Compila per più piattaforme # Compila i binari core per tutte le piattaforme gestite dal Makefile
make build-all make build-all
# Compila per Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Compila per Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ PicoClaw はほぼすべての Linux デバイスにデプロイできます!
### ソースからビルド(開発用) ### ソースからビルド(開発用)
前提条件:
- Go 1.25+
- Web UI / launcher のビルドには Corepack を有効にした Node.js 22+
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# リポジトリで宣言されたフロントエンド用パッケージマネージャーをインストール
(cd web/frontend && corepack install)
# コアバイナリをビルド # コアバイナリをビルド
make build make build
# Web UI Launcher をビルドWebUI モードに必要) # Web UI Launcher をビルドWebUI モードに必要)
make build-launcher make build-launcher
# 複数プラットフォーム向けビルド # Makefile が管理するすべてのプラットフォーム向けにコアバイナリをビルド
make build-all make build-all
# Raspberry Pi Zero 2 W 向けビルド32-bit: make build-linux-arm; 64-bit: make build-linux-arm64 # Raspberry Pi Zero 2 W 向けビルド32-bit: make build-linux-arm; 64-bit: make build-linux-arm64

View file

@ -164,19 +164,27 @@ PicoClaw는 사실상 거의 모든 Linux 장치에 배포할 수 있습니다!
### 소스에서 빌드(개발용) ### 소스에서 빌드(개발용)
필수 사항:
- Go 1.25+
- Web UI / launcher 빌드를 위한 Corepack 활성화된 Node.js 22+
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# 저장소에 선언된 프런트엔드 패키지 매니저 설치
(cd web/frontend && corepack install)
# 코어 바이너리 빌드 # 코어 바이너리 빌드
make build make build
# WebUI 런처 빌드 (WebUI 모드에 필요) # WebUI 런처 빌드 (WebUI 모드에 필요)
make build-launcher make build-launcher
# 여러 플랫폼용 빌드 # Makefile이 관리하는 모든 플랫폼용 코어 바이너리 빌드
make build-all make build-all
# Raspberry Pi Zero 2 W용 빌드 (32비트: make build-linux-arm, 64비트: make build-linux-arm64) # Raspberry Pi Zero 2 W용 빌드 (32비트: make build-linux-arm, 64비트: make build-linux-arm64)

View file

@ -164,22 +164,32 @@ Alternatively, download the binary for your platform from the [GitHub Releases](
### Build from source (for development) ### Build from source (for development)
Prerequisites:
- Go 1.25+
- Node.js 22+ with Corepack enabled for Web UI / launcher builds
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Build core binary # Install frontend package manager declared by the repo
(cd web/frontend && corepack install)
# Build the core binary for the current platform
make build make build
# Build Web UI Launcher (required for WebUI mode) # Build the Web UI Launcher (required for WebUI mode)
make build-launcher make build-launcher
# Build for multiple platforms # Build core binaries for all Makefile-managed platforms
make build-all make build-all
# Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Build for Raspberry Pi Zero 2 W
# 32-bit: make build-linux-arm
# 64-bit: make build-linux-arm64
make build-pi-zero make build-pi-zero
# Build and install # Build and install

View file

@ -165,18 +165,26 @@ Muat turun binari untuk platform anda dari halaman [GitHub Releases](https://git
### Bina dari sumber (untuk pembangunan) ### Bina dari sumber (untuk pembangunan)
Prasyarat:
- Go 1.25+
- Node.js 22+ dengan Corepack diaktifkan untuk binaan Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Pasang pengurus pakej frontend yang diisytiharkan oleh repositori
(cd web/frontend && corepack install)
# Bina binari teras # Bina binari teras
make build make build
# Bina Pelancar Web UI (diperlukan untuk mod WebUI) # Bina Pelancar Web UI (diperlukan untuk mod WebUI)
make build-launcher make build-launcher
# Bina untuk pelbagai platform # Bina binari teras untuk semua platform yang diuruskan oleh Makefile
make build-all make build-all
# Bina untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Bina untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ Alternativamente, baixe o binário para sua plataforma na página de [GitHub Rel
### Compilar a partir do código-fonte (para desenvolvimento) ### Compilar a partir do código-fonte (para desenvolvimento)
Pré-requisitos:
- Go 1.25+
- Node.js 22+ com Corepack habilitado para builds do Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Instalar o gerenciador de pacotes de frontend declarado pelo repositório
(cd web/frontend && corepack install)
# Compilar o binário principal # Compilar o binário principal
make build make build
# Compilar o Web UI Launcher (necessário para o modo WebUI) # Compilar o Web UI Launcher (necessário para o modo WebUI)
make build-launcher make build-launcher
# Compilar para múltiplas plataformas # Compilar os binários core para todas as plataformas gerenciadas pelo Makefile
make build-all make build-all
# Compilar para Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Compilar para Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ Ngoài ra, tải binary cho nền tảng của bạn từ trang [GitHub Releases
### Xây dựng từ mã nguồn (để phát triển) ### Xây dựng từ mã nguồn (để phát triển)
Yêu cầu:
- Go 1.25+
- Node.js 22+ với Corepack được bật cho các bản build Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Build core binary # Cài đặt trình quản lý gói frontend được khai báo bởi repo
(cd web/frontend && corepack install)
# Build binary lõi
make build make build
# Build Web UI Launcher (required for WebUI mode) # Build Web UI Launcher (cần cho chế độ WebUI)
make build-launcher make build-launcher
# Build for multiple platforms # Build các binary lõi cho mọi nền tảng do Makefile quản lý
make build-all make build-all
# Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ PicoClaw 几乎可以部署在任何 Linux 设备上!
### 从源码构建(开发用) ### 从源码构建(开发用)
前置要求:
- Go 1.25+
- Node.js 22+,并启用 Corepack用于 Web UI / launcher 构建)
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# 安装仓库声明的前端包管理器
(cd web/frontend && corepack install)
# 构建核心二进制文件 # 构建核心二进制文件
make build make build
# 构建 Web UI LauncherWebUI 模式必需) # 构建 Web UI LauncherWebUI 模式必需)
make build-launcher make build-launcher
# 为多平台构建 # 为 Makefile 管理的所有平台构建核心二进制文件
make build-all make build-all
# 为 Raspberry Pi Zero 2 W 构建32位: make build-linux-arm; 64位: make build-linux-arm64 # 为 Raspberry Pi Zero 2 W 构建32位: make build-linux-arm; 64位: make build-linux-arm64
@ -618,5 +626,3 @@ WeChat:
<img src="assets/wechat.png" alt="WeChat group QR code" width="512"> <img src="assets/wechat.png" alt="WeChat group QR code" width="512">

View file

@ -61,6 +61,16 @@ func (m *legacyContextManager) Ingest(_ context.Context, _ *IngestRequest) error
return nil return nil
} }
func (m *legacyContextManager) Clear(_ context.Context, sessionKey string) error {
agent := m.al.registry.GetDefaultAgent()
if agent == nil || agent.Sessions == nil {
return fmt.Errorf("sessions not initialized")
}
agent.Sessions.SetHistory(sessionKey, []providers.Message{})
agent.Sessions.SetSummary(sessionKey, "")
return agent.Sessions.Save(sessionKey)
}
// maybeSummarize triggers summarization if the session history exceeds thresholds. // maybeSummarize triggers summarization if the session history exceeds thresholds.
// It runs asynchronously in a goroutine. // It runs asynchronously in a goroutine.
func (m *legacyContextManager) maybeSummarize(sessionKey string) { func (m *legacyContextManager) maybeSummarize(sessionKey string) {

View file

@ -24,6 +24,10 @@ type ContextManager interface {
// Ingest records a message into the ContextManager's own storage. // Ingest records a message into the ContextManager's own storage.
// Called after each message is persisted to session JSONL. // Called after each message is persisted to session JSONL.
Ingest(ctx context.Context, req *IngestRequest) error Ingest(ctx context.Context, req *IngestRequest) error
// Clear removes all stored context for a session (messages, summaries, etc.).
// Called when the user issues /clear or /reset.
Clear(ctx context.Context, sessionKey string) error
} }
// AssembleRequest is the input to Assemble. // AssembleRequest is the input to Assemble.

View file

@ -690,6 +690,7 @@ func (m *noopContextManager) Assemble(_ context.Context, req *AssembleRequest) (
} }
func (m *noopContextManager) Compact(_ context.Context, _ *CompactRequest) error { return nil } func (m *noopContextManager) Compact(_ context.Context, _ *CompactRequest) error { return nil }
func (m *noopContextManager) Ingest(_ context.Context, _ *IngestRequest) error { return nil } func (m *noopContextManager) Ingest(_ context.Context, _ *IngestRequest) error { return nil }
func (m *noopContextManager) Clear(_ context.Context, _ string) error { return nil }
// trackingContextManager tracks call counts for each method. // trackingContextManager tracks call counts for each method.
type trackingContextManager struct { type trackingContextManager struct {
@ -726,6 +727,8 @@ func (m *trackingContextManager) Ingest(_ context.Context, req *IngestRequest) e
return nil return nil
} }
func (m *trackingContextManager) Clear(_ context.Context, _ string) error { return nil }
// resetCMRegistry clears the global factory registry and returns a cleanup // resetCMRegistry clears the global factory registry and returns a cleanup
// function that restores the original state after the test. // function that restores the original state after the test.
func resetCMRegistry() func() { func resetCMRegistry() func() {

View file

@ -154,6 +154,19 @@ func (m *seahorseContextManager) Ingest(ctx context.Context, req *IngestRequest)
return err return err
} }
// Clear removes all stored context for a session (seahorse DB + JSONL).
func (m *seahorseContextManager) Clear(ctx context.Context, sessionKey string) error {
if err := m.engine.ClearSession(ctx, sessionKey); err != nil {
return err
}
if m.sessions != nil {
m.sessions.SetHistory(sessionKey, []providers.Message{})
m.sessions.SetSummary(sessionKey, "")
return m.sessions.Save(sessionKey)
}
return nil
}
// bootstrapSession reconciles JSONL session history into seahorse SQLite. // bootstrapSession reconciles JSONL session history into seahorse SQLite.
func (m *seahorseContextManager) bootstrapSession(ctx context.Context, sessionKey string) { func (m *seahorseContextManager) bootstrapSession(ctx context.Context, sessionKey string) {
if m.sessions == nil { if m.sessions == nil {

View file

@ -3639,7 +3639,7 @@ func (al *AgentLoop) handleCommand(
return "", false return "", false
} }
rt := al.buildCommandsRuntime(agent, opts) rt := al.buildCommandsRuntime(ctx, agent, opts)
executor := commands.NewExecutor(al.cmdRegistry, rt) executor := commands.NewExecutor(al.cmdRegistry, rt)
var commandReply string var commandReply string
@ -3762,7 +3762,11 @@ func (al *AgentLoop) applyExplicitSkillCommand(
return true, false, "" return true, false, ""
} }
func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOptions) *commands.Runtime { func (al *AgentLoop) buildCommandsRuntime(
ctx context.Context,
agent *AgentInstance,
opts *processOptions,
) *commands.Runtime {
normalizeProcessOptionsInPlace(opts) normalizeProcessOptionsInPlace(opts)
registry := al.GetRegistry() registry := al.GetRegistry()
@ -3846,14 +3850,7 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
if opts == nil { if opts == nil {
return fmt.Errorf("process options not available") return fmt.Errorf("process options not available")
} }
if agent.Sessions == nil { return al.contextManager.Clear(ctx, opts.SessionKey)
return fmt.Errorf("sessions not initialized for agent")
}
agent.Sessions.SetHistory(opts.Dispatch.SessionKey, make([]providers.Message, 0))
agent.Sessions.SetSummary(opts.Dispatch.SessionKey, "")
agent.Sessions.Save(opts.Dispatch.SessionKey)
return nil
} }
} }
return rt return rt

View file

@ -118,26 +118,35 @@ func runSchema(db *sql.DB) error {
`CREATE INDEX IF NOT EXISTS idx_summary_messages_message ON summary_messages(message_id)`, `CREATE INDEX IF NOT EXISTS idx_summary_messages_message ON summary_messages(message_id)`,
`CREATE INDEX IF NOT EXISTS idx_context_items_conv ON context_items(conversation_id, ordinal)`, `CREATE INDEX IF NOT EXISTS idx_context_items_conv ON context_items(conversation_id, ordinal)`,
// Drop old triggers before creating new ones so existing DBs get updated bodies.
// (CREATE TRIGGER IF NOT EXISTS does NOT replace an existing trigger body.)
`DROP TRIGGER IF EXISTS summaries_ai`,
`DROP TRIGGER IF EXISTS summaries_ad`,
`DROP TRIGGER IF EXISTS summaries_au`,
`DROP TRIGGER IF EXISTS messages_ai`,
`DROP TRIGGER IF EXISTS messages_ad`,
`DROP TRIGGER IF EXISTS messages_au`,
// FTS5 triggers to keep summaries_fts in sync with summaries table // FTS5 triggers to keep summaries_fts in sync with summaries table
`CREATE TRIGGER IF NOT EXISTS summaries_ai AFTER INSERT ON summaries BEGIN `CREATE TRIGGER summaries_ai AFTER INSERT ON summaries BEGIN
INSERT INTO summaries_fts (summary_id, content) VALUES (new.summary_id, new.content); INSERT INTO summaries_fts (summary_id, content) VALUES (new.summary_id, new.content);
END`, END`,
`CREATE TRIGGER IF NOT EXISTS summaries_ad AFTER DELETE ON summaries BEGIN `CREATE TRIGGER summaries_ad AFTER DELETE ON summaries BEGIN
INSERT INTO summaries_fts (summaries_fts, summary_id, content) VALUES ('delete', old.summary_id, old.content); DELETE FROM summaries_fts WHERE summary_id = old.summary_id;
END`, END`,
`CREATE TRIGGER IF NOT EXISTS summaries_au AFTER UPDATE ON summaries BEGIN `CREATE TRIGGER summaries_au AFTER UPDATE ON summaries BEGIN
INSERT INTO summaries_fts (summaries_fts, summary_id, content) VALUES ('delete', old.summary_id, old.content); DELETE FROM summaries_fts WHERE summary_id = old.summary_id;
INSERT INTO summaries_fts (summary_id, content) VALUES (new.summary_id, new.content); INSERT INTO summaries_fts (summary_id, content) VALUES (new.summary_id, new.content);
END`, END`,
// FTS5 triggers to keep messages_fts in sync with messages table // FTS5 triggers to keep messages_fts in sync with messages table
`CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN `CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts (message_id, content) VALUES (new.message_id, new.content); INSERT INTO messages_fts (message_id, content) VALUES (new.message_id, new.content);
END`, END`,
`CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN `CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
DELETE FROM messages_fts WHERE message_id = old.message_id; DELETE FROM messages_fts WHERE message_id = old.message_id;
END`, END`,
`CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN `CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
DELETE FROM messages_fts WHERE message_id = old.message_id; DELETE FROM messages_fts WHERE message_id = old.message_id;
INSERT INTO messages_fts (message_id, content) VALUES (new.message_id, new.content); INSERT INTO messages_fts (message_id, content) VALUES (new.message_id, new.content);
END`, END`,

View file

@ -194,6 +194,84 @@ func TestMigrationSummaryParentsPK(t *testing.T) {
} }
} }
func TestTriggerMigration(t *testing.T) {
db := openTestDB(t)
// Run schema once to create tables and (correct) triggers
if err := runSchema(db); err != nil {
t.Fatalf("runSchema: %v", err)
}
// Drop correct triggers and recreate them with the old buggy body.
// The old trigger used INSERT INTO fts VALUES('delete', ...) which is wrong
// for non-external-content FTS5 tables.
oldSummariesDelete := `CREATE TRIGGER summaries_ad AFTER DELETE ON summaries BEGIN
INSERT INTO summaries_fts (summaries_fts, summary_id, content) VALUES('delete', old.summary_id, old.content);
END`
oldMessagesDelete := `CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, message_id, content) VALUES('delete', old.message_id, old.content);
END`
for _, sql := range []string{
`DROP TRIGGER IF EXISTS summaries_ad`,
`DROP TRIGGER IF EXISTS messages_ad`,
oldSummariesDelete,
oldMessagesDelete,
} {
if _, err := db.Exec(sql); err != nil {
t.Fatalf("setup old trigger: %v", err)
}
}
// Insert a conversation and summary so we have something to delete
_, err := db.Exec(`INSERT INTO conversations (session_key) VALUES ('old-db-test')`)
if err != nil {
t.Fatalf("insert conversation: %v", err)
}
_, err = db.Exec(`INSERT INTO summaries (summary_id, conversation_id, kind, depth, content, token_count)
VALUES ('old-sum', 1, 'leaf', 0, 'old content', 5)`)
if err != nil {
t.Fatalf("insert summary: %v", err)
}
// The old trigger body is wrong for normal FTS5 — DELETE should fail.
_, err = db.Exec(`DELETE FROM summaries WHERE summary_id = 'old-sum'`)
if err == nil {
t.Error("expected error from old buggy trigger, but DELETE succeeded")
} else {
t.Logf("old trigger correctly causes error: %v", err)
}
// Now runSchema again — this drops and recreates the triggers with correct bodies.
err = runSchema(db)
if err != nil {
t.Fatalf("runSchema migration: %v", err)
}
// Insert again so we have data to delete
_, err = db.Exec(`INSERT INTO summaries (summary_id, conversation_id, kind, depth, content, token_count)
VALUES ('migrated-sum', 1, 'leaf', 0, 'new content', 5)`)
if err != nil {
t.Fatalf("insert after migration: %v", err)
}
// DELETE should now work with the corrected trigger body.
_, err = db.Exec(`DELETE FROM summaries WHERE summary_id = 'migrated-sum'`)
if err != nil {
t.Fatalf("DELETE after migration failed (trigger not corrected): %v", err)
}
// Verify the summary is gone
var count int
err = db.QueryRow(`SELECT count(*) FROM summaries WHERE summary_id = 'migrated-sum'`).Scan(&count)
if err != nil {
t.Fatalf("query after delete: %v", err)
}
if count != 0 {
t.Errorf("summary should be gone after DELETE, got count=%d", count)
}
}
func TestFTS5SQLConstants(t *testing.T) { func TestFTS5SQLConstants(t *testing.T) {
db := openTestDB(t) db := openTestDB(t)

View file

@ -377,6 +377,19 @@ func (e *Engine) IngestMessages(ctx context.Context, sessionKey string, messages
return e.Ingest(ctx, sessionKey, messages) return e.Ingest(ctx, sessionKey, messages)
} }
// ClearSession removes all stored data for a session (messages, summaries, context).
// If the session has no prior seahorse record, it is a no-op.
func (e *Engine) ClearSession(ctx context.Context, sessionKey string) error {
conv, err := e.store.GetConversationBySessionKey(ctx, sessionKey)
if err != nil {
return err
}
if conv == nil {
return nil // session never ingested, nothing to clear
}
return e.store.ClearConversation(ctx, conv.ConversationID)
}
// Bootstrap reconciles a session's messages with the database. // Bootstrap reconciles a session's messages with the database.
// Called once at startup for each known session. // Called once at startup for each known session.
// Bootstrap reconciles JSONL history with SQLite by ingesting only the delta. // Bootstrap reconciles JSONL history with SQLite by ingesting only the delta.

View file

@ -728,6 +728,57 @@ func (s *Store) DeleteMessagesAfterID(ctx context.Context, convID int64, afterID
return tx.Commit() return tx.Commit()
} }
// ClearConversation removes all data for a conversation from all tables.
// Deletes context_items, summary_messages, summary_parents (via subquery), summaries,
// message_parts, and messages. FTS entries are handled automatically by triggers.
// Uses a transaction for atomicity.
func (s *Store) ClearConversation(ctx context.Context, convID int64) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Delete in child→parent order. FTS tables (messages_fts, summaries_fts) are
// kept in sync by DELETE triggers, so we just delete from the parent tables.
if _, err := tx.ExecContext(ctx,
"DELETE FROM context_items WHERE conversation_id = ?", convID); err != nil {
return fmt.Errorf("context_items: %w", err)
}
if _, err := tx.ExecContext(ctx,
`DELETE FROM summary_messages WHERE summary_id IN (
SELECT summary_id FROM summaries WHERE conversation_id = ?
)`, convID); err != nil {
return fmt.Errorf("summary_messages: %w", err)
}
// Note: summary_parents has no convID column; delete via subquery on summaries
if _, err := tx.ExecContext(ctx,
`DELETE FROM summary_parents WHERE summary_id IN (
SELECT summary_id FROM summaries WHERE conversation_id = ?
) OR parent_summary_id IN (
SELECT summary_id FROM summaries WHERE conversation_id = ?
)`, convID, convID); err != nil {
return fmt.Errorf("summary_parents: %w", err)
}
if _, err := tx.ExecContext(ctx,
"DELETE FROM summaries WHERE conversation_id = ?", convID); err != nil {
return fmt.Errorf("summaries: %w", err)
}
if _, err := tx.ExecContext(ctx,
`DELETE FROM message_parts WHERE message_id IN (
SELECT message_id FROM messages WHERE conversation_id = ?
)`, convID); err != nil {
return fmt.Errorf("message_parts: %w", err)
}
if _, err := tx.ExecContext(ctx,
"DELETE FROM messages WHERE conversation_id = ?", convID); err != nil {
return fmt.Errorf("messages: %w", err)
}
return tx.Commit()
}
// AppendContextMessage appends a single message to context_items at next ordinal. // AppendContextMessage appends a single message to context_items at next ordinal.
func (s *Store) AppendContextMessage(ctx context.Context, convID int64, messageID int64) error { func (s *Store) AppendContextMessage(ctx context.Context, convID int64, messageID int64) error {
return s.appendContextItems(ctx, convID, []ContextItem{ return s.appendContextItems(ctx, convID, []ContextItem{

View file

@ -79,7 +79,95 @@ func TestStoreGetConversationBySessionKey(t *testing.T) {
} }
} }
// --- Message Operations --- // --- Conversation Clear ---
func TestStoreClearConversation(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
conv, err := s.GetOrCreateConversation(ctx, "agent:clear-test")
if err != nil {
t.Fatalf("create conversation: %v", err)
}
// Add messages
msg1, err := s.AddMessage(ctx, conv.ConversationID, "user", "hello", 5)
if err != nil {
t.Fatalf("add message 1: %v", err)
}
msg2, err := s.AddMessage(ctx, conv.ConversationID, "assistant", "hi", 5)
if err != nil {
t.Fatalf("add message 2: %v", err)
}
// Add a summary
_, err = s.CreateSummary(ctx, CreateSummaryInput{
ConversationID: conv.ConversationID,
Content: "test summary",
TokenCount: 10,
Kind: SummaryKindLeaf,
})
if err != nil {
t.Fatalf("create summary: %v", err)
}
// Verify data exists
msgs, err := s.GetMessages(ctx, conv.ConversationID, 0, 0)
if err != nil {
t.Fatalf("get messages before clear: %v", err)
}
if len(msgs) != 2 {
t.Fatalf("expected 2 messages before clear, got %d", len(msgs))
}
sums, err := s.GetSummariesByConversation(ctx, conv.ConversationID)
if err != nil {
t.Fatalf("get summaries before clear: %v", err)
}
if len(sums) != 1 {
t.Fatalf("expected 1 summary before clear, got %d", len(sums))
}
// Clear
if err = s.ClearConversation(ctx, conv.ConversationID); err != nil {
t.Fatalf("clear conversation: %v", err)
}
// Verify all data is gone
msgs, err = s.GetMessages(ctx, conv.ConversationID, 0, 0)
if err != nil {
t.Fatalf("get messages after clear: %v", err)
}
if len(msgs) != 0 {
t.Fatalf("expected 0 messages after clear, got %d", len(msgs))
}
sums, err = s.GetSummariesByConversation(ctx, conv.ConversationID)
if err != nil {
t.Fatalf("get summaries after clear: %v", err)
}
if len(sums) != 0 {
t.Fatalf("expected 0 summaries after clear, got %d", len(sums))
}
items, err := s.GetContextItems(ctx, conv.ConversationID)
if err != nil {
t.Fatalf("get context items after clear: %v", err)
}
if len(items) != 0 {
t.Fatalf("expected 0 context items after clear, got %d", len(items))
}
var count int
if err := s.db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM message_parts WHERE message_id = ? OR message_id = ?",
msg1.ID, msg2.ID).Scan(&count); err != nil {
t.Fatalf("count message parts: %v", err)
}
if count != 0 {
t.Fatalf("expected 0 message parts after clear, got %d", count)
}
}
func TestStoreAddAndGetMessages(t *testing.T) { func TestStoreAddAndGetMessages(t *testing.T) {
s := openTestStore(t) s := openTestStore(t)

View file

@ -12,6 +12,7 @@ BUILD_DIR=build
OUTPUT?=$(BUILD_DIR)/picoclaw-launcher OUTPUT?=$(BUILD_DIR)/picoclaw-launcher
OUTPUT_ANDROID_ARM64?=$(BUILD_DIR)/picoclaw-launcher-android-arm64 OUTPUT_ANDROID_ARM64?=$(BUILD_DIR)/picoclaw-launcher-android-arm64
FRONTEND_DIR=frontend FRONTEND_DIR=frontend
FRONTEND_INSTALL_STAMP=$(FRONTEND_DIR)/node_modules/.picoclaw-install-stamp
BACKEND_DIR=backend BACKEND_DIR=backend
BACKEND_DIST=$(BACKEND_DIR)/dist BACKEND_DIST=$(BACKEND_DIR)/dist
PICOCLAW_BINARY_NAME=picoclaw PICOCLAW_BINARY_NAME=picoclaw
@ -105,11 +106,14 @@ build-android-bundle: build-frontend
@echo "All Android launcher builds complete" @echo "All Android launcher builds complete"
build-frontend: build-frontend:
@if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \
[ $(FRONTEND_DIR)/package.json -nt $(FRONTEND_DIR)/node_modules ] || \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \
[ $(FRONTEND_DIR)/pnpm-lock.yaml -nt $(FRONTEND_DIR)/node_modules ]; then \ [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \
[ ! -f $(FRONTEND_INSTALL_STAMP) ] || \
[ "$$(cat $(FRONTEND_INSTALL_STAMP) 2>/dev/null)" != "$$expected_stamp" ]; then \
echo "Installing frontend dependencies..."; \ echo "Installing frontend dependencies..."; \
cd $(FRONTEND_DIR) && pnpm install --frozen-lockfile; \ (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \
printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \
fi fi
@echo "Building frontend..." @echo "Building frontend..."
@cd $(FRONTEND_DIR) && pnpm build:backend @cd $(FRONTEND_DIR) && pnpm build:backend

View file

@ -3,6 +3,7 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"packageManager": "pnpm@10.33.0",
"engines": { "engines": {
"node": "^20.19.0 || ^22.13.0 || >=24" "node": "^20.19.0 || ^22.13.0 || >=24"
}, },