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 ADDED
@@ -0,0 +1,318 @@
1
+ # GEO Wiki CLI
2
+
3
+ > AI Agent 与开发者的命令行管理工具
4
+
5
+ [![npm version](https://img.shields.io/npm/v/geowiki-cli)](https://www.npmjs.com/package/geowiki-cli)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ }