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/lib/run.js ADDED
@@ -0,0 +1,509 @@
1
+ // 组 → 动作 的分发逻辑。
2
+ // runOne 负责读配置建 client;dispatch 只做分发,便于用 stub client 做覆盖测试。
3
+ import { loadEnv } from './env.js';
4
+ import { buildConfig, createClient } from './client.js';
5
+ import { UsageError } from './errors.js';
6
+ import * as cmd from './commands.js';
7
+ import * as searchMod from './search.js';
8
+ import * as settingsMod from './settings.js';
9
+ import * as introspectMod from './introspect.js';
10
+ import * as revisionsMod from './revisions.js';
11
+ import * as blocksMod from './blocks.js';
12
+ import * as navigationMod from './navigation.js';
13
+ import * as templatesMod from './templates.js';
14
+ import * as globalStylesMod from './global-styles.js';
15
+ import * as widgetsMod from './widgets.js';
16
+ import * as appPasswordsMod from './app-passwords.js';
17
+ import * as abilitiesMod from './abilities.js';
18
+ import * as healthMod from './health.js';
19
+ import * as batchMod from './batch.js';
20
+
21
+ /** 纯分发:把组/动作路由到具体命令实现。 */
22
+ export async function dispatch(client, cfg, group, action, rest, flags) {
23
+ const needFirst = (what) => {
24
+ const value = rest[0];
25
+ if (!value) {
26
+ const label = [group, action].filter(Boolean).join(' ');
27
+ const detail = what && what !== action ? `:${what}` : '';
28
+ throw new UsageError(`${label} 缺少必需参数${detail}`, { group, action });
29
+ }
30
+ return value;
31
+ };
32
+
33
+ const content = async (type) => {
34
+ switch (action) {
35
+ case undefined:
36
+ case 'list':
37
+ return cmd.listContent(client, type, flags);
38
+ case 'get':
39
+ return cmd.getContent(client, type, needFirst('get'), flags);
40
+ case 'create':
41
+ case 'new':
42
+ return cmd.createContent(client, type, flags);
43
+ case 'update':
44
+ case 'edit':
45
+ return cmd.updateContent(client, type, needFirst('update'), flags);
46
+ case 'delete':
47
+ case 'rm':
48
+ return cmd.deleteContent(client, type, needFirst('delete'), flags);
49
+ default:
50
+ throw new UsageError(`${type} 未知子命令:${action}`);
51
+ }
52
+ };
53
+
54
+ const taxonomy = async (tax) => {
55
+ switch (action) {
56
+ case undefined:
57
+ case 'list':
58
+ return cmd.listTaxonomy(client, tax, flags);
59
+ case 'get':
60
+ return cmd.getTaxonomy(client, tax, needFirst('get'));
61
+ case 'create':
62
+ case 'new':
63
+ return cmd.createTaxonomy(client, tax, flags);
64
+ case 'update':
65
+ case 'edit':
66
+ return cmd.updateTaxonomy(client, tax, needFirst('update'), flags);
67
+ case 'delete':
68
+ case 'rm':
69
+ return cmd.deleteTaxonomy(client, tax, needFirst('delete'), flags);
70
+ default:
71
+ throw new UsageError(`${tax} 未知子命令:${action}`);
72
+ }
73
+ };
74
+
75
+ switch (group) {
76
+ case 'doctor':
77
+ return cmd.doctor(client, cfg, flags);
78
+ case 'me':
79
+ return cmd.me(client, flags);
80
+ case 'posts':
81
+ case 'post':
82
+ return content('posts');
83
+ case 'pages':
84
+ case 'page':
85
+ return content('pages');
86
+ case 'content': {
87
+ const type = rest[0];
88
+ if (!type) throw new UsageError('content 用法:wpops content list|get|create|update|delete <rest_base> [id]');
89
+ const id = rest[1];
90
+ switch (action) {
91
+ case undefined:
92
+ case 'list':
93
+ return cmd.listContent(client, type, flags);
94
+ case 'get':
95
+ if (!id) throw new UsageError('content get 需要 id');
96
+ return cmd.getContent(client, type, id, flags);
97
+ case 'create':
98
+ case 'new':
99
+ return cmd.createContent(client, type, flags);
100
+ case 'update':
101
+ case 'edit':
102
+ if (!id) throw new UsageError('content update 需要 id');
103
+ return cmd.updateContent(client, type, id, flags);
104
+ case 'delete':
105
+ case 'rm':
106
+ if (!id) throw new UsageError('content delete 需要 id');
107
+ return cmd.deleteContent(client, type, id, flags);
108
+ default:
109
+ throw new UsageError(`content 未知子命令:${action}`);
110
+ }
111
+ }
112
+ case 'terms': {
113
+ const tax = rest[0];
114
+ if (!tax) throw new UsageError('terms 用法:wpops terms list|get|create|update|delete <rest_base> [id]');
115
+ const id = rest[1];
116
+ switch (action) {
117
+ case undefined:
118
+ case 'list':
119
+ return cmd.listTaxonomy(client, tax, flags);
120
+ case 'get':
121
+ if (!id) throw new UsageError('terms get 需要 id');
122
+ return cmd.getTaxonomy(client, tax, id);
123
+ case 'create':
124
+ case 'new':
125
+ return cmd.createTaxonomy(client, tax, flags);
126
+ case 'update':
127
+ case 'edit':
128
+ if (!id) throw new UsageError('terms update 需要 id');
129
+ return cmd.updateTaxonomy(client, tax, id, flags);
130
+ case 'delete':
131
+ case 'rm':
132
+ if (!id) throw new UsageError('terms delete 需要 id');
133
+ return cmd.deleteTaxonomy(client, tax, id, flags);
134
+ default:
135
+ throw new UsageError(`terms 未知子命令:${action}`);
136
+ }
137
+ }
138
+ case 'media':
139
+ switch (action) {
140
+ case undefined:
141
+ case 'list':
142
+ return cmd.listMedia(client, flags);
143
+ case 'get':
144
+ return cmd.getMedia(client, needFirst('get'), flags);
145
+ case 'upload':
146
+ return cmd.uploadMedia(client, needFirst('文件路径'), flags);
147
+ case 'sideload':
148
+ return cmd.sideloadMedia(client, flags);
149
+ case 'edit-image':
150
+ case 'crop':
151
+ return cmd.editMediaImage(client, needFirst('edit-image'), flags);
152
+ case 'update':
153
+ case 'edit':
154
+ return cmd.updateMedia(client, needFirst('update'), flags);
155
+ case 'delete':
156
+ case 'rm':
157
+ return cmd.deleteMedia(client, needFirst('delete'), flags);
158
+ default:
159
+ throw new UsageError(`media 未知子命令:${action}`);
160
+ }
161
+ case 'categories':
162
+ case 'category':
163
+ return taxonomy('categories');
164
+ case 'tags':
165
+ case 'tag':
166
+ return taxonomy('tags');
167
+ case 'comments':
168
+ case 'comment':
169
+ switch (action) {
170
+ case undefined:
171
+ case 'list':
172
+ return cmd.listComments(client, flags);
173
+ case 'get':
174
+ return cmd.getComment(client, needFirst('get'));
175
+ case 'create':
176
+ case 'new':
177
+ return cmd.createComment(client, flags);
178
+ case 'update':
179
+ case 'edit':
180
+ return cmd.updateComment(client, needFirst('update'), flags);
181
+ case 'delete':
182
+ case 'rm':
183
+ return cmd.deleteComment(client, needFirst('delete'), flags);
184
+ default:
185
+ throw new UsageError(`comments 未知子命令:${action}`);
186
+ }
187
+ case 'users':
188
+ case 'user':
189
+ switch (action) {
190
+ case undefined:
191
+ case 'list':
192
+ return cmd.listUsers(client, flags);
193
+ case 'me':
194
+ return cmd.me(client, flags);
195
+ case 'get':
196
+ return cmd.getUser(client, needFirst('get'));
197
+ case 'create':
198
+ case 'new':
199
+ return cmd.createUser(client, flags);
200
+ case 'update':
201
+ case 'edit':
202
+ return cmd.updateUser(client, needFirst('update'), flags);
203
+ case 'delete':
204
+ case 'rm':
205
+ return cmd.deleteUser(client, needFirst('delete'), flags);
206
+ default:
207
+ throw new UsageError(`users 未知子命令:${action}`);
208
+ }
209
+ case 'menus':
210
+ case 'menu':
211
+ switch (action) {
212
+ case undefined:
213
+ case 'list':
214
+ return cmd.listMenus(client, flags);
215
+ case 'get':
216
+ return cmd.getMenu(client, needFirst('get'), flags);
217
+ case 'create':
218
+ case 'new':
219
+ return cmd.createMenu(client, flags);
220
+ case 'update':
221
+ case 'edit':
222
+ return cmd.updateMenu(client, needFirst('update'), flags);
223
+ case 'delete':
224
+ case 'rm':
225
+ return cmd.deleteMenu(client, needFirst('delete'), flags);
226
+ default:
227
+ throw new UsageError(`menus 未知子命令:${action}`);
228
+ }
229
+ case 'menu-items':
230
+ case 'menu-item':
231
+ switch (action) {
232
+ case undefined:
233
+ case 'list':
234
+ return cmd.listMenuItems(client, flags);
235
+ case 'get':
236
+ return cmd.getMenuItem(client, needFirst('get'), flags);
237
+ case 'create':
238
+ case 'new':
239
+ return cmd.createMenuItem(client, flags);
240
+ case 'update':
241
+ case 'edit':
242
+ return cmd.updateMenuItem(client, needFirst('update'), flags);
243
+ case 'delete':
244
+ case 'rm':
245
+ return cmd.deleteMenuItem(client, needFirst('delete'), flags);
246
+ default:
247
+ throw new UsageError(`menu-items 未知子命令:${action}`);
248
+ }
249
+ case 'search':
250
+ return searchMod.search(client, action, flags);
251
+ case 'settings':
252
+ switch (action) {
253
+ case undefined:
254
+ case 'list':
255
+ case 'get':
256
+ return settingsMod.getSettings(client, flags);
257
+ case 'update':
258
+ case 'set':
259
+ case 'edit':
260
+ return settingsMod.updateSettings(client, flags);
261
+ default:
262
+ throw new UsageError(`settings 未知子命令:${action}`);
263
+ }
264
+ case 'types':
265
+ switch (action) {
266
+ case undefined:
267
+ case 'list':
268
+ return introspectMod.listTypes(client, flags);
269
+ case 'get':
270
+ return introspectMod.getType(client, needFirst('get'), flags);
271
+ default:
272
+ throw new UsageError(`types 未知子命令:${action}`);
273
+ }
274
+ case 'taxonomies':
275
+ case 'taxonomy':
276
+ switch (action) {
277
+ case undefined:
278
+ case 'list':
279
+ return introspectMod.listTaxonomies(client, flags);
280
+ case 'get':
281
+ return introspectMod.getTaxonomy(client, needFirst('get'), flags);
282
+ default:
283
+ throw new UsageError(`taxonomies 未知子命令:${action}`);
284
+ }
285
+ case 'statuses':
286
+ return introspectMod.listStatuses(client, flags);
287
+ case 'revisions':
288
+ case 'revision': {
289
+ const revType = flags.type || 'posts';
290
+ switch (action) {
291
+ case undefined:
292
+ case 'list':
293
+ return revisionsMod.listRevisions(client, revType, needFirst('list'), flags);
294
+ case 'get':
295
+ if (!rest[1]) throw new UsageError('revisions get 需要 <id> <rev>');
296
+ return revisionsMod.getRevision(client, revType, rest[0], rest[1], flags);
297
+ case 'delete':
298
+ case 'rm':
299
+ if (!rest[1]) throw new UsageError('revisions delete 需要 <id> <rev>');
300
+ return revisionsMod.deleteRevision(client, revType, rest[0], rest[1], flags);
301
+ case 'restore':
302
+ if (!rest[1]) throw new UsageError('revisions restore 需要 <id> <rev>');
303
+ return revisionsMod.restoreRevision(client, revType, rest[0], rest[1], flags);
304
+ default:
305
+ throw new UsageError(`revisions 未知子命令:${action}`);
306
+ }
307
+ }
308
+ case 'blocks':
309
+ case 'block':
310
+ switch (action) {
311
+ case undefined:
312
+ case 'list':
313
+ return blocksMod.listBlocks(client, flags);
314
+ case 'get':
315
+ return blocksMod.getBlock(client, needFirst('get'), flags);
316
+ case 'create':
317
+ case 'new':
318
+ return blocksMod.createBlock(client, flags);
319
+ case 'update':
320
+ case 'edit':
321
+ return blocksMod.updateBlock(client, needFirst('update'), flags);
322
+ case 'delete':
323
+ case 'rm':
324
+ return blocksMod.deleteBlock(client, needFirst('delete'), flags);
325
+ default:
326
+ throw new UsageError(`blocks 未知子命令:${action}`);
327
+ }
328
+ case 'navigation':
329
+ switch (action) {
330
+ case undefined:
331
+ case 'list':
332
+ return navigationMod.listNavigation(client, flags);
333
+ case 'get':
334
+ return navigationMod.getNavigation(client, needFirst('get'), flags);
335
+ case 'create':
336
+ case 'new':
337
+ return navigationMod.createNavigation(client, flags);
338
+ case 'update':
339
+ case 'edit':
340
+ return navigationMod.updateNavigation(client, needFirst('update'), flags);
341
+ case 'delete':
342
+ case 'rm':
343
+ return navigationMod.deleteNavigation(client, needFirst('delete'), flags);
344
+ default:
345
+ throw new UsageError(`navigation 未知子命令:${action}`);
346
+ }
347
+ case 'templates':
348
+ case 'template':
349
+ switch (action) {
350
+ case undefined:
351
+ case 'list':
352
+ return templatesMod.listTemplates(client, flags);
353
+ case 'get':
354
+ return templatesMod.getTemplate(client, needFirst('get'), flags);
355
+ case 'update':
356
+ case 'edit':
357
+ return templatesMod.updateTemplate(client, needFirst('update'), flags);
358
+ default:
359
+ throw new UsageError(`templates 未知子命令:${action}`);
360
+ }
361
+ case 'template-parts':
362
+ case 'template-part':
363
+ switch (action) {
364
+ case undefined:
365
+ case 'list':
366
+ return templatesMod.listTemplateParts(client, flags);
367
+ case 'get':
368
+ return templatesMod.getTemplatePart(client, needFirst('get'), flags);
369
+ case 'update':
370
+ case 'edit':
371
+ return templatesMod.updateTemplatePart(client, needFirst('update'), flags);
372
+ default:
373
+ throw new UsageError(`template-parts 未知子命令:${action}`);
374
+ }
375
+ case 'global-styles':
376
+ case 'global-style':
377
+ switch (action) {
378
+ case undefined:
379
+ case 'get':
380
+ case 'list':
381
+ return globalStylesMod.getGlobalStyles(client, needFirst('get'), flags);
382
+ case 'update':
383
+ case 'set':
384
+ return globalStylesMod.updateGlobalStyles(client, needFirst('update'), flags);
385
+ default:
386
+ throw new UsageError(`global-styles 未知子命令:${action}`);
387
+ }
388
+ case 'widgets':
389
+ case 'widget':
390
+ switch (action) {
391
+ case undefined:
392
+ case 'list':
393
+ return widgetsMod.listWidgets(client, flags);
394
+ case 'get':
395
+ return widgetsMod.getWidget(client, needFirst('get'), flags);
396
+ case 'create':
397
+ case 'new':
398
+ return widgetsMod.createWidget(client, flags);
399
+ case 'update':
400
+ case 'edit':
401
+ return widgetsMod.updateWidget(client, needFirst('update'), flags);
402
+ case 'delete':
403
+ case 'rm':
404
+ return widgetsMod.deleteWidget(client, needFirst('delete'), flags);
405
+ default:
406
+ throw new UsageError(`widgets 未知子命令:${action}`);
407
+ }
408
+ case 'sidebars':
409
+ case 'sidebar':
410
+ switch (action) {
411
+ case undefined:
412
+ case 'list':
413
+ return widgetsMod.listSidebars(client, flags);
414
+ case 'get':
415
+ return widgetsMod.getSidebar(client, needFirst('get'), flags);
416
+ case 'update':
417
+ case 'edit':
418
+ return widgetsMod.updateSidebar(client, needFirst('update'), flags);
419
+ default:
420
+ throw new UsageError(`sidebars 未知子命令:${action}`);
421
+ }
422
+ case 'app-passwords':
423
+ case 'app-password': {
424
+ const user = flags.user || 'me';
425
+ switch (action) {
426
+ case undefined:
427
+ case 'list':
428
+ return appPasswordsMod.listAppPasswords(client, user, flags);
429
+ case 'create':
430
+ case 'new':
431
+ return appPasswordsMod.createAppPassword(client, user, flags);
432
+ case 'delete':
433
+ case 'rm':
434
+ return appPasswordsMod.deleteAppPassword(client, user, needFirst('delete'), flags);
435
+ default:
436
+ throw new UsageError(`app-passwords 未知子命令:${action}`);
437
+ }
438
+ }
439
+ case 'abilities':
440
+ case 'ability':
441
+ switch (action) {
442
+ case undefined:
443
+ case 'list':
444
+ return abilitiesMod.listAbilities(client, flags);
445
+ case 'get':
446
+ return abilitiesMod.getAbility(client, needFirst('get'), flags);
447
+ case 'run':
448
+ return abilitiesMod.runAbility(client, needFirst('run'), flags);
449
+ default:
450
+ throw new UsageError(`abilities 未知子命令:${action}`);
451
+ }
452
+ case 'health':
453
+ if (action === undefined || action === 'list') return healthMod.listHealth(client, flags);
454
+ return healthMod.runHealth(client, action, flags);
455
+ case 'batch':
456
+ return batchMod.batch(client, flags);
457
+ case 'plugins':
458
+ case 'plugin':
459
+ switch (action) {
460
+ case undefined:
461
+ case 'list':
462
+ return cmd.listPlugins(client, flags);
463
+ case 'install':
464
+ if (!rest[0]) {
465
+ throw new UsageError('plugins install 需要 wordpress.org 插件别名,例如 wpops plugins install akismet');
466
+ }
467
+ return cmd.installPlugin(client, rest[0], flags);
468
+ case 'activate':
469
+ return cmd.setPluginStatus(client, needFirst('activate'), true, flags);
470
+ case 'deactivate':
471
+ return cmd.setPluginStatus(client, needFirst('deactivate'), false, flags);
472
+ case 'delete':
473
+ case 'rm':
474
+ return cmd.deletePlugin(client, needFirst('delete'), flags);
475
+ default:
476
+ throw new UsageError(`plugins 未知子命令:${action}`);
477
+ }
478
+ case 'themes':
479
+ case 'theme':
480
+ switch (action) {
481
+ case undefined:
482
+ case 'list':
483
+ return cmd.listThemes(client, flags);
484
+ case 'activate':
485
+ case 'switch':
486
+ return cmd.activateTheme(client, needFirst('activate'), flags);
487
+ default:
488
+ throw new UsageError(`themes 未知子命令:${action}`);
489
+ }
490
+ case 'raw':
491
+ if (!action || !rest[0]) throw new UsageError('raw 用法:wpops raw <METHOD> <path> [--data JSON]');
492
+ return cmd.raw(client, action, rest[0], flags);
493
+ default:
494
+ throw new UsageError(`未知命令:${group}(运行 wpops help 查看用法)`);
495
+ }
496
+ }
497
+
498
+ /** 读取站点配置、建 client,再分发(--timeout/--retries 可覆盖)。 */
499
+ export async function runOne(site, group, action, rest, flags, showHeader) {
500
+ const cfg = buildConfig(loadEnv(site));
501
+ if (flags.timeout !== undefined) cfg.timeoutMs = Number(flags.timeout);
502
+ if (flags.retries !== undefined) cfg.retries = Number(flags.retries);
503
+ const client = createClient(cfg);
504
+
505
+ if (showHeader) {
506
+ console.log(`\n=== ${site || '(default)'} — ${cfg.url || '(未配置 WP_URL)'} ===`);
507
+ }
508
+ return dispatch(client, cfg, group, action, rest, flags);
509
+ }
package/lib/search.js ADDED
@@ -0,0 +1,31 @@
1
+ // 跨内容搜索:调用核心 /wp/v2/search 端点。
2
+ // 与 commands.js 保持一致的输出约定(--json / 中文人类可读)。
3
+
4
+ import { stripTags, printJson } from './util.js';
5
+ import { UsageError } from './errors.js';
6
+
7
+ const SEARCH = '/wp-json/wp/v2/search';
8
+
9
+ export async function search(client, term, flags) {
10
+ const keyword = term || flags.search;
11
+ if (!keyword) {
12
+ throw new UsageError('search 需要一个搜索词:wpops search <词> [--type post]');
13
+ }
14
+
15
+ const query = {
16
+ search: keyword,
17
+ type: flags.type,
18
+ subtype: flags.subtype,
19
+ per_page: flags['per-page'] ?? 10,
20
+ page: flags.page,
21
+ };
22
+ const { data } = await client.request('GET', SEARCH, { query });
23
+
24
+ if (flags.json) return printJson(data);
25
+
26
+ const items = Array.isArray(data) ? data : [];
27
+ console.log(`搜索 "${keyword}" 共 ${items.length} 条:`);
28
+ for (const item of items) {
29
+ console.log(` #${item.id} [${item.subtype}] ${stripTags(item.title || '')} ${item.url}`);
30
+ }
31
+ }
@@ -0,0 +1,74 @@
1
+ // 站点设置:读取 / 更新核心 /wp/v2/settings 端点。
2
+ // flag 名(- 分隔)映射到 REST 字段(_ 分隔),数值字段自动转 Number。
3
+
4
+ import { printJson, dry, parseJson, readJsonFile } from './util.js';
5
+ import { UsageError } from './errors.js';
6
+
7
+ const SETTINGS = '/wp-json/wp/v2/settings';
8
+
9
+ // 顺序即帮助/干跑输出顺序。
10
+ const SETTINGS_FIELDS = {
11
+ title: 'title',
12
+ description: 'description',
13
+ url: 'url',
14
+ email: 'email',
15
+ timezone: 'timezone',
16
+ 'date-format': 'date_format',
17
+ 'time-format': 'time_format',
18
+ 'start-of-week': 'start_of_week',
19
+ language: 'language',
20
+ 'posts-per-page': 'posts_per_page',
21
+ 'show-on-front': 'show_on_front',
22
+ 'page-on-front': 'page_on_front',
23
+ 'page-for-posts': 'page_for_posts',
24
+ 'default-category': 'default_category',
25
+ 'default-post-format': 'default_post_format',
26
+ 'default-ping-status': 'default_ping_status',
27
+ 'default-comment-status': 'default_comment_status',
28
+ };
29
+
30
+ const NUMERIC_FIELDS = new Set([
31
+ 'posts_per_page',
32
+ 'start_of_week',
33
+ 'page_on_front',
34
+ 'page_for_posts',
35
+ 'default_category',
36
+ ]);
37
+
38
+ export async function getSettings(client, flags) {
39
+ const { data } = await client.request('GET', SETTINGS);
40
+
41
+ if (flags.json) return printJson(data);
42
+
43
+ for (const [key, value] of Object.entries(data || {})) {
44
+ const shown = value !== null && typeof value === 'object' ? JSON.stringify(value) : value;
45
+ console.log(` ${key}: ${shown}`);
46
+ }
47
+ }
48
+
49
+ export async function updateSettings(client, flags) {
50
+ let body;
51
+ if (flags.data !== undefined) {
52
+ body = parseJson(flags.data);
53
+ } else if (flags['from-file'] !== undefined) {
54
+ body = readJsonFile(flags['from-file']);
55
+ } else {
56
+ body = {};
57
+ for (const [flagName, restName] of Object.entries(SETTINGS_FIELDS)) {
58
+ if (flags[flagName] === undefined) continue;
59
+ body[restName] = NUMERIC_FIELDS.has(restName)
60
+ ? Number(flags[flagName])
61
+ : String(flags[flagName]);
62
+ }
63
+ }
64
+
65
+ if (!body || !Object.keys(body).length) {
66
+ throw new UsageError('没有可更新的字段(--title/--description/--timezone/--posts-per-page/... 或 --data JSON)');
67
+ }
68
+
69
+ if (flags['dry-run']) return dry(`更新站点设置 ${JSON.stringify(body)}`);
70
+
71
+ const { data } = await client.request('POST', SETTINGS, { body });
72
+ if (flags.json) return printJson(data);
73
+ console.log(`✓ 已更新站点设置(${Object.keys(body).length} 项)`);
74
+ }