geowiki-cli 1.0.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.
- package/README.md +318 -0
- package/bin/geo.js +22 -0
- package/cli/commands/category.js +171 -0
- package/cli/commands/config.js +180 -0
- package/cli/commands/doc.js +380 -0
- package/cli/commands/draft.js +113 -0
- package/cli/commands/feedback.js +84 -0
- package/cli/commands/geo.js +144 -0
- package/cli/commands/guestbook.js +114 -0
- package/cli/commands/login.js +181 -0
- package/cli/commands/media.js +187 -0
- package/cli/commands/search.js +67 -0
- package/cli/commands/stats.js +90 -0
- package/cli/commands/tag.js +131 -0
- package/cli/commands/user.js +195 -0
- package/cli/index.js +178 -0
- package/cli/package.json +41 -0
- package/cli/utils/api.js +197 -0
- package/cli/utils/args.js +25 -0
- package/cli/utils/config.js +94 -0
- package/cli/utils/dispatch.js +20 -0
- package/cli/utils/output.js +41 -0
- package/package.json +98 -0
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# GEO Wiki CLI
|
|
2
|
+
|
|
3
|
+
> AI Agent 与开发者的命令行管理工具
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/geowiki-cli)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
GEO Wiki CLI 是 [GEO Wiki Pro](https://geowiki.pro) 的命令行工具,专为 AI Agent 和开发者设计,可通过命令行管理知识库的所有内容。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g geowiki-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
验证安装:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
geo --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
### 1. 登录
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
geo login --url https://your-site.com --user admin --pass YourPassword
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
登录后会保存会话,后续命令自动使用。
|
|
35
|
+
|
|
36
|
+
### 2. 查看状态
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
geo status
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. 列出文档
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
geo doc list
|
|
46
|
+
geo doc list --json # JSON 格式(供 Agent 使用)
|
|
47
|
+
geo doc list --category cli # 按分类筛选
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 命令参考
|
|
53
|
+
|
|
54
|
+
### 认证
|
|
55
|
+
|
|
56
|
+
| 命令 | 说明 |
|
|
57
|
+
|------|------|
|
|
58
|
+
| `geo login --url URL --user USER --pass PASS` | 登录并保存会话 |
|
|
59
|
+
| `geo logout` | 清除保存的凭据 |
|
|
60
|
+
| `geo status` | 显示当前连接状态 |
|
|
61
|
+
|
|
62
|
+
### 文档管理
|
|
63
|
+
|
|
64
|
+
| 命令 | 说明 |
|
|
65
|
+
|------|------|
|
|
66
|
+
| `geo doc list` | 列出所有文档 |
|
|
67
|
+
| `geo doc list --category SLUG` | 按分类筛选 |
|
|
68
|
+
| `geo doc list --lang en` | 指定语言 |
|
|
69
|
+
| `geo doc get --slug SLUG` | 获取文档内容 |
|
|
70
|
+
| `geo doc create --file FILE --category CAT` | 创建文档 |
|
|
71
|
+
| `geo doc create --file FILE --category CAT --slug SLUG --lang en` | 创建英文文档 |
|
|
72
|
+
| `geo doc update --slug SLUG --file FILE` | 更新文档 |
|
|
73
|
+
| `geo doc update --slug SLUG --content "新内容"` | 直接更新内容 |
|
|
74
|
+
| `geo doc update --slug SLUG --sort 2` | 更新排序 |
|
|
75
|
+
| `geo doc delete --slug SLUG` | 删除文档(移到回收站) |
|
|
76
|
+
| `geo doc delete --slug SLUG --yes` | 跳过确认删除 |
|
|
77
|
+
| `geo doc trash` | 查看回收站 |
|
|
78
|
+
| `geo doc trash --clear` | 清空回收站 |
|
|
79
|
+
| `geo doc recover --file FILE` | 恢复文档 |
|
|
80
|
+
| `geo doc reorder --orders "slug1:0,slug2:1"` | 批量排序 |
|
|
81
|
+
|
|
82
|
+
**创建文档示例:**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 从文件创建
|
|
86
|
+
geo doc create --file ./product.md --category products --slug product-a
|
|
87
|
+
|
|
88
|
+
# 指定语言
|
|
89
|
+
geo doc create --file ./product-en.md --category products --slug product-a --lang en
|
|
90
|
+
|
|
91
|
+
# 带标签
|
|
92
|
+
geo doc create --file ./guide.md --category docs --tags "guide,tutorial"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 分类管理
|
|
96
|
+
|
|
97
|
+
| 命令 | 说明 |
|
|
98
|
+
|------|------|
|
|
99
|
+
| `geo category list` | 列出所有分类 |
|
|
100
|
+
| `geo category create --name "名称" --slug SLUG` | 创建分类 |
|
|
101
|
+
| `geo category create --name "名称" --name-en "Name" --slug SLUG` | 创建分类(含英文名) |
|
|
102
|
+
| `geo category update --slug SLUG --name "新名称"` | 更新分类 |
|
|
103
|
+
| `geo category delete --slug SLUG` | 删除分类 |
|
|
104
|
+
|
|
105
|
+
### 标签管理
|
|
106
|
+
|
|
107
|
+
| 命令 | 说明 |
|
|
108
|
+
|------|------|
|
|
109
|
+
| `geo tag list` | 列出所有标签 |
|
|
110
|
+
| `geo tag create --name "名称" --slug SLUG` | 创建标签 |
|
|
111
|
+
| `geo tag delete --slug SLUG` | 删除标签 |
|
|
112
|
+
|
|
113
|
+
### 媒体管理
|
|
114
|
+
|
|
115
|
+
| 命令 | 说明 |
|
|
116
|
+
|------|------|
|
|
117
|
+
| `geo media list` | 列出根目录文件 |
|
|
118
|
+
| `geo media list --dir DIR` | 列出子目录文件 |
|
|
119
|
+
| `geo media tree` | 显示目录树 |
|
|
120
|
+
| `geo media upload --file FILE` | 上传文件 |
|
|
121
|
+
| `geo media upload --file FILE --directory DIR` | 上传到子目录 |
|
|
122
|
+
| `geo media mkdir --name NAME` | 创建文件夹 |
|
|
123
|
+
| `geo media mkdir --name NAME --parent PARENT` | 在子目录创建文件夹 |
|
|
124
|
+
| `geo media rmdir --path PATH` | 删除空文件夹 |
|
|
125
|
+
| `geo media move --source SRC --target DIR` | 移动文件 |
|
|
126
|
+
| `geo media move --source DIR/FILE` | 移动到根目录 |
|
|
127
|
+
| `geo media delete --file FILE` | 删除文件 |
|
|
128
|
+
|
|
129
|
+
**媒体操作示例:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 上传图片到子目录
|
|
133
|
+
geo media upload --file ./photo.jpg --directory products
|
|
134
|
+
|
|
135
|
+
# 创建嵌套文件夹
|
|
136
|
+
geo media mkdir --name images --parent products
|
|
137
|
+
|
|
138
|
+
# 移动文件
|
|
139
|
+
geo media move --source temp/photo.jpg --target products/images
|
|
140
|
+
|
|
141
|
+
# 查看目录结构
|
|
142
|
+
geo media tree
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 用户管理
|
|
146
|
+
|
|
147
|
+
| 命令 | 说明 |
|
|
148
|
+
|------|------|
|
|
149
|
+
| `geo user list` | 列出所有用户 |
|
|
150
|
+
| `geo user create --user NAME --pass PASS --role admin` | 创建用户 |
|
|
151
|
+
| `geo user update --user NAME --role editor` | 更新用户角色 |
|
|
152
|
+
| `geo user delete --user NAME` | 删除用户 |
|
|
153
|
+
| `geo user reset-password --user NAME --pass NEWPASS` | 重置密码 |
|
|
154
|
+
|
|
155
|
+
### 站点配置
|
|
156
|
+
|
|
157
|
+
| 命令 | 说明 |
|
|
158
|
+
|------|------|
|
|
159
|
+
| `geo config get` | 获取配置 |
|
|
160
|
+
| `geo config get --json` | JSON 格式获取 |
|
|
161
|
+
| `geo config update --hero-title "标题"` | 更新首页标题 |
|
|
162
|
+
| `geo config update --logo-url "/media/logo.png"` | 更新 Logo |
|
|
163
|
+
| `geo config update --favicon-url "/media/favicon.ico"` | 更新 Favicon |
|
|
164
|
+
| `geo config update --featured-slugs "slug1,slug2"` | 设置推荐文档 |
|
|
165
|
+
|
|
166
|
+
### 草稿管理
|
|
167
|
+
|
|
168
|
+
| 命令 | 说明 |
|
|
169
|
+
|------|------|
|
|
170
|
+
| `geo draft list` | 列出草稿 |
|
|
171
|
+
| `geo draft get --slug SLUG` | 获取草稿 |
|
|
172
|
+
| `geo draft publish --slug SLUG` | 发布草稿 |
|
|
173
|
+
| `geo draft delete --slug SLUG` | 拒绝/删除草稿 |
|
|
174
|
+
|
|
175
|
+
### 搜索
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
geo search "关键词"
|
|
179
|
+
geo search "CAN bus" --category products
|
|
180
|
+
geo search "protocol" --lang en --json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 统计
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
geo stats
|
|
187
|
+
geo stats --json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 反馈管理
|
|
191
|
+
|
|
192
|
+
| 命令 | 说明 |
|
|
193
|
+
|------|------|
|
|
194
|
+
| `geo feedback list` | 列出反馈 |
|
|
195
|
+
| `geo feedback delete --id ID` | 删除反馈 |
|
|
196
|
+
| `geo feedback promote --id ID` | 提升为评论 |
|
|
197
|
+
|
|
198
|
+
### 留言板
|
|
199
|
+
|
|
200
|
+
| 命令 | 说明 |
|
|
201
|
+
|------|------|
|
|
202
|
+
| `geo guestbook list` | 列出留言 |
|
|
203
|
+
| `geo guestbook toggle` | 开关留言板 |
|
|
204
|
+
| `geo guestbook update --id ID --status approved` | 审核留言 |
|
|
205
|
+
| `geo guestbook delete --id ID` | 删除留言 |
|
|
206
|
+
|
|
207
|
+
### GEO 优化
|
|
208
|
+
|
|
209
|
+
| 命令 | 说明 |
|
|
210
|
+
|------|------|
|
|
211
|
+
| `geo geo status` | GEO 状态概览 |
|
|
212
|
+
| `geo geo report` | GEO 详细报告 |
|
|
213
|
+
| `geo geo rebuild` | 重建 llms.txt 和 sitemap |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## AI Agent 使用指南
|
|
218
|
+
|
|
219
|
+
GEO Wiki CLI 专为 AI Agent 设计,所有命令都支持 `--json` 输出:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# Agent 获取文档列表
|
|
223
|
+
geo doc list --json
|
|
224
|
+
|
|
225
|
+
# Agent 搜索文档
|
|
226
|
+
geo search "CAN bus protocol" --json
|
|
227
|
+
|
|
228
|
+
# Agent 创建文档
|
|
229
|
+
geo doc create --file ./content.md --category products --slug new-product --json
|
|
230
|
+
|
|
231
|
+
# Agent 上传图片
|
|
232
|
+
geo media upload --file ./diagram.png --directory products --json
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Agent 工作流示例
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# 1. 获取所有分类
|
|
239
|
+
geo category list --json
|
|
240
|
+
|
|
241
|
+
# 2. 创建文档
|
|
242
|
+
geo doc create --file ./article.md --category tech --slug my-article
|
|
243
|
+
|
|
244
|
+
# 3. 上传相关图片
|
|
245
|
+
geo media upload --file ./image.png --directory tech
|
|
246
|
+
|
|
247
|
+
# 4. 验证创建成功
|
|
248
|
+
geo doc get --slug my-article --json
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 环境变量
|
|
254
|
+
|
|
255
|
+
| 变量 | 说明 | 默认值 |
|
|
256
|
+
|------|------|--------|
|
|
257
|
+
| `GEO_URL` | 站点 URL | — |
|
|
258
|
+
| `GEO_TOKEN` | API Token | — |
|
|
259
|
+
|
|
260
|
+
使用 API Token 登录(适合 CI/CD):
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
geo login --url https://your-site.com --token geo_xxxxxxxxxxxx
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 常见问题
|
|
269
|
+
|
|
270
|
+
### 登录失败
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# 检查连接
|
|
274
|
+
geo status
|
|
275
|
+
|
|
276
|
+
# 重新登录
|
|
277
|
+
geo login --url https://your-site.com --user admin --pass YourPassword
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 权限不足
|
|
281
|
+
|
|
282
|
+
确保用户有 `admin` 或 `editor` 角色:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
geo user list --json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 文件上传失败
|
|
289
|
+
|
|
290
|
+
检查文件大小(默认最大 50MB)和文件类型:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
geo media upload --file ./large-file.zip
|
|
294
|
+
# Error: 文件过大,最大支持 50MB
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 开发
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# 克隆仓库
|
|
303
|
+
git clone https://github.com/your-org/geowiki-pro.git
|
|
304
|
+
cd geowiki-pro
|
|
305
|
+
|
|
306
|
+
# 安装依赖
|
|
307
|
+
npm install
|
|
308
|
+
|
|
309
|
+
# 本地测试 CLI
|
|
310
|
+
node bin/geo.js --help
|
|
311
|
+
node bin/geo.js login --url http://localhost:3002 --user admin --pass Admin@123
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT © GEO Wiki Pro
|
package/bin/geo.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GEO Wiki CLI - Entry point
|
|
4
|
+
* Run via: node bin/geo.js <command>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
// Global error handler for unhandled promise rejections
|
|
15
|
+
process.on('unhandledRejection', (reason) => {
|
|
16
|
+
console.error(`Error: ${reason?.message || reason || 'Unknown error'}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const cliPath = `file://${projectRoot}/cli/index.js`;
|
|
21
|
+
const { default: main } = await import(cliPath);
|
|
22
|
+
main();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category management commands
|
|
3
|
+
* Usage: geo category [list|create|update|reorder|delete] [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { extractArg, hasFlag } from '../utils/args.js';
|
|
7
|
+
import { apiGet, apiPost, apiPut, apiDelete } from '../utils/api.js';
|
|
8
|
+
import { outputJson, outputSuccess, confirmDelete } from '../utils/output.js';
|
|
9
|
+
import { dispatch } from '../utils/dispatch.js';
|
|
10
|
+
|
|
11
|
+
export async function category(args) {
|
|
12
|
+
const result = dispatch(
|
|
13
|
+
args,
|
|
14
|
+
actions,
|
|
15
|
+
['list', 'create', 'update', 'reorder', 'delete'],
|
|
16
|
+
printCategoryHelp
|
|
17
|
+
);
|
|
18
|
+
if (result) await actions[result.action](result.subArgs);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const actions = {
|
|
22
|
+
async list(args) {
|
|
23
|
+
const json = hasFlag(args, '--json');
|
|
24
|
+
const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
|
|
25
|
+
|
|
26
|
+
const data = await apiGet('/api/v1/categories');
|
|
27
|
+
const cats = data.data || [];
|
|
28
|
+
|
|
29
|
+
if (outputJson(cats, json)) return;
|
|
30
|
+
|
|
31
|
+
console.log(`\nCategories (${cats.length} found):\n`);
|
|
32
|
+
cats.forEach(c => {
|
|
33
|
+
const displayName = lang === 'jp' ? (c.name_jp || c.name_en || c.name) :
|
|
34
|
+
lang === 'en' ? (c.name_en || c.name) : c.name;
|
|
35
|
+
console.log(` ${c.slug.padEnd(30)} | sort=${String(c.sort ?? '-').padEnd(3)} | ${displayName}`);
|
|
36
|
+
if (c.description) console.log(` ${''.padEnd(30)} | ${c.description}`);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async create(args) {
|
|
41
|
+
const name = extractArg(args, '--name') || extractArg(args, '-n');
|
|
42
|
+
const slug = extractArg(args, '--slug') || extractArg(args, '-s');
|
|
43
|
+
const description = extractArg(args, '--description') || extractArg(args, '-d') || '';
|
|
44
|
+
const nameEn = extractArg(args, '--name-en');
|
|
45
|
+
const nameJp = extractArg(args, '--name-jp');
|
|
46
|
+
const descriptionEn = extractArg(args, '--description-en');
|
|
47
|
+
const descriptionJp = extractArg(args, '--description-jp');
|
|
48
|
+
const json = hasFlag(args, '--json');
|
|
49
|
+
|
|
50
|
+
if (!name || !slug) {
|
|
51
|
+
console.error('Error: --name and --slug are required');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const payload = { name, slug, description };
|
|
56
|
+
if (nameEn != null) payload.name_en = nameEn;
|
|
57
|
+
if (nameJp != null) payload.name_jp = nameJp;
|
|
58
|
+
if (descriptionEn != null) payload.description_en = descriptionEn;
|
|
59
|
+
if (descriptionJp != null) payload.description_jp = descriptionJp;
|
|
60
|
+
|
|
61
|
+
await apiPost('/api/v1/admin/categories', payload);
|
|
62
|
+
outputSuccess(`Category created: ${name} (${slug})`, json, { slug, name });
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async update(args) {
|
|
66
|
+
const slug = extractArg(args, '--slug') || extractArg(args, '-s');
|
|
67
|
+
const name = extractArg(args, '--name') || extractArg(args, '-n');
|
|
68
|
+
const description = extractArg(args, '--description') || extractArg(args, '-d');
|
|
69
|
+
const nameEn = extractArg(args, '--name-en');
|
|
70
|
+
const nameJp = extractArg(args, '--name-jp');
|
|
71
|
+
const descriptionEn = extractArg(args, '--description-en');
|
|
72
|
+
const descriptionJp = extractArg(args, '--description-jp');
|
|
73
|
+
const json = hasFlag(args, '--json');
|
|
74
|
+
|
|
75
|
+
if (!slug) {
|
|
76
|
+
console.error('Error: --slug is required');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const payload = {};
|
|
81
|
+
if (name != null) payload.name = name;
|
|
82
|
+
if (description != null) payload.description = description;
|
|
83
|
+
if (nameEn != null) payload.name_en = nameEn;
|
|
84
|
+
if (nameJp != null) payload.name_jp = nameJp;
|
|
85
|
+
if (descriptionEn != null) payload.description_en = descriptionEn;
|
|
86
|
+
if (descriptionJp != null) payload.description_jp = descriptionJp;
|
|
87
|
+
|
|
88
|
+
if (Object.keys(payload).length === 0) {
|
|
89
|
+
console.error('Error: nothing to update (use --name, --description, --name-en, --name-jp, --description-en, or --description-jp)');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await apiPut(`/api/v1/admin/categories/${encodeURIComponent(slug)}`, payload);
|
|
94
|
+
outputSuccess(`Category updated: ${slug}`, json, { slug, ...payload });
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async reorder(args) {
|
|
98
|
+
const ordersFlag = extractArg(args, '--orders') || extractArg(args, '-o');
|
|
99
|
+
const json = hasFlag(args, '--json');
|
|
100
|
+
|
|
101
|
+
if (!ordersFlag) {
|
|
102
|
+
console.error('Error: --orders is required (e.g. --orders "products:0,technical:1,support:2")');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const orders = ordersFlag.split(',').map(pair => {
|
|
107
|
+
const [slug, sort] = pair.split(':').map(s => s && s.trim());
|
|
108
|
+
const sortNum = Number(sort);
|
|
109
|
+
if (!slug || Number.isNaN(sortNum)) {
|
|
110
|
+
console.error(`Error: invalid pair "${pair}". Expected slug:sort.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
return { slug, sort: sortNum };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await apiPut('/api/v1/admin/categories/reorder', { orders });
|
|
117
|
+
outputSuccess(`Reordered ${orders.length} categories`, json, { orders });
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async delete(args) {
|
|
121
|
+
const slug = extractArg(args, '--slug') || extractArg(args, '-s');
|
|
122
|
+
const json = hasFlag(args, '--json');
|
|
123
|
+
|
|
124
|
+
if (!slug) {
|
|
125
|
+
console.error('Error: --slug is required');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const confirmed = await confirmDelete(`Delete category "${slug}"?`, args);
|
|
130
|
+
if (!confirmed) {
|
|
131
|
+
console.log('Cancelled.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await apiDelete(`/api/v1/admin/categories/${encodeURIComponent(slug)}`);
|
|
136
|
+
outputSuccess(`Category deleted: ${slug}`, json, { deleted: slug });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function printCategoryHelp() {
|
|
141
|
+
console.log(`
|
|
142
|
+
Usage: geo category <action> [options]
|
|
143
|
+
|
|
144
|
+
Actions:
|
|
145
|
+
list List all categories
|
|
146
|
+
create Create a new category
|
|
147
|
+
update Update name/description (admin/editor)
|
|
148
|
+
reorder Batch update sort order (admin)
|
|
149
|
+
delete Delete a category (admin)
|
|
150
|
+
|
|
151
|
+
Options:
|
|
152
|
+
--slug,-s Category slug (kebab-case)
|
|
153
|
+
--name,-n Display name (default language)
|
|
154
|
+
--name-en English name
|
|
155
|
+
--name-jp Japanese name
|
|
156
|
+
--description,-d Short description (default language)
|
|
157
|
+
--description-en English description
|
|
158
|
+
--description-jp Japanese description
|
|
159
|
+
--lang,-l Language for display (zh/en/jp) — list only
|
|
160
|
+
--orders,-o For reorder: "slug1:0,slug2:1,slug3:2"
|
|
161
|
+
--json Machine-readable JSON output
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
geo category list --json
|
|
165
|
+
geo category create --name "CAN 电机" --name-en "CAN MOTION" --slug can-motion \\
|
|
166
|
+
--description "CAN总线闭环步进电机" --description-en "CAN bus closed-loop stepper motors"
|
|
167
|
+
geo category update --slug can-motion --name "新名称" --name-en "New Name"
|
|
168
|
+
geo category reorder --orders "products:0,technical:1,support:2"
|
|
169
|
+
geo category delete --slug old-category
|
|
170
|
+
`);
|
|
171
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site configuration commands
|
|
3
|
+
* Usage: geo config [get|update] [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { extractArg, hasFlag } from '../utils/args.js';
|
|
7
|
+
import { apiGet, apiPut } from '../utils/api.js';
|
|
8
|
+
import { outputJson, outputSuccess } from '../utils/output.js';
|
|
9
|
+
import { dispatch } from '../utils/dispatch.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Deep merge a partial update into an existing object.
|
|
13
|
+
* Only overwrites keys present in `patch`; nested objects are merged recursively.
|
|
14
|
+
*/
|
|
15
|
+
function deepMerge(target, patch) {
|
|
16
|
+
const result = { ...target };
|
|
17
|
+
for (const key of Object.keys(patch)) {
|
|
18
|
+
if (
|
|
19
|
+
patch[key] !== null &&
|
|
20
|
+
typeof patch[key] === 'object' &&
|
|
21
|
+
!Array.isArray(patch[key]) &&
|
|
22
|
+
typeof result[key] === 'object' &&
|
|
23
|
+
result[key] !== null &&
|
|
24
|
+
!Array.isArray(result[key])
|
|
25
|
+
) {
|
|
26
|
+
result[key] = deepMerge(result[key], patch[key]);
|
|
27
|
+
} else {
|
|
28
|
+
result[key] = patch[key];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function config(args) {
|
|
35
|
+
const result = dispatch(args, actions, ['get', 'update'], printConfigHelp);
|
|
36
|
+
if (result) await actions[result.action](result.subArgs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const actions = {
|
|
40
|
+
async get(args) {
|
|
41
|
+
const json = hasFlag(args, '--json');
|
|
42
|
+
|
|
43
|
+
const data = await apiGet('/api/v1/admin/config');
|
|
44
|
+
const cfg = data.data || {};
|
|
45
|
+
|
|
46
|
+
if (outputJson(cfg, json)) return;
|
|
47
|
+
|
|
48
|
+
console.log('\nSite Configuration:\n');
|
|
49
|
+
if (cfg.siteName) console.log(` Site Name: ${cfg.siteName}`);
|
|
50
|
+
if (cfg.siteDescription) console.log(` Description: ${cfg.siteDescription}`);
|
|
51
|
+
if (cfg.defaultLang) console.log(` Default Language: ${cfg.defaultLang}`);
|
|
52
|
+
if (cfg.homepageLayout) console.log(` Homepage Layout: ${JSON.stringify(cfg.homepageLayout)}`);
|
|
53
|
+
console.log('');
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async update(args) {
|
|
57
|
+
const siteName = extractArg(args, '--site-name');
|
|
58
|
+
const siteDesc = extractArg(args, '--site-desc');
|
|
59
|
+
const defaultLang = extractArg(args, '--default-lang');
|
|
60
|
+
const heroTitle = extractArg(args, '--hero-title');
|
|
61
|
+
const heroSubtitle = extractArg(args, '--hero-subtitle');
|
|
62
|
+
const featuredSlugs = extractArg(args, '--featured-slugs');
|
|
63
|
+
const logoUrl = extractArg(args, '--logo-url');
|
|
64
|
+
const faviconUrl = extractArg(args, '--favicon-url');
|
|
65
|
+
const customHead = extractArg(args, '--custom-head');
|
|
66
|
+
const json = hasFlag(args, '--json');
|
|
67
|
+
|
|
68
|
+
const payload = {};
|
|
69
|
+
if (siteName != null) payload.siteName = siteName;
|
|
70
|
+
if (siteDesc != null) payload.siteDescription = siteDesc;
|
|
71
|
+
if (defaultLang != null) payload.defaultLang = defaultLang;
|
|
72
|
+
if (logoUrl != null) payload.logoUrl = logoUrl;
|
|
73
|
+
if (faviconUrl != null) payload.faviconUrl = faviconUrl;
|
|
74
|
+
if (customHead != null) payload.customHeadHtml = customHead;
|
|
75
|
+
|
|
76
|
+
// Hero fields are multilingual objects { zh, en, jp }.
|
|
77
|
+
// Accept plain string (sets default lang) or --hero-title-zh/--hero-title-en etc.
|
|
78
|
+
// Always merge with existing values to avoid losing other languages.
|
|
79
|
+
const hasHeroTitleUpdate = heroTitle != null || ['zh', 'en', 'jp'].some(l => extractArg(args, `--hero-title-${l}`) != null);
|
|
80
|
+
const hasHeroSubtitleUpdate = heroSubtitle != null || ['zh', 'en', 'jp'].some(l => extractArg(args, `--hero-subtitle-${l}`) != null);
|
|
81
|
+
|
|
82
|
+
if (hasHeroTitleUpdate || hasHeroSubtitleUpdate) {
|
|
83
|
+
// Read current config to preserve other languages
|
|
84
|
+
const current = await apiGet('/api/v1/admin/config');
|
|
85
|
+
const currentCfg = current.data || {};
|
|
86
|
+
|
|
87
|
+
if (hasHeroTitleUpdate) {
|
|
88
|
+
const merged = { ...(currentCfg.heroTitle || {}) };
|
|
89
|
+
if (heroTitle != null) {
|
|
90
|
+
merged[defaultLang || 'zh'] = heroTitle;
|
|
91
|
+
}
|
|
92
|
+
for (const lang of ['zh', 'en', 'jp']) {
|
|
93
|
+
const val = extractArg(args, `--hero-title-${lang}`);
|
|
94
|
+
if (val != null && val !== null) merged[lang] = val;
|
|
95
|
+
}
|
|
96
|
+
payload.heroTitle = merged;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasHeroSubtitleUpdate) {
|
|
100
|
+
const merged = { ...(currentCfg.heroSubtitle || {}) };
|
|
101
|
+
if (heroSubtitle != null) {
|
|
102
|
+
merged[defaultLang || 'zh'] = heroSubtitle;
|
|
103
|
+
}
|
|
104
|
+
for (const lang of ['zh', 'en', 'jp']) {
|
|
105
|
+
const val = extractArg(args, `--hero-subtitle-${lang}`);
|
|
106
|
+
if (val != null && val !== null) merged[lang] = val;
|
|
107
|
+
}
|
|
108
|
+
payload.heroSubtitle = merged;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (featuredSlugs != null) {
|
|
113
|
+
payload._homepageLayoutPatch = { blocks: [{ id: 'featured', config: { slugs: featuredSlugs.split(',').map(s => s.trim()).filter(Boolean) } }] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Object.keys(payload).length === 0) {
|
|
117
|
+
console.error('Error: nothing to update (use --site-name, --site-desc, --default-lang, --hero-title, --hero-subtitle, --featured-slugs, --logo-url, --favicon-url, or --custom-head)');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// For nested homepageLayout patches, read current config and merge
|
|
122
|
+
const homepagePatch = payload._homepageLayoutPatch;
|
|
123
|
+
delete payload._homepageLayoutPatch;
|
|
124
|
+
|
|
125
|
+
if (homepagePatch) {
|
|
126
|
+
const current = await apiGet('/api/v1/admin/config');
|
|
127
|
+
const currentLayout = (current.data || {}).homepageLayout || {};
|
|
128
|
+
const currentBlocks = Array.isArray(currentLayout.blocks) ? currentLayout.blocks : [];
|
|
129
|
+
const newBlocks = [...currentBlocks];
|
|
130
|
+
for (const patch of homepagePatch.blocks) {
|
|
131
|
+
const idx = newBlocks.findIndex(b => b.id === patch.id);
|
|
132
|
+
if (idx !== -1) {
|
|
133
|
+
newBlocks[idx] = deepMerge(newBlocks[idx], patch);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
payload.homepageLayout = { ...currentLayout, blocks: newBlocks };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await apiPut('/api/v1/admin/config', payload);
|
|
140
|
+
outputSuccess('Site configuration updated', json, payload);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function printConfigHelp() {
|
|
145
|
+
console.log(`
|
|
146
|
+
Usage: geo config <action> [options]
|
|
147
|
+
|
|
148
|
+
Actions:
|
|
149
|
+
get Get current site configuration
|
|
150
|
+
update Update site configuration
|
|
151
|
+
|
|
152
|
+
Options:
|
|
153
|
+
--site-name Site name
|
|
154
|
+
--site-desc Site description (for SEO)
|
|
155
|
+
--default-lang Default language (zh, en, jp, ...)
|
|
156
|
+
--hero-title TEXT Hero section title (sets default language)
|
|
157
|
+
--hero-title-zh TEXT Hero title (Chinese)
|
|
158
|
+
--hero-title-en TEXT Hero title (English)
|
|
159
|
+
--hero-title-jp TEXT Hero title (Japanese)
|
|
160
|
+
--hero-subtitle TEXT Hero section subtitle (sets default language)
|
|
161
|
+
--hero-subtitle-zh TEXT Hero subtitle (Chinese)
|
|
162
|
+
--hero-subtitle-en TEXT Hero subtitle (English)
|
|
163
|
+
--hero-subtitle-jp TEXT Hero subtitle (Japanese)
|
|
164
|
+
--featured-slugs SLUGS Comma-separated featured doc slugs (e.g. "slug1,slug2,slug3")
|
|
165
|
+
--logo-url URL Logo image path (e.g. /media/logo.png)
|
|
166
|
+
--favicon-url URL Favicon path (e.g. /media/favicon.ico)
|
|
167
|
+
--custom-head HTML Custom HTML for <head> (e.g. '<script>...</script>')
|
|
168
|
+
--json Machine-readable JSON output
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
geo config get --json
|
|
172
|
+
geo config update --site-name "My Wiki" --site-desc "Product documentation"
|
|
173
|
+
geo config update --hero-title "Welcome" --hero-subtitle "Product docs"
|
|
174
|
+
geo config update --hero-title-en "Welcome" --hero-subtitle-en "Product docs"
|
|
175
|
+
geo config update --featured-slugs "quick-start,faq,product-overview"
|
|
176
|
+
geo config update --logo-url "/media/shared/logo.png" --favicon-url "/media/shared/favicon.ico"
|
|
177
|
+
geo config update --custom-head '<meta name="theme-color" content="#2563EB">'
|
|
178
|
+
geo config update --default-lang en
|
|
179
|
+
`);
|
|
180
|
+
}
|