wpops 0.1.0 → 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 +111 -7
- package/bin/wpops.js +201 -244
- package/lib/abilities.js +51 -0
- package/lib/app-passwords.js +39 -0
- package/lib/args.js +10 -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/bin/wpops.js
CHANGED
|
@@ -1,72 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
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';
|
|
4
7
|
import { parseArgs, isMutation } from '../lib/args.js';
|
|
5
8
|
import { installSkill } from '../lib/skill.js';
|
|
6
|
-
import
|
|
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';
|
|
7
14
|
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
用法: wpops [--site 名字 | --all] <组> <动作> [参数] [--flags]
|
|
16
|
-
|
|
17
|
-
setup 交互式配置一个站点(写入 sites/<名字>.env 并体检)
|
|
18
|
-
doctor 连接与权限体检(先跑这个)
|
|
19
|
-
me 当前用户与能力
|
|
20
|
-
sites 列出已配置的站点
|
|
21
|
-
install-skill 把内置 skill 装到 agent 发现目录
|
|
22
|
-
|
|
23
|
-
posts list|get|create|update|delete ...
|
|
24
|
-
pages list|get|create|update|delete ...
|
|
25
|
-
media list|get|upload|update|delete ...
|
|
26
|
-
plugins list|install|activate|deactivate ...
|
|
27
|
-
themes list|activate ...
|
|
28
|
-
categories list|get|create|update|delete ...
|
|
29
|
-
tags list|get|create|update|delete ...
|
|
30
|
-
comments list|get|update|delete ...
|
|
31
|
-
users list|get|create|update|delete|me ...
|
|
32
|
-
raw <METHOD> <path> [--data JSON] 任意端点兜底
|
|
33
|
-
|
|
34
|
-
多站点:
|
|
35
|
-
--site <名字> / -s <名字> 对 sites/<名字>.env 执行
|
|
36
|
-
--all / -a 对所有已配置站点批量执行(写操作需 --yes)
|
|
37
|
-
|
|
38
|
-
安全:
|
|
39
|
-
--dry-run 只预览不执行(写操作)
|
|
40
|
-
--yes / -y --all 写操作的确认开关
|
|
41
|
-
|
|
42
|
-
常用参数:
|
|
43
|
-
--per-page N · --page N 分页(REST 上限 100)
|
|
44
|
-
--status <s> 状态(posts/comments 等)
|
|
45
|
-
--search <s> · --orderby <f> · --order <asc|desc>
|
|
46
|
-
--categories 1,2 · --tags 3,4 分类/标签 ID(过滤或设置)
|
|
47
|
-
--author <id> · --after <日期> · --before <日期>
|
|
48
|
-
--title/--content/--from-file 标题/正文
|
|
49
|
-
--slug <s> · --excerpt <s> 别名 / 摘要
|
|
50
|
-
--featured-media <id> 特色图
|
|
51
|
-
--alt <文本> · --caption <文本> 媒体替代文本 / 说明
|
|
52
|
-
--name/--parent/--description 分类/标签字段
|
|
53
|
-
--username/--email/--role 用户字段
|
|
54
|
-
--force 彻底删除 / 危险操作确认
|
|
55
|
-
--data <JSON> raw 的请求体
|
|
56
|
-
--json 输出原始 JSON
|
|
57
|
-
|
|
58
|
-
示例:
|
|
59
|
-
wpops doctor
|
|
60
|
-
wpops -s blog-a posts list --per-page 5 --orderby date --order desc
|
|
61
|
-
wpops posts list --categories 3,7
|
|
62
|
-
wpops posts update 12 --from-file ./post.md --status draft
|
|
63
|
-
wpops media update 44 --alt "配图"
|
|
64
|
-
wpops categories list
|
|
65
|
-
wpops comments list --status hold
|
|
66
|
-
wpops --all posts list
|
|
67
|
-
wpops --all plugins list
|
|
68
|
-
wpops --all posts delete 5 --dry-run
|
|
69
|
-
wpops plugins install wp-super-cache --activate`);
|
|
15
|
+
function version() {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
18
|
+
} catch {
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
70
21
|
}
|
|
71
22
|
|
|
72
23
|
function printSites() {
|
|
@@ -89,217 +40,210 @@ function printSites() {
|
|
|
89
40
|
}
|
|
90
41
|
}
|
|
91
42
|
|
|
92
|
-
|
|
93
|
-
const cfg = buildConfig(loadEnv(site));
|
|
94
|
-
const client = createClient(cfg);
|
|
43
|
+
// ------------------------------------------------------------------ sites 管理
|
|
95
44
|
|
|
96
|
-
|
|
97
|
-
|
|
45
|
+
const SENSITIVE = /PASSWORD|SECRET|TOKEN|KEY/i;
|
|
46
|
+
|
|
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)`);
|
|
98
85
|
}
|
|
86
|
+
}
|
|
99
87
|
|
|
100
|
-
|
|
101
|
-
const value = rest[0];
|
|
102
|
-
if (!value) throw new Error(`${what} 需要一个参数`);
|
|
103
|
-
return value;
|
|
104
|
-
};
|
|
88
|
+
// ------------------------------------------------------------------ 输出包装
|
|
105
89
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
case 'rm':
|
|
121
|
-
return cmd.deleteContent(client, type, needFirst('delete'), flags);
|
|
122
|
-
default:
|
|
123
|
-
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}`);
|
|
124
104
|
}
|
|
125
|
-
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
switch (action) {
|
|
129
|
-
case undefined:
|
|
130
|
-
case 'list':
|
|
131
|
-
return cmd.listTaxonomy(client, tax, flags);
|
|
132
|
-
case 'get':
|
|
133
|
-
return cmd.getTaxonomy(client, tax, needFirst('get'));
|
|
134
|
-
case 'create':
|
|
135
|
-
case 'new':
|
|
136
|
-
return cmd.createTaxonomy(client, tax, flags);
|
|
137
|
-
case 'update':
|
|
138
|
-
case 'edit':
|
|
139
|
-
return cmd.updateTaxonomy(client, tax, needFirst('update'), flags);
|
|
140
|
-
case 'delete':
|
|
141
|
-
case 'rm':
|
|
142
|
-
return cmd.deleteTaxonomy(client, tax, needFirst('delete'), flags);
|
|
143
|
-
default:
|
|
144
|
-
throw new Error(`${tax} 未知子命令:${action}`);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
108
|
+
// ------------------------------------------------------------------ 用法错误
|
|
147
109
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
case '
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
case 'list':
|
|
182
|
-
return cmd.listPlugins(client, flags);
|
|
183
|
-
case 'install':
|
|
184
|
-
if (!rest[0]) {
|
|
185
|
-
throw new Error('plugins install 需要 wordpress.org 插件别名,例如 wpops plugins install akismet');
|
|
186
|
-
}
|
|
187
|
-
return cmd.installPlugin(client, rest[0], flags);
|
|
188
|
-
case 'activate':
|
|
189
|
-
return cmd.setPluginStatus(client, needFirst('activate'), true, flags);
|
|
190
|
-
case 'deactivate':
|
|
191
|
-
return cmd.setPluginStatus(client, needFirst('deactivate'), false, flags);
|
|
192
|
-
default:
|
|
193
|
-
throw new Error(`plugins 未知子命令:${action}`);
|
|
194
|
-
}
|
|
195
|
-
case 'themes':
|
|
196
|
-
case 'theme':
|
|
197
|
-
switch (action) {
|
|
198
|
-
case undefined:
|
|
199
|
-
case 'list':
|
|
200
|
-
return cmd.listThemes(client, flags);
|
|
201
|
-
case 'activate':
|
|
202
|
-
case 'switch':
|
|
203
|
-
return cmd.activateTheme(client, needFirst('activate'), flags);
|
|
204
|
-
default:
|
|
205
|
-
throw new Error(`themes 未知子命令:${action}`);
|
|
206
|
-
}
|
|
207
|
-
case 'categories':
|
|
208
|
-
case 'category':
|
|
209
|
-
return taxonomy('categories');
|
|
210
|
-
case 'tags':
|
|
211
|
-
case 'tag':
|
|
212
|
-
return taxonomy('tags');
|
|
213
|
-
case 'comments':
|
|
214
|
-
case 'comment':
|
|
215
|
-
switch (action) {
|
|
216
|
-
case undefined:
|
|
217
|
-
case 'list':
|
|
218
|
-
return cmd.listComments(client, flags);
|
|
219
|
-
case 'get':
|
|
220
|
-
return cmd.getComment(client, needFirst('get'));
|
|
221
|
-
case 'update':
|
|
222
|
-
case 'edit':
|
|
223
|
-
return cmd.updateComment(client, needFirst('update'), flags);
|
|
224
|
-
case 'delete':
|
|
225
|
-
case 'rm':
|
|
226
|
-
return cmd.deleteComment(client, needFirst('delete'), flags);
|
|
227
|
-
default:
|
|
228
|
-
throw new Error(`comments 未知子命令:${action}`);
|
|
229
|
-
}
|
|
230
|
-
case 'users':
|
|
231
|
-
case 'user':
|
|
232
|
-
switch (action) {
|
|
233
|
-
case undefined:
|
|
234
|
-
case 'list':
|
|
235
|
-
return cmd.listUsers(client, flags);
|
|
236
|
-
case 'me':
|
|
237
|
-
return cmd.me(client, flags);
|
|
238
|
-
case 'get':
|
|
239
|
-
return cmd.getUser(client, needFirst('get'));
|
|
240
|
-
case 'create':
|
|
241
|
-
case 'new':
|
|
242
|
-
return cmd.createUser(client, flags);
|
|
243
|
-
case 'update':
|
|
244
|
-
case 'edit':
|
|
245
|
-
return cmd.updateUser(client, needFirst('update'), flags);
|
|
246
|
-
case 'delete':
|
|
247
|
-
case 'rm':
|
|
248
|
-
return cmd.deleteUser(client, needFirst('delete'), flags);
|
|
249
|
-
default:
|
|
250
|
-
throw new Error(`users 未知子命令:${action}`);
|
|
251
|
-
}
|
|
252
|
-
case 'raw':
|
|
253
|
-
if (!action || !rest[0]) throw new Error('raw 用法:wpops raw <METHOD> <path> [--data JSON]');
|
|
254
|
-
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;
|
|
255
143
|
default:
|
|
256
|
-
throw new Error(
|
|
144
|
+
throw new Error('completion 支持:bash|zsh|fish|powershell');
|
|
257
145
|
}
|
|
258
146
|
}
|
|
259
147
|
|
|
148
|
+
// ------------------------------------------------------------------ main
|
|
149
|
+
|
|
260
150
|
async function main() {
|
|
261
151
|
const { positionals, flags } = parseArgs(process.argv.slice(2));
|
|
262
152
|
const [group, action, ...rest] = positionals;
|
|
263
153
|
|
|
154
|
+
configureOutput(flags);
|
|
155
|
+
if (flags.paginate) flags['all-pages'] = true;
|
|
156
|
+
if (flags.csv || flags.table || flags.fields) flags.json = true;
|
|
157
|
+
|
|
158
|
+
if (flags.version) {
|
|
159
|
+
console.log(version());
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// help:wpops / wpops help [组] [动作] / wpops <组> [动作] --help
|
|
264
164
|
if (!group || group === 'help' || flags.help) {
|
|
265
|
-
|
|
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()));
|
|
266
178
|
return;
|
|
267
179
|
}
|
|
180
|
+
|
|
268
181
|
if (group === 'sites') {
|
|
269
|
-
|
|
182
|
+
await withOutput(flags, () => sitesCommand(action, rest));
|
|
270
183
|
return;
|
|
271
184
|
}
|
|
272
185
|
if (group === 'setup') {
|
|
273
|
-
await
|
|
186
|
+
await setup(flags);
|
|
274
187
|
return;
|
|
275
188
|
}
|
|
276
189
|
if (group === 'install-skill') {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (group === 'commands') {
|
|
205
|
+
await withOutput(flags, () => console.log(commandsJson(version())));
|
|
283
206
|
return;
|
|
284
207
|
}
|
|
285
208
|
|
|
286
|
-
const
|
|
287
|
-
const
|
|
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
|
+
: [];
|
|
288
213
|
|
|
289
|
-
|
|
290
|
-
|
|
214
|
+
let targets = null;
|
|
215
|
+
if (flags.all) targets = listSites();
|
|
216
|
+
else if (named.length) targets = named.map((name) => ({ name, label: name }));
|
|
217
|
+
|
|
218
|
+
if (targets) {
|
|
291
219
|
if (!targets.length) throw new Error('还没有配置任何站点');
|
|
292
|
-
if (
|
|
293
|
-
|
|
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;
|
|
294
227
|
}
|
|
295
228
|
let failures = 0;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
242
|
}
|
|
243
|
+
});
|
|
244
|
+
if (usageError) {
|
|
245
|
+
reportUsageError(usageError, usageError.group || group, usageError.action ?? action);
|
|
246
|
+
return;
|
|
303
247
|
}
|
|
304
248
|
if (failures) {
|
|
305
249
|
console.error(`\n完成:${failures}/${targets.length} 个站点出错。`);
|
|
@@ -308,10 +252,23 @@ async function main() {
|
|
|
308
252
|
return;
|
|
309
253
|
}
|
|
310
254
|
|
|
311
|
-
|
|
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
|
+
}
|
|
312
264
|
}
|
|
313
265
|
|
|
314
266
|
main().catch((err) => {
|
|
267
|
+
if (err instanceof UsageError) {
|
|
268
|
+
console.error(`✗ ${err.message}`);
|
|
269
|
+
console.error('提示:wpops help 查看全部命令');
|
|
270
|
+
process.exit(2);
|
|
271
|
+
}
|
|
315
272
|
if (err instanceof WpError) {
|
|
316
273
|
console.error(`✗ ${err.message}`);
|
|
317
274
|
if (err.code) console.error(` 代码:${err.code}`);
|
package/lib/abilities.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Abilities API 命令(/wp-json/wp-abilities/v1/abilities)。
|
|
2
|
+
// 需要站点安装并启用 Abilities API 插件。
|
|
3
|
+
import { printJson, dry, parseJson } from './util.js';
|
|
4
|
+
|
|
5
|
+
const ABILITIES = '/wp-json/wp-abilities/v1/abilities';
|
|
6
|
+
|
|
7
|
+
/** 端点可能返回数组、{ abilities: [...] } 或按名字索引的对象,统一成数组。 */
|
|
8
|
+
function asList(data) {
|
|
9
|
+
if (Array.isArray(data)) return data;
|
|
10
|
+
if (Array.isArray(data?.abilities)) return data.abilities;
|
|
11
|
+
if (data && typeof data === 'object') return Object.values(data);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 列表:GET /abilities。字段可能缺失,逐项容错。 */
|
|
16
|
+
export async function listAbilities(client, flags) {
|
|
17
|
+
const { data } = await client.request('GET', ABILITIES);
|
|
18
|
+
if (flags.json) return printJson(data);
|
|
19
|
+
const items = asList(data);
|
|
20
|
+
console.log(`Abilities ${items.length} 个:`);
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const name = item?.name ?? item?.id ?? '?';
|
|
23
|
+
const label = item?.label ?? '';
|
|
24
|
+
const description = item?.description ?? '';
|
|
25
|
+
console.log(` ${name} ${label} ${description}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 详情:GET /abilities/{name}。 */
|
|
30
|
+
export async function getAbility(client, name, flags) {
|
|
31
|
+
const { data } = await client.request('GET', `${ABILITIES}/${encodeURIComponent(name)}`);
|
|
32
|
+
if (flags.json) return printJson(data);
|
|
33
|
+
console.log(`${data?.name ?? name} ${data?.label ?? ''}`);
|
|
34
|
+
if (data?.description) console.log(` 描述:${data.description}`);
|
|
35
|
+
if (data?.input_schema) console.log(` 输入:${JSON.stringify(data.input_schema)}`);
|
|
36
|
+
if (data?.output_schema) console.log(` 输出:${JSON.stringify(data.output_schema)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 运行:POST /abilities/{name}/run。
|
|
41
|
+
* body 来自 --input 或 --data(JSON 字符串),都没有则 {}。
|
|
42
|
+
*/
|
|
43
|
+
export async function runAbility(client, name, flags) {
|
|
44
|
+
const raw = flags.input !== undefined ? flags.input : flags.data;
|
|
45
|
+
const body = raw !== undefined ? parseJson(raw) : {};
|
|
46
|
+
if (flags['dry-run']) return dry(`运行 Ability ${name} ${JSON.stringify(body)}`);
|
|
47
|
+
const { data } = await client.request('POST', `${ABILITIES}/${encodeURIComponent(name)}/run`, { body });
|
|
48
|
+
if (flags.json) return printJson(data);
|
|
49
|
+
console.log(`✓ 已运行 ${name}`);
|
|
50
|
+
console.log(JSON.stringify(data, null, 2));
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// 用户应用密码(/wp-json/wp/v2/users/<user|me>/application-passwords)。
|
|
2
|
+
// 创建是唯一会返回明文 password 的接口,该字段只在此处打印一次。
|
|
3
|
+
import { printJson, dry } from './util.js';
|
|
4
|
+
import { UsageError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
function appPasswordsPath(user) {
|
|
7
|
+
return `/wp-json/wp/v2/users/${user || 'me'}/application-passwords`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function listAppPasswords(client, user, flags) {
|
|
11
|
+
const base = appPasswordsPath(user);
|
|
12
|
+
const { data } = await client.request('GET', base);
|
|
13
|
+
if (flags.json) return printJson(data);
|
|
14
|
+
console.log(`应用密码 ${data.length} 个:`);
|
|
15
|
+
for (const p of data) {
|
|
16
|
+
console.log(` ${p.uuid} ${p.name} ${p.last_used ?? '—'} ${p.created ?? '—'}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createAppPassword(client, user, flags) {
|
|
21
|
+
if (flags.name === undefined) throw new UsageError('创建应用密码需要 --name');
|
|
22
|
+
const body = { name: String(flags.name) };
|
|
23
|
+
if (flags['app-id'] !== undefined) body.app_id = String(flags['app-id']);
|
|
24
|
+
const base = appPasswordsPath(user);
|
|
25
|
+
if (flags['dry-run']) return dry(`创建应用密码 ${body.name}`);
|
|
26
|
+
const { data } = await client.request('POST', base, { body });
|
|
27
|
+
if (flags.json) return printJson(data);
|
|
28
|
+
console.log(`${data.uuid} ${data.name}`);
|
|
29
|
+
console.log(`password: ${data.password}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deleteAppPassword(client, user, uuid, flags) {
|
|
33
|
+
const base = appPasswordsPath(user);
|
|
34
|
+
if (flags['dry-run']) return dry(`删除应用密码 ${uuid}`);
|
|
35
|
+
if (!flags.force) throw new UsageError('删除应用密码需要 --force(不可撤销)');
|
|
36
|
+
const { data } = await client.request('DELETE', `${base}/${uuid}`);
|
|
37
|
+
if (flags.json) return printJson(data);
|
|
38
|
+
console.log(data.deleted ? `✓ 已删除应用密码 ${uuid}` : `? 应用密码 ${uuid} 未删除`);
|
|
39
|
+
}
|
package/lib/args.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// 已知的布尔开关:遇到它们不吞下一个参数,避免 `--json -s a` 把 -s 当值。
|
|
2
|
-
const BOOLEAN_FLAGS = new Set([
|
|
2
|
+
const BOOLEAN_FLAGS = new Set([
|
|
3
|
+
'json', 'force', 'activate', 'dry-run', 'yes', 'all', 'help', 'version',
|
|
4
|
+
'csv', 'table', 'quiet', 'verbose', 'all-pages', 'paginate', 'auto-add',
|
|
5
|
+
]);
|
|
3
6
|
|
|
4
7
|
/** 解析命令行。支持 --flag value、--flag=value、布尔 --flag、以及 -s/-a/-h/-y。 */
|
|
5
8
|
export function parseArgs(argv) {
|
|
@@ -23,6 +26,10 @@ export function parseArgs(argv) {
|
|
|
23
26
|
flags.yes = true;
|
|
24
27
|
continue;
|
|
25
28
|
}
|
|
29
|
+
if (arg === '-v' || arg === '--version') {
|
|
30
|
+
flags.version = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
26
33
|
if (arg.startsWith('--')) {
|
|
27
34
|
const eq = arg.indexOf('=');
|
|
28
35
|
if (eq !== -1) {
|
|
@@ -49,37 +56,5 @@ export function parseArgs(argv) {
|
|
|
49
56
|
return { positionals, flags };
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/** 判断命令是否会修改站点数据(用于 --all 护栏与 --dry-run 提示)。 */
|
|
55
|
-
export function isMutation(group, action) {
|
|
56
|
-
const act = action || '';
|
|
57
|
-
switch (group) {
|
|
58
|
-
case 'posts':
|
|
59
|
-
case 'post':
|
|
60
|
-
case 'pages':
|
|
61
|
-
case 'page':
|
|
62
|
-
case 'categories':
|
|
63
|
-
case 'category':
|
|
64
|
-
case 'tags':
|
|
65
|
-
case 'tag':
|
|
66
|
-
return CONTENT_MUTATIONS.has(act);
|
|
67
|
-
case 'media':
|
|
68
|
-
return ['upload', 'update', 'edit', 'delete', 'rm'].includes(act);
|
|
69
|
-
case 'plugins':
|
|
70
|
-
case 'plugin':
|
|
71
|
-
return ['install', 'activate', 'deactivate'].includes(act);
|
|
72
|
-
case 'themes':
|
|
73
|
-
case 'theme':
|
|
74
|
-
return ['activate', 'switch'].includes(act);
|
|
75
|
-
case 'comments':
|
|
76
|
-
case 'comment':
|
|
77
|
-
return ['update', 'edit', 'delete', 'rm'].includes(act);
|
|
78
|
-
case 'users':
|
|
79
|
-
return ['create', 'update', 'edit', 'delete', 'rm'].includes(act);
|
|
80
|
-
case 'raw':
|
|
81
|
-
return !['GET', 'HEAD', 'OPTIONS'].includes(act.toUpperCase());
|
|
82
|
-
default:
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
59
|
+
// 写操作判定统一来自 lib/spec.js(单一事实来源),这里只做转发。
|
|
60
|
+
export { isMutation, groupByName } from './spec.js';
|