wpops 0.3.1 → 0.4.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 CHANGED
@@ -23,8 +23,8 @@ WordPress REST 操作工具。只依赖 Node 内置能力(无第三方包),用
23
23
  **最快:一条命令装好(含 skill)**
24
24
 
25
25
  ```powershell
26
- npm i -g wpops # 装 CLI;postinstall 会自动把 skill 装到 ~/.agents/skills/wpops
27
- wpops setup # 交互式配置站点:问 URL / 用户名 / 应用密码,写 sites/<名字>.env 并自动体检
26
+ npm i -g wpops # 装 CLI(无 postinstall,不会被 pnpm 等的「构建脚本确认」拦住)
27
+ wpops setup # 配置站点 + 自动装好内置 skill(写到 ~/.agents/skills/wpops)
28
28
  ```
29
29
 
30
30
  第二条是配置站点(密码必须你亲自输入,不回显)。之后即可 `wpops posts list` 等。
@@ -60,9 +60,9 @@ curl -fsSL https://raw.githubusercontent.com/qnnp-me/wpops/main/install.sh | sh
60
60
  npm publish
61
61
  ```
62
62
 
63
- > 凭据不进仓库 / 包:`sites/*.env` 与 `.env` 已忽略,且 `package.json` 的 `files`
64
- > 白名单不含 `sites/`。每台机器各自 `wpops setup`,工具与 skill 都不携带密钥。
65
- > `WPOPS_SKIP_SKILL=1 npm i -g wpops` 可跳过自动装 skill;`wpops install-skill` 可手动补装。
63
+ > 凭据不进仓库 / 包:配置默认写在**用户目录**(见「1. 配置」),`package.json` 的 `files`
64
+ > 白名单也不含它。每台机器各自 `wpops setup`,工具与 skill 都不携带密钥。
65
+ > `WPOPS_SKIP_SKILL=1` 可跳过装 skill;`wpops install-skill` 可手动装/刷新。安装不再用 postinstall,免得被 pnpm 等包管理器的脚本确认拦住。
66
66
 
67
67
  ### 本机(开发) vs 其他机器(使用)
68
68
 
@@ -88,15 +88,13 @@ WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
88
88
 
89
89
  > `sites/*.env` 已在 `.gitignore` 中,不会被提交。
90
90
 
91
- **配置位置**:默认读**包目录**下的 `.env` / `sites/`(git clone 就在仓库里)。
92
- 若用 `npm i -g wpops` 全局安装,建议设环境变量 `WPOPS_HOME` 指向固定目录,
93
- 把配置放那里,升级/重装不会丢:
91
+ **配置位置**:默认在**用户级稳定目录**,升级/重装不会丢:
94
92
 
95
- ```powershell
96
- # 例如(Windows)
97
- setx WPOPS_HOME "%USERPROFILE%\.config\wpops"
98
- # 然后把 sites\<名字>.env 放到该目录下
99
- ```
93
+ - Windows:`%APPDATA%\wpops`(即 `C:\Users\<你>\AppData\Roaming\wpops`)
94
+ - macOS/Linux:`$XDG_CONFIG_HOME/wpops` 或 `~/.config/wpops`
95
+
96
+ 可用环境变量 **`WPOPS_HOME`** 覆盖(开发时想放仓库内,指向仓库即可)。
97
+ 旧版本把配置放在**包目录**里、升级会被清掉;wpops 首次运行会自动把旧配置**迁移**到上面的用户目录,旧文件保留、可自行删除。
100
98
 
101
99
  ## 2. 先体检
102
100
 
@@ -249,12 +247,12 @@ wpops --all plugins activate akismet/akismet --yes # 写操作必须 --yes
249
247
 
250
248
  ## 4. 多站点
251
249
 
252
- 所有站点都放在 `sites/`,一个站点一个文件:
250
+ 所有站点都放在**配置目录**的 `sites/` 下,一个站点一个文件(默认配置目录见「1. 配置」,如 Windows 的 `%APPDATA%\wpops`):
253
251
 
254
252
  ```
255
- App\wpops\
253
+ %APPDATA%\wpops\ (macOS/Linux:~/.config/wpops/)
256
254
  └── sites\
257
- ├── qnnp.env ← 站点:qnnp.me
255
+ ├── qnnp.me.env ← 站点:qnnp.me
258
256
  ├── blog-a.env
259
257
  └── blog-b.env
260
258
  ```
package/bin/wpops.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, writeFileSync, rmSync, renameSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
- import { loadEnv, listSites, configHome } from '../lib/env.js';
4
+ import { loadEnv, listSites, configHome, migrateLegacyConfig } from '../lib/env.js';
5
5
  import { WpError } from '../lib/client.js';
6
6
  import { UsageError } from '../lib/errors.js';
7
7
  import { parseArgs, isMutation } from '../lib/args.js';
8
- import { installSkill } from '../lib/skill.js';
8
+ import { installSkill, skillStale } from '../lib/skill.js';
9
9
  import { configureOutput } from '../lib/util.js';
10
10
  import { runOne } from '../lib/run.js';
11
11
  import { generalHelp, groupHelp, commandsJson, usageHint } from '../lib/help.js';
@@ -178,6 +178,26 @@ async function main() {
178
178
  return;
179
179
  }
180
180
 
181
+ // 首次运行:把旧版「包目录内」的配置迁到稳定目录,并静默刷新内置 skill。
182
+ // best-effort;不污染 --json(提示走 stderr),--quiet 静默;测试环境跳过。
183
+ if (!process.env.NODE_TEST_CONTEXT) {
184
+ try {
185
+ const moved = migrateLegacyConfig();
186
+ if (moved && !flags.quiet) {
187
+ console.error(
188
+ `注意:已把 ${moved} 个旧配置从包目录迁移到 ${configHome()}(以后升级不再丢;旧文件仍在,可自行删除)`,
189
+ );
190
+ }
191
+ } catch {
192
+ // 忽略
193
+ }
194
+ try {
195
+ if (skillStale()) installSkill({ quiet: true });
196
+ } catch {
197
+ // 忽略
198
+ }
199
+ }
200
+
181
201
  if (group === 'sites') {
182
202
  await withOutput(flags, () => sitesCommand(action, rest));
183
203
  return;
package/lib/commands.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
3
  import { createInterface } from 'node:readline';
4
- import { loadEnv, configHome } from './env.js';
4
+ import { loadEnv, configHome, hardenConfigPerms } from './env.js';
5
+ import { installSkill } from './skill.js';
5
6
  import { buildConfig, createClient, WpError } from './client.js';
6
7
  import { stripTags, printJson, dry, warn, pick, listRequest, parseJson, readJsonFile } from './util.js';
7
8
  import { UsageError } from './errors.js';
@@ -988,8 +989,14 @@ export async function setup(flags) {
988
989
  '',
989
990
  ].join('\n');
990
991
  writeFileSync(file, content, 'utf8');
992
+ hardenConfigPerms();
991
993
 
992
- console.log(`\n✓ 已写入 ${file}\n正在体检...`);
994
+ const skillDirs = installSkill();
995
+ const skillNote = skillDirs.length
996
+ ? `内置 skill 已安装到 ${skillDirs.join(', ')}`
997
+ : '内置 skill 未安装(可用 wpops install-skill)';
998
+
999
+ console.log(`\n✓ 已写入 ${file}\n✓ ${skillNote}\n正在体检...`);
993
1000
  const cfg = buildConfig(loadEnv(name));
994
1001
  const client = createClient(cfg);
995
1002
  return doctor(client, cfg, {});
package/lib/env.js CHANGED
@@ -1,17 +1,104 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
1
+ import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { UsageError } from './errors.js';
5
6
 
6
7
  const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
7
8
 
9
+ /**
10
+ * 用户级配置目录(稳定、升级不丢):
11
+ * - Windows:%APPDATA%\wpops
12
+ * - 其他:$XDG_CONFIG_HOME/wpops 或 ~/.config/wpops
13
+ */
14
+ export function userConfigHome() {
15
+ if (process.platform === 'win32' && process.env.APPDATA) {
16
+ return resolve(process.env.APPDATA, 'wpops');
17
+ }
18
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
19
+ return resolve(base, 'wpops');
20
+ }
21
+
8
22
  /**
9
23
  * 配置根目录。
10
- * - 默认:包目录(git clone / 本地开发,配置文件就在仓库里)
11
- * - 可用 WPOPS_HOME 覆盖(推荐给 npm 全局安装的用户,例如 ~/.config/wpops)
24
+ * - 默认:用户级稳定目录(见 userConfigHome)。**不再默认用包目录**——升级会整目录替换包,配置会被清掉。
25
+ * - `WPOPS_HOME` 覆盖(开发时想用仓库内目录,把它设成仓库路径即可)。
12
26
  */
