wkgeo-cli 0.2.0 → 0.2.1

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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 「当前选中站点」的本地状态:**`<凭据目录>/config.json`**(默认 `~/.wkongai/config.json`)。
3
+ *
4
+ * 为什么需要它:CMS(栏目 / 内容 / 素材 / 模板)的每个动作都必须落在一个站点上,
5
+ * 而 CMS 的 `site_id` **只从会话或 `x-cms-site` 头里取**(body 里的 `site_id` 后端故意忽略,
6
+ * 见 `tencent/functions/cms/index.js` 的 `resolveSite`)。CLI 没有 Cookie 会话,
7
+ * 所以只能把"我这次要操作哪个站"记在本地,之后每个请求带 `x-cms-site`。
8
+ *
9
+ * 为什么不放进 `credentials.json`:
10
+ * * 那个文件是**凭据**(0600、只存令牌与账号),删除它 = 退出登录;
11
+ * * "选了哪个站"只是操作上下文,`logout` 不该把它一起带走,反过来重新登录也不该丢。
12
+ * 分成两个文件之后,两件事的语义各自清楚。
13
+ *
14
+ * 为什么放在凭据文件同目录:`--credentials` / `WKONGAI_CREDENTIALS` 已经在做"换一套身份"的隔离
15
+ * (单测、多人共机、联调都靠它),站点选择跟着走最不容易串;
16
+ * 默认情况下就是规划文档里写的 `~/.wkongai/config.json`。
17
+ *
18
+ * ⚠️ 这个文件**不含任何秘密**(只有站点 id / 名字),但仍然用原子写 + 0600:
19
+ * 权限口径统一,省得以后往里加别的东西时忘了。
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ import { CliError } from './errors.mjs';
26
+
27
+ /** 状态文件版本(将来改结构时用来判断要不要迁移;现在只写不读) */
28
+ export const SITE_STATE_VERSION = 1;
29
+
30
+ /** 文件名(与规划文档一致:`~/.wkongai/config.json`) */
31
+ export const SITE_STATE_FILE_NAME = 'config.json';
32
+
33
+ /** 读原始 JSON 对象;文件不存在 / 坏掉都返回 `{}`(写路径用,绝不因为"读不懂"而写不进去) */
34
+ function readRawObjectQuiet(file) {
35
+ try {
36
+ const text = fs.readFileSync(file, 'utf8');
37
+ const parsed = JSON.parse(text);
38
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 读当前选中的站点。
46
+ *
47
+ * @param {string} file 状态文件路径
48
+ * @returns {{id:string, label:string, saved_at:string|null}|null} 没选过 → `null`
49
+ * @throws {CliError} 文件存在但**不是合法 JSON / 内容形状不对**(明确报错,不静默当"没选"——
50
+ * 否则用户会一直看到"还没有选择站点",却不知道自己有个坏文件在那儿)
51
+ */
52
+ export function readSiteState(file) {
53
+ if (!file) return null;
54
+ let text;
55
+ try {
56
+ text = fs.readFileSync(file, 'utf8');
57
+ } catch (e) {
58
+ if (e && e.code === 'ENOENT') return null;
59
+ throw new CliError(`读取本地站点配置失败:${e.message}`, {
60
+ kind: 'config',
61
+ hint: `文件位置:${file}`,
62
+ cause: e,
63
+ });
64
+ }
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(text);
68
+ } catch (e) {
69
+ throw new CliError(`本地站点配置不是合法 JSON:${file}`, {
70
+ kind: 'config',
71
+ hint: '删掉它,或重新运行 `wkgeo site use <站点ID>`(会覆盖写入)',
72
+ cause: e,
73
+ });
74
+ }
75
+ const one = parsed && typeof parsed === 'object' ? parsed.cms_site : null;
76
+ if (!one || typeof one !== 'object' || !one.id) return null;
77
+ return {
78
+ id: String(one.id),
79
+ label: String(one.label || ''),
80
+ saved_at: one.saved_at || null,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * 记住"当前站点"(**保留文件里的其它键**:以后往里放别的本地设置时不用改这里)。
86
+ *
87
+ * @param {string} file
88
+ * @param {{id:string, label?:string}} site
89
+ */
90
+ export function writeSiteState(file, site, { now = Date.now() } = {}) {
91
+ const id = String((site && site.id) || '').trim();
92
+ if (!id) throw new CliError('内部错误:写本地站点配置时缺少站点 id');
93
+ const dir = path.dirname(file);
94
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
95
+
96
+ const payload = {
97
+ version: SITE_STATE_VERSION,
98
+ ...readRawObjectQuiet(file),
99
+ cms_site: {
100
+ id,
101
+ label: String((site && site.label) || ''),
102
+ saved_at: new Date(now).toISOString(),
103
+ },
104
+ };
105
+
106
+ const tmp = `${file}.tmp-${process.pid}`;
107
+ const fd = fs.openSync(tmp, 'w', 0o600);
108
+ try {
109
+ fs.writeFileSync(fd, JSON.stringify(payload, null, 2) + '\n', 'utf8');
110
+ } finally {
111
+ fs.closeSync(fd);
112
+ }
113
+ fs.renameSync(tmp, file);
114
+ return payload.cms_site;
115
+ }
116
+
117
+ /**
118
+ * 取消选中(`wkgeo site use --clear`)。
119
+ *
120
+ * @returns {boolean} 之前有没有选中过(用来决定提示文案)
121
+ */
122
+ export function clearSiteState(file) {
123
+ const raw = readRawObjectQuiet(file);
124
+ const had = Boolean(raw.cms_site && raw.cms_site.id);
125
+ delete raw.cms_site;
126
+ const dir = path.dirname(file);
127
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
128
+ const tmp = `${file}.tmp-${process.pid}`;
129
+ const fd = fs.openSync(tmp, 'w', 0o600);
130
+ try {
131
+ fs.writeFileSync(fd, JSON.stringify({ version: SITE_STATE_VERSION, ...raw }, null, 2) + '\n', 'utf8');
132
+ } finally {
133
+ fs.closeSync(fd);
134
+ }
135
+ fs.renameSync(tmp, file);
136
+ return had;
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wkgeo-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "云站平台官方命令行客户端:授权登录一次,之后建站 / 下单 / 钱包支付 / 查余额都不用再登录(OAuth 授权码 + PKCE,公共客户端,零第三方依赖)",
5
5
  "keywords": [
6
6
  "wkgeo",