wpops 0.1.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/env.js ADDED
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
6
+
7
+ /**
8
+ * 配置根目录。
9
+ * - 默认:包目录(git clone / 本地开发,配置文件就在仓库里)
10
+ * - 可用 WPOPS_HOME 覆盖(推荐给 npm 全局安装的用户,例如 ~/.config/wpops)
11
+ */
12
+ export function configHome() {
13
+ return process.env.WPOPS_HOME ? resolve(process.env.WPOPS_HOME) : pkgRoot;
14
+ }
15
+
16
+ function sitesDir() {
17
+ return resolve(configHome(), 'sites');
18
+ }
19
+
20
+ function rootEnvFile() {
21
+ return resolve(configHome(), '.env');
22
+ }
23
+
24
+ /** 解析 .env 文本为对象(导出以便测试)。 */
25
+ export function parseDotenv(text) {
26
+ const vars = {};
27
+ for (const rawLine of text.split(/\r?\n/)) {
28
+ const line = rawLine.trim();
29
+ if (!line || line.startsWith('#')) continue;
30
+ const eq = line.indexOf('=');
31
+ if (eq === -1) continue;
32
+ const key = line.slice(0, eq).trim();
33
+ let value = line.slice(eq + 1).trim();
34
+ if (
35
+ (value.startsWith('"') && value.endsWith('"')) ||
36
+ (value.startsWith("'") && value.endsWith("'"))
37
+ ) {
38
+ value = value.slice(1, -1);
39
+ }
40
+ vars[key] = value;
41
+ }
42
+ return vars;
43
+ }
44
+
45
+ function assertSiteName(site) {
46
+ if (site && !/^[A-Za-z0-9._-]+$/.test(site)) {
47
+ throw new Error(`非法站点名 "${site}":只允许字母、数字、点、下划线、连字符`);
48
+ }
49
+ }
50
+
51
+ /** 某站点对应哪个配置文件。site 为空→根 .env;有值→sites/<site>.env。 */
52
+ export function envFileFor(site) {
53
+ if (process.env.WPOPS_ENV) return process.env.WPOPS_ENV;
54
+ if (site) return resolve(sitesDir(), `${site}.env`);
55
+ return rootEnvFile();
56
+ }
57
+
58
+ /** 列出所有已配置站点(默认站 + sites/ 下的名字)。 */
59
+ export function listSites() {
60
+ const sites = [];
61
+ if (existsSync(rootEnvFile())) {
62
+ sites.push({ name: null, label: '(default)', file: rootEnvFile() });
63
+ }
64
+ const dir = sitesDir();
65
+ if (existsSync(dir)) {
66
+ for (const file of readdirSync(dir).sort()) {
67
+ if (file.endsWith('.env')) {
68
+ const name = file.slice(0, -4);
69
+ sites.push({ name, label: name, file: resolve(dir, file) });
70
+ }
71
+ }
72
+ }
73
+ return sites;
74
+ }
75
+
76
+ /**
77
+ * 读取配置。
78
+ * - 指定 site → 只读 sites/<site>.env(不叠加环境变量,避免多站串味)
79
+ * - 未指定,但有根 .env → 读根 .env,允许真实环境变量覆盖
80
+ * - 未指定,且根 .env 不存在 → 若 sites/ 下只有一个站点,就当作默认站
81
+ */
82
+ export function loadEnv(site) {
83
+ assertSiteName(site);
84
+ let effective = site || null;
85
+
86
+ if (!effective && !process.env.WPOPS_ENV && !existsSync(rootEnvFile())) {
87
+ const named = listSites();
88
+ if (named.length === 1) effective = named[0].name;
89
+ }
90
+
91
+ const file = envFileFor(effective);
92
+ if (!existsSync(file)) {
93
+ if (effective) throw new Error(`找不到站点配置 "${effective}":${file}`);
94
+ return { ...process.env };
95
+ }
96
+
97
+ const fromFile = parseDotenv(readFileSync(file, 'utf8'));
98
+ return effective ? fromFile : { ...fromFile, ...process.env };
99
+ }
package/lib/skill.js ADDED
@@ -0,0 +1,37 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
7
+
8
+ /** 内置 skill 的源文件路径。 */
9
+ export function skillSource() {
10
+ return join(pkgRoot, 'skills', 'wpops', 'SKILL.md');
11
+ }
12
+
13
+ /** skill 要安装到的目录(可通过 WPOPS_SKILL_DIR 覆盖)。 */
14
+ export function skillTargets() {
15
+ const targets = [];
16
+ if (process.env.WPOPS_SKILL_DIR) targets.push(resolve(process.env.WPOPS_SKILL_DIR));
17
+ targets.push(join(homedir(), '.agents', 'skills', 'wpops'));
18
+ return targets;
19
+ }
20
+
21
+ /** 把内置 skill 复制到发现目录,返回成功写入的目录列表。best-effort。 */
22
+ export function installSkill() {
23
+ if (process.env.WPOPS_SKIP_SKILL) return [];
24
+ const src = skillSource();
25
+ const installed = [];
26
+ if (!existsSync(src)) return installed;
27
+ for (const dir of skillTargets()) {
28
+ try {
29
+ mkdirSync(dir, { recursive: true });
30
+ copyFileSync(src, join(dir, 'SKILL.md'));
31
+ installed.push(dir);
32
+ } catch {
33
+ // 忽略:安装脚本不应因写 skill 失败而中断
34
+ }
35
+ }
36
+ return installed;
37
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "wpops",
3
+ "version": "0.1.0",
4
+ "description": "WordPress REST operations toolkit — manage posts, pages, media, comments, users and plugins from the CLI. Multi-site, safety-first, zero dependencies.",
5
+ "license": "MIT",
6
+ "author": "qnnp",
7
+ "type": "module",
8
+ "bin": {
9
+ "wpops": "bin/wpops.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "lib",
14
+ "scripts",
15
+ "skills",
16
+ ".env.example",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "wordpress",
21
+ "wp",
22
+ "rest-api",
23
+ "wp-json",
24
+ "cli",
25
+ "application-password",
26
+ "content-management",
27
+ "multi-site"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/qnnp-me/wpops.git"
32
+ },
33
+ "homepage": "https://github.com/qnnp-me/wpops#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/qnnp-me/wpops/issues"
36
+ },
37
+ "scripts": {
38
+ "setup": "node bin/wpops.js setup",
39
+ "doctor": "node bin/wpops.js doctor",
40
+ "help": "node bin/wpops.js help",
41
+ "postinstall": "node scripts/postinstall.mjs",
42
+ "test": "node --test"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ }
47
+ }
@@ -0,0 +1,15 @@
1
+ // npm postinstall:把内置 skill 安装到 agent 的发现目录。
2
+ // 任何失败都不应中断 npm 安装本身。
3
+ import { installSkill } from '../lib/skill.js';
4
+
5
+ try {
6
+ const installed = installSkill();
7
+ for (const dir of installed) {
8
+ console.log(`wpops: skill 已安装到 ${dir}`);
9
+ }
10
+ if (!installed.length) {
11
+ console.log('wpops: 未自动安装 skill(可用 `wpops install-skill` 手动安装)');
12
+ }
13
+ } catch {
14
+ // 静默:postinstall 出错不影响安装
15
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: wpops
3
+ description: Use when managing a WordPress site through wpops, the REST CLI (npm package `wpops`, command `wpops`) — listing/creating/updating/deleting posts, pages, media, categories, tags, comments, or users; installing/activating/deactivating plugins; switching themes; configuring a site; or running operations across multiple configured sites. Covers the required safe workflow (doctor first, --dry-run before writes, --all needs --yes) and the REST capability boundary. Prefer wpops over hand-written curl or raw HTTP for any WordPress task.
4
+ ---
5
+
6
+ # wpops —— WordPress REST 操作
7
+
8
+ ## 何时用
9
+ 用户要管理 WordPress 站点的**内容**或**插件/主题**时,优先用 `wpops`,不要手搓 curl/HTTP 请求。
10
+
11
+ ## 安装与位置
12
+ - 全局命令 `wpops`(`npm i -g wpops` 安装;其 postinstall 会顺带装好本 skill)。
13
+ - 未安装时也可一次性运行:`npx wpops ...`。
14
+ - 配置目录:默认是包目录下的 `sites/`;若设置了环境变量 `WPOPS_HOME`,则读 `$WPOPS_HOME/sites/`。
15
+ - 凭据:`sites/<名字>.env`。**不要读取、不要打印其中的应用密码。**
16
+ - 首次配置:`wpops setup`(交互式写入凭据并自动体检)。
17
+
18
+ ## 站点与多站
19
+ - `wpops sites` — 列出已配置站点
20
+ - `wpops -s <名字> ...` — 指定站点;只有一个站点时可省略 `-s`
21
+ - `wpops --all ...` — 对所有站点批量执行
22
+
23
+ ## 安全流程(必须遵守)
24
+ 1. 首次操作某站先跑 `wpops doctor`(检查连接与权限;失败返回非 0)。
25
+ 2. 任何写操作先加 `--dry-run` 预览目标与内容。
26
+ 3. `--all` 的**写操作必须加 `--yes`**,否则会被拒绝。
27
+ 4. 需要程序化解析输出时加 `--json`。
28
+ 5. 应用密码绝不写进命令、日志或对话。
29
+ 6. 破坏性操作(`delete --force`、分类/用户删除)先确认目标存在且正确。
30
+
31
+ ## 常用命令
32
+ ```
33
+ wpops doctor
34
+ wpops setup
35
+ wpops posts list|get|create|update|delete [--per-page N] [--page N] [--status s] [--search 词] [--orderby date] [--order desc] [--categories 1,2]
36
+ wpops pages ... # 同 posts
37
+ wpops media list|get|upload|update|delete [--alt "文本"]
38
+ wpops categories list|create|update|delete ...
39
+ wpops tags list|create|update|delete ...
40
+ wpops comments list|get|update|delete [--status hold|approved|spam|trash]
41
+ wpops users list|get|create|update|delete|me ...
42
+ wpops plugins list|install <别名>|activate <plugin>|deactivate <plugin>
43
+ wpops themes list|activate <stylesheet>
44
+ wpops install-skill # 手动补装本 skill
45
+ wpops raw <METHOD> <path> [--data JSON] # 兜底:任意端点
46
+ ```
47
+
48
+ ## 能力边界(不要向用户承诺)
49
+ - **REST 做不到**:插件/主题/核心**更新**、备份/还原、安全扫描、性能优化、文件系统操作。
50
+ → 这些需要 WP-CLI + SSH,或 MainWP。详见 skill `wp-wpcli-and-ops`。
51
+ - **能做的**:文章/页面/媒体/分类/标签/评论/用户的增删改查、插件安装与启停、主题切换、连接与权限体检。
52
+
53
+ ## 相关 skill
54
+ `wp-rest-api`(REST 端点)、`wp-wpcli-and-ops`(WP-CLI/SSH 运维)、`wp-performance`(性能)、`wordpress-router`(项目分类入口)。