13
27
  export function configHome() {
14
- return process.env.WPOPS_HOME ? resolve(process.env.WPOPS_HOME) : pkgRoot;
28
+ return process.env.WPOPS_HOME ? resolve(process.env.WPOPS_HOME) : userConfigHome();
29
+ }
30
+
31
+ function hasConfig(dir) {
32
+ return existsSync(join(dir, '.env')) || existsSync(join(dir, 'sites'));
33
+ }
34
+
35
+ /**
36
+ * 把旧版「包目录内」的配置搬到稳定的用户目录。
37
+ * 仅当:未显式指定 WPOPS_HOME/WPOPS_ENV、旧目录有配置、新目录还没有配置时执行;best-effort。
38
+ * 返回迁移的条目数(0 表示无需/未迁移)。
39
+ */
40
+ export function migrateLegacyConfig() {
41
+ if (process.env.WPOPS_HOME || process.env.WPOPS_ENV) return 0;
42
+ const from = pkgRoot;
43
+ const to = userConfigHome();
44
+ if (resolve(from) === resolve(to)) return 0;
45
+ if (!hasConfig(from) || hasConfig(to)) return 0;
46
+
47
+ let migrated = 0;
48
+ try {
49
+ mkdirSync(join(to, 'sites'), { recursive: true });
50
+ const legacyRoot = join(from, '.env');
51
+ if (existsSync(legacyRoot)) {
52
+ cpSync(legacyRoot, join(to, '.env'));
53
+ migrated++;
54
+ }
55
+ const legacySites = join(from, 'sites');
56
+ if (existsSync(legacySites)) {
57
+ for (const file of readdirSync(legacySites)) {
58
+ if (!file.endsWith('.env')) continue;
59
+ cpSync(join(legacySites, file), join(to, 'sites', file));
60
+ migrated++;
61
+ }
62
+ }
63
+ if (migrated) hardenConfigPerms();
64
+ } catch {
65
+ return 0;
66
+ }
67
+ return migrated;
68
+ }
69
+
70
+ /** unix 下收紧配置权限(里面有应用密码)。Windows 跳过(用默认 ACL)。 */
71
+ export function hardenConfigPerms() {
72
+ if (process.platform === 'win32') return;
73
+ const home = configHome();
74
+ try {
75
+ chmodSync(resolve(home), 0o700);
76
+ } catch {
77
+ // 目录可能刚被删/无权限,忽略
78
+ }
79
+ const sites = resolve(home, 'sites');
80
+ if (existsSync(sites)) {
81
+ try {
82
+ chmodSync(sites, 0o700);
83
+ } catch {
84
+ // 忽略
85
+ }
86
+ try {
87
+ for (const file of readdirSync(sites)) {
88
+ if (file.endsWith('.env')) chmodSync(join(sites, file), 0o600);
89
+ }
90
+ } catch {
91
+ // 忽略
92
+ }
93
+ }
94
+ const root = resolve(home, '.env');
95
+ if (existsSync(root)) {
96
+ try {
97
+ chmodSync(root, 0o600);
98
+ } catch {
99
+ // 忽略
100
+ }
101
+ }
15
102
  }
