# openclaw-plugins-cli

CLI wrapper for `openclaw plugins install/update` with three-tier fallback: remote → local tgz → manual bypass.

## 安装

```bash
npm install @lark-apaas/openclaw-scripts-plugins-cli
```

## 用法

```bash
# 全局安装后直接使用
openclaw-plugins install <npm-spec> [options]
openclaw-plugins update <npm-spec> [options]
openclaw-plugins update --all [options]

# 或通过 npx 免安装使用
npx -y @lark-apaas/openclaw-scripts-plugins-cli install <npm-spec> [options]
npx -y @lark-apaas/openclaw-scripts-plugins-cli update <npm-spec> [options]
npx -y @lark-apaas/openclaw-scripts-plugins-cli update --all [options]
```

### 支持的 spec 格式

| 格式 | 示例 | 说明 |
|------|------|------|
| 精确版本 | `openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@1.0.2` | 安装指定版本 |
| 无版本号 | `openclaw-plugins install @lark-apaas/openclaw-extension-miaoda` | 安装 latest |
| 范围版本 `^` | `openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@^1.0.0` | 安装兼容的最新版本 |
| 范围版本 `~` | `openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@~1.0.0` | 安装补丁级最新版本 |
| 预发布版本 | `openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@1.0.1-alpha.6` | 安装指定预发布版本 |
| 无 scope 包 | `openclaw-plugins install openclaw-foo@2.0.0` | 安装无 scope 的包 |

### Options

| 选项 | 说明 |
|------|------|
| `--all` | 批量更新所有已安装的插件（仅 update 支持） |
| `--legacy-hook-packs` | 同时更新已废弃的 hook packs（需配合 `--all`） |
| `--no-lock` | 安装后去除 spec 中的版本锁定，使后续 `update --all` 可升级到最新（仅 install 支持） |
| `--dry-run` | 预览将要执行的命令，不实际执行 |
| `--help` | 显示帮助信息 |

### 环境变量

| 变量 | 说明 |
|------|------|
| `OPENCLAW_FORCE_LOCAL_INSTALL=1` | 跳过远程安装，直接走本地 fallback |
| `OPENCLAW_FORCE_MANUAL_INSTALL=1` | 完全绕过 openclaw CLI，直接走 manual bypass（解压 tgz + 手动写配置） |
| `LOG_LEVEL=0` | 静默模式，不输出到 stdout（日志文件仍会写入） |

## 三级降级策略

install 和 update 共用同一套三级降级链：

```
1. Remote       openclaw plugins install/update <spec> [--dangerously-force-unsafe-install]
    ↓ 失败
2. Local        npm pack → openclaw plugins install <tgz> [--dangerously-force-unsafe-install]
    ↓ 安全扫描阻止 (security_scan_blocked)
3. Manual       npm pack → tar 解压 → 复制到 extensions/ → npm install deps → 写配置
```

- 第 1→2 级降级：**任何**远程错误都会触发（429 限流、网络错误等）
- 第 2→3 级降级：**仅安全扫描阻止**时触发（匹配 `security_scan_blocked` / `security_scan_failed`），其他错误直接失败
- `--dangerously-force-unsafe-install`：通过检测 `openclaw plugins install --help` 输出自动判断是否支持，低版本不传

### 环境变量快捷跳转

| 环境变量 | 效果 |
|----------|------|
| 不设置 | 1 → 2 → 3 完整链路 |
| `OPENCLAW_FORCE_LOCAL_INSTALL=1` | 跳过第 1 级，从第 2 级开始，安全扫描阻止时仍会降级到第 3 级 |
| `OPENCLAW_FORCE_MANUAL_INSTALL=1` | 直接走第 3 级，完全不调用 `openclaw plugins install/update` |

## 实现细节

### 第 1 级：远程安装

```bash
openclaw plugins install <spec> [--dangerously-force-unsafe-install]
openclaw plugins update <pluginName> [--dangerously-force-unsafe-install]
```

- 自动检测 openclaw 是否支持 `--dangerously-force-unsafe-install`（通过 `--help` 输出探测，结果缓存）
- 支持时自动追加，用于绕过 openclaw 的内置安全扫描（如 `env-harvesting` 规则）

### 第 2 级：本地 fallback — `installViaLocalFallback()`

远程失败时自动降级。`npm pack` 下载 tgz，再用 `openclaw plugins install <tgz>` 本地安装：

#### Step 1: npm pack 下载包

```bash
npm pack <spec> --json    # 在 /tmp 目录执行
```

- 从 `npm pack --json` 输出中解析 `{ filename, version, name, integrity, shasum }`
- 安全检查：拒绝包含路径分隔符的文件名（防止路径穿越）

#### Step 2: 本地安装 tgz

```bash
openclaw plugins install /tmp/<tgz> [--dangerously-force-unsafe-install]
```

**插件已存在处理：**

| 场景 | 行为 |
|------|------|
| 版本相同 | 跳过安装 |
| 版本不同（升级或降级） | `openclaw plugins uninstall --force` 后重装 |
| 无法确定版本 | 强制重装 |

**容错处理：**
- openclaw `Config validation failed: plugins.allow: plugin not found` 已知 bug — 若插件文件已存在则视为成功，后续通过 config set 修复
- 安全扫描阻止 — 抛出错误，由上层触发第 3 级降级

