wpops 0.1.1 → 0.3.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 +109 -10
- 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 +380 -88
- 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 +517 -0
- package/lib/search.js +31 -0
- package/lib/settings.js +75 -0
- package/lib/spec.js +405 -0
- package/lib/templates.js +69 -0
- package/lib/util.js +149 -0
- package/lib/widgets.js +93 -0
- package/package.json +3 -2
- package/skills/wpops/SKILL.md +59 -36
package/lib/commands.js
CHANGED
|
@@ -2,7 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { loadEnv, configHome } from './env.js';
|
|
5
|
-
import { buildConfig, createClient } from './client.js';
|
|
5
|
+
import { buildConfig, createClient, WpError } from './client.js';
|
|
6
|
+
import { stripTags, printJson, dry, warn, pick, listRequest, parseJson, readJsonFile } from './util.js';
|
|
7
|
+
import { UsageError } from './errors.js';
|
|
6
8
|
|
|
7
9
|
const CONTENT = {
|
|
8
10
|
posts: '/wp-json/wp/v2/posts',
|
|
@@ -14,37 +16,29 @@ const TAX = {
|
|
|
14
16
|
tags: '/wp-json/wp/v2/tags',
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function stripTags(html = '') {
|
|
22
|
-
return String(html).replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
19
|
+
/** 内置类型走映射;其余按 rest_base 直接拼,支持任意 show_in_rest 的 CPT。 */
|
|
20
|
+
function contentPath(type) {
|
|
21
|
+
return CONTENT[type] || `/wp-json/wp/v2/${String(type).replace(/^\/+/, '')}`;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
function
|
|
26
|
-
|
|
24
|
+
function taxPath(tax) {
|
|
25
|
+
return TAX[tax] || `/wp-json/wp/v2/${String(tax).replace(/^\/+/, '')}`;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const MEDIA = '/wp-json/wp/v2/media';
|
|
29
|
+
const COMMENTS = '/wp-json/wp/v2/comments';
|
|
30
|
+
const USERS = '/wp-json/wp/v2/users';
|
|
31
|
+
const MENUS = '/wp-json/wp/v2/menus';
|
|
32
|
+
const MENU_ITEMS = '/wp-json/wp/v2/menu-items';
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
/** 内容写操作的结果输出:草稿/待审等非公开状态额外回显后台编辑(预览)链接。 */
|
|
35
|
+
function printItem(data, flags, client) {
|
|
34
36
|
if (flags.json) return printJson(data);
|
|
35
37
|
console.log(`✓ #${data.id} [${data.status}] ${stripTags(data.title?.rendered || '')}`);
|
|
36
|
-
if (data.link) console.log(`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/** 把 flags 中的指定键转成 REST 查询参数(- 转 _)。 */
|
|
40
|
-
function pick(flags, keys) {
|
|
41
|
-
const query = {};
|
|
42
|
-
for (const key of keys) {
|
|
43
|
-
const value = flags[key];
|
|
44
|
-
if (value === undefined || value === null || value === false) continue;
|
|
45
|
-
query[key.replace(/-/g, '_')] = String(value);
|
|
38
|
+
if (data.link) console.log(` 链接:${data.link}`);
|
|
39
|
+
if (data.status && data.status !== 'publish' && client?.root) {
|
|
40
|
+
console.log(` 编辑/预览:${client.root}/wp-admin/post.php?post=${Number(data.id)}&action=edit`);
|
|
46
41
|
}
|
|
47
|
-
return query;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
// ------------------------------------------------------------------ doctor
|
|
@@ -216,10 +210,8 @@ export async function listContent(client, type, flags) {
|
|
|
216
210
|
context: flags.context,
|
|
217
211
|
...pick(flags, CONTENT_FILTERS[type] || []),
|
|
218
212
|
};
|
|
219
|
-
const { data,
|
|
213
|
+
const { data, total, totalPages } = await listRequest(client, contentPath(type), query, flags);
|
|
220
214
|
if (flags.json) return printJson(data);
|
|
221
|
-
const total = headers.get('x-wp-total');
|
|
222
|
-
const totalPages = headers.get('x-wp-totalpages');
|
|
223
215
|
console.log(
|
|
224
216
|
`${type} 共 ${total ?? '?'} 条 / ${totalPages ?? '?'} 页;第 ${page} 页${totalPages ? `/${totalPages}` : ''},本页 ${data.length} 条:`,
|
|
225
217
|
);
|
|
@@ -232,7 +224,7 @@ export async function listContent(client, type, flags) {
|
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
export async function getContent(client, type, id, flags) {
|
|
235
|
-
const { data } = await client.request('GET', `${
|
|
227
|
+
const { data } = await client.request('GET', `${contentPath(type)}/${Number(id)}`, {
|
|
236
228
|
query: { context: flags.context || 'edit' },
|
|
237
229
|
});
|
|
238
230
|
if (flags.json) return printJson(data);
|
|
@@ -265,24 +257,68 @@ function contentBody(flags) {
|
|
|
265
257
|
export async function createContent(client, type, flags) {
|
|
266
258
|
const body = contentBody(flags);
|
|
267
259
|
if (body.title === undefined && body.content === undefined) {
|
|
268
|
-
throw new
|
|
260
|
+
throw new UsageError('至少需要 --title 或 --content/--from-file');
|
|
269
261
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
262
|
+
// 未显式指定状态时按草稿创建,不依赖服务端默认(避免"直接发布")。
|
|
263
|
+
if (body.status === undefined) body.status = 'draft';
|
|
264
|
+
if (flags['dry-run']) return dry(`将创建 ${type}(status=${body.status}) ${JSON.stringify(body)}`);
|
|
265
|
+
const { data } = await client.request('POST', contentPath(type), { body });
|
|
266
|
+
printItem(data, flags, client);
|
|
273
267
|
}
|
|
274
268
|
|
|
275
269
|
export async function updateContent(client, type, id, flags) {
|
|
276
270
|
const body = contentBody(flags);
|
|
277
|
-
if (!Object.keys(body).length) throw new
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
271
|
+
if (!Object.keys(body).length) throw new UsageError('没有可更新的字段');
|
|
272
|
+
// 修改已发布内容的正文(未显式改状态)会立即影响线上:先提示可回滚(--yes 跳过预检与提示)。
|
|
273
|
+
if (body.status === undefined && !flags.yes) {
|
|
274
|
+
try {
|
|
275
|
+
const { data: current } = await client.request('GET', `${contentPath(type)}/${Number(id)}`, {
|
|
276
|
+
query: { context: 'edit' },
|
|
277
|
+
});
|
|
278
|
+
if (current.status === 'publish') {
|
|
279
|
+
warn(
|
|
280
|
+
`正在修改已发布内容 ${type} #${id};已生成修订,回滚:wpops revisions restore ${id} <rev> --type ${type}`,
|
|
281
|
+
flags,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// 预检失败(权限/网络)不阻断写入,也不提示。
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (flags['dry-run']) return dry(`将更新 ${type} #${id} ${JSON.stringify(body)}`);
|
|
289
|
+
const { data } = await client.request('POST', `${contentPath(type)}/${Number(id)}`, { body });
|
|
290
|
+
printItem(data, flags, client);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 显式发布 / 下线为草稿。 */
|
|
294
|
+
export async function setContentStatus(client, type, id, status, flags) {
|
|
295
|
+
const label = status === 'publish' ? '发布' : '下线为草稿';
|
|
296
|
+
if (flags['dry-run']) return dry(`将${label} ${type} #${id}`);
|
|
297
|
+
const { data } = await client.request('POST', `${contentPath(type)}/${Number(id)}`, { body: { status } });
|
|
298
|
+
if (flags.json) return printJson(data);
|
|
299
|
+
console.log(`✓ #${data.id} → ${data.status} ${data.link || ''}`);
|
|
300
|
+
if (data.status !== 'publish' && client.root) {
|
|
301
|
+
console.log(` 编辑/预览:${client.root}/wp-admin/post.php?post=${Number(data.id)}&action=edit`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 导出原始正文 content.raw 到文件(或 stdout),便于本地改排版后再 `update --from-file`。 */
|
|
306
|
+
export async function exportContent(client, type, id, flags) {
|
|
307
|
+
const { data } = await client.request('GET', `${contentPath(type)}/${Number(id)}`, {
|
|
308
|
+
query: { context: 'edit' },
|
|
309
|
+
});
|
|
310
|
+
const raw = data.content?.raw ?? stripTags(data.content?.rendered || '');
|
|
311
|
+
if (flags.file !== undefined) {
|
|
312
|
+
writeFileSync(String(flags.file), raw, 'utf8');
|
|
313
|
+
console.log(`✓ 已导出 ${type} #${id} → ${flags.file}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
process.stdout.write(raw.endsWith('\n') ? raw : `${raw}\n`);
|
|
281
317
|
}
|
|
282
318
|
|
|
283
319
|
export async function deleteContent(client, type, id, flags) {
|
|
284
320
|
if (flags['dry-run']) return dry(`删除 ${type} #${id}${flags.force ? '(彻底)' : '(回收站)'}`);
|
|
285
|
-
const { data } = await client.request('DELETE', `${
|
|
321
|
+
const { data } = await client.request('DELETE', `${contentPath(type)}/${Number(id)}`, {
|
|
286
322
|
query: { force: flags.force ? 'true' : undefined },
|
|
287
323
|
});
|
|
288
324
|
if (flags.json) return printJson(data);
|
|
@@ -299,10 +335,8 @@ export async function listMedia(client, flags) {
|
|
|
299
335
|
page,
|
|
300
336
|
...pick(flags, ['media_type', 'mime_type', 'parent', 'author', 'after', 'before', 'orderby', 'order', 'offset', 'include', 'exclude']),
|
|
301
337
|
};
|
|
302
|
-
const { data,
|
|
338
|
+
const { data, total, totalPages } = await listRequest(client, MEDIA, query, flags);
|
|
303
339
|
if (flags.json) return printJson(data);
|
|
304
|
-
const total = headers.get('x-wp-total');
|
|
305
|
-
const totalPages = headers.get('x-wp-totalpages');
|
|
306
340
|
console.log(
|
|
307
341
|
`媒体共 ${total ?? '?'} 项 / ${totalPages ?? '?'} 页;第 ${page} 页${totalPages ? `/${totalPages}` : ''},本页 ${data.length} 项:`,
|
|
308
342
|
);
|
|
@@ -344,7 +378,7 @@ export async function updateMedia(client, id, flags) {
|
|
|
344
378
|
if (flags.description !== undefined) body.description = String(flags.description);
|
|
345
379
|
if (flags.post !== undefined) body.post = Number(flags.post);
|
|
346
380
|
if (!Object.keys(body).length) {
|
|
347
|
-
throw new
|
|
381
|
+
throw new UsageError('没有可更新的字段(--title/--alt/--caption/--description/--post)');
|
|
348
382
|
}
|
|
349
383
|
if (flags['dry-run']) return dry(`更新媒体 #${id} ${JSON.stringify(body)}`);
|
|
350
384
|
const { data } = await client.request('POST', `${MEDIA}/${Number(id)}`, { body });
|
|
@@ -352,6 +386,56 @@ export async function updateMedia(client, id, flags) {
|
|
|
352
386
|
console.log(`✓ 已更新媒体 #${data.id}`);
|
|
353
387
|
}
|
|
354
388
|
|
|
389
|
+
/** 图像编辑(裁剪/旋转),POST /media/<id>/edit。缺 --src 时自动取当前 source_url。 */
|
|
390
|
+
export async function editMediaImage(client, id, flags) {
|
|
391
|
+
const body = {};
|
|
392
|
+
if (flags.src !== undefined) body.src = String(flags.src);
|
|
393
|
+
if (flags.rotation !== undefined) body.rotation = Number(flags.rotation);
|
|
394
|
+
if (flags['dest-width'] !== undefined) body.width = Number(flags['dest-width']);
|
|
395
|
+
if (flags['dest-height'] !== undefined) body.height = Number(flags['dest-height']);
|
|
396
|
+
if (flags.x !== undefined) body.x = Number(flags.x);
|
|
397
|
+
if (flags.y !== undefined) body.y = Number(flags.y);
|
|
398
|
+
const modifiers = [];
|
|
399
|
+
if (flags.crop) modifiers.push('crop');
|
|
400
|
+
if (flags.modifiers !== undefined) {
|
|
401
|
+
modifiers.push(...String(flags.modifiers).split(',').map((s) => s.trim()).filter(Boolean));
|
|
402
|
+
}
|
|
403
|
+
if (modifiers.length) body.modifiers = modifiers;
|
|
404
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
405
|
+
if (flags.alt !== undefined) body.alt_text = String(flags.alt);
|
|
406
|
+
if (flags.caption !== undefined) body.caption = String(flags.caption);
|
|
407
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
408
|
+
if (flags.post !== undefined) body.post = Number(flags.post);
|
|
409
|
+
if (!Object.keys(body).length) {
|
|
410
|
+
throw new UsageError('图像编辑需要至少一个参数(--rotation/--crop/--x/--y/--dest-width/--dest-height/--modifiers/--src/...)');
|
|
411
|
+
}
|
|
412
|
+
if (flags['dry-run']) return dry(`编辑媒体 #${id} ${JSON.stringify(body)}`);
|
|
413
|
+
if (body.src === undefined) {
|
|
414
|
+
const { data } = await client.request('GET', `${MEDIA}/${Number(id)}`, { query: { context: 'edit' } });
|
|
415
|
+
body.src = data.source_url;
|
|
416
|
+
}
|
|
417
|
+
if (!body.src) throw new UsageError('无法确定图片来源,请显式传 --src <url>');
|
|
418
|
+
const { data } = await client.request('POST', `${MEDIA}/${Number(id)}/edit`, { body });
|
|
419
|
+
if (flags.json) return printJson(data);
|
|
420
|
+
console.log(`✓ 已编辑媒体 #${data.id} ${data.source_url || ''}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 从 URL 导入媒体:sideload,POST /media(带 url)。 */
|
|
424
|
+
export async function sideloadMedia(client, flags) {
|
|
425
|
+
if (flags.url === undefined) throw new UsageError('media sideload 需要 --url <图片地址>');
|
|
426
|
+
const body = { url: String(flags.url) };
|
|
427
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
428
|
+
if (flags.alt !== undefined) body.alt_text = String(flags.alt);
|
|
429
|
+
if (flags.caption !== undefined) body.caption = String(flags.caption);
|
|
430
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
431
|
+
if (flags.slug !== undefined) body.slug = String(flags.slug);
|
|
432
|
+
if (flags.post !== undefined) body.post = Number(flags.post);
|
|
433
|
+
if (flags['dry-run']) return dry(`从 URL 导入媒体 ${body.url}`);
|
|
434
|
+
const { data } = await client.request('POST', MEDIA, { body });
|
|
435
|
+
if (flags.json) return printJson(data);
|
|
436
|
+
console.log(`✓ 已导入媒体 #${data.id} ${data.source_url || ''}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
355
439
|
export async function deleteMedia(client, id, flags) {
|
|
356
440
|
if (flags['dry-run']) return dry(`删除媒体 #${id}${flags.force ? '(彻底)' : '(回收站)'}`);
|
|
357
441
|
const { data } = await client.request('DELETE', `${MEDIA}/${Number(id)}`, {
|
|
@@ -392,6 +476,14 @@ export async function setPluginStatus(client, plugin, activate, flags) {
|
|
|
392
476
|
console.log(`✓ ${data.name} → ${data.status === 'active' ? '已启用' : '已停用'}`);
|
|
393
477
|
}
|
|
394
478
|
|
|
479
|
+
export async function deletePlugin(client, plugin, flags) {
|
|
480
|
+
if (flags['dry-run']) return dry(`删除插件 ${plugin}`);
|
|
481
|
+
if (!flags.force) throw new UsageError('删除插件需要 --force(不可撤销)');
|
|
482
|
+
const { data } = await client.request('DELETE', `/wp-json/wp/v2/plugins/${encodeURIComponent(plugin)}`);
|
|
483
|
+
if (flags.json) return printJson(data);
|
|
484
|
+
console.log(data.deleted ? `✓ 已删除插件 ${plugin}` : `? 插件 ${plugin} 未删除`);
|
|
485
|
+
}
|
|
486
|
+
|
|
395
487
|
// ------------------------------------------------------------------ themes
|
|
396
488
|
|
|
397
489
|
export async function listThemes(client, flags) {
|
|
@@ -415,26 +507,25 @@ export async function activateTheme(client, stylesheet, flags) {
|
|
|
415
507
|
// ------------------------------------------------------------------ taxonomy
|
|
416
508
|
|
|
417
509
|
export async function listTaxonomy(client, tax, flags) {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
510
|
+
const query = {
|
|
511
|
+
per_page: flags['per-page'] ?? 100,
|
|
512
|
+
page: flags.page,
|
|
513
|
+
search: flags.search,
|
|
514
|
+
orderby: flags.orderby,
|
|
515
|
+
order: flags.order,
|
|
516
|
+
parent: flags.parent,
|
|
517
|
+
hide_empty: flags['hide-empty'] ? 'true' : 'false',
|
|
518
|
+
};
|
|
519
|
+
const { data, total } = await listRequest(client, taxPath(tax), query, flags);
|
|
429
520
|
if (flags.json) return printJson(data);
|
|
430
|
-
console.log(`${tax} 共 ${
|
|
521
|
+
console.log(`${tax} 共 ${total ?? '?'} 个:`);
|
|
431
522
|
for (const t of data) {
|
|
432
523
|
console.log(` #${t.id} ${t.name} (${t.count}) slug=${t.slug}${t.parent ? ` parent=#${t.parent}` : ''}`);
|
|
433
524
|
}
|
|
434
525
|
}
|
|
435
526
|
|
|
436
527
|
export async function getTaxonomy(client, tax, id) {
|
|
437
|
-
const { data } = await client.request('GET', `${
|
|
528
|
+
const { data } = await client.request('GET', `${taxPath(tax)}/${Number(id)}`);
|
|
438
529
|
printJson(data);
|
|
439
530
|
}
|
|
440
531
|
|
|
@@ -449,42 +540,224 @@ function taxonomyBody(flags) {
|
|
|
449
540
|
|
|
450
541
|
export async function createTaxonomy(client, tax, flags) {
|
|
451
542
|
const body = taxonomyBody(flags);
|
|
452
|
-
if (body.name === undefined) throw new
|
|
543
|
+
if (body.name === undefined) throw new UsageError(`创建 ${tax} 需要 --name`);
|
|
453
544
|
if (flags['dry-run']) return dry(`创建 ${tax} ${JSON.stringify(body)}`);
|
|
454
|
-
const { data } = await client.request('POST',
|
|
545
|
+
const { data } = await client.request('POST', taxPath(tax), { body });
|
|
455
546
|
if (flags.json) return printJson(data);
|
|
456
547
|
console.log(`✓ 已创建 ${tax} #${data.id} ${data.name}`);
|
|
457
548
|
}
|
|
458
549
|
|
|
459
550
|
export async function updateTaxonomy(client, tax, id, flags) {
|
|
460
551
|
const body = taxonomyBody(flags);
|
|
461
|
-
if (!Object.keys(body).length) throw new
|
|
552
|
+
if (!Object.keys(body).length) throw new UsageError('没有可更新的字段(--name/--slug/--parent/--description)');
|
|
462
553
|
if (flags['dry-run']) return dry(`更新 ${tax} #${id} ${JSON.stringify(body)}`);
|
|
463
|
-
const { data } = await client.request('POST', `${
|
|
554
|
+
const { data } = await client.request('POST', `${taxPath(tax)}/${Number(id)}`, { body });
|
|
464
555
|
if (flags.json) return printJson(data);
|
|
465
556
|
console.log(`✓ 已更新 ${tax} #${data.id} ${data.name}`);
|
|
466
557
|
}
|
|
467
558
|
|
|
468
559
|
export async function deleteTaxonomy(client, tax, id, flags) {
|
|
469
|
-
if (!flags.force) throw new Error(`删除 ${tax} 需要 --force(不可撤销)`);
|
|
470
560
|
if (flags['dry-run']) return dry(`删除 ${tax} #${id}`);
|
|
471
|
-
|
|
561
|
+
if (!flags.force) throw new UsageError(`删除 ${tax} 需要 --force(不可撤销)`);
|
|
562
|
+
const { data } = await client.request('DELETE', `${taxPath(tax)}/${Number(id)}`, { query: { force: 'true' } });
|
|
472
563
|
if (flags.json) return printJson(data);
|
|
473
564
|
console.log(data.deleted ? `✓ 已删除 ${tax} #${id}` : `? ${tax} #${id} 未删除`);
|
|
474
565
|
}
|
|
475
566
|
|
|
567
|
+
// ------------------------------------------------------------------ menus
|
|
568
|
+
|
|
569
|
+
const MENU_PLUGIN_HINT =
|
|
570
|
+
'菜单接口由插件提供(WordPress 核心 REST 没有 /menus、/menu-items);请确认站点已装并启用对应插件。';
|
|
571
|
+
|
|
572
|
+
/** 菜单端点请求:路由不存在时补充"需插件"的提示,便于排障。 */
|
|
573
|
+
async function menuRequest(client, method, path, opts) {
|
|
574
|
+
try {
|
|
575
|
+
return await client.request(method, path, opts);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
if (err?.code === 'rest_no_route') {
|
|
578
|
+
throw new WpError(`${err.message} —— ${MENU_PLUGIN_HINT}`, {
|
|
579
|
+
status: err.status,
|
|
580
|
+
code: err.code,
|
|
581
|
+
data: err.data,
|
|
582
|
+
method: err.method,
|
|
583
|
+
path: err.path,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export async function listMenus(client, flags) {
|
|
591
|
+
const { data, headers } = await menuRequest(client, 'GET', MENUS, {
|
|
592
|
+
query: { per_page: flags['per-page'] ?? 100, page: flags.page },
|
|
593
|
+
});
|
|
594
|
+
if (flags.json) return printJson(data);
|
|
595
|
+
console.log(`菜单共 ${headers.get('x-wp-total') ?? data.length} 个:`);
|
|
596
|
+
for (const m of data) {
|
|
597
|
+
console.log(` #${m.id} ${m.name} slug=${m.slug} locations=${(m.locations || []).join(',') || '—'}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function getMenu(client, id, flags) {
|
|
602
|
+
const { data } = await menuRequest(client, 'GET', `${MENUS}/${Number(id)}`);
|
|
603
|
+
if (flags.json) return printJson(data);
|
|
604
|
+
console.log(`#${data.id} ${data.name} slug=${data.slug}`);
|
|
605
|
+
console.log(` 位置:${(data.locations || []).join(', ') || '—'}`);
|
|
606
|
+
console.log(` 自动添加:${data.auto_add ? '是' : '否'}`);
|
|
607
|
+
if (data.description) console.log(` 描述:${data.description}`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** 菜单字段转请求体(locations/auto_add 用于给菜单绑定主题位置)。 */
|
|
611
|
+
function menuBody(flags) {
|
|
612
|
+
const body = {};
|
|
613
|
+
if (flags.name !== undefined) body.name = String(flags.name);
|
|
614
|
+
if (flags.slug !== undefined) body.slug = String(flags.slug);
|
|
615
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
616
|
+
if (flags.locations !== undefined) {
|
|
617
|
+
body.locations = String(flags.locations).split(',').map((s) => s.trim()).filter(Boolean);
|
|
618
|
+
}
|
|
619
|
+
if (flags['auto-add'] !== undefined) body.auto_add = Boolean(flags['auto-add']);
|
|
620
|
+
return body;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export async function createMenu(client, flags) {
|
|
624
|
+
const body = menuBody(flags);
|
|
625
|
+
if (body.name === undefined) throw new UsageError('创建菜单需要 --name');
|
|
626
|
+
if (flags['dry-run']) return dry(`创建菜单 ${JSON.stringify(body)}`);
|
|
627
|
+
const { data } = await menuRequest(client, 'POST', MENUS, { body });
|
|
628
|
+
if (flags.json) return printJson(data);
|
|
629
|
+
console.log(`✓ 已创建菜单 #${data.id} ${data.name}`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export async function updateMenu(client, id, flags) {
|
|
633
|
+
const body = menuBody(flags);
|
|
634
|
+
if (!Object.keys(body).length) {
|
|
635
|
+
throw new UsageError('没有可更新的字段(--name/--slug/--description/--locations/--auto-add)');
|
|
636
|
+
}
|
|
637
|
+
if (body.locations !== undefined) {
|
|
638
|
+
warn('正在修改菜单位置绑定,会立即影响站点导航', flags);
|
|
639
|
+
}
|
|
640
|
+
if (flags['dry-run']) return dry(`更新菜单 #${id} ${JSON.stringify(body)}`);
|
|
641
|
+
const { data } = await menuRequest(client, 'POST', `${MENUS}/${Number(id)}`, { body });
|
|
642
|
+
if (flags.json) return printJson(data);
|
|
643
|
+
console.log(`✓ 已更新菜单 #${data.id} ${data.name} 位置:${(data.locations || []).join(',') || '—'}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export async function deleteMenu(client, id, flags) {
|
|
647
|
+
warn('删除菜单会影响站点导航', flags);
|
|
648
|
+
if (flags['dry-run']) return dry(`删除菜单 #${id}`);
|
|
649
|
+
if (!flags.force) throw new UsageError('删除菜单需要 --force(不可撤销)');
|
|
650
|
+
const { data } = await menuRequest(client, 'DELETE', `${MENUS}/${Number(id)}`, { query: { force: 'true' } });
|
|
651
|
+
if (flags.json) return printJson(data);
|
|
652
|
+
console.log(data.deleted ? `✓ 已删除菜单 #${id}` : `? 菜单 #${id} 未删除`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** 把 flags 中的菜单项字段转成 REST 请求体(注意数值/数组类型)。 */
|
|
656
|
+
function menuItemBody(flags) {
|
|
657
|
+
const body = {};
|
|
658
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
659
|
+
if (flags.url !== undefined) body.url = String(flags.url);
|
|
660
|
+
if (flags.menus !== undefined) body.menus = Number(flags.menus);
|
|
661
|
+
if (flags['menu-order'] !== undefined) body.menu_order = Number(flags['menu-order']);
|
|
662
|
+
if (flags.parent !== undefined) body.parent = Number(flags.parent);
|
|
663
|
+
if (flags.status !== undefined) body.status = String(flags.status);
|
|
664
|
+
if (flags.type !== undefined) body.type = String(flags.type);
|
|
665
|
+
if (flags.object !== undefined) body.object = String(flags.object);
|
|
666
|
+
if (flags['object-id'] !== undefined) body.object_id = Number(flags['object-id']);
|
|
667
|
+
if (flags.target !== undefined) body.target = String(flags.target);
|
|
668
|
+
if (flags.classes !== undefined) {
|
|
669
|
+
body.classes = String(flags.classes).split(',').map((s) => s.trim()).filter(Boolean);
|
|
670
|
+
}
|
|
671
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
672
|
+
if (flags['attr-title'] !== undefined) body.attr_title = String(flags['attr-title']);
|
|
673
|
+
if (flags.xfn !== undefined) body.xfn = String(flags.xfn).split(',').map((s) => s.trim()).filter(Boolean);
|
|
674
|
+
return body;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export async function listMenuItems(client, flags) {
|
|
678
|
+
const perPage = Number(flags['per-page'] ?? 100) || 100;
|
|
679
|
+
const page = Number(flags.page ?? 1) || 1;
|
|
680
|
+
const query = {
|
|
681
|
+
per_page: perPage,
|
|
682
|
+
page,
|
|
683
|
+
...pick(flags, ['menus', 'search', 'status', 'orderby', 'order', 'menu-order']),
|
|
684
|
+
};
|
|
685
|
+
const { data, total, totalPages } = await listRequest(
|
|
686
|
+
client,
|
|
687
|
+
MENU_ITEMS,
|
|
688
|
+
query,
|
|
689
|
+
flags,
|
|
690
|
+
(method, path, opts) => menuRequest(client, method, path, opts),
|
|
691
|
+
);
|
|
692
|
+
if (flags.json) return printJson(data);
|
|
693
|
+
console.log(
|
|
694
|
+
`菜单项共 ${total ?? '?'} 项 / ${totalPages ?? '?'} 页;第 ${page} 页${totalPages ? `/${totalPages}` : ''},本页 ${data.length} 项:`,
|
|
695
|
+
);
|
|
696
|
+
for (const item of data) {
|
|
697
|
+
const title = stripTags(item.title?.rendered || '(无标题)');
|
|
698
|
+
console.log(
|
|
699
|
+
` #${item.id} [${item.status}] order=${item.menu_order} parent=${item.parent} ${item.type}/${item.object} ${title} ${item.url}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (totalPages && page < Number(totalPages)) {
|
|
703
|
+
console.log(` …下一页:wpops menu-items list --page ${page + 1} --per-page ${perPage}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export async function getMenuItem(client, id, flags) {
|
|
708
|
+
const { data } = await menuRequest(client, 'GET', `${MENU_ITEMS}/${Number(id)}`);
|
|
709
|
+
if (flags.json) return printJson(data);
|
|
710
|
+
console.log(`#${data.id} [${data.status}] ${stripTags(data.title?.rendered || '')}`);
|
|
711
|
+
console.log(` 顺序:${data.menu_order} 父级:#${data.parent} 类型:${data.type}/${data.object} object_id=${data.object_id}`);
|
|
712
|
+
console.log(` 链接:${data.url}`);
|
|
713
|
+
if (data.attr_title) console.log(` attr_title:${data.attr_title}`);
|
|
714
|
+
if (data.target) console.log(` target:${data.target}`);
|
|
715
|
+
if (data.classes?.filter(Boolean).length) console.log(` classes:${data.classes.filter(Boolean).join(',')}`);
|
|
716
|
+
if (data.description) console.log(` 描述:${data.description}`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export async function createMenuItem(client, flags) {
|
|
720
|
+
const body = menuItemBody(flags);
|
|
721
|
+
if (body.title === undefined) throw new UsageError('创建菜单项需要 --title');
|
|
722
|
+
if (body.url === undefined && body.object === undefined) {
|
|
723
|
+
throw new UsageError('创建菜单项需要 --url(自定义链接)或 --object/--object-id(指向现有内容)');
|
|
724
|
+
}
|
|
725
|
+
if (flags['dry-run']) return dry(`创建菜单项 ${JSON.stringify(body)}`);
|
|
726
|
+
const { data } = await menuRequest(client, 'POST', MENU_ITEMS, { body });
|
|
727
|
+
if (flags.json) return printJson(data);
|
|
728
|
+
console.log(`✓ 已创建菜单项 #${data.id} [${data.status}] ${stripTags(data.title?.rendered || '')}`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export async function updateMenuItem(client, id, flags) {
|
|
732
|
+
const body = menuItemBody(flags);
|
|
733
|
+
if (!Object.keys(body).length) {
|
|
734
|
+
throw new UsageError('没有可更新的字段(--title/--url/--menus/--menu-order/--parent/--status/--type/--object/--object-id/--target/--classes/--description/--attr-title)');
|
|
735
|
+
}
|
|
736
|
+
if (flags['dry-run']) return dry(`更新菜单项 #${id} ${JSON.stringify(body)}`);
|
|
737
|
+
const { data } = await menuRequest(client, 'POST', `${MENU_ITEMS}/${Number(id)}`, { body });
|
|
738
|
+
if (flags.json) return printJson(data);
|
|
739
|
+
console.log(`✓ 已更新菜单项 #${data.id} ${stripTags(data.title?.rendered || '')}`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export async function deleteMenuItem(client, id, flags) {
|
|
743
|
+
if (flags['dry-run']) return dry(`删除菜单项 #${id}`);
|
|
744
|
+
if (!flags.force) throw new UsageError('删除菜单项需要 --force(不可撤销)');
|
|
745
|
+
const { data } = await menuRequest(client, 'DELETE', `${MENU_ITEMS}/${Number(id)}`, { query: { force: 'true' } });
|
|
746
|
+
if (flags.json) return printJson(data);
|
|
747
|
+
console.log(data.deleted ? `✓ 已删除菜单项 #${id}` : `? 菜单项 #${id} 未删除`);
|
|
748
|
+
}
|
|
749
|
+
|
|
476
750
|
// ------------------------------------------------------------------ comments
|
|
477
751
|
|
|
478
752
|
export async function listComments(client, flags) {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
});
|
|
753
|
+
const query = {
|
|
754
|
+
per_page: flags['per-page'] ?? 10,
|
|
755
|
+
page: flags.page,
|
|
756
|
+
...pick(flags, ['post', 'status', 'search', 'author', 'orderby', 'order', 'type']),
|
|
757
|
+
};
|
|
758
|
+
const { data, total } = await listRequest(client, COMMENTS, query, flags);
|
|
486
759
|
if (flags.json) return printJson(data);
|
|
487
|
-
console.log(`评论共 ${
|
|
760
|
+
console.log(`评论共 ${total ?? '?'} 条,本页 ${data.length}:`);
|
|
488
761
|
for (const c of data) {
|
|
489
762
|
console.log(` #${c.id} [${c.status}] post#${c.post} ${stripTags(c.author_name || '')}: ${stripTags(c.content?.rendered || '')}`);
|
|
490
763
|
}
|
|
@@ -495,12 +768,29 @@ export async function getComment(client, id) {
|
|
|
495
768
|
printJson(data);
|
|
496
769
|
}
|
|
497
770
|
|
|
771
|
+
export async function createComment(client, flags) {
|
|
772
|
+
const post = flags.post ?? flags['post-id'];
|
|
773
|
+
if (post === undefined) throw new UsageError('创建评论需要 --post <id>');
|
|
774
|
+
const body = { post: Number(post) };
|
|
775
|
+
if (flags['from-file'] !== undefined) body.content = readFileSync(String(flags['from-file']), 'utf8');
|
|
776
|
+
else if (flags.content !== undefined) body.content = String(flags.content);
|
|
777
|
+
if (body.content === undefined) throw new UsageError('创建评论需要 --content 或 --from-file');
|
|
778
|
+
if (flags['author-name'] !== undefined) body.author_name = String(flags['author-name']);
|
|
779
|
+
if (flags['author-email'] !== undefined) body.author_email = String(flags['author-email']);
|
|
780
|
+
if (flags.parent !== undefined) body.parent = Number(flags.parent);
|
|
781
|
+
if (flags.status !== undefined) body.status = String(flags.status);
|
|
782
|
+
if (flags['dry-run']) return dry(`创建评论 ${JSON.stringify(body)}`);
|
|
783
|
+
const { data } = await client.request('POST', COMMENTS, { body });
|
|
784
|
+
if (flags.json) return printJson(data);
|
|
785
|
+
console.log(`✓ 已创建评论 #${data.id} post#${data.post} [${data.status}]`);
|
|
786
|
+
}
|
|
787
|
+
|
|
498
788
|
export async function updateComment(client, id, flags) {
|
|
499
789
|
const body = {};
|
|
500
790
|
if (flags.status !== undefined) body.status = String(flags.status);
|
|
501
791
|
if (flags.content !== undefined) body.content = String(flags.content);
|
|
502
792
|
if (!Object.keys(body).length) {
|
|
503
|
-
throw new
|
|
793
|
+
throw new UsageError('没有可更新的字段(--status approve|hold|spam|trash / --content)');
|
|
504
794
|
}
|
|
505
795
|
if (flags['dry-run']) return dry(`更新评论 #${id} ${JSON.stringify(body)}`);
|
|
506
796
|
const { data } = await client.request('POST', `${COMMENTS}/${Number(id)}`, { body });
|
|
@@ -520,16 +810,15 @@ export async function deleteComment(client, id, flags) {
|
|
|
520
810
|
// ------------------------------------------------------------------ users
|
|
521
811
|
|
|
522
812
|
export async function listUsers(client, flags) {
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
});
|
|
813
|
+
const query = {
|
|
814
|
+
per_page: flags['per-page'] ?? 10,
|
|
815
|
+
page: flags.page,
|
|
816
|
+
context: 'edit',
|
|
817
|
+
...pick(flags, ['search', 'roles', 'orderby', 'order', 'who']),
|
|
818
|
+
};
|
|
819
|
+
const { data, total } = await listRequest(client, USERS, query, flags);
|
|
531
820
|
if (flags.json) return printJson(data);
|
|
532
|
-
console.log(`用户共 ${
|
|
821
|
+
console.log(`用户共 ${total ?? '?'} 个,本页 ${data.length}:`);
|
|
533
822
|
for (const u of data) {
|
|
534
823
|
console.log(` #${u.id} ${u.name} (@${u.slug}) ${(u.roles || []).join(',')} ${u.email || ''}`);
|
|
535
824
|
}
|
|
@@ -541,8 +830,8 @@ export async function getUser(client, id) {
|
|
|
541
830
|
}
|
|
542
831
|
|
|
543
832
|
export async function createUser(client, flags) {
|
|
544
|
-
if (flags.username === undefined) throw new
|
|
545
|
-
if (flags.email === undefined) throw new
|
|
833
|
+
if (flags.username === undefined) throw new UsageError('创建用户需要 --username');
|
|
834
|
+
if (flags.email === undefined) throw new UsageError('创建用户需要 --email');
|
|
546
835
|
const body = { username: String(flags.username), email: String(flags.email) };
|
|
547
836
|
if (flags.password !== undefined) body.password = String(flags.password);
|
|
548
837
|
if (flags.name !== undefined) body.name = String(flags.name);
|
|
@@ -559,7 +848,7 @@ export async function updateUser(client, id, flags) {
|
|
|
559
848
|
if (flags[key] !== undefined) body[key] = String(flags[key]);
|
|
560
849
|
}
|
|
561
850
|
if (flags.role !== undefined) body.roles = String(flags.role).split(',').map((s) => s.trim()).filter(Boolean);
|
|
562
|
-
if (!Object.keys(body).length) throw new
|
|
851
|
+
if (!Object.keys(body).length) throw new UsageError('没有可更新的字段(--name/--email/--password/--role/...)');
|
|
563
852
|
if (flags['dry-run']) return dry(`更新用户 #${id} ${JSON.stringify({ ...body, password: body.password ? '***' : undefined })}`);
|
|
564
853
|
const { data } = await client.request('POST', `${USERS}/${Number(id)}`, { body });
|
|
565
854
|
if (flags.json) return printJson(data);
|
|
@@ -567,8 +856,8 @@ export async function updateUser(client, id, flags) {
|
|
|
567
856
|
}
|
|
568
857
|
|
|
569
858
|
export async function deleteUser(client, id, flags) {
|
|
570
|
-
if (!flags.force) throw new Error('删除用户需要 --force(不可撤销)');
|
|
571
859
|
if (flags['dry-run']) return dry(`删除用户 #${id}${flags.reassign ? `(内容转交 #${flags.reassign})` : ''}`);
|
|
860
|
+
if (!flags.force) throw new UsageError('删除用户需要 --force(不可撤销)');
|
|
572
861
|
const { data } = await client.request('DELETE', `${USERS}/${Number(id)}`, {
|
|
573
862
|
query: { force: 'true', reassign: flags.reassign },
|
|
574
863
|
});
|
|
@@ -581,8 +870,11 @@ export async function deleteUser(client, id, flags) {
|
|
|
581
870
|
export async function raw(client, method, path, flags) {
|
|
582
871
|
const upper = String(method).toUpperCase();
|
|
583
872
|
let body;
|
|
584
|
-
if (flags.data !== undefined) body =
|
|
585
|
-
else if (flags['data-file'] !== undefined) body =
|
|
873
|
+
if (flags.data !== undefined) body = parseJson(flags.data);
|
|
874
|
+
else if (flags['data-file'] !== undefined) body = readJsonFile(flags['data-file']);
|
|
875
|
+
if (body !== undefined && ['GET', 'HEAD'].includes(upper)) {
|
|
876
|
+
throw new UsageError(`${upper} 请求不能带 --data(--data 仅用于写操作)`, { group: 'raw', action: upper });
|
|
877
|
+
}
|
|
586
878
|
|
|
587
879
|
if (flags['dry-run'] && !['GET', 'HEAD', 'OPTIONS'].includes(upper)) {
|
|
588
880
|
return dry(`${upper} ${path}${body !== undefined ? ` ${JSON.stringify(body)}` : ''}`);
|
|
@@ -643,7 +935,7 @@ function promptHidden(query) {
|
|
|
643
935
|
export async function setup(flags) {
|
|
644
936
|
const tty = Boolean(process.stdin.isTTY);
|
|
645
937
|
const need = (what) => {
|
|
646
|
-
if (!tty) throw new
|
|
938
|
+
if (!tty) throw new UsageError(`非交互模式请提供 ${what}`);
|
|
647
939
|
};
|
|
648
940
|
|
|
649
941
|
let url = flags.url;
|
|
@@ -652,7 +944,7 @@ export async function setup(flags) {
|
|
|
652
944
|
url = await promptLine('站点 URL(https://...):');
|
|
653
945
|
}
|
|
654
946
|
url = String(url).replace(/\/+$/, '');
|
|
655
|
-
if (!/^https:\/\//i.test(url)) throw new
|
|
947
|
+
if (!/^https:\/\//i.test(url)) throw new UsageError('URL 必须以 https:// 开头');
|
|
656
948
|
|
|
657
949
|
let user = flags.user;
|
|
658
950
|
if (!user) {
|
|
@@ -665,7 +957,7 @@ export async function setup(flags) {
|
|
|
665
957
|
need('--password');
|
|
666
958
|
password = await promptHidden('应用密码(输入不回显):');
|
|
667
959
|
}
|
|
668
|
-
if (!password) throw new
|
|
960
|
+
if (!password) throw new UsageError('应用密码为空');
|
|
669
961
|
|
|
670
962
|
const defaultName = (() => {
|
|
671
963
|
try {
|
|
@@ -685,7 +977,7 @@ export async function setup(flags) {
|
|
|
685
977
|
mkdirSync(dir, { recursive: true });
|
|
686
978
|
const file = join(dir, `${name}.env`);
|
|
687
979
|
if (existsSync(file) && !flags.force) {
|
|
688
|
-
throw new
|
|
980
|
+
throw new UsageError(`已存在 ${file};要覆盖请加 --force`);
|
|
689
981
|
}
|
|
690
982
|
|
|
691
983
|
const content = [
|