wpops 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -6
- package/bin/wpops.js +191 -249
- package/lib/abilities.js +51 -0
- package/lib/app-passwords.js +39 -0
- package/lib/args.js +6 -35
- package/lib/batch.js +54 -0
- package/lib/blocks.js +72 -0
- package/lib/commands.js +323 -83
- package/lib/env.js +2 -1
- package/lib/errors.js +10 -0
- package/lib/global-styles.js +25 -0
- package/lib/health.js +41 -0
- package/lib/help.js +117 -0
- package/lib/introspect.js +59 -0
- package/lib/navigation.js +71 -0
- package/lib/revisions.js +56 -0
- package/lib/run.js +509 -0
- package/lib/search.js +31 -0
- package/lib/settings.js +74 -0
- package/lib/spec.js +391 -0
- package/lib/templates.js +69 -0
- package/lib/util.js +143 -0
- package/lib/widgets.js +93 -0
- package/package.json +3 -2
- package/skills/wpops/SKILL.md +56 -34
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](package.json)
|
|
7
7
|
|
|
8
|
-
> WordPress REST operations toolkit — a zero-dependency CLI (plus agent skill) to manage posts, pages, media, comments, users and plugins across one or many sites.
|
|
8
|
+
> WordPress REST operations toolkit — a zero-dependency CLI (plus agent skill) to manage posts, pages, media, comments, users, navigation menus and plugins across one or many sites.
|
|
9
9
|
|
|
10
10
|
WordPress REST 操作工具。只依赖 Node 内置能力(无第三方包),用应用密码通过
|
|
11
11
|
`/wp-json/` 管理站点内容与插件。
|
|
@@ -64,6 +64,14 @@ npm publish
|
|
|
64
64
|
> 白名单不含 `sites/`。每台机器各自 `wpops setup`,工具与 skill 都不携带密钥。
|
|
65
65
|
> `WPOPS_SKIP_SKILL=1 npm i -g wpops` 可跳过自动装 skill;`wpops install-skill` 可手动补装。
|
|
66
66
|
|
|
67
|
+
### 本机(开发) vs 其他机器(使用)
|
|
68
|
+
|
|
69
|
+
- **本机开发**:`pnpm add -g .` 会在 pnpm 全局建一个**指向本仓库的 Junction**,
|
|
70
|
+
于是命令行的 `wpops` 就是**源码实时版** —— 改代码立即生效,无需重装。
|
|
71
|
+
验证:`where.exe wpops`(应指向 `...\pnpm\bin`)。
|
|
72
|
+
- **其他机器使用**:`npm i -g wpops`,装的是 **npm 上发布的版本**;升级用 `npm i -g wpops@latest`。
|
|
73
|
+
- `npx wpops ...` 只在临时缓存里跑一次,不落 PATH。
|
|
74
|
+
|
|
67
75
|
## 1. 配置
|
|
68
76
|
|
|
69
77
|
给每个站点建一个档案。**推荐用向导**:`wpops setup`(问 URL/用户名/应用密码,自动写入并体检)。
|
|
@@ -101,6 +109,11 @@ node bin/wpops.js doctor
|
|
|
101
109
|
|
|
102
110
|
## 3. 命令
|
|
103
111
|
|
|
112
|
+
> **现查现用**:`wpops help <组>` 或 `wpops <组> --help` 看某组动作与参数;`wpops commands --json` 拿机器可读清单(全部组/动作/是否写操作)。
|
|
113
|
+
> 帮助与清单都由 `lib/spec.js` 单一事实来源生成,不会和实现漂移。
|
|
114
|
+
>
|
|
115
|
+
> **退出码**:`0` 成功;`2` 用法/参数错误(stderr 打印错误 + 精简用法 + `--help` 提示);`1` 运行期错误(HTTP/网络/配置)。`--help` 始终走 stdout 且为 `0`。
|
|
116
|
+
|
|
104
117
|
```
|
|
105
118
|
# 内容
|
|
106
119
|
wpops posts list [--per-page 10] [--page 2] [--status draft] [--search 关键词]
|
|
@@ -118,6 +131,8 @@ wpops me
|
|
|
118
131
|
wpops media list [--per-page 20] [--media-type image]
|
|
119
132
|
wpops media get <id>
|
|
120
133
|
wpops media upload ./pic.jpg [--title "标题"] [--alt "替代文本"]
|
|
134
|
+
wpops media sideload --url https://example.com/pic.jpg [--title "标题"] # 从 URL 导入
|
|
135
|
+
wpops media edit-image <id> --rotation 90 [--crop] [--dest-width 800] # 裁剪/旋转(缺 --src 自动取当前图)
|
|
121
136
|
wpops media update <id> [--alt "替代文本"] [--caption "说明"] [--post 123]
|
|
122
137
|
wpops media delete <id>
|
|
123
138
|
|
|
@@ -138,6 +153,68 @@ wpops users update <id> --role editor
|
|
|
138
153
|
wpops users delete <id> --force [--reassign 1] # 有内容的用户需指定内容转交人
|
|
139
154
|
wpops users me
|
|
140
155
|
|
|
156
|
+
# 导航菜单(接口由插件提供;核心 REST 没有)
|
|
157
|
+
wpops menus list [--per-page 100] # 列出菜单(id / name / slug / locations)
|
|
158
|
+
wpops menus get <id>
|
|
159
|
+
wpops menu-items list [--menus 190] [--search 词] [--per-page 100] [--page 1]
|
|
160
|
+
wpops menu-items get <id>
|
|
161
|
+
wpops menu-items create --title "Projects" --url https://example.com/projects --menus 190 --menu-order 6
|
|
162
|
+
wpops menu-items create --title "Projects" --menus 190 --type post_type --object page --object-id 4976
|
|
163
|
+
wpops menu-items update <id> --menu-order 3 [--parent 4979] [--target _blank]
|
|
164
|
+
wpops menu-items delete <id> --dry-run # 预览不需要 --force
|
|
165
|
+
wpops menu-items delete <id> --force # 真正删除才必须 --force(不可撤销)
|
|
166
|
+
wpops menus create --name Header --locations header,mobile
|
|
167
|
+
wpops menus update 190 --locations header,mobile # 位置分配靠更新菜单的 locations
|
|
168
|
+
wpops menus delete 190 --force
|
|
169
|
+
|
|
170
|
+
# 搜索 / 站点设置 / 自省
|
|
171
|
+
wpops search 关键词 [--type post] [--subtype post] [--per-page 10]
|
|
172
|
+
wpops settings # 查看站点设置
|
|
173
|
+
wpops settings update --title "新标题" [--description "..."] [--timezone Asia/Shanghai] [--posts-per-page 10]
|
|
174
|
+
wpops settings update --data '{"show_on_front":"page"}'
|
|
175
|
+
wpops types list|get <type> # 已注册文章类型(含 rest_base)
|
|
176
|
+
wpops taxonomies list|get <tax> # 已注册分类法
|
|
177
|
+
wpops statuses # 文章状态
|
|
178
|
+
|
|
179
|
+
# 任意自定义类型(show_in_rest 的 CPT / 自定义分类)
|
|
180
|
+
wpops content list books [--per-page 10] # rest_base 直接当 type
|
|
181
|
+
wpops content get books 12
|
|
182
|
+
wpops content create books --title "..." [--status draft]
|
|
183
|
+
wpops content update books 12 --title "..."
|
|
184
|
+
wpops content delete books 12 --force
|
|
185
|
+
wpops terms list genres # 自定义分类法同理
|
|
186
|
+
|
|
187
|
+
# 修订 / 区块 / 区块主题结构
|
|
188
|
+
wpops revisions list <id> [--type posts|pages]
|
|
189
|
+
wpops revisions get <id> <rev>
|
|
190
|
+
wpops revisions restore <id> <rev> # 用旧修订覆盖正文
|
|
191
|
+
wpops revisions delete <id> <rev> --force
|
|
192
|
+
wpops blocks list|get|create|update|delete ... # 可复用区块
|
|
193
|
+
wpops navigation list|get|create|update|delete ... # 区块主题导航
|
|
194
|
+
wpops templates list|get|update ... # 模板(update --content/--from-file)
|
|
195
|
+
wpops template-parts list|get|update ...
|
|
196
|
+
wpops global-styles get <id> · global-styles update <id> --styles '{"...":...}'
|
|
197
|
+
|
|
198
|
+
# 小工具 / 应用密码 / 插件能力
|
|
199
|
+
wpops widgets list|get|create|update|delete ... # --id --sidebar --instance JSON
|
|
200
|
+
wpops sidebars list|get|update ... # update --widgets a,b,c
|
|
201
|
+
wpops app-passwords list [--user me] # 列出应用密码(不含明文)
|
|
202
|
+
wpops app-passwords create --name ci-token [--user me] # 明文密码只打印这一次
|
|
203
|
+
wpops app-passwords delete <uuid> --force
|
|
204
|
+
wpops abilities list|get|run <name> [--input JSON] # Abilities API(需插件)
|
|
205
|
+
wpops health list · health <test> # 站点健康测试(需插件)
|
|
206
|
+
wpops batch --requests '[{"method":"DELETE","path":"/wp/v2/menu-items/123"}]' # 仅 POST/PUT/PATCH/DELETE
|
|
207
|
+
|
|
208
|
+
# 输出与批量
|
|
209
|
+
wpops posts list --all-pages # 自动翻完所有页
|
|
210
|
+
wpops posts list --fields id,title,status --csv # 字段投影 / CSV(--table 表格)
|
|
211
|
+
wpops menus list --quiet # 只出错时输出
|
|
212
|
+
wpops menus list --output menus.txt # 写入文件
|
|
213
|
+
wpops sites show qnnp.me # 查看站点配置(密码打码)
|
|
214
|
+
wpops sites rm qnnp.me · sites rename 旧 新 # 管理站点档案(确认后再用)
|
|
215
|
+
wpops completion bash # 输出补全脚本
|
|
216
|
+
wpops help menus # 某组用法
|
|
217
|
+
|
|
141
218
|
# 插件 / 主题(见下方能力说明)
|
|
142
219
|
wpops plugins list [--status active] [--search 关键词]
|
|
143
220
|
wpops plugins install <wordpress.org 别名> [--activate]
|
|
@@ -193,10 +270,17 @@ wpops --all plugins list # 巡检所有站的插件
|
|
|
193
270
|
| 事项 | 核心 REST 能做到吗 |
|
|
194
271
|
|---|---|
|
|
195
272
|
| 文章 / 页面 / 媒体 / 分类 / 标签 / 评论 / 用户的增删改查 | ✅ 完整 |
|
|
273
|
+
| 搜索、站点设置(site title/时区/…)、类型/分类/状态自省 | ✅ |
|
|
274
|
+
| 自定义类型 / 自定义分类(`show_in_rest` 的 CPT) | ✅(`content` / `terms` 通用命令) |
|
|
275
|
+
| 修订历史(列/看/删/恢复)、可复用区块、区块主题导航/模板/全局样式 | ✅ |
|
|
276
|
+
| 小工具 / 边栏、用户应用密码 | ✅ |
|
|
277
|
+
| 导航菜单 / 菜单项(列表、增删改) | ⚠️ 需站点装有暴露 `/menus`、`/menu-items` 的插件 |
|
|
196
278
|
| 插件:列出、**安装**(按 wp.org 别名)、启用/停用、删除 | ✅ |
|
|
197
279
|
| 插件:**更新已装插件代码** | ❌ 核心 REST 没有该端点 |
|
|
198
280
|
| 主题:列出、切换 | ✅ |
|
|
199
281
|
| 主题:安装 / 更新 | ❌ |
|
|
282
|
+
| Abilities API(`abilities list/run`)、站点健康测试(`health`) | ⚠️ 需对应插件 |
|
|
283
|
+
| 批量请求(`batch`) | ✅(仅 POST/PUT/PATCH/DELETE,核心限制) |
|
|
200
284
|
| WordPress 核心更新 | ❌ |
|
|
201
285
|
| 备份 / 数据库优化 / 缓存清理 | ❌(需服务器侧) |
|
|
202
286
|
|
|
@@ -209,16 +293,18 @@ wpops --all plugins list # 巡检所有站的插件
|
|
|
209
293
|
|
|
210
294
|
- `sites/*.env` 存放应用密码,等同于该账号登录凭据;专用账号 + 可随时在后台吊销。
|
|
211
295
|
- 应用密码要求 HTTPS。
|
|
212
|
-
- **写操作前先 `--dry-run`
|
|
296
|
+
- **写操作前先 `--dry-run` 预览**(`delete` 的预览不需要 `--force`);真正执行破坏性删除(`delete --force`、分类/用户/菜单项删除)才必须加 `--force`。
|
|
213
297
|
- **`--all` 的写操作必须加 `--yes`**,否则直接拒绝,避免手滑波及全线。
|
|
214
298
|
- 网络抖动/429/5xx 会自动重试(默认 2 次,幂等方法才重试网络错误)。
|
|
215
299
|
- 这是本地脚本,不对外开端口。
|
|
216
300
|
|
|
217
|
-
## 7.
|
|
301
|
+
## 7. 自洽执行器
|
|
302
|
+
|
|
303
|
+
本项目是**执行器**(真正发请求),内置 agent skill(`skills/wpops/SKILL.md`)自洽、不依赖其他 skill:
|
|
304
|
+
它只讲清**何时用、安全流程、能力索引、如何现查现用**(`wpops help` / `wpops commands --json` / `wpops raw`),
|
|
305
|
+
精确的字段与枚举一律在运行时向 CLI 或站点查询,因此不会随文档过时。
|
|
218
306
|
|
|
219
|
-
|
|
220
|
-
(`wp-rest-api`、`wp-wpcli-and-ops`、`wp-performance`、`wordpress-router`)
|
|
221
|
-
是**知识/规范**,指导"该怎么安全地做"。
|
|
307
|
+
更广义的 WordPress 知识(主题/插件开发、WP-CLI 运维、性能等)属于其他领域,与本工具正交,按需另取即可。
|
|
222
308
|
|
|
223
309
|
## 8. 全局命令(可选)
|
|
224
310
|
|
package/bin/wpops.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { loadEnv, listSites, configHome } from '../lib/env.js';
|
|
5
|
+
import { WpError } from '../lib/client.js';
|
|
6
|
+
import { UsageError } from '../lib/errors.js';
|
|
5
7
|
import { parseArgs, isMutation } from '../lib/args.js';
|
|
6
8
|
import { installSkill } from '../lib/skill.js';
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
9
|
+
import { configureOutput } from '../lib/util.js';
|
|
10
|
+
import { runOne } from '../lib/run.js';
|
|
11
|
+
import { generalHelp, groupHelp, commandsJson, usageHint } from '../lib/help.js';
|
|
12
|
+
import { completionWords, groupByName } from '../lib/spec.js';
|
|
13
|
+
import { setup } from '../lib/commands.js';
|
|
12
14
|
|
|
13
15
|
function version() {
|
|
14
16
|
try {
|
|
@@ -18,68 +20,6 @@ function version() {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function printHelp() {
|
|
22
|
-
console.log(`wpops —— WordPress REST 操作工具
|
|
23
|
-
|
|
24
|
-
用法: wpops [--site 名字 | --all] <组> <动作> [参数] [--flags]
|
|
25
|
-
|
|
26
|
-
--version / -v 显示版本
|
|
27
|
-
|
|
28
|
-
setup 交互式配置一个站点(写入 sites/<名字>.env 并体检)
|
|
29
|
-
doctor 连接与权限体检(先跑这个)
|
|
30
|
-
me 当前用户与能力
|
|
31
|
-
sites 列出已配置的站点
|
|
32
|
-
install-skill 把内置 skill 装到 agent 发现目录
|
|
33
|
-
|
|
34
|
-
posts list|get|create|update|delete ...
|
|
35
|
-
pages list|get|create|update|delete ...
|
|
36
|
-
media list|get|upload|update|delete ...
|
|
37
|
-
plugins list|install|activate|deactivate ...
|
|
38
|
-
themes list|activate ...
|
|
39
|
-
categories list|get|create|update|delete ...
|
|
40
|
-
tags list|get|create|update|delete ...
|
|
41
|
-
comments list|get|update|delete ...
|
|
42
|
-
users list|get|create|update|delete|me ...
|
|
43
|
-
raw <METHOD> <path> [--data JSON] 任意端点兜底
|
|
44
|
-
|
|
45
|
-
多站点:
|
|
46
|
-
--site <名字> / -s <名字> 对 sites/<名字>.env 执行
|
|
47
|
-
--all / -a 对所有已配置站点批量执行(写操作需 --yes)
|
|
48
|
-
|
|
49
|
-
安全:
|
|
50
|
-
--dry-run 只预览不执行(写操作)
|
|
51
|
-
--yes / -y --all 写操作的确认开关
|
|
52
|
-
|
|
53
|
-
常用参数:
|
|
54
|
-
--per-page N · --page N 分页(REST 上限 100)
|
|
55
|
-
--status <s> 状态(posts/comments 等)
|
|
56
|
-
--search <s> · --orderby <f> · --order <asc|desc>
|
|
57
|
-
--categories 1,2 · --tags 3,4 分类/标签 ID(过滤或设置)
|
|
58
|
-
--author <id> · --after <日期> · --before <日期>
|
|
59
|
-
--title/--content/--from-file 标题/正文
|
|
60
|
-
--slug <s> · --excerpt <s> 别名 / 摘要
|
|
61
|
-
--featured-media <id> 特色图
|
|
62
|
-
--alt <文本> · --caption <文本> 媒体替代文本 / 说明
|
|
63
|
-
--name/--parent/--description 分类/标签字段
|
|
64
|
-
--username/--email/--role 用户字段
|
|
65
|
-
--force 彻底删除 / 危险操作确认
|
|
66
|
-
--data <JSON> raw 的请求体
|
|
67
|
-
--json 输出原始 JSON
|
|
68
|
-
|
|
69
|
-
示例:
|
|
70
|
-
wpops doctor
|
|
71
|
-
wpops -s blog-a posts list --per-page 5 --orderby date --order desc
|
|
72
|
-
wpops posts list --categories 3,7
|
|
73
|
-
wpops posts update 12 --from-file ./post.md --status draft
|
|
74
|
-
wpops media update 44 --alt "配图"
|
|
75
|
-
wpops categories list
|
|
76
|
-
wpops comments list --status hold
|
|
77
|
-
wpops --all posts list
|
|
78
|
-
wpops --all plugins list
|
|
79
|
-
wpops --all posts delete 5 --dry-run
|
|
80
|
-
wpops plugins install wp-super-cache --activate`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
23
|
function printSites() {
|
|
84
24
|
const sites = listSites();
|
|
85
25
|
if (!sites.length) {
|
|
@@ -100,221 +40,210 @@ function printSites() {
|
|
|
100
40
|
}
|
|
101
41
|
}
|
|
102
42
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
43
|
+
// ------------------------------------------------------------------ sites 管理
|
|
44
|
+
|
|
45
|
+
const SENSITIVE = /PASSWORD|SECRET|TOKEN|KEY/i;
|
|
106
46
|
|
|
107
|
-
|
|
108
|
-
|
|
47
|
+
function sitesCommand(action, rest) {
|
|
48
|
+
const dir = join(configHome(), 'sites');
|
|
49
|
+
switch (action) {
|
|
50
|
+
case undefined:
|
|
51
|
+
case 'list':
|
|
52
|
+
return printSites();
|
|
53
|
+
case 'show': {
|
|
54
|
+
const name = rest[0];
|
|
55
|
+
if (!name) throw new Error('sites show 需要站点名');
|
|
56
|
+
const env = loadEnv(name);
|
|
57
|
+
console.log(`站点 ${name}:`);
|
|
58
|
+
for (const [key, value] of Object.entries(env)) {
|
|
59
|
+
console.log(` ${key}=${SENSITIVE.test(key) ? '***' : value}`);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
case 'rm': {
|
|
64
|
+
const name = rest[0];
|
|
65
|
+
if (!name) throw new Error('sites rm 需要站点名');
|
|
66
|
+
const file = join(dir, `${name}.env`);
|
|
67
|
+
if (!existsSync(file)) throw new Error(`找不到站点配置 ${file}`);
|
|
68
|
+
rmSync(file);
|
|
69
|
+
console.log(`✓ 已删除站点配置 ${name}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
case 'rename': {
|
|
73
|
+
const [from, to] = rest;
|
|
74
|
+
if (!from || !to) throw new Error('sites rename 需要 <旧名> <新名>');
|
|
75
|
+
const src = join(dir, `${from}.env`);
|
|
76
|
+
const dst = join(dir, `${to}.env`);
|
|
77
|
+
if (!existsSync(src)) throw new Error(`找不到站点配置 ${src}`);
|
|
78
|
+
if (existsSync(dst)) throw new Error(`目标已存在 ${dst}`);
|
|
79
|
+
renameSync(src, dst);
|
|
80
|
+
console.log(`✓ 已重命名站点 ${from} → ${to}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`sites 未知子命令:${action}(list|show|rm|rename)`);
|
|
109
85
|
}
|
|
86
|
+
}
|
|
110
87
|
|
|
111
|
-
|
|
112
|
-
const value = rest[0];
|
|
113
|
-
if (!value) throw new Error(`${what} 需要一个参数`);
|
|
114
|
-
return value;
|
|
115
|
-
};
|
|
88
|
+
// ------------------------------------------------------------------ 输出包装
|
|
116
89
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
case 'rm':
|
|
132
|
-
return cmd.deleteContent(client, type, needFirst('delete'), flags);
|
|
133
|
-
default:
|
|
134
|
-
throw new Error(`${type} 未知子命令:${action}`);
|
|
90
|
+
/** 统一处理 --quiet / --output:抑制或把命令的人类可读输出写入文件。 */
|
|
91
|
+
async function withOutput(flags, fn) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
const originalLog = console.log;
|
|
94
|
+
if (flags.quiet) console.log = () => {};
|
|
95
|
+
else if (flags.output) console.log = (...args) => lines.push(args.join(' '));
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
} finally {
|
|
99
|
+
console.log = originalLog;
|
|
100
|
+
if (!flags.quiet && flags.output && lines.length) {
|
|
101
|
+
const file = resolve(String(flags.output));
|
|
102
|
+
writeFileSync(file, lines.join('\n') + '\n', 'utf8');
|
|
103
|
+
originalLog(`✓ 输出已写入 ${file}`);
|
|
135
104
|
}
|
|
136
|
-
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
137
107
|
|
|
138
|
-
|
|
139
|
-
switch (action) {
|
|
140
|
-
case undefined:
|
|
141
|
-
case 'list':
|
|
142
|
-
return cmd.listTaxonomy(client, tax, flags);
|
|
143
|
-
case 'get':
|
|
144
|
-
return cmd.getTaxonomy(client, tax, needFirst('get'));
|
|
145
|
-
case 'create':
|
|
146
|
-
case 'new':
|
|
147
|
-
return cmd.createTaxonomy(client, tax, flags);
|
|
148
|
-
case 'update':
|
|
149
|
-
case 'edit':
|
|
150
|
-
return cmd.updateTaxonomy(client, tax, needFirst('update'), flags);
|
|
151
|
-
case 'delete':
|
|
152
|
-
case 'rm':
|
|
153
|
-
return cmd.deleteTaxonomy(client, tax, needFirst('delete'), flags);
|
|
154
|
-
default:
|
|
155
|
-
throw new Error(`${tax} 未知子命令:${action}`);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
108
|
+
// ------------------------------------------------------------------ 用法错误
|
|
158
109
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
case '
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
case 'list':
|
|
193
|
-
return cmd.listPlugins(client, flags);
|
|
194
|
-
case 'install':
|
|
195
|
-
if (!rest[0]) {
|
|
196
|
-
throw new Error('plugins install 需要 wordpress.org 插件别名,例如 wpops plugins install akismet');
|
|
197
|
-
}
|
|
198
|
-
return cmd.installPlugin(client, rest[0], flags);
|
|
199
|
-
case 'activate':
|
|
200
|
-
return cmd.setPluginStatus(client, needFirst('activate'), true, flags);
|
|
201
|
-
case 'deactivate':
|
|
202
|
-
return cmd.setPluginStatus(client, needFirst('deactivate'), false, flags);
|
|
203
|
-
default:
|
|
204
|
-
throw new Error(`plugins 未知子命令:${action}`);
|
|
205
|
-
}
|
|
206
|
-
case 'themes':
|
|
207
|
-
case 'theme':
|
|
208
|
-
switch (action) {
|
|
209
|
-
case undefined:
|
|
210
|
-
case 'list':
|
|
211
|
-
return cmd.listThemes(client, flags);
|
|
212
|
-
case 'activate':
|
|
213
|
-
case 'switch':
|
|
214
|
-
return cmd.activateTheme(client, needFirst('activate'), flags);
|
|
215
|
-
default:
|
|
216
|
-
throw new Error(`themes 未知子命令:${action}`);
|
|
217
|
-
}
|
|
218
|
-
case 'categories':
|
|
219
|
-
case 'category':
|
|
220
|
-
return taxonomy('categories');
|
|
221
|
-
case 'tags':
|
|
222
|
-
case 'tag':
|
|
223
|
-
return taxonomy('tags');
|
|
224
|
-
case 'comments':
|
|
225
|
-
case 'comment':
|
|
226
|
-
switch (action) {
|
|
227
|
-
case undefined:
|
|
228
|
-
case 'list':
|
|
229
|
-
return cmd.listComments(client, flags);
|
|
230
|
-
case 'get':
|
|
231
|
-
return cmd.getComment(client, needFirst('get'));
|
|
232
|
-
case 'update':
|
|
233
|
-
case 'edit':
|
|
234
|
-
return cmd.updateComment(client, needFirst('update'), flags);
|
|
235
|
-
case 'delete':
|
|
236
|
-
case 'rm':
|
|
237
|
-
return cmd.deleteComment(client, needFirst('delete'), flags);
|
|
238
|
-
default:
|
|
239
|
-
throw new Error(`comments 未知子命令:${action}`);
|
|
240
|
-
}
|
|
241
|
-
case 'users':
|
|
242
|
-
case 'user':
|
|
243
|
-
switch (action) {
|
|
244
|
-
case undefined:
|
|
245
|
-
case 'list':
|
|
246
|
-
return cmd.listUsers(client, flags);
|
|
247
|
-
case 'me':
|
|
248
|
-
return cmd.me(client, flags);
|
|
249
|
-
case 'get':
|
|
250
|
-
return cmd.getUser(client, needFirst('get'));
|
|
251
|
-
case 'create':
|
|
252
|
-
case 'new':
|
|
253
|
-
return cmd.createUser(client, flags);
|
|
254
|
-
case 'update':
|
|
255
|
-
case 'edit':
|
|
256
|
-
return cmd.updateUser(client, needFirst('update'), flags);
|
|
257
|
-
case 'delete':
|
|
258
|
-
case 'rm':
|
|
259
|
-
return cmd.deleteUser(client, needFirst('delete'), flags);
|
|
260
|
-
default:
|
|
261
|
-
throw new Error(`users 未知子命令:${action}`);
|
|
262
|
-
}
|
|
263
|
-
case 'raw':
|
|
264
|
-
if (!action || !rest[0]) throw new Error('raw 用法:wpops raw <METHOD> <path> [--data JSON]');
|
|
265
|
-
return cmd.raw(client, action, rest[0], flags);
|
|
110
|
+
/** 用法错误:stderr 打印「消息 + 精简用法 + --help 提示」,退出码 2(与运行期错误的 1 区分)。 */
|
|
111
|
+
function reportUsageError(err, group, action) {
|
|
112
|
+
console.error(`✗ ${err.message}`);
|
|
113
|
+
const lines = usageHint(group, action);
|
|
114
|
+
if (lines.length) {
|
|
115
|
+
console.error('用法:');
|
|
116
|
+
for (const line of lines) console.error(` ${line}`);
|
|
117
|
+
}
|
|
118
|
+
if (group && groupByName(group)) {
|
|
119
|
+
console.error(`提示:wpops ${group} --help 查看完整用法`);
|
|
120
|
+
} else {
|
|
121
|
+
console.error('提示:wpops help 查看全部命令');
|
|
122
|
+
}
|
|
123
|
+
process.exitCode = 2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ------------------------------------------------------------------ completion
|
|
127
|
+
|
|
128
|
+
function printCompletion(shell) {
|
|
129
|
+
const words = completionWords(['help']).join(' ');
|
|
130
|
+
switch (shell || 'bash') {
|
|
131
|
+
case 'bash':
|
|
132
|
+
console.log(`# wpops bash completion(用法:eval "$(wpops completion bash)")\n_wpops() {\n local cur="\${COMP_WORDS[COMP_CWORD]}"\n COMPREPLY=( $(compgen -W "${words}" -- "$cur") )\n}\ncomplete -F _wpops wpops`);
|
|
133
|
+
return;
|
|
134
|
+
case 'zsh':
|
|
135
|
+
console.log(`# wpops zsh completion(用法:eval "$(wpops completion zsh)")\n_wpops() { compadd ${words} }\ncompdef _wpops wpops`);
|
|
136
|
+
return;
|
|
137
|
+
case 'fish':
|
|
138
|
+
console.log(`# wpops fish completion(用法:wpops completion fish | source)\ncomplete -c wpops -f -a "${words}"`);
|
|
139
|
+
return;
|
|
140
|
+
case 'powershell':
|
|
141
|
+
console.log(`# wpops PowerShell completion(用法:wpops completion powershell | Out-String | Invoke-Expression)\nRegister-ArgumentCompleter -Native -CommandName wpops -ScriptBlock {\n param($wordToComplete)\n "${words}".Split(' ') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }\n}`);
|
|
142
|
+
return;
|
|
266
143
|
default:
|
|
267
|
-
throw new Error(
|
|
144
|
+
throw new Error('completion 支持:bash|zsh|fish|powershell');
|
|
268
145
|
}
|
|
269
146
|
}
|
|
270
147
|
|
|
148
|
+
// ------------------------------------------------------------------ main
|
|
149
|
+
|
|
271
150
|
async function main() {
|
|
272
151
|
const { positionals, flags } = parseArgs(process.argv.slice(2));
|
|
273
152
|
const [group, action, ...rest] = positionals;
|
|
274
153
|
|
|
154
|
+
configureOutput(flags);
|
|
155
|
+
if (flags.paginate) flags['all-pages'] = true;
|
|
156
|
+
if (flags.csv || flags.table || flags.fields) flags.json = true;
|
|
157
|
+
|
|
275
158
|
if (flags.version) {
|
|
276
159
|
console.log(version());
|
|
277
160
|
return;
|
|
278
161
|
}
|
|
162
|
+
|
|
163
|
+
// help:wpops / wpops help [组] [动作] / wpops <组> [动作] --help
|
|
279
164
|
if (!group || group === 'help' || flags.help) {
|
|
280
|
-
|
|
165
|
+
const helpGroup = group === 'help' ? action : group;
|
|
166
|
+
const helpAction = group === 'help' ? rest[0] : action;
|
|
167
|
+
if (helpGroup) {
|
|
168
|
+
const text = groupHelp(helpGroup, helpAction);
|
|
169
|
+
if (!text) {
|
|
170
|
+
console.error(`未知命令组:${helpGroup}(运行 wpops help 查看全部)`);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log(text);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(generalHelp(version()));
|
|
281
178
|
return;
|
|
282
179
|
}
|
|
180
|
+
|
|
283
181
|
if (group === 'sites') {
|
|
284
|
-
|
|
182
|
+
await withOutput(flags, () => sitesCommand(action, rest));
|
|
285
183
|
return;
|
|
286
184
|
}
|
|
287
185
|
if (group === 'setup') {
|
|
288
|
-
await
|
|
186
|
+
await setup(flags);
|
|
289
187
|
return;
|
|
290
188
|
}
|
|
291
189
|
if (group === 'install-skill') {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
190
|
+
await withOutput(flags, () => {
|
|
191
|
+
const dirs = installSkill();
|
|
192
|
+
if (!dirs.length) {
|
|
193
|
+
console.log('未安装 skill(可能已存在同名目录被跳过,或用 WPOPS_SKIP_SKILL 禁用)。');
|
|
194
|
+
} else {
|
|
195
|
+
for (const dir of dirs) console.log(`✓ skill 已安装到 ${dir}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (group === 'completion') {
|
|
201
|
+
await withOutput(flags, () => printCompletion(action));
|
|
298
202
|
return;
|
|
299
203
|
}
|
|
204
|
+
if (group === 'commands') {
|
|
205
|
+
await withOutput(flags, () => console.log(commandsJson(version())));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const single = flags.site || process.env.WPOPS_SITE || null;
|
|
210
|
+
const named = flags.site && String(flags.site).includes(',')
|
|
211
|
+
? String(flags.site).split(',').map((s) => s.trim()).filter(Boolean)
|
|
212
|
+
: [];
|
|
300
213
|
|
|
301
|
-
|
|
302
|
-
|
|
214
|
+
let targets = null;
|
|
215
|
+
if (flags.all) targets = listSites();
|
|
216
|
+
else if (named.length) targets = named.map((name) => ({ name, label: name }));
|
|
303
217
|
|
|
304
|
-
if (
|
|
305
|
-
const targets = listSites();
|
|
218
|
+
if (targets) {
|
|
306
219
|
if (!targets.length) throw new Error('还没有配置任何站点');
|
|
307
|
-
if (
|
|
308
|
-
|
|
220
|
+
if (isMutation(group, action) && !flags.yes && !flags['dry-run']) {
|
|
221
|
+
reportUsageError(
|
|
222
|
+
new UsageError('批量执行会作用于多个站点。写操作请加 --yes 确认,或先用 --dry-run 预览。', { group, action }),
|
|
223
|
+
group,
|
|
224
|
+
action,
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
309
227
|
}
|
|
310
228
|
let failures = 0;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
229
|
+
let usageError = null;
|
|
230
|
+
await withOutput(flags, async () => {
|
|
231
|
+
for (const target of targets) {
|
|
232
|
+
try {
|
|
233
|
+
await runOne(target.name, group, action, rest, flags, true);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err instanceof UsageError) {
|
|
236
|
+
usageError = err; // 用法与站点无关:立即中止,不逐站重复
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
failures++;
|
|
240
|
+
console.error(`✗ [${target.label}] ${err.message}`);
|
|
241
|
+
}
|
|
317
242
|
}
|
|
243
|
+
});
|
|
244
|
+
if (usageError) {
|
|
245
|
+
reportUsageError(usageError, usageError.group || group, usageError.action ?? action);
|
|
246
|
+
return;
|
|
318
247
|
}
|
|
319
248
|
if (failures) {
|
|
320
249
|
console.error(`\n完成:${failures}/${targets.length} 个站点出错。`);
|
|
@@ -323,10 +252,23 @@ async function main() {
|
|
|
323
252
|
return;
|
|
324
253
|
}
|
|
325
254
|
|
|
326
|
-
|
|
255
|
+
try {
|
|
256
|
+
await withOutput(flags, () => runOne(single, group, action, rest, flags, false));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof UsageError) {
|
|
259
|
+
reportUsageError(err, err.group || group, err.action ?? action);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
327
264
|
}
|
|
328
265
|
|
|
329
266
|
main().catch((err) => {
|
|
267
|
+
if (err instanceof UsageError) {
|
|
268
|
+
console.error(`✗ ${err.message}`);
|
|
269
|
+
console.error('提示:wpops help 查看全部命令');
|
|
270
|
+
process.exit(2);
|
|
271
|
+
}
|
|
330
272
|
if (err instanceof WpError) {
|
|
331
273
|
console.error(`✗ ${err.message}`);
|
|
332
274
|
if (err.code) console.error(` 代码:${err.code}`);
|