#### Step 3-4: 清理 + 写配置

清理临时 tgz 文件，通过 `openclaw config set` 写入安装元数据（source, spec, version, integrity 等）和 allowlist。

### 第 3 级：Manual Bypass — `installViaManualBypass()`

完全绕过 `openclaw plugins install`，手动复现其安装逻辑。仅在安全扫描阻止时触发（或通过 `OPENCLAW_FORCE_MANUAL_INSTALL=1` 强制）。

#### 流程

1. **npm pack** — 下载 tgz
2. **解压** — `tar xzf` 到临时目录（`mkdtempSync`）
3. **复制** — `cpSync` 到 `<stateDir>/extensions/<pluginName>/`（stateDir 解析见下文）
4. **权限** — `chmodSync(targetDir, 0o700)` 对齐 openclaw 行为
5. **依赖** — 若 `package.json` 有 `dependencies`，执行 `npm install --omit=dev --ignore-scripts`
   - `workspace:` 引用会先从 `package.json` 中移除并回写磁盘
   - 依赖安装失败不中断（best-effort），插件可能不需要这些依赖也能工作
6. **配置** — 通过 `openclaw config set` 写入：
   - `plugins.installs.<name>.*` — 安装元数据（source, spec, version, integrity, shasum 等）
   - `plugins.entries.<name>.enabled` — 启用插件
   - `plugins.allow` — 加入 allowlist（若 allowlist 存在）
7. **清理** — 删除临时解压目录和 tgz 文件

#### State 目录解析

Manual bypass 需要知道 openclaw 的 extensions 目录位置。解析逻辑与 openclaw 的 `resolveStateDir()` 完全对齐：

| 优先级 | 来源 | 说明 |
|--------|------|------|
| 1 | `OPENCLAW_STATE_DIR` | 显式指定，支持 `~` 展开 |
| 2 | `~/.openclaw` | 新版标准目录（存在则用） |
| 3 | `~/.clawdbot` | Legacy 目录（存在则用） |
| 4 | `~/.openclaw` | 默认值 |

其中 `~` 的展开尊重 `OPENCLAW_HOME` 环境变量（与 openclaw 行为一致）。

#### 与 openclaw 原生安装的对齐验证

已通过实际对比验证（以 `@larksuite/openclaw-lark` 为例，含 4 个运行时依赖、43 个 node_modules 包）：

| 对比项 | 结果 |
|--------|------|
| 安装文件列表 | 完全一致 |
| `node_modules/` 内容 | 完全一致（`diff` 无输出） |
| 插件目录权限 | 0700 一致 |
| config `installs.*` 字段 | 一致 |
| config `entries.*.enabled` | 一致 |
| config `plugins.allow` | 一致 |

### 完整流程图

```
openclaw-plugins <command> <spec>
        │
        ├─ OPENCLAW_FORCE_MANUAL_INSTALL=1 ?
        │   └── 是 → 直接走 manual bypass (第 3 级)
        │
        ├─ OPENCLAW_FORCE_LOCAL_INSTALL=1 ?
        │   └── 是 → 直接走本地 fallback (第 2 级)
        │            └── 安全扫描阻止 → manual bypass (第 3 级)
        │
        ├─ 第 1 级：远程安装
        │   │
        │   ├── 成功 → done
        │   │
        │   └── 失败 → 第 2 级 ↓
        │
        ├─ 第 2 级：本地 fallback
        │   │
        │   ├── npm pack <spec> --json → /tmp/<tgz>
        │   │
        │   ├── openclaw plugins install /tmp/<tgz> [--dangerously-force-unsafe-install]
        │   │   │
        │   │   ├── 成功 → 写配置, done
        │   │   │
        │   │   ├── plugin already exists → 版本比较 → 跳过/替换
        │   │   │
        │   │   └── 安全扫描阻止 → 第 3 级 ↓
        │   │
        │   └── 其他错误 → exit 1
        │
        └─ 第 3 级：manual bypass
            │
            ├── npm pack <spec> → /tmp/<tgz>
            ├── tar xzf → 临时目录
            ├── cp → $OPENCLAW_STATE_DIR/extensions/<name>/  (默认 ~/.openclaw)
            ├── chmod 0700
            ├── npm install --omit=dev --ignore-scripts (有依赖时)
            ├── openclaw config set (元数据 + enable + allowlist)
            └── 清理临时文件
```

## 示例

```bash
# 安装插件
openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@1.0.2

# 更新插件
openclaw-plugins update @lark-apaas/openclaw-extension-miaoda@^1.0.0

# 通过 npx 安装
npx @lark-apaas/openclaw-scripts-plugins-cli install @lark-apaas/openclaw-extension-miaoda@1.0.2

# 预览模式
openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@1.0.2 --dry-run

# 强制本地安装（跳过远程，安全扫描阻止时仍会降级到 manual bypass）
OPENCLAW_FORCE_LOCAL_INSTALL=1 openclaw-plugins install @lark-apaas/openclaw-extension-miaoda@^1.0.0

# 强制 manual bypass（完全绕过 openclaw CLI 和安全扫描）
OPENCLAW_FORCE_MANUAL_INSTALL=1 openclaw-plugins install @lark-apaas/openclaw-extension-miaoda-coding@1.0.0
```

## 日志

所有操作日志写入 `/tmp/openclaw-plugins-cli.log`。
