# pi-keepawake

[![npm](https://img.shields.io/npm/v/pi-keepawake.svg)](https://www.npmjs.com/package/pi-keepawake)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

[English](./README.md) | **简体中文**

> 一个 [pi](https://pi.dev) 扩展：**只要还有 pi 进程在运行，电脑就不会进入睡眠/休眠**。
> 熄屏（显示器休眠）和锁屏照常工作，不做任何干预。

零配置：装好就生效。MIT 许可。

---

## 特性

- **进程级语义**：从 `session_start` 持有到 `session_shutdown`，也就是「pi 还开着就拦睡眠」。
  每个 pi 进程各持一把独立的 OS 断言，由操作系统聚合 —— 开 3 个 pi，关掉 2 个，机器照样不睡。
- **只拦系统睡眠**：不加 `ES_DISPLAY_REQUIRED`（Windows）、不加 `-d`（macOS）、
  不拦 `idle` / `handle-lid-switch`（Linux）。**熄屏可以，锁屏可以**。
- **零配置**：无 flag、无环境变量、无命令。
- **fail-safe**：容器里、没有 PowerShell、平台不支持时，pi 照常运行，只在 TUI 里警告一次。
- **跨平台**：Windows / macOS / Linux 都走系统自带机制，不需要管理员权限、没有 native addon、不需要 node-gyp。
- **可观测**：TUI footer 显示 `☕ keep-awake`；底层 helper 进程和命令行都能直接查（见下文）。
- **不残留**：正常退出、Ctrl-C、`process.exit()` 都会释放；OS 原语被杀后会自动重挂。每次
  reload / 会话切换之后的 `session_start` 会先清扫「同一 pi 进程内上一个运行时」遗留的 helper，
  所以 reload 不会多留一把锁。

## 安装

需要 Node.js ≥ 20（pi 自带）。

### 1. 从 npm 安装（推荐）

```bash
pi install npm:pi-keepawake
```

### 2. 从 Gitee 安装

```bash
pi install git:gitee.com/zxh126/pi-keepaware
```

以上两种方式 pi 都会自动安装依赖（`pervigil`）。装完 `pi list` 能看到它，`/reload` 生效。
需要钉死版本时写全：`pi install npm:pi-keepawake@1.0.0`。

### 3. 手动放进用户扩展目录

```bash
git clone https://gitee.com/zxh126/pi-keepaware "$HOME/.pi/agent/extensions/keep-awake"
cd "$HOME/.pi/agent/extensions/keep-awake" && npm install
```

Windows 对应路径是 `%USERPROFILE%\.pi\agent\extensions\keep-awake`。
pi 会自动发现 `extensions/` 下所有含 `index.ts` 的子目录。

> 本地目录形式的包 pi **不会**替你装依赖，所以这一步的 `npm install` 不能省。

### 4. 只试用一次

```bash
pi -e npm:pi-keepawake
pi -e git:gitee.com/zxh126/pi-keepaware
pi -e ./pi-keepawake            # 本地路径，需先 npm install
```

### 卸载

```bash
pi remove npm:pi-keepawake
pi remove git:gitee.com/zxh126/pi-keepaware
```

或者直接删掉 `~/.pi/agent/extensions/keep-awake` 目录。

## 它到底做了什么

扩展本身只负责「什么时候持有/释放」；真正和操作系统打交道的是
[pervigil](https://www.npmjs.com/package/pervigil)（零依赖、无 native addon、MIT）。

| 平台 | 机制 | 具体参数 |
| --- | --- | --- |
| Windows | 一个 PowerShell 子进程调用 `SetThreadExecutionState`，然后阻塞住保住该线程 | `ES_CONTINUOUS \| ES_SYSTEM_REQUIRED`（**没有** `ES_DISPLAY_REQUIRED`） |
| macOS | `caffeinate(1)` | `-i`（**没有** `-d`） |
| Linux | `systemd-inhibit(1)`，没有 systemd 时退回 `/sys/power/wake_lock` | `--what=sleep --mode=block`（**没有** `idle`、`handle-lid-switch`） |

生命周期：

```
session_start ──▶ acquire ──▶ [ 整段会话都持有 ] ──▶ session_shutdown ──▶ release
                                      │
                                      └─ helper 意外死亡 ⇒ primitiveDied ⇒ 自动重挂（最多 5 次）
```

补充说明：

- 不用改电源计划（`powercfg`）、不用 presentation mode、不模拟输入 —— 所以锁屏和屏保都不会被顶掉。
- 用户**主动**点「睡眠/休眠」不会被阻止，这是系统断言的语义，也是本扩展刻意保留的行为。
- 容器里（`/.dockerenv`、`container` 环境变量等）pervigil 直接 no-op，不会报错。

## 验证它在工作

最直观的：pi 的 footer 出现 `☕ keep-awake`。

命令行验证（用系统自身的视角看断言是否存在）：

**Windows**（PowerShell，无需管理员）

```powershell
Get-CimInstance Win32_Process -Filter "Name='powershell.exe' OR Name='pwsh.exe'" |
  Where-Object { $_.CommandLine -like '*Pervigil*' } |
  Select-Object ProcessId, ParentProcessId,
    @{ n = 'flags'; e = {
        @(
          if ($_.CommandLine -match 'ES_SYSTEM_REQUIRED')  { 'ES_SYSTEM_REQUIRED' }
          if ($_.CommandLine -match 'ES_DISPLAY_REQUIRED') { 'ES_DISPLAY_REQUIRED' }
        ) -join ','
      } }
```

pi 运行期间应该恰好出现一个 helper，`flags` 为 `ES_SYSTEM_REQUIRED`（**不含** `ES_DISPLAY_REQUIRED`）；
pi 退出后它应该消失。

> `powercfg /requests` 也能看到 request，但它**需要管理员权限**，普通终端里跑会直接报错。

**macOS**

```bash
pmset -g assertions | grep -i caffeinate
```

**Linux**

```bash
systemd-inhibit --list | grep -i pi
```

## 已知限制

- **Windows 上常驻一个 PowerShell 进程**（实测 WorkingSet ≈ 74 MB / Private ≈ 59 MB）——
  这是「整个会话都持有」这个语义的直接成本，pi 退出时回收。
- Linux 需要 systemd 用户会话（`systemd-inhibit`）或可写的 `/sys/power/wake_lock`；否则降级为 no-op 并警告一次。
- 不阻止合盖休眠（Linux 的 `handle-lid-switch` 未加入 inhibitor）、不阻止用户主动睡眠。
- 仅本仓库在 **Windows 11 实机**上验证过（helper 出现/释放、多进程聚合、helper 被杀后重挂、硬杀不残留）。
  macOS / Linux 的代码路径按 pervigil 的语义核对过，但**没有实机运行记录**。
- pi 被以无法被捕获的方式强杀（TerminateProcess / OOM）时，扩展自身的清理钩子不会执行；
  本机上实测 helper 会随父进程一起消失，另外扩展在每次 `session_start` 时会顺手清理
  「父进程是本进程」的遗留 helper（reload 未能走 `session_shutdown` 的情况）以及「父进程已死」的
  历史 helper 作为兜底；其他存活进程的 helper 一律不动。

## 开发

```text
pi-keepawake/
├── index.ts          # 扩展本体（唯一源码）
├── package.json      # pi 清单：pi.extensions -> ./index.ts，依赖 pervigil
├── LICENSE           # MIT
├── README.md         # 英文版
└── README.zh-CN.md   # 本文件
```

- 扩展运行在 pi 进程内，走 pi 的 jiti loader，**不需要编译**。
- 只有 `pervigil` 一个运行时依赖；pi 提供的 `@earendil-works/*` 包只用 `import type`，不进 `dependencies`。
- 改完在 pi 里 `/reload` 即可生效；想临时试装可以 `pi -e <本地路径>`。

涉及的关键 pi 事件：`session_start`（创建 wake lock 控制器并 acquire）、
`session_shutdown`（释放并清 footer 状态）。运行期复用 pervigil 的
`primitiveDied` / `degraded` 事件做重挂与告警。

发布流程（维护者）：改 `package.json` 的 `version` → 提交 → 推 Gitee → `npm publish --registry=https://registry.npmjs.org`
（本机 `.npmrc` 的默认 registry 指向 npmmirror 镜像，只影响安装、发布必须显式指定官方源）。

## 依赖与致谢

- [pervigil](https://github.com/rejifald/pervigil) — 跨平台 sleep inhibitor（MIT，作者 Oleksandr Zhuravlov）。
- [pi](https://pi.dev) — 扩展宿主。

## License

[MIT](./LICENSE) © 2026 zxh126