16
103
 
17
104
  function sitesDir() {
package/lib/skill.js CHANGED
@@ -1,4 +1,4 @@
1
- import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -18,8 +18,12 @@ export function skillTargets() {
18
18
  return targets;
19
19
  }
20
20
 
21
- /** 把内置 skill 复制到发现目录,返回成功写入的目录列表。best-effort。 */
22
- export function installSkill() {
21
+ /**
22
+ * 把内置 skill 复制到发现目录,返回成功写入的目录列表。best-effort。
23
+ * `quiet` 目前只是语义标记(本函数不打印);保留以便调用方统一传参。
24
+ */
25
+ export function installSkill({ quiet = false } = {}) {
26
+ void quiet;
23
27
  if (process.env.WPOPS_SKIP_SKILL) return [];
24
28
  const src = skillSource();
25
29
  const installed = [];
@@ -30,8 +34,33 @@ export function installSkill() {
30
34
  copyFileSync(src, join(dir, 'SKILL.md'));
31
35
  installed.push(dir);
32
36
  } catch {
33
- // 忽略:安装脚本不应因写 skill 失败而中断
37
+ // 忽略:安装 skill 失败不应中断命令
34
38
  }
35
39
  }
36
40
  return installed;
37
41
  }
42
+
43
+ /**
44
+ * 已安装的 skill 是否缺失或与内置版本不一致(用于启动时静默刷新)。
45
+ * 只比对 SKILL.md;任一目标缺失/内容不同即认为需要刷新。
46
+ */
47
+ export function skillStale() {
48
+ if (process.env.WPOPS_SKIP_SKILL) return false;
49
+ const src = skillSource();
50
+ if (!existsSync(src)) return false;
51
+ let want;
52
+ try {
53
+ want = readFileSync(src, 'utf8');
54
+ } catch {
55
+ return false;
56
+ }
57
+ for (const dir of skillTargets()) {
58
+ const file = join(dir, 'SKILL.md');
59
+ try {
60
+ if (!existsSync(file) || readFileSync(file, 'utf8') !== want) return true;
61
+ } catch {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wpops",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "WordPress REST operations toolkit — manage posts, pages, media, menus, comments, users, custom types, revisions, blocks, templates, widgets, settings and plugins from the CLI. Multi-site, safety-first, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "qnnp",
@@ -11,7 +11,6 @@
11
11
  "files": [
12
12
  "bin",
13
13
  "lib",
14
- "scripts",
15
14
  "skills",
16
15
  ".env.example",
17
16
  "README.md"
@@ -39,7 +38,6 @@
39
38
  "setup": "node bin/wpops.js setup",
40
39
  "doctor": "node bin/wpops.js doctor",
41
40
  "help": "node bin/wpops.js help",
42
- "postinstall": "node scripts/postinstall.mjs",
43
41
  "test": "node --test"
44
42
  },
45
43
  "engines": {
@@ -11,9 +11,9 @@ license: MIT
11
11
  本 skill 自洽,不依赖其他 skill;需要精确细节时**现查现用**(见下)。
12
12
 
13
13
  ## 安装与位置
14
- - 全局命令 `wpops`(`npm i -g wpops` 安装;postinstall 会顺带装好本 skill)。
14
+ - 全局命令 `wpops`(`npm i -g wpops` 安装)。内置 skill 由 `wpops setup` 安装,首次运行也会按需自动刷新。
15
15
  - 未安装时也可:`npx wpops ...`。
16
- - 配置目录:默认包目录下的 `sites/`;设了 `WPOPS_HOME` 则读 `$WPOPS_HOME/sites/`。
16
+ - 配置目录:默认在用户级稳定目录(Windows `%APPDATA%\wpops`,其他 `~/.config/wpops`),升级不丢;`WPOPS_HOME` 可覆盖。旧版放在包目录的配置会在首次运行时自动迁移。
17
17
  - 凭据:`sites/<名字>.env`。**不要读取、不要打印其中的应用密码。**
18
18
  - 首次配置:`wpops setup`(交互式写入凭据并自动体检)。
19
19
 
@@ -1,15 +0,0 @@
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
- }