commit b749b4e0e57a5199f63b1ae704f5d5280b1d1ef0 Author: Jack Date: Mon Sep 14 16:55:15 2026 +0800 pi-launcher: macOS/Windows desktop launcher for @agegr/pi-web 按需启动 pi-web 本地 Web UI(端口 30141,URL http://localhost:30141/), 基于 Wails v2 (Go + Vue 3) 构建,逻辑与 dsh-launcher 同构但适配 pi: - 命令 pi-web、端口 30141、~/.pi/ 凭据 - 接管旧进程(关 app 不留 node 残留) - 三栏环境检查(Node / pi-web CLI / 端口) 构建: macOS: ./scripts/build-mac.sh Windows: scripts\build-windows.bat Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c13485b --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# ─── Go ─────────────────────────────────────────────── +# 编译产物(.app/.exe 整个目录) +build/bin/ +# go build 出来的裸 binary(--selftest 等调试命令) +/pi-launcher +# macOS 开发期构建的 Info.plist(带 dev id),不入仓;release 用 Info.plist 入仓 +build/darwin/Info.dev.plist +# Windows 打包临时 +build/windows/installer/ +build/windows/wails.exe.manifest +# Go 覆盖率/测试产物 +*.test +*.out +coverage.* + +# ─── Node / Vite / Wails 前端 ──────────────────────── +node_modules/ +frontend/dist/ +# frontend/wailsjs/ 是 wails build 自动生成的 JS 绑定,建议入仓(clone 后开箱即用) +# 顶部 banner 图片是 build 临时文件,不入仓 +frontend/src/assets/images/banner.png + +# ─── Wails 框架生成的本地开发配置 ───────────────────── +# wails.json 是项目配置,应该 commit +# 但 build/ 下生成的 Info.plist / icon.icns 已经包含在 build/bin/,无需额外忽略 + +# ─── macOS ─────────────────────────────────────────── +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# ─── Windows ───────────────────────────────────────── +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# ─── IDE ───────────────────────────────────────────── +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# ─── 日志 / 临时文件 ────────────────────────────────── +*.log +.DS_Store +.tmp/ +*.tmp +.env +.env.local +.env.*.local + +# ─── 应用运行产生的缓存 ────────────────────────────── +# Wails macOS app 运行时会在 ~/Library/{Caches,Logs} 下产数据,不入仓 +# (路径在 $HOME 下,不会进项目) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..485b983 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# pi-launcher + +macOS / Windows 桌面启动器:按需启动 [pi-agent](https://www.npmjs.com/package/@agegr/pi-web) 的本地 Web UI,免去手动开终端、敲命令、等端口、点浏览器的重复操作。 + +基于 [Wails v2](https://wails.io/) (Go + Vue 3) 构建。逻辑与 [`dsh-launcher`](../dsh-launcher) 几乎一致,仅替换为目标 CLI 与图标。 + +--- + +## 这是什么 + +`@agegr/pi-web` 是一个 npm 全局安装的命令行工具,启动后会: + +1. 拉起本地 HTTP 服务(默认端口 `30141`) +2. 浏览器访问 `http://localhost:30141/` + +`pi-launcher` 把这串动作封装成一个独立的 `.app`(macOS)/ `.exe`(Windows): + +- 启动前自检:Node / pi-web 命令 / 凭据 / 30141 端口 +- 一键启动 → 等端口就绪 → 自动开浏览器 → 最小化到 Dock +- 一键停止 / 一键安装(`npm install -g @agegr/pi-web`) / 一键升级 +- 退出时自动清理:杀子进程、释放端口、不留孤儿 node + +适合场景:日常用 pi 但不想每次都开终端敲命令。 + +--- + +## 截图 + +(启动器窗口 1040 × 1280,深色背景,左右两栏:左环境检查,右操作 + 日志) + +--- + +## 安装 pi CLI(目标程序) + +启动器只是壳,真正干活的 `pi` 命令需要先手动装一次: + +```bash +npm install -g @agegr/pi-web +``` + +启动器自带的「安装 / 升级」按钮会帮你跑同样的命令,不用自己再开终端。 + +> **注意**:`@agegr/pi-web` 默认会拉取一份 Electron 浏览器壳 + 本地凭据目录 `~/.pi/`(类比 `dsh` 的 `~/.dsh/`)。首次启动器自检会提示你 `pi-web login`。 + +--- + +## 编译启动器本身 + +需要: + +- Go ≥ 1.25 +- Node ≥ 18 +- Wails v2:`go install github.com/wailsapp/wails/v2/cmd/wails@latest` +- macOS 编译产物需要 macOS;Windows 同理(见下方说明) + +### macOS + +```bash +./scripts/build-mac.sh +# 产物: build/bin/pi-launcher.app + +./scripts/install-app.sh +# 复制到 /Applications/ 并刷新 LaunchServices 图标缓存 +``` + +### Windows + +在 Windows 上跑: + +```bat +scripts\build-windows.bat +``` + +> Wails v2 跨平台必须本机编译,Mac 上 build 不了 Windows exe。 + +--- + +## 开发模式 + +```bash +wails dev +``` + +Vite 热重载前端 + Go 后端,浏览器另开 devtools:http://localhost:34115 + +--- + +## 核心架构 + +``` +pi-launcher/ +├── main.go # Wails 入口 + macOS 窗口配置 + --selftest 调试模式 +├── app.go # 后端:CheckEnv / Start / Stop / Install / Update +├── wails.json # 项目元数据(name / outputfilename) +├── go.mod +├── build/ +│ ├── darwin/Info.plist +│ ├── windows/icon.ico +│ └── appicon.png / iconfile.icns # 应用图标(替换此处换图标) +├── frontend/ +│ ├── src/App.vue # 单文件 SPA,环境检查 + 操作面板 + 日志 +│ ├── src/main.js +│ ├── package.json +│ └── vite.config.js +├── scripts/ +│ ├── build-mac.sh # macOS 构建脚本 +│ ├── build-windows.bat +│ └── install-app.sh # 复制到 /Applications + 刷新图标缓存 +└── README.md +``` + +### 后端关键点 + +- `initNpmPrefix()` — 启动时一次性 `npm prefix -g` 缓存,后续读缓存 +- `buildChildEnv()` — 给子进程手动塞 `/opt/homebrew/bin` 等路径,GUI 进程 `~/.zshrc` 不读 +- `waitForPort(30141, 30s)` — 轮询端口 LISTEN 替代等 URL 字符串 +- `killPITree()` + `freePort(30141)` — 退出 / 停止时双保险,杀进程树 + 精准释放端口 +- 全部外部命令都包 `recover()` + 超时,任何子命令崩都不会拖垮 GUI + +### 前端关键点 + +- 单文件 `App.vue`,分左右两栏:左环境检查(Node / pi / 凭据 / 端口),右操作按钮 + 实时日志流 +- 通过 Wails `EventsOn` 订阅 `pi:install:log` / `pi:install:done` 拿到 npm 子进程实时输出 +- `Start` 成功后自动调 `WindowMinimise` 收进 Dock + +--- + +## 与 dsh-launcher 的差异 + +| 项 | dsh-launcher | pi-launcher | +|---|---|---| +| 目标 CLI | `@deepseek-ai/dsh` | `@agegr/pi-web` | +| 命令名 | `dsh` | `pi-web` | +| 凭据目录 | `~/.dsh/.credentials.yaml` | `~/.pi/.credentials.yaml`(待确认) | +| 端口 | 3080 | 30141 | +| 进程匹配 | `dsh/lib/bin.js` / `dsh-pet` | `pi-web` 相关进程(待定) | +| 安装包名 | `@deepseek-ai/dsh` | `@agegr/pi-web` | +| 启动 URL | `http://127.0.0.1:3080/` | `http://localhost:30141/` | +| 图标 | `build/appicon.png` | 同位置替换 | + +迁移思路:把 `app.go` 里所有 `dsh` 字面量改成 `pi`、`dshPkg` 常量改成 `@agegr/pi-web`、`HomeDshDir` 改成 `HomePiDir`(`~/.pi`),端口 `3080` 改成 `30141`,再换图标即可。 + +--- + +## 自检模式(调试用) + +不启 GUI,直接调后端走完流程: + +```bash +./build/bin/pi-launcher --selftest +``` + +会打印:`nodeOK` / `piPath` / `piVer` / `cred` / `portBusy` / 进程列表 / `waitForPort` 测试结果。 + +--- + +## 已知问题 + +- **Windows 图标闪烁**:首次安装 Windows 版后任务栏图标可能延迟更新,等几秒或重新 pin。 +- **macOS Dock 图标缓存**:`scripts/install-app.sh` 已处理 `lsregister -f` + Dock 重启,通常一次就好。 +- **端口 30141 被占**:启动器会自动 `kill -9` 残留 PID,但如果你跑别的服务也用 30141 会被误杀——届时改 `waitForPort` / `freePort` 的端口号。 + +--- + +## License + +个人项目,MIT。 \ No newline at end of file diff --git a/app.go b/app.go new file mode 100644 index 0000000..17d4643 --- /dev/null +++ b/app.go @@ -0,0 +1,921 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "runtime/debug" + "strconv" + "strings" + "sync" + "time" + + wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime" +) + +// pi 默认 Web UI 端口 +const piPort = 30141 + +// App is the Wails application backend. +type App struct { + ctx context.Context + + mu sync.Mutex + piCmd *exec.Cmd + piURL string + startedAt time.Time + running bool +} + +// NewApp creates a new App application struct. +func NewApp() *App { + initCrashLog() + // 在进程启动时预先初始化 npm prefix 缓存,后续 buildChildEnv 直接读缓存。 + // 这步可能 exec npm 子命令(限 2s 超时),但不会和 buildChildEnv 互相递归。 + initNpmPrefix() + return &App{} +} + +// initNpmPrefix triggers the cached npm prefix lookup exactly once. +// 必须在启动早期(且不依赖 buildChildEnv)调用,避免 buildChildEnv → cachedNpmPrefix +// 第一次执行时栈还未充分展开导致后续路径无法解析。 +func initNpmPrefix() { + _, _ = cachedNpmPrefix() +} + +// initCrashLog wires a panic logger that writes to ~/Library/Logs/pi-launcher.log. +// macOS GUI 进程 stderr 没终端,panic 信息看不到,落盘最稳。 +var crashLogOnce sync.Once + +func initCrashLog() { + crashLogOnce.Do(func() { + home, _ := os.UserHomeDir() + logPath := filepath.Join(home, "Library", "Logs", "pi-launcher.log") + _ = os.MkdirAll(filepath.Dir(logPath), 0o755) + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + return + } + log.SetOutput(f) + log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile) + }) +} + +// safeGo wraps a goroutine with recover so a panic in any background +// task can't take down the whole Wails app. +func safeGo(name string, fn func()) { + go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("[panic:%s] %v\n%s", name, r, debugStack()) + } + }() + fn() + }() +} + +func debugStack() string { + return string(debug.Stack()) +} + +// startup is called when the app starts. +func (a *App) startup(ctx context.Context) { + a.ctx = ctx + // 接管旧进程: 用户可能之前手动启动过 pi-web / 残留 node 占着 30141。 + // launcher 作为唯一入口,启动时强制清场,确保 UI 状态与实际一致。 + safeGo("takeOver", func() { + pids := listPIPids() + freePort(piPort) + killed := len(pids) + if killed > 0 && a.ctx != nil { + wailsruntime.EventsEmit(a.ctx, "pi:takeover:done", killed) + } + }) +} + +// shutdown is called when the app is closing. Ensure pi is killed +// so we never leak a child process. +func (a *App) shutdown(ctx context.Context) { + a.stopPI() +} + +// ---- Types exposed to the frontend ---- + +// CheckResult is returned by CheckEnv. +type CheckResult struct { + NodePath string `json:"nodePath"` + NodeVersion string `json:"nodeVersion"` + NodeOK bool `json:"nodeOK"` + PiPath string `json:"piPath"` + PiVersion string `json:"piVersion"` + CredFile string `json:"credFile"` + CredExists bool `json:"credExists"` + HomePiDir string `json:"homePiDir"` + PortBusy bool `json:"portBusy"` + PortBusyBy string `json:"portBusyBy"` + Issues []string `json:"issues"` // 始终非 nil,前端可放心访问 +} + +// StartResult is returned by Start. +type StartResult struct { + OK bool `json:"ok"` + URL string `json:"url"` + Error string `json:"error"` + Output string `json:"output"` + Hidden bool `json:"hidden"` // 是否已自动隐藏窗口 +} + +// StatusResult is returned by GetStatus. +type StatusResult struct { + Running bool `json:"running"` + URL string `json:"url"` + StartedAt string `json:"startedAt"` + Pid int `json:"pid"` +} + +// ---- 3 core methods ---- + +// CheckEnv inspects Node, pi-web, ~/.pi credentials and port 30141. +// 所有外部命令都套超时 + panic recover,任何一个失败不能让整个 app 闪退。 +func (a *App) CheckEnv() (res CheckResult) { + defer func() { + if r := recover(); r != nil { + log.Printf("[panic:CheckEnv] %v\n%s", r, debugStack()) + res.Issues = append(res.Issues, fmt.Sprintf("CheckEnv 内部错误: %v", r)) + } + }() + + home := os.Getenv("HOME") + if home == "" { + home = "/tmp" + } + res = CheckResult{ + HomePiDir: filepath.Join(home, ".pi"), + CredFile: filepath.Join(home, ".pi", ".credentials.yaml"), + PortBusy: false, + Issues: []string{}, // 确保 JSON 序列化为 [] 而非 null + } + + // node + res.NodePath = lookPath("node") + if res.NodePath != "" { + if v, err := runQuiet(res.NodePath, "-v"); err == nil { + res.NodeVersion = strings.TrimSpace(v) + if nodeMajor(res.NodeVersion) >= 18 { + res.NodeOK = true + } + } else { + log.Printf("CheckEnv node -v err: %v", err) + } + } + if !res.NodeOK { + res.Issues = append(res.Issues, "Node >= 18 未安装或不可用") + } + + // pi-web + res.PiPath = lookPath("pi-web") + piPkgDir := "" // 用来读 package.json 拿版本 + if res.PiPath == "" { + // fallback to npm prefix bin + if npm, err := runQuiet("npm", "prefix", "-g"); err == nil { + prefix := strings.TrimSpace(npm) + candidate := filepath.Join(prefix, "bin", "pi-web") + if _, statErr := os.Stat(candidate); statErr == nil { + res.PiPath = candidate + } + piPkgDir = filepath.Join(prefix, "lib", "node_modules", "@agegr", "pi-web") + } else { + log.Printf("CheckEnv npm prefix err: %v", err) + } + } else { + // 从 symlink 反推 package.json 路径: .../bin/pi-web -> ../lib/node_modules/@agegr/pi-web/bin/pi-web.js + // 走 npm prefix -g 是稳的 + if npm, err := runQuiet("npm", "prefix", "-g"); err == nil { + prefix := strings.TrimSpace(npm) + piPkgDir = filepath.Join(prefix, "lib", "node_modules", "@agegr", "pi-web") + } + } + if res.PiPath != "" { + // 注意: pi-web --version 实际上会启动 server 然后失败,不安全。 + // 改用读 npm 全局 package.json 拿版本号。 + if data, err := os.ReadFile(filepath.Join(piPkgDir, "package.json")); err == nil { + var pkg struct { + Version string `json:"version"` + } + if jerr := json.Unmarshal(data, &pkg); jerr == nil && pkg.Version != "" { + res.PiVersion = pkg.Version + } + } + if res.PiVersion == "" { + log.Printf("CheckEnv: 找不到 pi-web package.json (尝试: %s)", piPkgDir) + } + } else { + res.Issues = append(res.Issues, "pi-web 命令未找到(请 npm i -g @agegr/pi-web)") + } + + // port 30141 — lsof 在受限进程下可能挂死,超时保护 + if pids, err := lsofPort(piPort); err == nil && len(pids) > 0 { + res.PortBusy = true + res.PortBusyBy = strings.Join(pids, ", ") + } + + return res +} + +// Start launches pi-web, waits for the service port, and opens the browser. +func (a *App) Start() StartResult { + a.mu.Lock() + if a.running { + a.mu.Unlock() + return StartResult{OK: false, URL: a.piURL, Error: "已在运行中"} + } + a.mu.Unlock() + + // 启动前先清理:杀残留 pi 进程,再强杀占 30141 端口的残留 PID(孤儿 node 等) + killPITree() + freePort(piPort) + + piBin := lookPath("pi-web") + if piBin == "" { + if npm, err := runQuiet("npm", "prefix", "-g"); err == nil { + candidate := filepath.Join(strings.TrimSpace(npm), "bin", "pi-web") + if _, statErr := os.Stat(candidate); statErr == nil { + piBin = candidate + } + } + } + if piBin == "" { + return StartResult{OK: false, Error: "找不到 pi-web 可执行文件"} + } + + // --no-open 让 pi-web 只起服务不自动开浏览器,我们自己开 + cmd := exec.Command(piBin, "--no-open") + cmd.Env = buildChildEnv() + + stdout, err := cmd.StdoutPipe() + if err != nil { + return StartResult{OK: false, Error: "无法创建 stdout 管道: " + err.Error()} + } + stderr, err := cmd.StderrPipe() + if err != nil { + return StartResult{OK: false, Error: "无法创建 stderr 管道: " + err.Error()} + } + + if err := cmd.Start(); err != nil { + return StartResult{OK: false, Error: "启动 pi-web 失败: " + err.Error()} + } + + a.mu.Lock() + a.piCmd = cmd + a.running = true + a.startedAt = time.Now() + a.piURL = "" + a.mu.Unlock() + + // drain 掉 stdout/stderr 避免阻塞 + safeGo("drainStdout", func() { drainLog(stdout, "pi") }) + safeGo("drainStderr", func() { drainLog(stderr, "pi") }) + + // 监听子进程退出 + safeGo("piWait", func() { + _ = cmd.Wait() + a.mu.Lock() + a.running = false + a.mu.Unlock() + }) + + // polling 等端口 30141 LISTEN(替代等 URL 字符串) + const timeout = 30 * time.Second + ready := waitForPort(piPort, timeout) + if !ready { + _ = cmd.Process.Kill() + return StartResult{OK: false, Error: fmt.Sprintf("等待端口 %d 监听超时(30s),pi-web 可能没正常启动", piPort)} + } + + url := fmt.Sprintf("http://localhost:%d/", piPort) + a.mu.Lock() + a.piURL = url + a.mu.Unlock() + + // 用系统 open 唤起浏览器(cookie session 自动登录) + if openErr := openBrowser(url); openErr != nil { + return StartResult{OK: true, URL: url, Error: "已启动,但自动开浏览器失败: " + openErr.Error()} + } + + // 启动成功 + 已打开浏览器 → 最小化到 Dock + if a.ctx != nil { + wailsruntime.WindowMinimise(a.ctx) + } + return StartResult{OK: true, URL: url, Hidden: true} +} + +// waitForPort 轮询检查端口是否处于 LISTEN,timeout 内返回 true。 +func waitForPort(port int, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for { + if pids, _ := lsofPort(port); len(pids) > 0 { + return true + } + if time.Now().After(deadline) { + return false + } + time.Sleep(300 * time.Millisecond) + } +} + +// freePort 强制释放端口: lsof 拿到 LISTEN 的所有 PID,逐一 kill -9。 +// 返回被杀的 PID 数量。 +// 适用于: +// - Start 前:如果旧 pi-web 进程杀完但端口还被占(孤儿 node 进程),再补刀 +// - Stop 后:如果 pi-web 进程杀完但端口还在 TIME_WAIT,精准 kill 残留 PID +// - OnShutdown:app 退出前保证不残留任何 pi-web 进程 + 端口 +func freePort(port int) int { + pids, _ := lsofPort(port) + if len(pids) == 0 { + return 0 + } + log.Printf("freePort: %d pid(s) holding :%d", len(pids), port) + for _, pid := range pids { + if err := exec.Command("kill", "-9", pid).Run(); err != nil { + log.Printf("freePort: kill -9 %s: %v", pid, err) + } + } + // 等系统释放 socket + time.Sleep(300 * time.Millisecond) + return len(pids) +} + +// drainLog 读子进程输出并丢弃(不阻塞子进程 stdout 缓冲区)。 +func drainLog(r io.Reader, tag string) { + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 64*1024), 1024*1024) + for scanner.Scan() { + _ = scanner.Text() + } +} + +// Stop kills the pi-web process and verifies the port is released. +func (a *App) Stop() StartResult { + a.mu.Lock() + cmd := a.piCmd + a.mu.Unlock() + + if cmd != nil && cmd.Process != nil { + _ = cmd.Process.Kill() + _, _ = cmd.Process.Wait() + } + + // 兜底:把 pi-web 整个进程树都杀(pi-web 主进程 + Electron helper 等) + killPITree() + + // 再精准 kill 占 30141 的残留 PID(孤儿 node、TIME_WAIT 等) + freePort(piPort) + + time.Sleep(500 * time.Millisecond) + + a.mu.Lock() + a.piCmd = nil + a.piURL = "" + a.running = false + a.mu.Unlock() + + if pids, err := lsofPort(piPort); err == nil && len(pids) > 0 { + return StartResult{OK: false, Error: fmt.Sprintf("端口 %d 仍被占用: %s", piPort, strings.Join(pids, ", "))} + } + return StartResult{OK: true} +} + +// ShowWindow brings the application window to the front. +func (a *App) ShowWindow() { + if a.ctx != nil { + wailsruntime.WindowShow(a.ctx) + } +} + +// HideWindow minimises the application window to Dock. +// 用 WindowMinimise 而不是 WindowHide,这样 macOS 上点 Dock 图标系统会 +// 自动 unminimise 恢复窗口(WindowHide 之后点 Dock 不会自动恢复)。 +func (a *App) HideWindow() { + if a.ctx != nil { + wailsruntime.WindowMinimise(a.ctx) + } +} + +// GetStatus returns the current pi state. +func (a *App) GetStatus() StatusResult { + a.mu.Lock() + defer a.mu.Unlock() + res := StatusResult{Running: a.running, URL: a.piURL} + if a.piCmd != nil && a.piCmd.Process != nil { + res.Pid = a.piCmd.Process.Pid + } + if !a.startedAt.IsZero() { + res.StartedAt = a.startedAt.Format(time.RFC3339) + } + return res +} + +// ---- internal helpers ---- + +func (a *App) stopPI() { + a.mu.Lock() + cmd := a.piCmd + a.running = false + a.mu.Unlock() + if cmd != nil && cmd.Process != nil { + _ = cmd.Process.Kill() + } + // 按 X 关 app / Stop 按钮 / OnShutdown 统一都走这个流程: + // 1) 杀整个 pi-web 进程树 + // 2) 再精准 kill 占 30141 端口的残留 PID + killPITree() + freePort(piPort) +} + +// buildChildEnv 手动把 homebrew 路径塞进子进程 PATH。 +// macOS GUI 启动的进程不读 ~/.zshrc,直接拿环境变量 PATH 经常缺 /opt/homebrew/bin。 +// 注意:本函数不能调任何 exec 子命令(包括 npm),否则会触发栈溢出。 +// npm 全局 bin 路径由 cachedNpmPrefix() 在 initNpmPrefix() 中预先算好。 +func buildChildEnv() []string { + env := os.Environ() + home := os.Getenv("HOME") + extra := []string{ + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + } + // 加上缓存好的 npm 全局 bin(如果有) + if prefix, ok := cachedNpmPrefix(); ok { + extra = append(extra, filepath.Join(prefix, "bin")) + } + + hasPath := false + for i, e := range env { + if strings.HasPrefix(e, "PATH=") { + hasPath = true + cur := strings.TrimPrefix(e, "PATH=") + merged := mergePath(cur, extra) + env[i] = "PATH=" + merged + } + } + if !hasPath { + env = append(env, "PATH="+strings.Join(extra, ":")) + } + if home == "" { + env = append(env, "HOME=/tmp") + } + return env +} + +// mergePath 拼接 PATH。绝对不能调 runQuiet / 任何 exec 子命令, +// 否则会触发 buildChildEnv → mergePath 的无限递归 → 栈溢出。 +// npm 全局 bin 用 sync.Once 懒加载 + 超时兜底,在 initEnvOnce 里完成。 +func mergePath(cur string, extra []string) string { + seen := map[string]bool{} + out := []string{} + add := func(p string) { + if p == "" || seen[p] { + return + } + seen[p] = true + out = append(out, p) + } + for _, p := range strings.Split(cur, ":") { + add(p) + } + for _, p := range extra { + add(p) + } + return strings.Join(out, ":") +} + +// npmPrefixCache 用 sync.Once 缓存 `npm prefix -g` 的输出, +// 避免每次 buildChildEnv 都 exec npm。 +var ( + npmPrefixOnce sync.Once + npmPrefixValue string + npmPrefixReady bool +) + +func cachedNpmPrefix() (string, bool) { + npmPrefixOnce.Do(func() { + // 只在确实有 npm 时才跑 + bin := lookPath("npm") + if bin == "" { + return + } + // 硬超时 2s,失败就算了,不致命。 + // 注意:这里直接 exec 用 os.Environ(),不走 buildChildEnv, + // 否则初始化期间可能再次触发 buildChildEnv 形成循环。 + cmd := exec.Command(bin, "prefix", "-g") + cmd.Env = os.Environ() + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + if err := cmd.Start(); err != nil { + return + } + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + select { + case <-done: + npmPrefixValue = strings.TrimSpace(buf.String()) + npmPrefixReady = npmPrefixValue != "" + case <-time.After(2 * time.Second): + _ = cmd.Process.Kill() + <-done + } + }) + return npmPrefixValue, npmPrefixReady +} + +func openBrowser(url string) error { + switch runtime.GOOS { + case "darwin": + bin := lookPath("open") + if bin == "" { + bin = "/usr/bin/open" + } + return exec.Command(bin, url).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + default: + bin := lookPath("xdg-open") + if bin == "" { + return fmt.Errorf("xdg-open not found") + } + return exec.Command(bin, url).Start() + } +} + +func lookPath(name string) string { + // 先用 PATH + if p, err := exec.LookPath(name); err == nil { + return p + } + // macOS GUI 进程常缺 PATH,兜底几个常见位置 + for _, dir := range []string{"/opt/homebrew/bin", "/usr/local/bin", "/usr/bin"} { + candidate := filepath.Join(dir, name) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + return "" +} + +func runQuiet(name string, args ...string) (string, error) { + return runQuietTimeout(name, 3*time.Second, args...) +} + +// runQuietTimeout runs a command with a hard timeout. If it doesn't finish in time, +// the child is killed and an error is yielded. macOS 上某些 fork-exec 失败场景, +// 没有超时保护会一直 hang,进而拖垮调用方。 +func runQuietTimeout(name string, timeout time.Duration, args ...string) (string, error) { + if name == "" { + return "", fmt.Errorf("empty command") + } + // 用我们自己的 lookPath(GUI 进程 PATH 缺失时也能找到 /opt/homebrew/bin 等), + // 不能用 exec.LookPath —— 它只读系统 PATH,看不到我们手动塞的路径。 + bin := lookPath(name) + if bin == "" { + return "", fmt.Errorf("not found: %s (也没在 /opt/homebrew/bin 找到)", name) + } + cmd := exec.Command(bin, args...) + cmd.Env = buildChildEnv() + // 输出大小限制,防止 npm 输出爆 buffer 卡死 + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + + if err := cmd.Start(); err != nil { + return "", fmt.Errorf("start %s: %w", bin, err) + } + + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + + select { + case err := <-done: + return buf.String(), err + case <-time.After(timeout): + _ = cmd.Process.Kill() + <-done + return "", fmt.Errorf("timeout after %s: %s %s", timeout, bin, strings.Join(args, " ")) + } +} + +func nodeMajor(v string) int { + v = strings.TrimPrefix(v, "v") + parts := strings.SplitN(v, ".", 2) + if len(parts) == 0 { + return 0 + } + n := 0 + for _, ch := range parts[0] { + if ch < '0' || ch > '9' { + break + } + n = n*10 + int(ch-'0') + } + return n +} + +// lsofPort 返回占用该端口的 PID 列表(macOS)。带超时 + panic recover。 +func lsofPort(port int) (res []string, err error) { + defer func() { + if r := recover(); r != nil { + log.Printf("[panic:lsofPort] %v\n%s", r, debugStack()) + err = fmt.Errorf("lsof panic: %v", r) + } + }() + + lsofBin := lookPath("lsof") + if lsofBin == "" { + return nil, nil // 没 lsof,跳过 + } + cmd := exec.Command(lsofBin, "-nP", fmt.Sprintf("-iTCP:%d", port), "-sTCP:LISTEN", "-t") + cmd.Env = buildChildEnv() + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("lsof start: %w", err) + } + + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + + select { + case err := <-done: + out := buf.Bytes() + if err != nil && len(out) == 0 { + // lsof 无命中时退出码非 0 + 空输出 = 端口空闲 + return nil, nil + } + for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") { + l = strings.TrimSpace(l) + if l != "" { + res = append(res, l) + } + } + return res, err + case <-time.After(3 * time.Second): + _ = cmd.Process.Kill() + <-done + return nil, fmt.Errorf("lsof timeout") + } +} + +// ---- Install / Update ---- + +const piPkg = "@agegr/pi-web" + +// UpdateInfo describes the available update for pi-web. +type UpdateInfo struct { + Installed string `json:"installed"` + Latest string `json:"latest"` + HasUpdate bool `json:"hasUpdate"` + Raw string `json:"raw"` +} + +// InstallResult is returned by InstallPi / UpdatePi. +type InstallResult struct { + OK bool `json:"ok"` + Error string `json:"error"` + Output string `json:"output"` +} + +// killPITree kills all processes related to the pi-web service: +// - the pi-web CLI itself (npm/node pi-web/lib/bin.js) +// - any Electron helper (main / renderer / gpu / utility / network) +// +// 不只 kill 主进程: Electron helper 还在跑时,EPIPE 会在 pipe 关闭后让它抛 +// "Uncaught Exception" 弹窗,污染退出 UX。 +func killPITree() { + pids := listPIPids() + if len(pids) == 0 { + return + } + log.Printf("killPITree: %d pids to kill", len(pids)) + for _, pid := range pids { + // kill -9 强杀,避免 Electron helper 还在做 graceful shutdown 又触发 EPIPE + if err := exec.Command("kill", "-9", pid).Run(); err != nil { + log.Printf("killPITree: kill %s: %v", pid, err) + } + } +} + +// listPIPids 列出所有跟 pi-web 相关的进程 PID(去重 + 排除自身)。 +// 用 ps + grep 而不是 pgrep,以兼容 macOS 默认 PATH 不带 /usr/bin 的情况。 +func listPIPids() []string { + out, err := runQuiet("ps", "-axo", "pid=,command=") + if err != nil { + log.Printf("listPIPids: ps err: %v", err) + return nil + } + seen := map[string]bool{} + var pids []string + self := os.Getpid() + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // 格式: "PID COMMAND..." + sp := strings.IndexByte(line, ' ') + if sp <= 0 { + continue + } + pidStr := line[:sp] + cmd := line[sp+1:] + // 跳过自身进程,免得自杀 + if pid, _ := strconv.Atoi(pidStr); pid == self { + continue + } + // 匹配规则: pi-web 启动的所有相关进程 + if isPIProcess(cmd) { + if !seen[pidStr] { + seen[pidStr] = true + pids = append(pids, pidStr) + } + } + } + return pids +} + +// isPIProcess 判断一条 ps command 是不是 pi-web 相关的进程。 +// pi-web 基于 Next.js,真正 listen 30141 的是 next-server 进程; +// 主进程 / Electron helper 用包名路径也能匹到。 +func isPIProcess(cmd string) bool { + matches := []string{ + "pi-web", // pi-web 任意引用(主进程、cli、helper) + "@agegr/pi-web", // npm 包路径 + ".pi/electron", // pi-web 内置 Electron runtime + "next-server", // Next.js server(实际 listen 端口的进程) + } + for _, m := range matches { + if strings.Contains(cmd, m) { + return true + } + } + return false +} + +func (a *App) CheckUpdate() UpdateInfo { + installed := "" + if p := lookPath("pi-web"); p != "" { + if v, err := runQuietTimeout(p, 5*time.Second, "--version"); err == nil { + installed = strings.TrimSpace(v) + } else { + log.Printf("CheckUpdate pi-web --version err: %v", err) + } + } + + latest, raw := latestVersionFromNPM(piPkg) + + info := UpdateInfo{ + Installed: installed, + Latest: latest, + Raw: raw, + } + if installed != "" && latest != "" && installed != latest { + info.HasUpdate = true + } + return info +} + +// latestVersionFromNPM 用 `npm view version` 取最新版。 +// 带 10 秒超时,带 stderr 捕获以便诊断。 +func latestVersionFromNPM(pkg string) (string, string) { + npmBin := lookPath("npm") + if npmBin == "" { + return "", "npm not found in PATH" + } + cmd := exec.Command(npmBin, "view", pkg, "version", "--registry=https://registry.npmjs.org/") + cmd.Env = buildChildEnv() + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + return "", "start npm: " + err.Error() + } + + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + + select { + case err := <-done: + out := strings.TrimSpace(stdout.String()) + errMsg := strings.TrimSpace(stderr.String()) + if err != nil { + raw := errMsg + if raw == "" { + raw = err.Error() + } + return "", raw + } + // npm 输出可能带换行 / 警告,只取第一行 token + first := out + if i := strings.IndexAny(out, "\n\r"); i >= 0 { + first = strings.TrimSpace(out[:i]) + } + // 清理 npm 有时会附加的 @ 标签 + first = strings.TrimPrefix(first, "@") + return first, out + case <-time.After(15 * time.Second): + _ = cmd.Process.Kill() + <-done + return "", "npm view 超时(15s),请检查网络" + } +} + +// InstallPi runs `npm install -g @agegr/pi-web` and streams output via Wails events. +// Returns InstallResult once the command exits. +func (a *App) InstallPi() InstallResult { + return a.runNpmEvent("install", []string{"install", "-g", piPkg}, "pi:install:done") +} + +// UpdatePi runs `npm install -g @agegr/pi-web@latest` and streams output. +func (a *App) UpdatePi() InstallResult { + return a.runNpmEvent("update", []string{"install", "-g", piPkg + "@latest"}, "pi:update:done") +} + +func (a *App) runNpmEvent(phase string, args []string, doneEvent string) InstallResult { + npmBin := lookPath("npm") + if npmBin == "" { + errMsg := "找不到 npm(系统 PATH 和 /opt/homebrew/bin 都没找到)" + if a.ctx != nil { + wailsruntime.EventsEmit(a.ctx, "pi:"+phase+":log", "✗ "+errMsg+"\n") + wailsruntime.EventsEmit(a.ctx, doneEvent, InstallResult{Error: errMsg}) + } + return InstallResult{Error: errMsg} + } + if a.ctx != nil { + wailsruntime.EventsEmit(a.ctx, "pi:"+phase+":log", "▶ "+npmBin+" "+strings.Join(args, " ")+"\n") + } + + cmd := exec.Command(npmBin, args...) + cmd.Env = buildChildEnv() + stdout, err := cmd.StdoutPipe() + if err != nil { + return InstallResult{Error: err.Error()} + } + stderr, err := cmd.StderrPipe() + if err != nil { + return InstallResult{Error: err.Error()} + } + if err := cmd.Start(); err != nil { + return InstallResult{Error: "启动 npm 失败: " + err.Error()} + } + + var buf strings.Builder + var mu sync.Mutex + collect := func(line string) { + mu.Lock() + buf.WriteString(line) + buf.WriteString("\n") + mu.Unlock() + if a.ctx != nil { + wailsruntime.EventsEmit(a.ctx, "pi:"+phase+":log", line+"\n") + } + } + safeGo("scanStdout", func() { scanLines(stdout, collect) }) + safeGo("scanStderr", func() { scanLines(stderr, collect) }) + + waitErr := cmd.Wait() + mu.Lock() + out := buf.String() + mu.Unlock() + + res := InstallResult{Output: out} + if waitErr != nil { + res.Error = waitErr.Error() + } else { + res.OK = true + } + if a.ctx != nil { + wailsruntime.EventsEmit(a.ctx, doneEvent, res) + } + return res +} + +func scanLines(r io.Reader, fn func(string)) { + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 64*1024), 1024*1024) + for scanner.Scan() { + fn(scanner.Text()) + } +} \ No newline at end of file diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..e781eb5 --- /dev/null +++ b/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building with `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building with `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..744693e Binary files /dev/null and b/build/appicon.png differ diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 0000000..5c5086f --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{safeBundleID .Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + \ No newline at end of file diff --git a/build/iconfile.icns b/build/iconfile.icns new file mode 100644 index 0000000..2286706 Binary files /dev/null and b/build/iconfile.icns differ diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..0e1c8b5 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..f6e2123 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,8 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..872eff9 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1392 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "vue": "^3.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.0", + "vite": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz", + "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz", + "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz", + "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz", + "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz", + "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz", + "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz", + "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz", + "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz", + "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz", + "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz", + "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz", + "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz", + "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz", + "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz", + "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz", + "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz", + "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz", + "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz", + "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz", + "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz", + "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz", + "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz", + "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz", + "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz", + "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.9", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", + "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.2.tgz", + "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.2", + "@rollup/rollup-android-arm64": "4.63.2", + "@rollup/rollup-darwin-arm64": "4.63.2", + "@rollup/rollup-darwin-x64": "4.63.2", + "@rollup/rollup-freebsd-arm64": "4.63.2", + "@rollup/rollup-freebsd-x64": "4.63.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.2", + "@rollup/rollup-linux-arm-musleabihf": "4.63.2", + "@rollup/rollup-linux-arm64-gnu": "4.63.2", + "@rollup/rollup-linux-arm64-musl": "4.63.2", + "@rollup/rollup-linux-loong64-gnu": "4.63.2", + "@rollup/rollup-linux-loong64-musl": "4.63.2", + "@rollup/rollup-linux-ppc64-gnu": "4.63.2", + "@rollup/rollup-linux-ppc64-musl": "4.63.2", + "@rollup/rollup-linux-riscv64-gnu": "4.63.2", + "@rollup/rollup-linux-riscv64-musl": "4.63.2", + "@rollup/rollup-linux-s390x-gnu": "4.63.2", + "@rollup/rollup-linux-x64-gnu": "4.63.2", + "@rollup/rollup-linux-x64-musl": "4.63.2", + "@rollup/rollup-openbsd-x64": "4.63.2", + "@rollup/rollup-openharmony-arm64": "4.63.2", + "@rollup/rollup-win32-arm64-msvc": "4.63.2", + "@rollup/rollup-win32-ia32-msvc": "4.63.2", + "@rollup/rollup-win32-x64-gnu": "4.63.2", + "@rollup/rollup-win32-x64-msvc": "4.63.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a34ac1f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.0", + "vite": "^7.0.0" + } +} \ No newline at end of file diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..96e6058 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +71e741a4044d4a0f11cff668ef3f7ad4 \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..856478a --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,1011 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/frontend/src/assets/images/appicon.png b/frontend/src/assets/images/appicon.png new file mode 100644 index 0000000..fc8259f Binary files /dev/null and b/frontend/src/assets/images/appicon.png differ diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..aa51d27 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,5 @@ +import {createApp} from 'vue' +import App from './App.vue' +import './style.css'; + +createApp(App).mount('#app') \ No newline at end of file diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..9dd6d9a --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1 @@ +/* 全局样式交给 App.vue 设计稿 token;此处不引入冲突样式 */ \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..c35a183 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()] +}) \ No newline at end of file diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 0000000..72372af --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,21 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {main} from '../models'; + +export function CheckEnv():Promise; + +export function CheckUpdate():Promise; + +export function GetStatus():Promise; + +export function HideWindow():Promise; + +export function InstallPi():Promise; + +export function ShowWindow():Promise; + +export function Start():Promise; + +export function Stop():Promise; + +export function UpdatePi():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100755 index 0000000..7b363a0 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,39 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function CheckEnv() { + return window['go']['main']['App']['CheckEnv'](); +} + +export function CheckUpdate() { + return window['go']['main']['App']['CheckUpdate'](); +} + +export function GetStatus() { + return window['go']['main']['App']['GetStatus'](); +} + +export function HideWindow() { + return window['go']['main']['App']['HideWindow'](); +} + +export function InstallPi() { + return window['go']['main']['App']['InstallPi'](); +} + +export function ShowWindow() { + return window['go']['main']['App']['ShowWindow'](); +} + +export function Start() { + return window['go']['main']['App']['Start'](); +} + +export function Stop() { + return window['go']['main']['App']['Stop'](); +} + +export function UpdatePi() { + return window['go']['main']['App']['UpdatePi'](); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100755 index 0000000..14d1d0d --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -0,0 +1,109 @@ +export namespace main { + + export class CheckResult { + nodePath: string; + nodeVersion: string; + nodeOK: boolean; + piPath: string; + piVersion: string; + credFile: string; + credExists: boolean; + homePiDir: string; + portBusy: boolean; + portBusyBy: string; + issues: string[]; + + static createFrom(source: any = {}) { + return new CheckResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.nodePath = source["nodePath"]; + this.nodeVersion = source["nodeVersion"]; + this.nodeOK = source["nodeOK"]; + this.piPath = source["piPath"]; + this.piVersion = source["piVersion"]; + this.credFile = source["credFile"]; + this.credExists = source["credExists"]; + this.homePiDir = source["homePiDir"]; + this.portBusy = source["portBusy"]; + this.portBusyBy = source["portBusyBy"]; + this.issues = source["issues"]; + } + } + export class InstallResult { + ok: boolean; + error: string; + output: string; + + static createFrom(source: any = {}) { + return new InstallResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ok = source["ok"]; + this.error = source["error"]; + this.output = source["output"]; + } + } + export class StartResult { + ok: boolean; + url: string; + error: string; + output: string; + hidden: boolean; + + static createFrom(source: any = {}) { + return new StartResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ok = source["ok"]; + this.url = source["url"]; + this.error = source["error"]; + this.output = source["output"]; + this.hidden = source["hidden"]; + } + } + export class StatusResult { + running: boolean; + url: string; + startedAt: string; + pid: number; + + static createFrom(source: any = {}) { + return new StatusResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.running = source["running"]; + this.url = source["url"]; + this.startedAt = source["startedAt"]; + this.pid = source["pid"]; + } + } + export class UpdateInfo { + installed: string; + latest: string; + hasUpdate: boolean; + raw: string; + + static createFrom(source: any = {}) { + return new UpdateInfo(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.installed = source["installed"]; + this.latest = source["latest"]; + this.hasUpdate = source["hasUpdate"]; + this.raw = source["raw"]; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,330 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,298 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4883060 --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module pi-launcher + +go 1.25.0 + +require github.com/wailsapp/wails/v2 v2.13.0 + +require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b664a33 --- /dev/null +++ b/go.sum @@ -0,0 +1,83 @@ +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.13.0 h1:S7OgXWpj72V91unF8iDWJKbcS9ZpwCT3R0QVru4v2Mg= +github.com/wailsapp/wails/v2 v2.13.0/go.mod h1:nVr/wSIEZ7xxKPkzK65mjpKpaOPQI2k4pvLwGR/i4kc= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2efaef6 --- /dev/null +++ b/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "embed" + "fmt" + "os" + "time" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/mac" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // 调试模式:不启 GUI,直接调 CheckEnv 走完流程,看是不是会崩。 + if len(os.Args) > 1 && os.Args[1] == "--selftest" { + app := NewApp() + fmt.Println("=== CheckEnv ===") + r := app.CheckEnv() + fmt.Printf("nodeOK=%v piPath=%q piVer=%q cred=%v portBusy=%v\n", + r.NodeOK, r.PiPath, r.PiVersion, r.CredExists, r.PortBusy) + fmt.Printf("issues=%v\n", r.Issues) + fmt.Println("=== CheckUpdate ===") + u := app.CheckUpdate() + fmt.Printf("installed=%q latest=%q hasUpdate=%v\n", + u.Installed, u.Latest, u.HasUpdate) + fmt.Println("=== listPIPids ===") + pids := listPIPids() + fmt.Printf("found %d pi-related pids:\n", len(pids)) + for _, p := range pids { + fmt.Printf(" - %s\n", p) + } + fmt.Println("=== waitForPort test (30141, 1s) ===") + fmt.Printf("port ready: %v\n", waitForPort(30141, 1*time.Second)) + fmt.Println("=== freePort test (30141) ===") + killed := freePort(30141) + fmt.Printf("killed: %d pid(s)\n", killed) + fmt.Println("=== done, exiting ===") + return + } + + app := NewApp() + + err := wails.Run(&options.App{ + Title: "pi-launcher", + Width: 1040, + Height: 1280, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 17, G: 24, B: 39, A: 1}, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Bind: []interface{}{ + app, + }, + Mac: &mac.Options{ + TitleBar: &mac.TitleBar{ + TitlebarAppearsTransparent: true, + HideTitle: false, + HideTitleBar: false, + FullSizeContent: false, + }, + Appearance: mac.NSAppearanceNameDarkAqua, + WebviewIsTransparent: false, + About: &mac.AboutInfo{ + Title: "pi-launcher", + Message: "本地按需启动 pi-web UI。", + }, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} \ No newline at end of file diff --git a/scripts/build-mac.sh b/scripts/build-mac.sh new file mode 100755 index 0000000..9b767d3 --- /dev/null +++ b/scripts/build-mac.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Build pi-launcher.app for macOS (universal arm64/x86_64). +# +# 用法: ./scripts/build-mac.sh +# +# 产物: build/bin/pi-launcher.app +# 安装: ./scripts/install-app.sh (拷贝到 /Applications/ + 刷新图标缓存) +# +# 注意: Wails v2 跨平台 build 需要在目标平台编译。 +# 想出 Windows 版,必须在 Windows 上跑 scripts/build-windows.bat。 + +set -euo pipefail + +cd "$(dirname "$0")/.." + +echo "▶ pi-launcher · macOS build" +echo "" + +# 1. 基础工具检查 +command -v go >/dev/null 2>&1 || { echo "✗ go not installed"; exit 1; } +command -v node >/dev/null 2>&1 || { echo "✗ node not installed"; exit 1; } +command -v npm >/dev/null 2>&1 || { echo "✗ npm not installed"; exit 1; } +command -v wails >/dev/null 2>&1 || { echo "✗ wails not installed (go install github.com/wailsapp/wails/v2/cmd/wails@latest)"; exit 1; } + +GOOS=$(go env GOOS) +if [[ "${GOOS}" != "darwin" ]]; then + echo "✗ must run on macOS (current: ${GOOS})" + exit 1 +fi + +# 2. 同步前端依赖(版本变更后才需要,但每次跑更稳) +echo "▶ npm install" +(cd frontend && npm install) + +# 3. 编译 +# -clean: 清掉 build/bin,确保 icns / Info.plist 跟当前源码一致 +# 不带 -platform: 当前平台 + 当前架构 +echo "" +echo "▶ wails build" +wails build -clean + +# 4. 校验产物 +APP="build/bin/pi-launcher.app" +if [[ ! -d "${APP}" ]]; then + echo "✗ ${APP} not found" + exit 1 +fi +if [[ ! -f "${APP}/Contents/Resources/iconfile.icns" ]]; then + echo "✗ iconfile.icns missing" + exit 1 +fi + +ICON_SIZE=$(stat -f%z "${APP}/Contents/Resources/iconfile.icns") +EXE_SIZE=$(stat -f%z "${APP}/Contents/MacOS/pi-launcher") +echo "" +echo "✓ app: ${APP}" +echo " binary: ${EXE_SIZE} bytes" +echo " icon: ${ICON_SIZE} bytes" +echo "" +echo " open: open ${APP}" +echo " install: ./scripts/install-app.sh" \ No newline at end of file diff --git a/scripts/build-windows.bat b/scripts/build-windows.bat new file mode 100644 index 0000000..c290dc7 --- /dev/null +++ b/scripts/build-windows.bat @@ -0,0 +1,87 @@ +@echo off +REM Build pi-launcher.exe for Windows (amd64). +REM +REM 用法: scripts\build-windows.bat +REM +REM 前置依赖(Windows 上装好后): +REM - Go 1.21+ https://go.dev/dl/ +REM - Node.js 18+ https://nodejs.org/ +REM - Wails CLI go install github.com/wailsapp/wails/v2/cmd/wails@latest +REM - WebView2 Runtime Win10 1803+ 自带,否则 https://aka.ms/webview2 +REM - MSVC build tools https://visualstudio.microsoft.com/downloads/ (Build Tools for Visual Studio, 勾选 "Desktop development with C++") +REM +REM 注意: Wails v2 的 Windows build 需要 MSVC toolchain (cgo 编译 webview2 loader)。 +REM 不能在 macOS / Linux 上 cross-compile 出 .exe。 +REM +REM 产物: build\bin\pi-launcher.exe + +setlocal enabledelayedexpansion + +cd /d "%~dp0\.." + +echo ^>^> pi-launcher · Windows build +echo. + +REM 1. 基础工具检查 +where go >nul 2>nul +if errorlevel 1 ( + echo [X] go not installed + exit /b 1 +) +where node >nul 2>nul +if errorlevel 1 ( + echo [X] node not installed + exit /b 1 +) +where npm >nul 2>nul +if errorlevel 1 ( + echo [X] npm not installed + exit /b 1 +) +where wails >nul 2>nul +if errorlevel 1 ( + echo [X] wails CLI not installed - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest + exit /b 1 +) + +REM 2. 检查 WINDOWS 环境 +if not "%OS%"=="Windows_NT" ( + echo [X] must run on Windows, current: %OS% + exit /b 1 +) + +REM 3. 同步前端依赖 +echo ^>^> npm install +cd frontend +call npm install +if errorlevel 1 ( + echo [X] npm install failed + exit /b 1 +) +cd .. + +REM 4. 编译 +echo. +echo ^>^> wails build +wails build -clean -platform windows/amd64 +if errorlevel 1 ( + echo [X] wails build failed + exit /b 1 +) + +REM 5. 校验产物 +set EXE=build\bin\pi-launcher.exe +if not exist "%EXE%" ( + echo [X] %EXE% not found + exit /b 1 +) + +echo. +echo [OK] exe: %EXE% +echo size: %~z1 bytes +echo. +echo run: %EXE% +echo build dir: build\bin\ +echo. + +endlocal \ No newline at end of file diff --git a/scripts/install-app.sh b/scripts/install-app.sh new file mode 100755 index 0000000..729ee66 --- /dev/null +++ b/scripts/install-app.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# 装 pi-launcher.app 到 /Applications/,并强制刷新 macOS 图标。 +# +# 背景: macOS LaunchServices 会把 com.wails.pi-launcher 的图标缓存成 +# 当前 app 文件的快照。每次 wails build 之后 .app 内容重建,缓存可能不刷新, +# 导致 Dock 显示默认 .app 占位图而不是真实 icns。 +# +# 解决: 把 .app 复制到一个稳定路径(/Applications/),重建时先 rm 再 cp, +# lsregister -f 强制重新注册。 +# +# 用法: ./scripts/install-app.sh + +set -euo pipefail + +APP_NAME="pi-launcher.app" +SRC="build/bin/${APP_NAME}" +DEST="/Applications/${APP_NAME}" + +if [[ ! -d "${SRC}" ]]; then + echo "✗ ${SRC} not found, run 'wails build' first" + exit 1 +fi + +# 1. 验证 icns 文件是新图 +if [[ ! -f "${SRC}/Contents/Resources/iconfile.icns" ]]; then + echo "✗ iconfile.icns missing — icon wasn't packaged" + exit 1 +fi +ICON_SIZE=$(stat -f%z "${SRC}/Contents/Resources/iconfile.icns") +if [[ ${ICON_SIZE} -lt 100000 ]]; then + echo "⚠ iconfile.icns suspiciously small (${ICON_SIZE} bytes)" +fi +echo "✓ iconfile.icns: ${ICON_SIZE} bytes" + +# 2. 删除旧版(让 lsregister 看到全新路径) +if [[ -d "${DEST}" ]]; then + rm -rf "${DEST}" +fi + +# 3. 复制到 /Applications +cp -R "${SRC}" "${DEST}" +echo "✓ copied to ${DEST}" + +# 4. 强制重新注册到 LaunchServices +LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister" +"${LSREGISTER}" -f "${DEST}" + +# 5. touch 一下让文件 mtime 更新 +/usr/bin/touch -c "${DEST}/Contents/Info.plist" +/usr/bin/touch -c "${DEST}/Contents/Resources/iconfile.icns" + +# 6. 清 Dock iconcache +DOCK_CACHE=$(find /var/folders -name "com.apple.dock.iconcache" 2>/dev/null | head -1) +if [[ -n "${DOCK_CACHE}" ]]; then + rm -f "${DOCK_CACHE}" + echo "✓ cleared dock iconcache" +fi + +# 7. 重启 Dock 强制重读 +killall Dock 2>/dev/null || true +sleep 1 + +echo "" +echo "✓ ${APP_NAME} installed at ${DEST}" +echo " open it via Finder / Spotlight / Dock (re-add to Dock if needed)" \ No newline at end of file diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..918a112 --- /dev/null +++ b/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "pi-launcher", + "outputfilename": "pi-launcher", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Jack", + "email": "jack0210_@hotmail.com" + } +} \ No newline at end of file