wpops 0.1.1 → 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 +92 -6
- 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 +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/abilities.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Abilities API 命令(/wp-json/wp-abilities/v1/abilities)。
|
|
2
|
+
// 需要站点安装并启用 Abilities API 插件。
|
|
3
|
+
import { printJson, dry, parseJson } from './util.js';
|
|
4
|
+
|
|
5
|
+
const ABILITIES = '/wp-json/wp-abilities/v1/abilities';
|
|
6
|
+
|
|
7
|
+
/** 端点可能返回数组、{ abilities: [...] } 或按名字索引的对象,统一成数组。 */
|
|
8
|
+
function asList(data) {
|
|
9
|
+
if (Array.isArray(data)) return data;
|
|
10
|
+
if (Array.isArray(data?.abilities)) return data.abilities;
|
|
11
|
+
if (data && typeof data === 'object') return Object.values(data);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 列表:GET /abilities。字段可能缺失,逐项容错。 */
|
|
16
|
+
export async function listAbilities(client, flags) {
|
|
17
|
+
const { data } = await client.request('GET', ABILITIES);
|
|
18
|
+
if (flags.json) return printJson(data);
|
|
19
|
+
const items = asList(data);
|
|
20
|
+
console.log(`Abilities ${items.length} 个:`);
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const name = item?.name ?? item?.id ?? '?';
|
|
23
|
+
const label = item?.label ?? '';
|
|
24
|
+
const description = item?.description ?? '';
|
|
25
|
+
console.log(` ${name} ${label} ${description}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 详情:GET /abilities/{name}。 */
|
|
30
|
+
export async function getAbility(client, name, flags) {
|
|
31
|
+
const { data } = await client.request('GET', `${ABILITIES}/${encodeURIComponent(name)}`);
|
|
32
|
+
if (flags.json) return printJson(data);
|
|
33
|
+
console.log(`${data?.name ?? name} ${data?.label ?? ''}`);
|
|
34
|
+
if (data?.description) console.log(` 描述:${data.description}`);
|
|
35
|
+
if (data?.input_schema) console.log(` 输入:${JSON.stringify(data.input_schema)}`);
|
|
36
|
+
if (data?.output_schema) console.log(` 输出:${JSON.stringify(data.output_schema)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 运行:POST /abilities/{name}/run。
|
|
41
|
+
* body 来自 --input 或 --data(JSON 字符串),都没有则 {}。
|
|
42
|
+
*/
|
|
43
|
+
export async function runAbility(client, name, flags) {
|
|
44
|
+
const raw = flags.input !== undefined ? flags.input : flags.data;
|
|
45
|
+
const body = raw !== undefined ? parseJson(raw) : {};
|
|
46
|
+
if (flags['dry-run']) return dry(`运行 Ability ${name} ${JSON.stringify(body)}`);
|
|
47
|
+
const { data } = await client.request('POST', `${ABILITIES}/${encodeURIComponent(name)}/run`, { body });
|
|
48
|
+
if (flags.json) return printJson(data);
|
|
49
|
+
console.log(`✓ 已运行 ${name}`);
|
|
50
|
+
console.log(JSON.stringify(data, null, 2));
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// 用户应用密码(/wp-json/wp/v2/users/<user|me>/application-passwords)。
|
|
2
|
+
// 创建是唯一会返回明文 password 的接口,该字段只在此处打印一次。
|
|
3
|
+
import { printJson, dry } from './util.js';
|
|
4
|
+
import { UsageError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
function appPasswordsPath(user) {
|
|
7
|
+
return `/wp-json/wp/v2/users/${user || 'me'}/application-passwords`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function listAppPasswords(client, user, flags) {
|
|
11
|
+
const base = appPasswordsPath(user);
|
|
12
|
+
const { data } = await client.request('GET', base);
|
|
13
|
+
if (flags.json) return printJson(data);
|
|
14
|
+
console.log(`应用密码 ${data.length} 个:`);
|
|
15
|
+
for (const p of data) {
|
|
16
|
+
console.log(` ${p.uuid} ${p.name} ${p.last_used ?? '—'} ${p.created ?? '—'}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createAppPassword(client, user, flags) {
|
|
21
|
+
if (flags.name === undefined) throw new UsageError('创建应用密码需要 --name');
|
|
22
|
+
const body = { name: String(flags.name) };
|
|
23
|
+
if (flags['app-id'] !== undefined) body.app_id = String(flags['app-id']);
|
|
24
|
+
const base = appPasswordsPath(user);
|
|
25
|
+
if (flags['dry-run']) return dry(`创建应用密码 ${body.name}`);
|
|
26
|
+
const { data } = await client.request('POST', base, { body });
|
|
27
|
+
if (flags.json) return printJson(data);
|
|
28
|
+
console.log(`${data.uuid} ${data.name}`);
|
|
29
|
+
console.log(`password: ${data.password}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deleteAppPassword(client, user, uuid, flags) {
|
|
33
|
+
const base = appPasswordsPath(user);
|
|
34
|
+
if (flags['dry-run']) return dry(`删除应用密码 ${uuid}`);
|
|
35
|
+
if (!flags.force) throw new UsageError('删除应用密码需要 --force(不可撤销)');
|
|
36
|
+
const { data } = await client.request('DELETE', `${base}/${uuid}`);
|
|
37
|
+
if (flags.json) return printJson(data);
|
|
38
|
+
console.log(data.deleted ? `✓ 已删除应用密码 ${uuid}` : `? 应用密码 ${uuid} 未删除`);
|
|
39
|
+
}
|
package/lib/args.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// 已知的布尔开关:遇到它们不吞下一个参数,避免 `--json -s a` 把 -s 当值。
|
|
2
|
-
const BOOLEAN_FLAGS = new Set([
|
|
2
|
+
const BOOLEAN_FLAGS = new Set([
|
|
3
|
+
'json', 'force', 'activate', 'dry-run', 'yes', 'all', 'help', 'version',
|
|
4
|
+
'csv', 'table', 'quiet', 'verbose', 'all-pages', 'paginate', 'auto-add',
|
|
5
|
+
]);
|
|
3
6
|
|
|
4
7
|
/** 解析命令行。支持 --flag value、--flag=value、布尔 --flag、以及 -s/-a/-h/-y。 */
|
|
5
8
|
export function parseArgs(argv) {
|
|
@@ -53,37 +56,5 @@ export function parseArgs(argv) {
|
|
|
53
56
|
return { positionals, flags };
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
/** 判断命令是否会修改站点数据(用于 --all 护栏与 --dry-run 提示)。 */
|
|
59
|
-
export function isMutation(group, action) {
|
|
60
|
-
const act = action || '';
|
|
61
|
-
switch (group) {
|
|
62
|
-
case 'posts':
|
|
63
|
-
case 'post':
|
|
64
|
-
case 'pages':
|
|
65
|
-
case 'page':
|
|
66
|
-
case 'categories':
|
|
67
|
-
case 'category':
|
|
68
|
-
case 'tags':
|
|
69
|
-
case 'tag':
|
|
70
|
-
return CONTENT_MUTATIONS.has(act);
|
|
71
|
-
case 'media':
|
|
72
|
-
return ['upload', 'update', 'edit', 'delete', 'rm'].includes(act);
|
|
73
|
-
case 'plugins':
|
|
74
|
-
case 'plugin':
|
|
75
|
-
return ['install', 'activate', 'deactivate'].includes(act);
|
|
76
|
-
case 'themes':
|
|
77
|
-
case 'theme':
|
|
78
|
-
return ['activate', 'switch'].includes(act);
|
|
79
|
-
case 'comments':
|
|
80
|
-
case 'comment':
|
|
81
|
-
return ['update', 'edit', 'delete', 'rm'].includes(act);
|
|
82
|
-
case 'users':
|
|
83
|
-
return ['create', 'update', 'edit', 'delete', 'rm'].includes(act);
|
|
84
|
-
case 'raw':
|
|
85
|
-
return !['GET', 'HEAD', 'OPTIONS'].includes(act.toUpperCase());
|
|
86
|
-
default:
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
59
|
+
// 写操作判定统一来自 lib/spec.js(单一事实来源),这里只做转发。
|
|
60
|
+
export { isMutation, groupByName } from './spec.js';
|
package/lib/batch.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// 批量请求命令(/wp-json/batch/v1)。
|
|
2
|
+
// 一次提交多条子请求,减少往返。
|
|
3
|
+
import { printJson, dry, parseJson, readJsonFile } from './util.js';
|
|
4
|
+
import { UsageError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const BATCH = '/wp-json/batch/v1';
|
|
7
|
+
|
|
8
|
+
const NEED =
|
|
9
|
+
'batch 需要 --requests JSON 数组 或 --requests-file 文件';
|
|
10
|
+
|
|
11
|
+
function readRequests(flags) {
|
|
12
|
+
if (flags.requests === undefined && flags['requests-file'] === undefined) return undefined;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = flags.requests !== undefined
|
|
15
|
+
? parseJson(flags.requests)
|
|
16
|
+
: readJsonFile(flags['requests-file']);
|
|
17
|
+
// 同时接受裸数组和 {"requests": [...]} 两种写法。
|
|
18
|
+
return Array.isArray(parsed?.requests) ? parsed.requests : parsed;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new UsageError(`${NEED}(${err.message})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** POST /batch/v1,body 为 { requests }。 */
|
|
25
|
+
export async function batch(client, flags) {
|
|
26
|
+
const requests = readRequests(flags);
|
|
27
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
28
|
+
throw new UsageError(NEED);
|
|
29
|
+
}
|
|
30
|
+
const BATCH_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
31
|
+
const bad = requests.find(
|
|
32
|
+
(r) => r && r.method && !BATCH_METHODS.includes(String(r.method).toUpperCase()),
|
|
33
|
+
);
|
|
34
|
+
if (bad) {
|
|
35
|
+
throw new UsageError(`batch 不支持 ${bad.method}:WordPress 批量端点只接受 ${BATCH_METHODS.join('/')}`);
|
|
36
|
+
}
|
|
37
|
+
if (flags['dry-run']) return dry(`批量请求 ${requests.length} 条`);
|
|
38
|
+
const { data } = await client.request('POST', BATCH, { body: { requests } });
|
|
39
|
+
if (flags.json) return printJson(data);
|
|
40
|
+
|
|
41
|
+
const responses = Array.isArray(data?.responses)
|
|
42
|
+
? data.responses
|
|
43
|
+
: Array.isArray(data)
|
|
44
|
+
? data
|
|
45
|
+
: [];
|
|
46
|
+
console.log(`批量返回 ${responses.length} 条:`);
|
|
47
|
+
responses.forEach((response, index) => {
|
|
48
|
+
const request = requests[index] || {};
|
|
49
|
+
const status = response?.status ?? request.status ?? '?';
|
|
50
|
+
const method = response?.method ?? request.method ?? '?';
|
|
51
|
+
const path = response?.path ?? request.path ?? '?';
|
|
52
|
+
console.log(` [${status}] ${method} ${path}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
package/lib/blocks.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 可复用区块(wp_block):/wp-json/wp/v2/blocks。
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { stripTags, printJson, dry, listRequest } from './util.js';
|
|
5
|
+
import { UsageError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
const BLOCKS = '/wp-json/wp/v2/blocks';
|
|
8
|
+
|
|
9
|
+
export async function listBlocks(client, flags) {
|
|
10
|
+
const query = {
|
|
11
|
+
per_page: flags['per-page'] ?? 20,
|
|
12
|
+
page: flags.page,
|
|
13
|
+
search: flags.search,
|
|
14
|
+
status: flags.status,
|
|
15
|
+
context: 'edit',
|
|
16
|
+
};
|
|
17
|
+
const { data, total } = await listRequest(client, BLOCKS, query, flags);
|
|
18
|
+
if (flags.json) return printJson(data);
|
|
19
|
+
console.log(`区块 ${total ?? data.length} 个:`);
|
|
20
|
+
for (const b of data) {
|
|
21
|
+
console.log(` #${b.id} [${b.status}] ${stripTags(b.title?.raw ?? b.title?.rendered ?? '(无标题)')}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getBlock(client, id, flags) {
|
|
26
|
+
const { data } = await client.request('GET', `${BLOCKS}/${Number(id)}`, {
|
|
27
|
+
query: { context: flags.context || 'edit' },
|
|
28
|
+
});
|
|
29
|
+
if (flags.json) return printJson(data);
|
|
30
|
+
console.log(`#${data.id} [${data.status}] ${stripTags(data.title?.raw ?? data.title?.rendered ?? '')}`);
|
|
31
|
+
console.log('─'.repeat(44));
|
|
32
|
+
console.log(data.content?.raw ?? stripTags(data.content?.rendered || ''));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function blockBody(flags) {
|
|
36
|
+
const body = {};
|
|
37
|
+
if (flags.title !== undefined) body.title = String(flags.title);
|
|
38
|
+
if (flags['from-file'] !== undefined) body.content = readFileSync(String(flags['from-file']), 'utf8');
|
|
39
|
+
else if (flags.content !== undefined) body.content = String(flags.content);
|
|
40
|
+
if (flags.status !== undefined) body.status = String(flags.status);
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function createBlock(client, flags) {
|
|
45
|
+
const body = blockBody(flags);
|
|
46
|
+
if (body.title === undefined && body.content === undefined) {
|
|
47
|
+
throw new UsageError('创建区块至少需要 --title 或 --content');
|
|
48
|
+
}
|
|
49
|
+
if (flags['dry-run']) return dry(`创建区块 ${JSON.stringify(body)}`);
|
|
50
|
+
const { data } = await client.request('POST', BLOCKS, { body });
|
|
51
|
+
if (flags.json) return printJson(data);
|
|
52
|
+
console.log(`✓ 已创建区块 #${data.id} [${data.status}]`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function updateBlock(client, id, flags) {
|
|
56
|
+
const body = blockBody(flags);
|
|
57
|
+
if (!Object.keys(body).length) {
|
|
58
|
+
throw new UsageError('没有可更新的字段(--title/--content/--from-file/--status)');
|
|
59
|
+
}
|
|
60
|
+
if (flags['dry-run']) return dry(`更新区块 #${id} ${JSON.stringify(body)}`);
|
|
61
|
+
const { data } = await client.request('POST', `${BLOCKS}/${Number(id)}`, { body });
|
|
62
|
+
if (flags.json) return printJson(data);
|
|
63
|
+
console.log(`✓ 已更新区块 #${data.id}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function deleteBlock(client, id, flags) {
|
|
67
|
+
if (flags['dry-run']) return dry(`删除区块 #${id}`);
|
|
68
|
+
if (!flags.force) throw new UsageError('删除区块需要 --force(不可撤销)');
|
|
69
|
+
const { data } = await client.request('DELETE', `${BLOCKS}/${Number(id)}`, { query: { force: 'true' } });
|
|
70
|
+
if (flags.json) return printJson(data);
|
|
71
|
+
console.log(data.deleted ? `✓ 已删除区块 #${id}` : `? 区块 #${id} 未删除`);
|
|
72
|
+
}
|