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/lib/spec.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// 命令的单一事实来源。
|
|
2
|
+
// help / `wpops commands --json` / --all 写操作护栏(isMutation)/ shell 补全 全部由这里派生,
|
|
3
|
+
// 避免文档与代码各写一份、越走越偏。
|
|
4
|
+
|
|
5
|
+
const CONTENT_MUT = ['create', 'new', 'update', 'edit', 'delete', 'rm'];
|
|
6
|
+
const ITEM_MUT = ['create', 'new', 'update', 'edit', 'delete', 'rm'];
|
|
7
|
+
|
|
8
|
+
/** 全局开关(所有组通用)。 */
|
|
9
|
+
export const GLOBAL_FLAGS = [
|
|
10
|
+
['--dry-run', '只预览不执行(写操作;delete 的预览不需要 --force)'],
|
|
11
|
+
['--force', '彻底删除 / 危险操作确认'],
|
|
12
|
+
['--yes / -y', '批量写操作确认(配合 --all)'],
|
|
13
|
+
['--site <名> / -s <名>', '指定站点,可逗号分隔多个'],
|
|
14
|
+
['--all / -a', '对所有已配置站点执行'],
|
|
15
|
+
['--per-page N · --page N', '分页(REST 上限 100)'],
|
|
16
|
+
['--all-pages / --paginate', '自动翻完所有页'],
|
|
17
|
+
['--fields a,b · --csv · --table', '输出字段投影 / CSV / 表格'],
|
|
18
|
+
['--json', '输出原始 JSON'],
|
|
19
|
+
['--quiet · --output <文件>', '只出错时输出 / 写入文件'],
|
|
20
|
+
['--timeout <ms> · --retries <n>', '请求超时 / 重试次数(也可用 WP_* 环境变量)'],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 组描述。
|
|
25
|
+
* - name/aliases:命令名
|
|
26
|
+
* - summary:一句话说明
|
|
27
|
+
* - actions:动作 → 说明(顺序即展示顺序;空对象表示无动作)
|
|
28
|
+
* - mutation:会改数据的动作集合;特殊值 'batch'(总是) / 'raw'(非 GET)
|
|
29
|
+
* - local:true 表示在本地执行,不走站点 REST(如 setup/sites/completion)
|
|
30
|
+
* - usage:示例行;flags:该组关键参数
|
|
31
|
+
*/
|
|
32
|
+
export const GROUPS = [
|
|
33
|
+
{
|
|
34
|
+
name: 'doctor', summary: '连接与权限体检(先跑这个)',
|
|
35
|
+
actions: {}, usage: ['wpops doctor'], flags: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'me', summary: '当前用户与能力',
|
|
39
|
+
actions: {}, usage: ['wpops me'], flags: ['--json'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'setup', summary: '交互式配置一个站点(写入 sites/<名字>.env)', local: true,
|
|
43
|
+
actions: {},
|
|
44
|
+
usage: ['wpops setup'],
|
|
45
|
+
flags: ['--url <URL>', '--user <用户名>', '--password <应用密码>', '--name <站点名>', '--force'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'sites', summary: '站点档案管理', aliases: [], local: true,
|
|
49
|
+
actions: {
|
|
50
|
+
list: '列出已配置站点', show: '查看站点配置(密码打码)',
|
|
51
|
+
rm: '删除站点档案', rename: '重命名站点档案',
|
|
52
|
+
},
|
|
53
|
+
usage: ['wpops sites list', 'wpops sites show qnnp.me', 'wpops sites rm qnnp.me', 'wpops sites rename 旧 新'],
|
|
54
|
+
flags: [],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'install-skill', summary: '把内置 agent skill 装到发现目录', local: true,
|
|
58
|
+
actions: {}, usage: ['wpops install-skill'], flags: [],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'completion', summary: '输出 shell 补全脚本', local: true,
|
|
62
|
+
actions: { bash: 'bash', zsh: 'zsh', fish: 'fish', powershell: 'Windows PowerShell' },
|
|
63
|
+
usage: ['wpops completion bash'],
|
|
64
|
+
flags: [],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'commands', summary: '输出机器可读的命令清单(agent 现查现用)', local: true,
|
|
68
|
+
actions: { list: '人类可读概览', show: 'JSON 清单' },
|
|
69
|
+
usage: ['wpops commands --json'],
|
|
70
|
+
flags: ['--json'],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
name: 'posts', summary: '文章:增删改查', aliases: ['post'],
|
|
75
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
76
|
+
mutation: CONTENT_MUT,
|
|
77
|
+
usage: [
|
|
78
|
+
'wpops posts list [--per-page 10] [--page 2] [--status draft] [--search 词] [--categories 1,2]',
|
|
79
|
+
'wpops posts get <id>',
|
|
80
|
+
'wpops posts create --title "标题" --content "正文" [--status draft] [--categories 1,2] [--tags 3]',
|
|
81
|
+
'wpops posts create --title "标题" --from-file ./post.md',
|
|
82
|
+
'wpops posts update <id> --title "新标题" [--status publish] [--featured-media 44]',
|
|
83
|
+
'wpops posts delete <id> [--force] # 默认进回收站',
|
|
84
|
+
],
|
|
85
|
+
flags: ['--title', '--content', '--from-file', '--status', '--slug', '--excerpt',
|
|
86
|
+
'--featured-media', '--author', '--date', '--categories', '--tags',
|
|
87
|
+
'--per-page', '--page', '--search', '--orderby', '--order'],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'pages', summary: '页面:增删改查', aliases: ['page'],
|
|
91
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
92
|
+
mutation: CONTENT_MUT,
|
|
93
|
+
usage: ['wpops pages ... # 与 posts 同'],
|
|
94
|
+
flags: ['同 posts'],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'content', summary: '任意 show_in_rest 的 CPT(按 rest_base)', local: false,
|
|
98
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
99
|
+
mutation: CONTENT_MUT,
|
|
100
|
+
usage: ['wpops content list books', 'wpops content get books 12', 'wpops content create books --title "..."'],
|
|
101
|
+
flags: ['同 posts;第一个位置参数是 rest_base'],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'media', summary: '媒体:列出/上传/导入/图像编辑/更新/删除',
|
|
105
|
+
actions: {
|
|
106
|
+
list: '列出', get: '查看', upload: '上传本地文件', sideload: '从 URL 导入',
|
|
107
|
+
'edit-image': '图像编辑(裁剪/旋转)', update: '更新元数据', delete: '删除',
|
|
108
|
+
},
|
|
109
|
+
mutation: ['upload', 'sideload', 'edit-image', 'crop', 'update', 'edit', 'delete', 'rm'],
|
|
110
|
+
usage: [
|
|
111
|
+
'wpops media list [--per-page 20] [--media-type image]',
|
|
112
|
+
'wpops media upload ./pic.jpg [--title "标题"] [--alt "替代文本"]',
|
|
113
|
+
'wpops media sideload --url https://example.com/pic.jpg [--title "标题"]',
|
|
114
|
+
'wpops media edit-image <id> --rotation 90 [--crop] [--dest-width 800] [--src <url>]',
|
|
115
|
+
'wpops media update <id> [--alt "替代文本"] [--caption "说明"] [--post 123]',
|
|
116
|
+
'wpops media delete <id> [--force]',
|
|
117
|
+
],
|
|
118
|
+
flags: ['--url', '--title', '--alt', '--caption', '--description', '--post', '--media-type',
|
|
119
|
+
'--rotation', '--crop', '--x', '--y', '--dest-width', '--dest-height', '--modifiers', '--src'],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'categories', summary: '分类:增删改查', aliases: ['category'],
|
|
123
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
124
|
+
mutation: CONTENT_MUT,
|
|
125
|
+
usage: ['wpops categories create --name "新分类" [--slug x] [--parent 3]', 'wpops categories delete <id> --force'],
|
|
126
|
+
flags: ['--name', '--slug', '--parent', '--description'],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'tags', summary: '标签:增删改查', aliases: ['tag'],
|
|
130
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
131
|
+
mutation: CONTENT_MUT,
|
|
132
|
+
usage: ['wpops tags ... # 与 categories 同'],
|
|
133
|
+
flags: ['--name', '--slug', '--parent', '--description'],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'terms', summary: '任意 show_in_rest 的自定义分类法(按 rest_base)', local: false,
|
|
137
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
138
|
+
mutation: CONTENT_MUT,
|
|
139
|
+
usage: ['wpops terms list genres', 'wpops terms create genres --name 科幻'],
|
|
140
|
+
flags: ['同 categories;第一个位置参数是 rest_base'],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'comments', summary: '评论:列出/创建/更新/删除', aliases: ['comment'],
|
|
144
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
145
|
+
mutation: ITEM_MUT,
|
|
146
|
+
usage: [
|
|
147
|
+
'wpops comments list [--status hold|approved|spam|trash] [--post 123]',
|
|
148
|
+
'wpops comments create --post 123 --content "文字" [--author-name x] [--author-email a@b.c]',
|
|
149
|
+
'wpops comments update <id> --status approved',
|
|
150
|
+
'wpops comments delete <id> [--force]',
|
|
151
|
+
],
|
|
152
|
+
flags: ['--post', '--content', '--from-file', '--author-name', '--author-email', '--parent', '--status'],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'users', summary: '用户:增删改查 / 当前用户',
|
|
156
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除', me: '当前用户' },
|
|
157
|
+
mutation: ['create', 'new', 'update', 'edit', 'delete', 'rm'],
|
|
158
|
+
usage: [
|
|
159
|
+
'wpops users list [--search 词] [--role administrator]',
|
|
160
|
+
'wpops users create --username bob --email bob@example.com --role editor',
|
|
161
|
+
'wpops users delete <id> --force [--reassign 1]',
|
|
162
|
+
'wpops users me',
|
|
163
|
+
],
|
|
164
|
+
flags: ['--username', '--email', '--name', '--role', '--password', '--reassign'],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'menus', summary: '导航菜单:增删改查', aliases: ['menu'],
|
|
168
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
169
|
+
mutation: ITEM_MUT,
|
|
170
|
+
usage: [
|
|
171
|
+
'wpops menus list',
|
|
172
|
+
'wpops menus get <id>',
|
|
173
|
+
'wpops menus create --name Header [--slug header] [--locations header,mobile]',
|
|
174
|
+
'wpops menus update <id> --locations header,mobile # 位置分配靠这个',
|
|
175
|
+
'wpops menus delete <id> --force',
|
|
176
|
+
],
|
|
177
|
+
flags: ['--name', '--slug', '--description', '--locations', '--auto-add'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'menu-items', summary: '菜单项:增删改查', aliases: ['menu-item'],
|
|
181
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
182
|
+
mutation: ITEM_MUT,
|
|
183
|
+
usage: [
|
|
184
|
+
'wpops menu-items list [--menus 190] [--search 词] [--per-page 100]',
|
|
185
|
+
'wpops menu-items get <id>',
|
|
186
|
+
'wpops menu-items create --title Projects --url https://... --menus 190 --menu-order 6',
|
|
187
|
+
'wpops menu-items create --title Projects --menus 190 --type post_type --object page --object-id 4976',
|
|
188
|
+
'wpops menu-items update <id> --menu-order 3 [--parent 4979]',
|
|
189
|
+
'wpops menu-items delete <id> --force',
|
|
190
|
+
],
|
|
191
|
+
flags: ['--title', '--url', '--menus', '--menu-order', '--parent', '--status', '--type',
|
|
192
|
+
'--object', '--object-id', '--target', '--classes', '--description', '--attr-title', '--xfn'],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'search', summary: '跨内容搜索',
|
|
196
|
+
actions: {}, usage: ['wpops search <词> [--type post] [--subtype post] [--per-page 10]'],
|
|
197
|
+
flags: ['--type', '--subtype', '--per-page', '--page'],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'settings', summary: '站点设置:读取/更新',
|
|
201
|
+
actions: { list: '查看', get: '查看', update: '更新', set: '更新', edit: '更新' },
|
|
202
|
+
mutation: ['update', 'set', 'edit'],
|
|
203
|
+
usage: [
|
|
204
|
+
'wpops settings',
|
|
205
|
+
'wpops settings update --title "新标题" [--description "..."] [--timezone Asia/Shanghai] [--posts-per-page 10]',
|
|
206
|
+
'wpops settings update --data \'{"show_on_front":"page"}\'',
|
|
207
|
+
],
|
|
208
|
+
flags: ['--title', '--description', '--url', '--email', '--timezone', '--date-format', '--time-format',
|
|
209
|
+
'--posts-per-page', '--show-on-front', '--page-on-front', '--default-category', '--data'],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'types', summary: '已注册文章类型(含 rest_base)',
|
|
213
|
+
actions: { list: '列出', get: '查看' }, usage: ['wpops types list|get <type>'], flags: [],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'taxonomies', summary: '已注册分类法', aliases: ['taxonomy'],
|
|
217
|
+
actions: { list: '列出', get: '查看' }, usage: ['wpops taxonomies list|get <tax>'], flags: [],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'statuses', summary: '文章状态',
|
|
221
|
+
actions: {}, usage: ['wpops statuses'], flags: [],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'revisions', summary: '内容修订:列/看/删/恢复', aliases: ['revision'],
|
|
225
|
+
actions: { list: '列出', get: '查看', delete: '删除', restore: '恢复到该修订' },
|
|
226
|
+
mutation: ['delete', 'rm', 'restore'],
|
|
227
|
+
usage: [
|
|
228
|
+
'wpops revisions list <id> [--type posts|pages]',
|
|
229
|
+
'wpops revisions get <id> <rev>',
|
|
230
|
+
'wpops revisions restore <id> <rev>',
|
|
231
|
+
'wpops revisions delete <id> <rev> --force',
|
|
232
|
+
],
|
|
233
|
+
flags: ['--type posts|pages', '--per-page'],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'blocks', summary: '可复用区块:增删改查', aliases: ['block'],
|
|
237
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
238
|
+
mutation: ITEM_MUT,
|
|
239
|
+
usage: ['wpops blocks list', 'wpops blocks create --title x --content y', 'wpops blocks delete <id> --force'],
|
|
240
|
+
flags: ['--title', '--content', '--from-file', '--status', '--search'],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'navigation', summary: '区块主题导航:增删改查',
|
|
244
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
245
|
+
mutation: ITEM_MUT,
|
|
246
|
+
usage: ['wpops navigation list', 'wpops navigation create --title "Main" --content "..."'],
|
|
247
|
+
flags: ['--title', '--content', '--from-file', '--status'],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'templates', summary: '区块主题模板', aliases: ['template'],
|
|
251
|
+
actions: { list: '列出', get: '查看', update: '更新' },
|
|
252
|
+
mutation: ['update', 'edit'],
|
|
253
|
+
usage: ['wpops templates list', 'wpops templates get theme//slug', 'wpops templates update theme//slug --from-file t.html'],
|
|
254
|
+
flags: ['--content', '--from-file', '--title'],
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'template-parts', summary: '区块主题模板片段', aliases: ['template-part'],
|
|
258
|
+
actions: { list: '列出', get: '查看', update: '更新' },
|
|
259
|
+
mutation: ['update', 'edit'],
|
|
260
|
+
usage: ['wpops template-parts list', 'wpops template-parts update theme//slug --content "..."'],
|
|
261
|
+
flags: ['--content', '--from-file', '--title'],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'global-styles', summary: 'theme.json 全局样式', aliases: ['global-style'],
|
|
265
|
+
actions: { get: '查看', update: '更新' },
|
|
266
|
+
mutation: ['update', 'set'],
|
|
267
|
+
usage: ['wpops global-styles get <id-or-stylesheet>', 'wpops global-styles update <id> --styles \'{"...":...}\''],
|
|
268
|
+
flags: ['--styles JSON', '--from-file'],
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'widgets', summary: '小工具:增删改查', aliases: ['widget'],
|
|
272
|
+
actions: { list: '列出', get: '查看', create: '新建', update: '更新', delete: '删除' },
|
|
273
|
+
mutation: ITEM_MUT,
|
|
274
|
+
usage: [
|
|
275
|
+
'wpops widgets list [--sidebar primary]',
|
|
276
|
+
'wpops widgets create --id custom_html-3 --sidebar primary [--instance \'{...}\']',
|
|
277
|
+
'wpops widgets delete <id> --force',
|
|
278
|
+
],
|
|
279
|
+
flags: ['--id', '--sidebar', '--instance JSON', '--from-file'],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'sidebars', summary: '边栏:列出/查看/更新', aliases: ['sidebar'],
|
|
283
|
+
actions: { list: '列出', get: '查看', update: '更新' },
|
|
284
|
+
mutation: ['update', 'edit'],
|
|
285
|
+
usage: ['wpops sidebars list', 'wpops sidebars update primary --widgets a,b,c'],
|
|
286
|
+
flags: ['--widgets a,b,c'],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'app-passwords', summary: '用户应用密码', aliases: ['app-password'],
|
|
290
|
+
actions: { list: '列出', create: '新建(明文只打印一次)', delete: '吊销' },
|
|
291
|
+
mutation: ['create', 'new', 'delete', 'rm'],
|
|
292
|
+
usage: [
|
|
293
|
+
'wpops app-passwords list [--user me]',
|
|
294
|
+
'wpops app-passwords create --name ci-token [--user me]',
|
|
295
|
+
'wpops app-passwords delete <uuid> --force [--user me]',
|
|
296
|
+
],
|
|
297
|
+
flags: ['--user', '--name', '--app-id'],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'abilities', summary: 'Abilities API:列出/查看/运行(需插件)', aliases: ['ability'],
|
|
301
|
+
actions: { list: '列出', get: '查看', run: '运行' },
|
|
302
|
+
mutation: ['run'],
|
|
303
|
+
usage: ['wpops abilities list', 'wpops abilities run core/get-site-info [--input \'{}\']'],
|
|
304
|
+
flags: ['--input JSON', '--data JSON'],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'health', summary: '站点健康测试(需插件)',
|
|
308
|
+
actions: { list: '列出测试名' },
|
|
309
|
+
usage: ['wpops health list', 'wpops health loopback-requests'],
|
|
310
|
+
flags: [],
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'batch', summary: '批量 REST 请求(仅 POST/PUT/PATCH/DELETE)',
|
|
314
|
+
actions: {}, mutation: 'batch',
|
|
315
|
+
usage: ['wpops batch --requests \'[{"method":"DELETE","path":"/wp/v2/menu-items/1"}]\'',
|
|
316
|
+
'wpops batch --requests-file reqs.json'],
|
|
317
|
+
flags: ['--requests JSON', '--requests-file <文件>'],
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'plugins', summary: '插件:列出/安装/启停/删除', aliases: ['plugin'],
|
|
321
|
+
actions: { list: '列出', install: '安装', activate: '启用', deactivate: '停用', delete: '删除' },
|
|
322
|
+
mutation: ['install', 'activate', 'deactivate', 'delete', 'rm'],
|
|
323
|
+
usage: [
|
|
324
|
+
'wpops plugins list [--status active] [--search 词]',
|
|
325
|
+
'wpops plugins install <wp.org 别名> [--activate]',
|
|
326
|
+
'wpops plugins activate <plugin> # 形如 akismet/akismet',
|
|
327
|
+
'wpops plugins delete <plugin> --force',
|
|
328
|
+
],
|
|
329
|
+
flags: ['--status', '--search', '--activate'],
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'themes', summary: '主题:列出/切换', aliases: ['theme'],
|
|
333
|
+
actions: { list: '列出', activate: '切换', switch: '切换' },
|
|
334
|
+
mutation: ['activate', 'switch'],
|
|
335
|
+
usage: ['wpops themes list', 'wpops themes activate <stylesheet>'],
|
|
336
|
+
flags: [],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'raw', summary: '任意端点兜底(任意 METHOD + path)', mutation: 'raw',
|
|
340
|
+
actions: {}, usage: ['wpops raw GET /wp-json/wp/v2/categories', 'wpops raw POST /wp-json/wp/v2/comments --data \'{"post":1,"content":"hi"}\''],
|
|
341
|
+
flags: ['--data JSON', '--data-file <文件>'],
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
/** 按名字或别名取组。 */
|
|
346
|
+
export function groupByName(name) {
|
|
347
|
+
if (!name) return null;
|
|
348
|
+
const key = String(name).toLowerCase();
|
|
349
|
+
return GROUPS.find((g) => g.name === key || (g.aliases || []).includes(key)) || null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 判断命令是否修改站点数据(供 --all 护栏)。 */
|
|
353
|
+
export function isMutation(group, action) {
|
|
354
|
+
const g = groupByName(group);
|
|
355
|
+
if (!g) return false;
|
|
356
|
+
const act = action || '';
|
|
357
|
+
if (g.mutation === 'batch') return true;
|
|
358
|
+
if (g.mutation === 'raw') return !['GET', 'HEAD', 'OPTIONS'].includes(act.toUpperCase());
|
|
359
|
+
return (g.mutation || []).includes(act);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** 机器可读清单(agent 现查现用)。 */
|
|
363
|
+
export function manifest(version = 'unknown') {
|
|
364
|
+
return {
|
|
365
|
+
version,
|
|
366
|
+
groups: GROUPS.map((g) => ({
|
|
367
|
+
name: g.name,
|
|
368
|
+
aliases: g.aliases || [],
|
|
369
|
+
summary: g.summary,
|
|
370
|
+
local: Boolean(g.local),
|
|
371
|
+
actions: Object.keys(g.actions || {}),
|
|
372
|
+
mutating: g.mutation === 'batch'
|
|
373
|
+
? ['<总是写>']
|
|
374
|
+
: g.mutation === 'raw'
|
|
375
|
+
? ['<非 GET>']
|
|
376
|
+
: (g.mutation || []),
|
|
377
|
+
})),
|
|
378
|
+
globalFlags: GLOBAL_FLAGS.map(([flag, description]) => ({ flag, description })),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** 供 shell 补全用:全部组名 + 别名。 */
|
|
383
|
+
export function completionWords(extra = []) {
|
|
384
|
+
const words = new Set();
|
|
385
|
+
for (const g of GROUPS) {
|
|
386
|
+
words.add(g.name);
|
|
387
|
+
for (const a of g.aliases || []) words.add(a);
|
|
388
|
+
}
|
|
389
|
+
for (const w of extra) words.add(w);
|
|
390
|
+
return [...words].sort();
|
|
391
|
+
}
|
package/lib/templates.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// 区块主题模板与模板片段(/wp-json/wp/v2/templates、/wp-json/wp/v2/template-parts)。
|
|
2
|
+
// 注意:模板 id 可能形如 `theme//slug`,必须原样拼进路径,不能 encodeURIComponent(会编坏 /)。
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { stripTags, printJson, dry, listRequest } from './util.js';
|
|
5
|
+
import { UsageError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
const TEMPLATES = '/wp-json/wp/v2/templates';
|
|
8
|
+
const TEMPLATE_PARTS = '/wp-json/wp/v2/template-parts';
|
|
9
|
+
|
|
10
|
+
/** 集合列表:分页 + 总数。 */
|
|
11
|
+
async function listCollection(client, base, label, flags) {
|
|
12
|
+
const query = {
|
|
13
|
+
per_page: flags['per-page'] ?? 100,
|
|
14
|
+
page: flags.page,
|
|
15
|
+
context: 'edit',
|
|
16
|
+
};
|
|
17
|
+
const { data, total } = await listRequest(client, base, query, flags);
|
|
18
|
+
if (flags.json) return printJson(data);
|
|
19
|
+
console.log(`${label} ${total ?? data.length} 个:`);
|
|
20
|
+
for (const item of data) {
|
|
21
|
+
console.log(` ${item.id} ${item.slug} ${item.status} ${stripTags(item.title?.rendered || '')}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 单项:json 或打印 id/slug/status 与 content。 */
|
|
26
|
+
async function getItem(client, base, id, flags) {
|
|
27
|
+
const { data } = await client.request('GET', `${base}/${id}`);
|
|
28
|
+
if (flags.json) return printJson(data);
|
|
29
|
+
console.log(`${data.id} ${data.slug} [${data.status}] ${stripTags(data.title?.rendered || '')}`);
|
|
30
|
+
console.log('─'.repeat(44));
|
|
31
|
+
console.log(data.content?.raw ?? data.content?.rendered ?? '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 更新:body { content }(--from-file 优先),可选 title。缺 content 报错。 */
|
|
35
|
+
async function updateItem(client, base, id, label, flags) {
|
|
36
|
+
const body = {};
|
|
37
|
+
if (flags['from-file'] !== undefined) body.content = readFileSync(String(flags['from-file']), 'utf8');
|
|
38
|
+
else if (flags.content !== undefined) body.content = String(flags.content);
|
|
39
|
+
if (body.content === undefined) throw new UsageError(`更新${label}需要 --content 或 --from-file`);
|
|
40
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
41
|
+
if (flags['dry-run']) return dry(`更新${label} ${id} ${JSON.stringify(body)}`);
|
|
42
|
+
const { data } = await client.request('POST', `${base}/${id}`, { body });
|
|
43
|
+
if (flags.json) return printJson(data);
|
|
44
|
+
console.log(`✓ 已更新${label} ${data.id}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function listTemplates(client, flags) {
|
|
48
|
+
return listCollection(client, TEMPLATES, '模板', flags);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getTemplate(client, id, flags) {
|
|
52
|
+
return getItem(client, TEMPLATES, id, flags);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function updateTemplate(client, id, flags) {
|
|
56
|
+
return updateItem(client, TEMPLATES, id, '模板', flags);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listTemplateParts(client, flags) {
|
|
60
|
+
return listCollection(client, TEMPLATE_PARTS, '模板片段', flags);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getTemplatePart(client, id, flags) {
|
|
64
|
+
return getItem(client, TEMPLATE_PARTS, id, flags);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function updateTemplatePart(client, id, flags) {
|
|
68
|
+
return updateItem(client, TEMPLATE_PARTS, id, '模板片段', flags);
|
|
69
|
+
}
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// 所有命令共用的输出与参数小工具。
|
|
2
|
+
// 新增命令模块一律从这里引入,避免各文件重复实现。
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
/** JSON.parse 的容错版:去掉 Windows 编辑器/重定向常见的 UTF-8 BOM。 */
|
|
7
|
+
export function parseJson(text) {
|
|
8
|
+
return JSON.parse(String(text).replace(/^\uFEFF/, ''));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 读文件并解析 JSON(同样容忍 BOM)。 */
|
|
12
|
+
export function readJsonFile(path) {
|
|
13
|
+
return parseJson(readFileSync(String(path), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripTags(html = '') {
|
|
17
|
+
return String(html).replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dry(message) {
|
|
21
|
+
console.log(`[dry-run] ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 把 flags 中的指定键转成 REST 查询参数(- 转 _)。 */
|
|
25
|
+
function pick(flags, keys) {
|
|
26
|
+
const query = {};
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const value = flags[key];
|
|
29
|
+
if (value === undefined || value === null || value === false) continue;
|
|
30
|
+
query[key.replace(/-/g, '_')] = String(value);
|
|
31
|
+
}
|
|
32
|
+
return query;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ------------------------------------------------------------------ 输出模式
|
|
36
|
+
|
|
37
|
+
const output = { fields: null, csv: false, table: false };
|
|
38
|
+
|
|
39
|
+
/** 由 bin 在解析参数后调用,配置 --fields / --csv / --table 全局输出。 */
|
|
40
|
+
export function configureOutput(flags = {}) {
|
|
41
|
+
output.fields = flags.fields
|
|
42
|
+
? String(flags.fields).split(',').map((s) => s.trim()).filter(Boolean)
|
|
43
|
+
: null;
|
|
44
|
+
output.csv = Boolean(flags.csv);
|
|
45
|
+
output.table = Boolean(flags.table);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getPath(obj, path) {
|
|
49
|
+
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function project(obj, fields) {
|
|
53
|
+
if (!fields || !obj || typeof obj !== 'object') return obj;
|
|
54
|
+
const picked = {};
|
|
55
|
+
for (const field of fields) {
|
|
56
|
+
const value = getPath(obj, field);
|
|
57
|
+
if (value !== undefined) picked[field] = value;
|
|
58
|
+
}
|
|
59
|
+
return picked;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function csvCell(value) {
|
|
63
|
+
const text = value === undefined || value === null
|
|
64
|
+
? ''
|
|
65
|
+
: typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
66
|
+
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toCsv(value, fields) {
|
|
70
|
+
const rows = Array.isArray(value) ? value : [value];
|
|
71
|
+
const columns = fields || [...new Set(rows.flatMap((r) => (r && typeof r === 'object' ? Object.keys(r) : [])))];
|
|
72
|
+
const lines = [columns.join(',')];
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
lines.push(columns.map((c) => csvCell(getPath(row, c))).join(','));
|
|
75
|
+
}
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pad(text, width) {
|
|
80
|
+
return String(text).length >= width ? String(text) : String(text) + ' '.repeat(width - String(text).length);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toTable(value, fields) {
|
|
84
|
+
const rows = Array.isArray(value) ? value : [value];
|
|
85
|
+
const columns = fields || [...new Set(rows.flatMap((r) => (r && typeof r === 'object' ? Object.keys(r) : [])))];
|
|
86
|
+
const matrix = rows.map((r) => columns.map((c) => {
|
|
87
|
+
const v = getPath(r, c);
|
|
88
|
+
return v === undefined || v === null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
89
|
+
}));
|
|
90
|
+
const widths = columns.map((c, i) => Math.max(String(c).length, ...matrix.map((row) => row[i].length)));
|
|
91
|
+
const head = columns.map((c, i) => pad(c, widths[i])).join(' ');
|
|
92
|
+
const sep = widths.map((w) => '-'.repeat(w)).join(' ');
|
|
93
|
+
return [head, sep, ...matrix.map((row) => row.map((cell, i) => pad(cell, widths[i])).join(' '))].join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printJson(value) {
|
|
97
|
+
let shaped = value;
|
|
98
|
+
if (output.fields) shaped = Array.isArray(value) ? value.map((o) => project(o, output.fields)) : project(value, output.fields);
|
|
99
|
+
if (output.csv) return console.log(toCsv(shaped, output.fields));
|
|
100
|
+
if (output.table) return console.log(toTable(shaped, output.fields));
|
|
101
|
+
console.log(JSON.stringify(shaped, null, 2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ------------------------------------------------------------------ 分页
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 拉取某个集合端点的全部页(--all-pages 用)。
|
|
108
|
+
* 默认 per_page=100,按 x-wp-totalpages 翻到最后一页。
|
|
109
|
+
*/
|
|
110
|
+
export async function fetchAll(client, path, query = {}, request) {
|
|
111
|
+
const call = request || ((method, p, opts) => client.request(method, p, opts));
|
|
112
|
+
const all = [];
|
|
113
|
+
let page = 1;
|
|
114
|
+
for (;;) {
|
|
115
|
+
const { data, headers } = await call('GET', path, {
|
|
116
|
+
query: { ...query, per_page: 100, page },
|
|
117
|
+
});
|
|
118
|
+
if (Array.isArray(data)) all.push(...data);
|
|
119
|
+
const totalPages = Number(headers.get('x-wp-totalpages') || 1);
|
|
120
|
+
if (!Array.isArray(data) || page >= totalPages || data.length === 0) break;
|
|
121
|
+
page++;
|
|
122
|
+
}
|
|
123
|
+
return all;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 统一集合请求:默认单页 + 返回总数;带 --all-pages 时自动翻完所有页。
|
|
128
|
+
* 返回 { data, total, totalPages }。
|
|
129
|
+
* request(可选):签名必须是 (method, path, opts) 的函数(用于注入带兜底/提示的请求器;
|
|
130
|
+
* 例如菜单端点要注入 menuRequest 时,请传 (m,p,o)=>menuRequest(client,m,p,o))。
|
|
131
|
+
*/
|
|
132
|
+
export async function listRequest(client, path, query, flags = {}, request) {
|
|
133
|
+
const call = request || ((method, p, opts) => client.request(method, p, opts));
|
|
134
|
+
if (flags['all-pages']) {
|
|
135
|
+
const data = await fetchAll(client, path, query, request);
|
|
136
|
+
return { data, total: data.length, totalPages: 1 };
|
|
137
|
+
}
|
|
138
|
+
const { data, headers } = await call('GET', path, { query });
|
|
139
|
+
return { data, total: headers.get('x-wp-total'), totalPages: headers.get('x-wp-totalpages') };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { stripTags, dry, pick, printJson, project, toCsv, toTable };
|
|
143
|
+
|