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/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, 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,21 +16,20 @@ 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
|
function printItem(data, flags) {
|
|
34
35
|
if (flags.json) return printJson(data);
|
|
@@ -36,17 +37,6 @@ function printItem(data, flags) {
|
|
|
36
37
|
if (data.link) console.log(` ${data.link}`);
|
|
37
38
|
}
|
|
38
39
|
|
|
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);
|
|
46
|
-
}
|
|
47
|
-
return query;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
40
|
// ------------------------------------------------------------------ doctor
|
|
51
41
|
|
|
52
42
|
export async function doctor(client, cfg, flags) {
|
|
@@ -216,10 +206,8 @@ export async function listContent(client, type, flags) {
|
|
|
216
206
|
context: flags.context,
|
|
217
207
|
...pick(flags, CONTENT_FILTERS[type] || []),
|
|
218
208
|
};
|
|
219
|
-
const { data,
|
|
209
|
+
const { data, total, totalPages } = await listRequest(client, contentPath(type), query, flags);
|
|
220
210
|
if (flags.json) return printJson(data);
|
|
221
|
-
const total = headers.get('x-wp-total');
|
|
222
|
-
const totalPages = headers.get('x-wp-totalpages');
|
|
223
211
|
console.log(
|
|
224
212
|
`${type} 共 ${total ?? '?'} 条 / ${totalPages ?? '?'} 页;第 ${page} 页${totalPages ? `/${totalPages}` : ''},本页 ${data.length} 条:`,
|
|
225
213
|
);
|
|
@@ -232,7 +220,7 @@ export async function listContent(client, type, flags) {
|
|
|
232
220
|
}
|
|
233
221
|
|
|
234
222
|
export async function getContent(client, type, id, flags) {
|
|
235
|
-
const { data } = await client.request('GET', `${
|
|
223
|
+
const { data } = await client.request('GET', `${contentPath(type)}/${Number(id)}`, {
|
|
236
224
|
query: { context: flags.context || 'edit' },
|
|
237
225
|
});
|
|
238
226
|
if (flags.json) return printJson(data);
|
|
@@ -265,24 +253,24 @@ function contentBody(flags) {
|
|
|
265
253
|
export async function createContent(client, type, flags) {
|
|
266
254
|
const body = contentBody(flags);
|
|
267
255
|
if (body.title === undefined && body.content === undefined) {
|
|
268
|
-
throw new
|
|
256
|
+
throw new UsageError('至少需要 --title 或 --content/--from-file');
|
|
269
257
|
}
|
|
270
258
|
if (flags['dry-run']) return dry(`创建 ${type} ${JSON.stringify(body)}`);
|
|
271
|
-
const { data } = await client.request('POST',
|
|
259
|
+
const { data } = await client.request('POST', contentPath(type), { body });
|
|
272
260
|
printItem(data, flags);
|
|
273
261
|
}
|
|
274
262
|
|
|
275
263
|
export async function updateContent(client, type, id, flags) {
|
|
276
264
|
const body = contentBody(flags);
|
|
277
|
-
if (!Object.keys(body).length) throw new
|
|
265
|
+
if (!Object.keys(body).length) throw new UsageError('没有可更新的字段');
|
|
278
266
|
if (flags['dry-run']) return dry(`更新 ${type} #${id} ${JSON.stringify(body)}`);
|
|
279
|
-
const { data } = await client.request('POST', `${
|
|
267
|
+
const { data } = await client.request('POST', `${contentPath(type)}/${Number(id)}`, { body });
|
|
280
268
|
printItem(data, flags);
|
|
281
269
|
}
|
|
282
270
|
|
|
283
271
|
export async function deleteContent(client, type, id, flags) {
|
|
284
272
|
if (flags['dry-run']) return dry(`删除 ${type} #${id}${flags.force ? '(彻底)' : '(回收站)'}`);
|
|
285
|
-
const { data } = await client.request('DELETE', `${
|
|
273
|
+
const { data } = await client.request('DELETE', `${contentPath(type)}/${Number(id)}`, {
|
|
286
274
|
query: { force: flags.force ? 'true' : undefined },
|
|
287
275
|
});
|
|
288
276
|
if (flags.json) return printJson(data);
|
|
@@ -299,10 +287,8 @@ export async function listMedia(client, flags) {
|
|
|
299
287
|
page,
|
|
300
288
|
...pick(flags, ['media_type', 'mime_type', 'parent', 'author', 'after', 'before', 'orderby', 'order', 'offset', 'include', 'exclude']),
|
|
301
289
|
};
|
|
302
|
-
const { data,
|
|
290
|
+
const { data, total, totalPages } = await listRequest(client, MEDIA, query, flags);
|
|
303
291
|
if (flags.json) return printJson(data);
|
|
304
|
-
const total = headers.get('x-wp-total');
|
|
305
|
-
const totalPages = headers.get('x-wp-totalpages');
|
|
306
292
|
console.log(
|
|
307
293
|
`媒体共 ${total ?? '?'} 项 / ${totalPages ?? '?'} 页;第 ${page} 页${totalPages ? `/${totalPages}` : ''},本页 ${data.length} 项:`,
|
|
308
294
|
);
|
|
@@ -344,7 +330,7 @@ export async function updateMedia(client, id, flags) {
|
|
|
344
330
|
if (flags.description !== undefined) body.description = String(flags.description);
|
|
345
331
|
if (flags.post !== undefined) body.post = Number(flags.post);
|
|
346
332
|
if (!Object.keys(body).length) {
|
|
347
|
-
throw new
|
|
333
|
+
throw new UsageError('没有可更新的字段(--title/--alt/--caption/--description/--post)');
|
|
348
334
|
}
|
|
349
335
|
if (flags['dry-run']) return dry(`更新媒体 #${id} ${JSON.stringify(body)}`);
|
|
350
336
|
const { data } = await client.request('POST', `${MEDIA}/${Number(id)}`, { body });
|
|
@@ -352,6 +338,56 @@ export async function updateMedia(client, id, flags) {
|
|
|
352
338
|
console.log(`✓ 已更新媒体 #${data.id}`);
|
|
353
339
|
}
|
|
354
340
|
|
|
341
|
+
/** 图像编辑(裁剪/旋转),POST /media/<id>/edit。缺 --src 时自动取当前 source_url。 */
|
|
342
|
+
export async function editMediaImage(client, id, flags) {
|
|
343
|
+
const body = {};
|
|
344
|
+
if (flags.src !== undefined) body.src = String(flags.src);
|
|
345
|
+
if (flags.rotation !== undefined) body.rotation = Number(flags.rotation);
|
|
346
|
+
if (flags['dest-width'] !== undefined) body.width = Number(flags['dest-width']);
|
|
347
|
+
if (flags['dest-height'] !== undefined) body.height = Number(flags['dest-height']);
|
|
348
|
+
if (flags.x !== undefined) body.x = Number(flags.x);
|
|
349
|
+
if (flags.y !== undefined) body.y = Number(flags.y);
|
|
350
|
+
const modifiers = [];
|
|
351
|
+
if (flags.crop) modifiers.push('crop');
|
|
352
|
+
if (flags.modifiers !== undefined) {
|
|
353
|
+
modifiers.push(...String(flags.modifiers).split(',').map((s) => s.trim()).filter(Boolean));
|
|
354
|
+
}
|
|
355
|
+
if (modifiers.length) body.modifiers = modifiers;
|
|
356
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
357
|
+
if (flags.alt !== undefined) body.alt_text = String(flags.alt);
|
|
358
|
+
if (flags.caption !== undefined) body.caption = String(flags.caption);
|
|
359
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
360
|
+
if (flags.post !== undefined) body.post = Number(flags.post);
|
|
361
|
+
if (!Object.keys(body).length) {
|
|
362
|
+
throw new UsageError('图像编辑需要至少一个参数(--rotation/--crop/--x/--y/--dest-width/--dest-height/--modifiers/--src/...)');
|
|
363
|
+
}
|
|
364
|
+
if (flags['dry-run']) return dry(`编辑媒体 #${id} ${JSON.stringify(body)}`);
|
|
365
|
+
if (body.src === undefined) {
|
|
366
|
+
const { data } = await client.request('GET', `${MEDIA}/${Number(id)}`, { query: { context: 'edit' } });
|
|
367
|
+
body.src = data.source_url;
|
|
368
|
+
}
|
|
369
|
+
if (!body.src) throw new UsageError('无法确定图片来源,请显式传 --src <url>');
|
|
370
|
+
const { data } = await client.request('POST', `${MEDIA}/${Number(id)}/edit`, { body });
|
|
371
|
+
if (flags.json) return printJson(data);
|
|
372
|
+
console.log(`✓ 已编辑媒体 #${data.id} ${data.source_url || ''}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 从 URL 导入媒体:sideload,POST /media(带 url)。 */
|
|
376
|
+
export async function sideloadMedia(client, flags) {
|
|
377
|
+
if (flags.url === undefined) throw new UsageError('media sideload 需要 --url <图片地址>');
|
|
378
|
+
const body = { url: String(flags.url) };
|
|
379
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
380
|
+
if (flags.alt !== undefined) body.alt_text = String(flags.alt);
|
|
381
|
+
if (flags.caption !== undefined) body.caption = String(flags.caption);
|
|
382
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
383
|
+
if (flags.slug !== undefined) body.slug = String(flags.slug);
|
|
384
|
+
if (flags.post !== undefined) body.post = Number(flags.post);
|
|
385
|
+
if (flags['dry-run']) return dry(`从 URL 导入媒体 ${body.url}`);
|
|
386
|
+
const { data } = await client.request('POST', MEDIA, { body });
|
|
387
|
+
if (flags.json) return printJson(data);
|
|
388
|
+
console.log(`✓ 已导入媒体 #${data.id} ${data.source_url || ''}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
355
391
|
export async function deleteMedia(client, id, flags) {
|
|
356
392
|
if (flags['dry-run']) return dry(`删除媒体 #${id}${flags.force ? '(彻底)' : '(回收站)'}`);
|
|
357
393
|
const { data } = await client.request('DELETE', `${MEDIA}/${Number(id)}`, {
|
|
@@ -392,6 +428,14 @@ export async function setPluginStatus(client, plugin, activate, flags) {
|
|
|
392
428
|
console.log(`✓ ${data.name} → ${data.status === 'active' ? '已启用' : '已停用'}`);
|
|
393
429
|
}
|
|
394
430
|
|
|
431
|
+
export async function deletePlugin(client, plugin, flags) {
|
|
432
|
+
if (flags['dry-run']) return dry(`删除插件 ${plugin}`);
|
|
433
|
+
if (!flags.force) throw new UsageError('删除插件需要 --force(不可撤销)');
|
|
434
|
+
const { data } = await client.request('DELETE', `/wp-json/wp/v2/plugins/${encodeURIComponent(plugin)}`);
|
|
435
|
+
if (flags.json) return printJson(data);
|
|
436
|
+
console.log(data.deleted ? `✓ 已删除插件 ${plugin}` : `? 插件 ${plugin} 未删除`);
|
|
437
|
+
}
|
|
438
|
+
|
|
395
439
|
// ------------------------------------------------------------------ themes
|
|
396
440
|
|
|
397
441
|
export async function listThemes(client, flags) {
|
|
@@ -415,26 +459,25 @@ export async function activateTheme(client, stylesheet, flags) {
|
|
|
415
459
|
// ------------------------------------------------------------------ taxonomy
|
|
416
460
|
|
|
417
461
|
export async function listTaxonomy(client, tax, flags) {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
462
|
+
const query = {
|
|
463
|
+
per_page: flags['per-page'] ?? 100,
|
|
464
|
+
page: flags.page,
|
|
465
|
+
search: flags.search,
|
|
466
|
+
orderby: flags.orderby,
|
|
467
|
+
order: flags.order,
|
|
468
|
+
parent: flags.parent,
|
|
469
|
+
hide_empty: flags['hide-empty'] ? 'true' : 'false',
|
|
470
|
+
};
|
|
471
|
+
const { data, total } = await listRequest(client, taxPath(tax), query, flags);
|
|
429
472
|
if (flags.json) return printJson(data);
|
|
430
|
-
console.log(`${tax} 共 ${
|
|
473
|
+
console.log(`${tax} 共 ${total ?? '?'} 个:`);
|
|
431
474
|
for (const t of data) {
|
|
432
475
|
console.log(` #${t.id} ${t.name} (${t.count}) slug=${t.slug}${t.parent ? ` parent=#${t.parent}` : ''}`);
|
|
433
476
|
}
|
|
434
477
|
}
|
|
435
478
|
|
|
436
479
|
export async function getTaxonomy(client, tax, id) {
|
|
437
|
-
const { data } = await client.request('GET', `${
|
|
480
|
+
const { data } = await client.request('GET', `${taxPath(tax)}/${Number(id)}`);
|
|
438
481
|
printJson(data);
|
|
439
482
|
}
|
|
440
483
|
|
|
@@ -449,42 +492,220 @@ function taxonomyBody(flags) {
|
|
|
449
492
|
|
|
450
493
|
export async function createTaxonomy(client, tax, flags) {
|
|
451
494
|
const body = taxonomyBody(flags);
|
|
452
|
-
if (body.name === undefined) throw new
|
|
495
|
+
if (body.name === undefined) throw new UsageError(`创建 ${tax} 需要 --name`);
|
|
453
496
|
if (flags['dry-run']) return dry(`创建 ${tax} ${JSON.stringify(body)}`);
|
|
454
|
-
const { data } = await client.request('POST',
|
|
497
|
+
const { data } = await client.request('POST', taxPath(tax), { body });
|
|
455
498
|
if (flags.json) return printJson(data);
|
|
456
499
|
console.log(`✓ 已创建 ${tax} #${data.id} ${data.name}`);
|
|
457
500
|
}
|
|
458
501
|
|
|
459
502
|
export async function updateTaxonomy(client, tax, id, flags) {
|
|
460
503
|
const body = taxonomyBody(flags);
|
|
461
|
-
if (!Object.keys(body).length) throw new
|
|
504
|
+
if (!Object.keys(body).length) throw new UsageError('没有可更新的字段(--name/--slug/--parent/--description)');
|
|
462
505
|
if (flags['dry-run']) return dry(`更新 ${tax} #${id} ${JSON.stringify(body)}`);
|
|
463
|
-
const { data } = await client.request('POST', `${
|
|
506
|
+
const { data } = await client.request('POST', `${taxPath(tax)}/${Number(id)}`, { body });
|
|
464
507
|
if (flags.json) return printJson(data);
|
|
465
508
|
console.log(`✓ 已更新 ${tax} #${data.id} ${data.name}`);
|
|
466
509
|
}
|
|
467
510
|
|
|
468
511
|
export async function deleteTaxonomy(client, tax, id, flags) {
|
|
469
|
-
if (!flags.force) throw new Error(`删除 ${tax} 需要 --force(不可撤销)`);
|
|
470
512
|
if (flags['dry-run']) return dry(`删除 ${tax} #${id}`);
|
|
471
|
-
|
|
513
|
+
if (!flags.force) throw new UsageError(`删除 ${tax} 需要 --force(不可撤销)`);
|
|
514
|
+
const { data } = await client.request('DELETE', `${taxPath(tax)}/${Number(id)}`, { query: { force: 'true' } });
|
|
472
515
|
if (flags.json) return printJson(data);
|
|
473
516
|
console.log(data.deleted ? `✓ 已删除 ${tax} #${id}` : `? ${tax} #${id} 未删除`);
|
|
474
517
|
}
|
|
475
518
|
|
|
519
|
+
// ------------------------------------------------------------------ menus
|
|
520
|
+
|
|
521
|
+
const MENU_PLUGIN_HINT =
|
|
522
|
+
'菜单接口由插件提供(WordPress 核心 REST 没有 /menus、/menu-items);请确认站点已装并启用对应插件。';
|
|
523
|
+
|
|
524
|
+
/** 菜单端点请求:路由不存在时补充"需插件"的提示,便于排障。 */
|
|
525
|
+
async function menuRequest(client, method, path, opts) {
|
|
526
|
+
try {
|
|
527
|
+
return await client.request(method, path, opts);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (err?.code === 'rest_no_route') {
|
|
530
|
+
throw new WpError(`${err.message} —— ${MENU_PLUGIN_HINT}`, {
|
|
531
|
+
status: err.status,
|
|
532
|
+
code: err.code,
|
|
533
|
+
data: err.data,
|
|
534
|
+
method: err.method,
|
|
535
|
+
path: err.path,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function listMenus(client, flags) {
|
|
543
|
+
const { data, headers } = await menuRequest(client, 'GET', MENUS, {
|
|
544
|
+
query: { per_page: flags['per-page'] ?? 100, page: flags.page },
|
|
545
|
+
});
|
|
546
|
+
if (flags.json) return printJson(data);
|
|
547
|
+
console.log(`菜单共 ${headers.get('x-wp-total') ?? data.length} 个:`);
|
|
548
|
+
for (const m of data) {
|
|
549
|
+
console.log(` #${m.id} ${m.name} slug=${m.slug} locations=${(m.locations || []).join(',') || '—'}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function getMenu(client, id, flags) {
|
|
554
|
+
const { data } = await menuRequest(client, 'GET', `${MENUS}/${Number(id)}`);
|
|
555
|
+
if (flags.json) return printJson(data);
|
|
556
|
+
console.log(`#${data.id} ${data.name} slug=${data.slug}`);
|
|
557
|
+
console.log(` 位置:${(data.locations || []).join(', ') || '—'}`);
|
|
558
|
+
console.log(` 自动添加:${data.auto_add ? '是' : '否'}`);
|
|
559
|
+
if (data.description) console.log(` 描述:${data.description}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** 菜单字段转请求体(locations/auto_add 用于给菜单绑定主题位置)。 */
|
|
563
|
+
function menuBody(flags) {
|
|
564
|
+
const body = {};
|
|
565
|
+
if (flags.name !== undefined) body.name = String(flags.name);
|
|
566
|
+
if (flags.slug !== undefined) body.slug = String(flags.slug);
|
|
567
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
568
|
+
if (flags.locations !== undefined) {
|
|
569
|
+
body.locations = String(flags.locations).split(',').map((s) => s.trim()).filter(Boolean);
|
|
570
|
+
}
|
|
571
|
+
if (flags['auto-add'] !== undefined) body.auto_add = Boolean(flags['auto-add']);
|
|
572
|
+
return body;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function createMenu(client, flags) {
|
|
576
|
+
const body = menuBody(flags);
|
|
577
|
+
if (body.name === undefined) throw new UsageError('创建菜单需要 --name');
|
|
578
|
+
if (flags['dry-run']) return dry(`创建菜单 ${JSON.stringify(body)}`);
|
|
579
|
+
const { data } = await menuRequest(client, 'POST', MENUS, { body });
|
|
580
|
+
if (flags.json) return printJson(data);
|
|
581
|
+
console.log(`✓ 已创建菜单 #${data.id} ${data.name}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export async function updateMenu(client, id, flags) {
|
|
585
|
+
const body = menuBody(flags);
|
|
586
|
+
if (!Object.keys(body).length) {
|
|
587
|
+
throw new UsageError('没有可更新的字段(--name/--slug/--description/--locations/--auto-add)');
|
|
588
|
+
}
|
|
589
|
+
if (flags['dry-run']) return dry(`更新菜单 #${id} ${JSON.stringify(body)}`);
|
|
590
|
+
const { data } = await menuRequest(client, 'POST', `${MENUS}/${Number(id)}`, { body });
|
|
591
|
+
if (flags.json) return printJson(data);
|
|
592
|
+
console.log(`✓ 已更新菜单 #${data.id} ${data.name} 位置:${(data.locations || []).join(',') || '—'}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function deleteMenu(client, id, flags) {
|
|
596
|
+
if (flags['dry-run']) return dry(`删除菜单 #${id}`);
|
|
597
|
+
if (!flags.force) throw new UsageError('删除菜单需要 --force(不可撤销)');
|
|
598
|
+
const { data } = await menuRequest(client, 'DELETE', `${MENUS}/${Number(id)}`, { query: { force: 'true' } });
|
|
599
|
+
if (flags.json) return printJson(data);
|
|
600
|
+
console.log(data.deleted ? `✓ 已删除菜单 #${id}` : `? 菜单 #${id} 未删除`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** 把 flags 中的菜单项字段转成 REST 请求体(注意数值/数组类型)。 */
|
|
604
|
+
function menuItemBody(flags) {
|
|
605
|
+
const body = {};
|
|
606
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
607
|
+
if (flags.url !== undefined) body.url = String(flags.url);
|
|
608
|
+
if (flags.menus !== undefined) body.menus = Number(flags.menus);
|
|
609
|
+
if (flags['menu-order'] !== undefined) body.menu_order = Number(flags['menu-order']);
|
|
610
|
+
if (flags.parent !== undefined) body.parent = Number(flags.parent);
|
|
611
|
+
if (flags.status !== undefined) body.status = String(flags.status);
|
|
612
|
+
if (flags.type !== undefined) body.type = String(flags.type);
|
|
613
|
+
if (flags.object !== undefined) body.object = String(flags.object);
|
|
614
|
+
if (flags['object-id'] !== undefined) body.object_id = Number(flags['object-id']);
|
|
615
|
+
if (flags.target !== undefined) body.target = String(flags.target);
|
|
616
|
+
if (flags.classes !== undefined) {
|
|
617
|
+
body.classes = String(flags.classes).split(',').map((s) => s.trim()).filter(Boolean);
|
|
618
|
+
}
|
|
619
|
+
if (flags.description !== undefined) body.description = String(flags.description);
|
|
620
|
+
if (flags['attr-title'] !== undefined) body.attr_title = String(flags['attr-title']);
|
|
621
|
+
if (flags.xfn !== undefined) body.xfn = String(flags.xfn).split(',').map((s) => s.trim()).filter(Boolean);
|
|
622
|
+
return body;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function listMenuItems(client, flags) {
|
|
626
|
+
const perPage = Number(flags['per-page'] ?? 100) || 100;
|
|
627
|
+
const page = Number(flags.page ?? 1) || 1;
|
|
628
|
+
const query = {
|
|
629
|
+
per_page: perPage,
|
|
630
|
+
page,
|
|
631
|
+
...pick(flags, ['menus', 'search', 'status', 'orderby', 'order', 'menu-order']),
|
|
632
|
+
};
|
|
633
|
+
const { data, total, totalPages } = await listRequest(
|
|
634
|
+
client,
|
|
635
|
+
MENU_ITEMS,
|
|
636
|
+
query,
|
|
637
|
+
flags,
|
|
638
|
+
(method, path, opts) => menuRequest(client, method, path, opts),
|
|
639
|
+
);
|
|
640
|
+
if (flags.json) return printJson(data);
|
|
641
|
+
console.log(
|
|
642
|
+
`菜单项共 ${total ?? '?'} 项 / ${totalPages ?? '?'} 页;第 ${page} 页${totalPages ? `/${totalPages}` : ''},本页 ${data.length} 项:`,
|
|
643
|
+
);
|
|
644
|
+
for (const item of data) {
|
|
645
|
+
const title = stripTags(item.title?.rendered || '(无标题)');
|
|
646
|
+
console.log(
|
|
647
|
+
` #${item.id} [${item.status}] order=${item.menu_order} parent=${item.parent} ${item.type}/${item.object} ${title} ${item.url}`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (totalPages && page < Number(totalPages)) {
|
|
651
|
+
console.log(` …下一页:wpops menu-items list --page ${page + 1} --per-page ${perPage}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export async function getMenuItem(client, id, flags) {
|
|
656
|
+
const { data } = await menuRequest(client, 'GET', `${MENU_ITEMS}/${Number(id)}`);
|
|
657
|
+
if (flags.json) return printJson(data);
|
|
658
|
+
console.log(`#${data.id} [${data.status}] ${stripTags(data.title?.rendered || '')}`);
|
|
659
|
+
console.log(` 顺序:${data.menu_order} 父级:#${data.parent} 类型:${data.type}/${data.object} object_id=${data.object_id}`);
|
|
660
|
+
console.log(` 链接:${data.url}`);
|
|
661
|
+
if (data.attr_title) console.log(` attr_title:${data.attr_title}`);
|
|
662
|
+
if (data.target) console.log(` target:${data.target}`);
|
|
663
|
+
if (data.classes?.filter(Boolean).length) console.log(` classes:${data.classes.filter(Boolean).join(',')}`);
|
|
664
|
+
if (data.description) console.log(` 描述:${data.description}`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export async function createMenuItem(client, flags) {
|
|
668
|
+
const body = menuItemBody(flags);
|
|
669
|
+
if (body.title === undefined) throw new UsageError('创建菜单项需要 --title');
|
|
670
|
+
if (body.url === undefined && body.object === undefined) {
|
|
671
|
+
throw new UsageError('创建菜单项需要 --url(自定义链接)或 --object/--object-id(指向现有内容)');
|
|
672
|
+
}
|
|
673
|
+
if (flags['dry-run']) return dry(`创建菜单项 ${JSON.stringify(body)}`);
|
|
674
|
+
const { data } = await menuRequest(client, 'POST', MENU_ITEMS, { body });
|
|
675
|
+
if (flags.json) return printJson(data);
|
|
676
|
+
console.log(`✓ 已创建菜单项 #${data.id} [${data.status}] ${stripTags(data.title?.rendered || '')}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export async function updateMenuItem(client, id, flags) {
|
|
680
|
+
const body = menuItemBody(flags);
|
|
681
|
+
if (!Object.keys(body).length) {
|
|
682
|
+
throw new UsageError('没有可更新的字段(--title/--url/--menus/--menu-order/--parent/--status/--type/--object/--object-id/--target/--classes/--description/--attr-title)');
|
|
683
|
+
}
|
|
684
|
+
if (flags['dry-run']) return dry(`更新菜单项 #${id} ${JSON.stringify(body)}`);
|
|
685
|
+
const { data } = await menuRequest(client, 'POST', `${MENU_ITEMS}/${Number(id)}`, { body });
|
|
686
|
+
if (flags.json) return printJson(data);
|
|
687
|
+
console.log(`✓ 已更新菜单项 #${data.id} ${stripTags(data.title?.rendered || '')}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export async function deleteMenuItem(client, id, flags) {
|
|
691
|
+
if (flags['dry-run']) return dry(`删除菜单项 #${id}`);
|
|
692
|
+
if (!flags.force) throw new UsageError('删除菜单项需要 --force(不可撤销)');
|
|
693
|
+
const { data } = await menuRequest(client, 'DELETE', `${MENU_ITEMS}/${Number(id)}`, { query: { force: 'true' } });
|
|
694
|
+
if (flags.json) return printJson(data);
|
|
695
|
+
console.log(data.deleted ? `✓ 已删除菜单项 #${id}` : `? 菜单项 #${id} 未删除`);
|
|
696
|
+
}
|
|
697
|
+
|
|
476
698
|
// ------------------------------------------------------------------ comments
|
|
477
699
|
|
|
478
700
|
export async function listComments(client, flags) {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
});
|
|
701
|
+
const query = {
|
|
702
|
+
per_page: flags['per-page'] ?? 10,
|
|
703
|
+
page: flags.page,
|
|
704
|
+
...pick(flags, ['post', 'status', 'search', 'author', 'orderby', 'order', 'type']),
|
|
705
|
+
};
|
|
706
|
+
const { data, total } = await listRequest(client, COMMENTS, query, flags);
|
|
486
707
|
if (flags.json) return printJson(data);
|
|
487
|
-
console.log(`评论共 ${
|
|
708
|
+
console.log(`评论共 ${total ?? '?'} 条,本页 ${data.length}:`);
|
|
488
709
|
for (const c of data) {
|
|
489
710
|
console.log(` #${c.id} [${c.status}] post#${c.post} ${stripTags(c.author_name || '')}: ${stripTags(c.content?.rendered || '')}`);
|
|
490
711
|
}
|
|
@@ -495,12 +716,29 @@ export async function getComment(client, id) {
|
|
|
495
716
|
printJson(data);
|
|
496
717
|
}
|
|
497
718
|
|
|
719
|
+
export async function createComment(client, flags) {
|
|
720
|
+
const post = flags.post ?? flags['post-id'];
|
|
721
|
+
if (post === undefined) throw new UsageError('创建评论需要 --post <id>');
|
|
722
|
+
const body = { post: Number(post) };
|
|
723
|
+
if (flags['from-file'] !== undefined) body.content = readFileSync(String(flags['from-file']), 'utf8');
|
|
724
|
+
else if (flags.content !== undefined) body.content = String(flags.content);
|
|
725
|
+
if (body.content === undefined) throw new UsageError('创建评论需要 --content 或 --from-file');
|
|
726
|
+
if (flags['author-name'] !== undefined) body.author_name = String(flags['author-name']);
|
|
727
|
+
if (flags['author-email'] !== undefined) body.author_email = String(flags['author-email']);
|
|
728
|
+
if (flags.parent !== undefined) body.parent = Number(flags.parent);
|
|
729
|
+
if (flags.status !== undefined) body.status = String(flags.status);
|
|
730
|
+
if (flags['dry-run']) return dry(`创建评论 ${JSON.stringify(body)}`);
|
|
731
|
+
const { data } = await client.request('POST', COMMENTS, { body });
|
|
732
|
+
if (flags.json) return printJson(data);
|
|
733
|
+
console.log(`✓ 已创建评论 #${data.id} post#${data.post} [${data.status}]`);
|
|
734
|
+
}
|
|
735
|
+
|
|
498
736
|
export async function updateComment(client, id, flags) {
|
|
499
737
|
const body = {};
|
|
500
738
|
if (flags.status !== undefined) body.status = String(flags.status);
|
|
501
739
|
if (flags.content !== undefined) body.content = String(flags.content);
|
|
502
740
|
if (!Object.keys(body).length) {
|
|
503
|
-
throw new
|
|
741
|
+
throw new UsageError('没有可更新的字段(--status approve|hold|spam|trash / --content)');
|
|
504
742
|
}
|
|
505
743
|
if (flags['dry-run']) return dry(`更新评论 #${id} ${JSON.stringify(body)}`);
|
|
506
744
|
const { data } = await client.request('POST', `${COMMENTS}/${Number(id)}`, { body });
|
|
@@ -520,16 +758,15 @@ export async function deleteComment(client, id, flags) {
|
|
|
520
758
|
// ------------------------------------------------------------------ users
|
|
521
759
|
|
|
522
760
|
export async function listUsers(client, flags) {
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
});
|
|
761
|
+
const query = {
|
|
762
|
+
per_page: flags['per-page'] ?? 10,
|
|
763
|
+
page: flags.page,
|
|
764
|
+
context: 'edit',
|
|
765
|
+
...pick(flags, ['search', 'roles', 'orderby', 'order', 'who']),
|
|
766
|
+
};
|
|
767
|
+
const { data, total } = await listRequest(client, USERS, query, flags);
|
|
531
768
|
if (flags.json) return printJson(data);
|
|
532
|
-
console.log(`用户共 ${
|
|
769
|
+
console.log(`用户共 ${total ?? '?'} 个,本页 ${data.length}:`);
|
|
533
770
|
for (const u of data) {
|
|
534
771
|
console.log(` #${u.id} ${u.name} (@${u.slug}) ${(u.roles || []).join(',')} ${u.email || ''}`);
|
|
535
772
|
}
|
|
@@ -541,8 +778,8 @@ export async function getUser(client, id) {
|
|
|
541
778
|
}
|
|
542
779
|
|
|
543
780
|
export async function createUser(client, flags) {
|
|
544
|
-
if (flags.username === undefined) throw new
|
|
545
|
-
if (flags.email === undefined) throw new
|
|
781
|
+
if (flags.username === undefined) throw new UsageError('创建用户需要 --username');
|
|
782
|
+
if (flags.email === undefined) throw new UsageError('创建用户需要 --email');
|
|
546
783
|
const body = { username: String(flags.username), email: String(flags.email) };
|
|
547
784
|
if (flags.password !== undefined) body.password = String(flags.password);
|
|
548
785
|
if (flags.name !== undefined) body.name = String(flags.name);
|
|
@@ -559,7 +796,7 @@ export async function updateUser(client, id, flags) {
|
|
|
559
796
|
if (flags[key] !== undefined) body[key] = String(flags[key]);
|
|
560
797
|
}
|
|
561
798
|
if (flags.role !== undefined) body.roles = String(flags.role).split(',').map((s) => s.trim()).filter(Boolean);
|
|
562
|
-
if (!Object.keys(body).length) throw new
|
|
799
|
+
if (!Object.keys(body).length) throw new UsageError('没有可更新的字段(--name/--email/--password/--role/...)');
|
|
563
800
|
if (flags['dry-run']) return dry(`更新用户 #${id} ${JSON.stringify({ ...body, password: body.password ? '***' : undefined })}`);
|
|
564
801
|
const { data } = await client.request('POST', `${USERS}/${Number(id)}`, { body });
|
|
565
802
|
if (flags.json) return printJson(data);
|
|
@@ -567,8 +804,8 @@ export async function updateUser(client, id, flags) {
|
|
|
567
804
|
}
|
|
568
805
|
|
|
569
806
|
export async function deleteUser(client, id, flags) {
|
|
570
|
-
if (!flags.force) throw new Error('删除用户需要 --force(不可撤销)');
|
|
571
807
|
if (flags['dry-run']) return dry(`删除用户 #${id}${flags.reassign ? `(内容转交 #${flags.reassign})` : ''}`);
|
|
808
|
+
if (!flags.force) throw new UsageError('删除用户需要 --force(不可撤销)');
|
|
572
809
|
const { data } = await client.request('DELETE', `${USERS}/${Number(id)}`, {
|
|
573
810
|
query: { force: 'true', reassign: flags.reassign },
|
|
574
811
|
});
|
|
@@ -581,8 +818,11 @@ export async function deleteUser(client, id, flags) {
|
|
|
581
818
|
export async function raw(client, method, path, flags) {
|
|
582
819
|
const upper = String(method).toUpperCase();
|
|
583
820
|
let body;
|
|
584
|
-
if (flags.data !== undefined) body =
|
|
585
|
-
else if (flags['data-file'] !== undefined) body =
|
|
821
|
+
if (flags.data !== undefined) body = parseJson(flags.data);
|
|
822
|
+
else if (flags['data-file'] !== undefined) body = readJsonFile(flags['data-file']);
|
|
823
|
+
if (body !== undefined && ['GET', 'HEAD'].includes(upper)) {
|
|
824
|
+
throw new UsageError(`${upper} 请求不能带 --data(--data 仅用于写操作)`, { group: 'raw', action: upper });
|
|
825
|
+
}
|
|
586
826
|
|
|
587
827
|
if (flags['dry-run'] && !['GET', 'HEAD', 'OPTIONS'].includes(upper)) {
|
|
588
828
|
return dry(`${upper} ${path}${body !== undefined ? ` ${JSON.stringify(body)}` : ''}`);
|
|
@@ -643,7 +883,7 @@ function promptHidden(query) {
|
|
|
643
883
|
export async function setup(flags) {
|
|
644
884
|
const tty = Boolean(process.stdin.isTTY);
|
|
645
885
|
const need = (what) => {
|
|
646
|
-
if (!tty) throw new
|
|
886
|
+
if (!tty) throw new UsageError(`非交互模式请提供 ${what}`);
|
|
647
887
|
};
|
|
648
888
|
|
|
649
889
|
let url = flags.url;
|
|
@@ -652,7 +892,7 @@ export async function setup(flags) {
|
|
|
652
892
|
url = await promptLine('站点 URL(https://...):');
|
|
653
893
|
}
|
|
654
894
|
url = String(url).replace(/\/+$/, '');
|
|
655
|
-
if (!/^https:\/\//i.test(url)) throw new
|
|
895
|
+
if (!/^https:\/\//i.test(url)) throw new UsageError('URL 必须以 https:// 开头');
|
|
656
896
|
|
|
657
897
|
let user = flags.user;
|
|
658
898
|
if (!user) {
|
|
@@ -665,7 +905,7 @@ export async function setup(flags) {
|
|
|
665
905
|
need('--password');
|
|
666
906
|
password = await promptHidden('应用密码(输入不回显):');
|
|
667
907
|
}
|
|
668
|
-
if (!password) throw new
|
|
908
|
+
if (!password) throw new UsageError('应用密码为空');
|
|
669
909
|
|
|
670
910
|
const defaultName = (() => {
|
|
671
911
|
try {
|
|
@@ -685,7 +925,7 @@ export async function setup(flags) {
|
|
|
685
925
|
mkdirSync(dir, { recursive: true });
|
|
686
926
|
const file = join(dir, `${name}.env`);
|
|
687
927
|
if (existsSync(file) && !flags.force) {
|
|
688
|
-
throw new
|
|
928
|
+
throw new UsageError(`已存在 ${file};要覆盖请加 --force`);
|
|
689
929
|
}
|
|
690
930
|
|
|
691
931
|
const content = [
|
package/lib/env.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { UsageError } from './errors.js';
|
|
4
5
|
|
|
5
6
|
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
7
|
|
|
@@ -44,7 +45,7 @@ export function parseDotenv(text) {
|
|
|
44
45
|
|
|
45
46
|
function assertSiteName(site) {
|
|
46
47
|
if (site && !/^[A-Za-z0-9._-]+$/.test(site)) {
|
|
47
|
-
throw new
|
|
48
|
+
throw new UsageError(`非法站点名 "${site}":只允许字母、数字、点、下划线、连字符`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// 用法/参数错误:表示“命令调用方式不对”(缺参数、未知动作、缺少必填开关等),
|
|
2
|
+
// 与运行期错误(HTTP/网络/配置)区分开,由 CLI 以退出码 2 处理并附带用法提示。
|
|
3
|
+
export class UsageError extends Error {
|
|
4
|
+
constructor(message, meta = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'UsageError';
|
|
7
|
+
this.group = meta.group;
|
|
8
|
+
this.action = meta.action;
|
|
9
|
+
}
|
|
10
|
+
}
|