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.
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Document management commands
3
+ * Usage: geo doc [create|list|get|update|delete] [options]
4
+ */
5
+
6
+ import { extractArg, hasFlag } from '../utils/args.js';
7
+ import { apiGet, apiPost, apiPut, apiDelete, getBaseUrl } from '../utils/api.js';
8
+ import { outputJson, outputSuccess } from '../utils/output.js';
9
+ import { dispatch } from '../utils/dispatch.js';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import matter from 'gray-matter';
13
+ import readline from 'readline';
14
+
15
+ export async function doc(args) {
16
+ const result = dispatch(args, actions, ['create', 'list', 'get', 'update', 'delete', 'reorder', 'trash', 'recover'], printDocHelp);
17
+ if (result) await actions[result.action](result.subArgs);
18
+ }
19
+
20
+ const actions = {
21
+ async create(args) {
22
+ const file = extractArg(args, '--file') || extractArg(args, '-f');
23
+ const title = extractArg(args, '--title') || extractArg(args, '-t');
24
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
25
+ const category = extractArg(args, '--category') || extractArg(args, '-c');
26
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
27
+ const tags = extractArg(args, '--tags') || extractArg(args, '-T');
28
+ let description = extractArg(args, '--description') || extractArg(args, '-d');
29
+ const author = extractArg(args, '--author') || extractArg(args, '-a') || 'Agent';
30
+ const sort = extractArg(args, '--sort');
31
+ const asDraft = hasFlag(args, '--draft');
32
+ const json = hasFlag(args, '--json');
33
+
34
+ if (!file && !title) {
35
+ console.error('Error: --file or --title is required');
36
+ process.exit(1);
37
+ }
38
+
39
+ let content = '';
40
+ let docTitle = title || '';
41
+ let docSlug = slug;
42
+ let docCategory = category;
43
+ let docTags = tags;
44
+ let docAuthor = author;
45
+ let docSort = sort;
46
+
47
+ if (file) {
48
+ if (!fs.existsSync(file)) {
49
+ console.error(`File not found: ${file}`);
50
+ process.exit(1);
51
+ }
52
+ const raw = fs.readFileSync(file, 'utf-8');
53
+ const parsed = matter(raw);
54
+ content = parsed.content;
55
+ const fm = parsed.data || {};
56
+
57
+ docTitle = docTitle || fm.title || '';
58
+ docSlug = docSlug || fm.slug;
59
+ docCategory = docCategory || fm.category;
60
+ docTags = docTags || (Array.isArray(fm.tags) ? fm.tags.join(', ') : undefined);
61
+ docAuthor = docAuthor !== 'Agent' ? docAuthor : (fm.author || docAuthor);
62
+ if (docSort == null && fm.sort != null) docSort = String(fm.sort);
63
+ if (!description && fm.description) description = fm.description;
64
+ }
65
+
66
+ if (!description && content) {
67
+ const bodyOnly = content.replace(/^---[\s\S]*?---\s*/, '');
68
+ description = bodyOnly.replace(/[\][#*`]/g, '').replace(/\n+/g, ' ').trim().substring(0, 100);
69
+ }
70
+
71
+ // Generate slug: use provided slug, or derive from title
72
+ let generatedSlug = docSlug;
73
+ if (!generatedSlug) {
74
+ generatedSlug = docTitle
75
+ .toLowerCase()
76
+ .replace(/\s+/g, '-')
77
+ .replace(/[^a-z0-9-]/g, '')
78
+ .replace(/-+/g, '-')
79
+ .replace(/^-|-$/g, '');
80
+ // If slug is empty after stripping (e.g., all Chinese), use timestamp fallback
81
+ if (!generatedSlug) {
82
+ generatedSlug = `doc-${Date.now()}`;
83
+ }
84
+ }
85
+
86
+ const payload = {
87
+ title: docTitle,
88
+ slug: generatedSlug,
89
+ category: docCategory || 'general',
90
+ tags: docTags ? docTags.split(',').map(t => t.trim()) : [],
91
+ author: docAuthor,
92
+ description: description || docTitle,
93
+ content,
94
+ language: lang,
95
+ draft: asDraft
96
+ };
97
+ if (docSort != null) {
98
+ const sortNum = Number(docSort);
99
+ if (!Number.isNaN(sortNum)) payload.sort = sortNum;
100
+ }
101
+
102
+ const data = await apiPost('/api/v1/admin/docs', payload);
103
+ const baseUrl = getBaseUrl();
104
+
105
+ outputSuccess(
106
+ `Document created successfully!\nSlug: ${data.data.slug}\nURL: ${baseUrl}/docs/${data.data.slug}`,
107
+ json,
108
+ { slug: data.data.slug, title: payload.title, url: `${baseUrl}/docs/${data.data.slug}` }
109
+ );
110
+ },
111
+
112
+ async list(args) {
113
+ const category = extractArg(args, '--category');
114
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
115
+ const page = extractArg(args, '--page') || '1';
116
+ const limit = extractArg(args, '--limit') || '50';
117
+ const json = hasFlag(args, '--json');
118
+
119
+ let url = `/api/v1/docs?lang=${lang}&page=${page}&limit=${limit}`;
120
+ if (category) url += `&category=${encodeURIComponent(category)}`;
121
+
122
+ const data = await apiGet(url);
123
+ const docs = Array.isArray(data.data) ? data.data : [];
124
+
125
+ if (outputJson(docs, json)) return;
126
+
127
+ console.log(`\nDocuments (${docs.length} found):\n`);
128
+ docs.forEach(d => {
129
+ console.log(` ${d.slug.padEnd(40)} | ${d.title}`);
130
+ });
131
+ },
132
+
133
+ async get(args) {
134
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
135
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
136
+ const format = extractArg(args, '--format') || extractArg(args, '-f') || 'markdown';
137
+ const json = hasFlag(args, '--json');
138
+
139
+ if (!slug) {
140
+ console.error('Error: --slug is required');
141
+ process.exit(1);
142
+ }
143
+
144
+ const data = await apiGet(`/api/v1/docs/${encodeURIComponent(slug)}?lang=${lang}`);
145
+
146
+ if (format === 'json' || json) {
147
+ console.log(JSON.stringify(data.data, null, 2));
148
+ } else {
149
+ const doc = data.data;
150
+ let md = `---\ntitle: "${doc.title}"\nslug: ${doc.slug}\ncategory: ${doc.category}\ntags: [${(doc.tags || []).join(', ')}]\n---\n\n`;
151
+ // Only add H1 if content doesn't already start with one
152
+ const content = doc.content || '';
153
+ if (!content.startsWith('# ')) {
154
+ md += `# ${doc.title}\n\n`;
155
+ }
156
+ md += content;
157
+ console.log(md);
158
+ }
159
+ },
160
+
161
+ async update(args) {
162
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
163
+ const file = extractArg(args, '--file') || extractArg(args, '-f');
164
+ const content = extractArg(args, '--content');
165
+ const category = extractArg(args, '--category') || extractArg(args, '-c');
166
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
167
+ const sort = extractArg(args, '--sort');
168
+ const contentOnly = hasFlag(args, '--content-only');
169
+ const asDraft = hasFlag(args, '--draft');
170
+ const json = hasFlag(args, '--json');
171
+
172
+ if (!slug) {
173
+ console.error('Error: --slug is required');
174
+ process.exit(1);
175
+ }
176
+
177
+ if (!file && !content && sort == null && !category) {
178
+ console.error('Error: --file, --content, --category, or --sort is required');
179
+ process.exit(1);
180
+ }
181
+
182
+ let body = {};
183
+ if (file) {
184
+ if (!fs.existsSync(file)) {
185
+ console.error(`File not found: ${file}`);
186
+ process.exit(1);
187
+ }
188
+ const raw = fs.readFileSync(file, 'utf-8');
189
+ const parsed = matter(raw);
190
+ body.content = parsed.content;
191
+ // Extract metadata from frontmatter (unless --content-only)
192
+ if (!contentOnly) {
193
+ const fm = parsed.data || {};
194
+ if (fm.title) body.title = fm.title;
195
+ if (fm.description) body.description = fm.description;
196
+ if (fm.category) body.category = fm.category;
197
+ if (fm.author) body.author = fm.author;
198
+ if (Array.isArray(fm.tags)) body.tags = fm.tags;
199
+ if (fm.sort !== undefined) body.sort = fm.sort;
200
+ }
201
+ }
202
+ if (content) body.content = content;
203
+ if (category) body.category = category;
204
+ if (sort != null) {
205
+ const sortNum = Number(sort);
206
+ if (Number.isNaN(sortNum)) {
207
+ console.error('Error: --sort must be a number');
208
+ process.exit(1);
209
+ }
210
+ body.sort = sortNum;
211
+ }
212
+
213
+ // Server reads req.body.language for the target language
214
+ body.language = lang;
215
+ if (asDraft) body.draft = true;
216
+
217
+ await apiPut(`/api/v1/admin/docs/${encodeURIComponent(slug)}?lang=${lang}`, body);
218
+ outputSuccess('Document updated successfully!', json, { slug });
219
+ },
220
+
221
+ async delete(args) {
222
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
223
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
224
+ const json = hasFlag(args, '--json');
225
+ const yes = hasFlag(args, '--yes') || hasFlag(args, '-y');
226
+
227
+ if (!slug) {
228
+ console.error('Error: --slug is required');
229
+ process.exit(1);
230
+ }
231
+
232
+ // Confirmation prompt
233
+ if (!yes && !json) {
234
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
235
+ const answer = await new Promise(resolve => {
236
+ rl.question(`⚠️ Delete "${slug}" (${lang})? This moves it to trash for 30 days. [y/N] `, resolve);
237
+ });
238
+ rl.close();
239
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
240
+ console.log('Cancelled.');
241
+ return;
242
+ }
243
+ }
244
+
245
+ // Server-side delete (server handles trash backup automatically)
246
+ await apiDelete(`/api/v1/admin/docs/${encodeURIComponent(slug)}?lang=${lang}`);
247
+ outputSuccess('Document deleted successfully! (moved to server trash)', json, { deleted: slug, language: lang });
248
+ },
249
+
250
+ async trash(args) {
251
+ const json = hasFlag(args, '--json');
252
+ const clear = hasFlag(args, '--clear');
253
+ const yes = hasFlag(args, '--yes') || hasFlag(args, '-y');
254
+
255
+ // Use server-side API
256
+ const data = await apiGet('/api/v1/admin/docs/trash');
257
+ const items = data.data || [];
258
+
259
+ if (json) {
260
+ outputJson({ trash: items });
261
+ } else {
262
+ if (items.length === 0) {
263
+ console.log('🗑️ Trash is empty.');
264
+ } else {
265
+ console.log(`\n🗑️ Trash (${items.length} items):\n`);
266
+ for (const item of items) {
267
+ const date = item.deleted_at ? new Date(item.deleted_at).toLocaleDateString() : '?';
268
+ console.log(` ${item.slug} (${item.language}) — ${item.title} — deleted ${date}`);
269
+ }
270
+ console.log('\nRecover: geo doc recover --file <filename>');
271
+ console.log('Empty: geo doc trash --clear');
272
+ }
273
+ }
274
+
275
+ // Handle --clear flag
276
+ if (clear) {
277
+ if (!yes && !json) {
278
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
279
+ const answer = await new Promise(resolve => {
280
+ rl.question(`⚠️ Permanently delete ${items.length} items from trash? [y/N] `, resolve);
281
+ });
282
+ rl.close();
283
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
284
+ console.log('Cancelled.');
285
+ return;
286
+ }
287
+ }
288
+ await apiDelete('/api/v1/admin/docs/trash');
289
+ if (!json) console.log(`🗑️ Cleared ${items.length} items from trash.`);
290
+ }
291
+ },
292
+
293
+ async recover(args) {
294
+ const file = extractArg(args, '--file') || extractArg(args, '-f');
295
+ const json = hasFlag(args, '--json');
296
+
297
+ if (!file) {
298
+ console.error('Error: --file is required (e.g. --file my-doc-zh-1234567890.md)');
299
+ console.error('Run "geo doc trash" to see available files.');
300
+ process.exit(1);
301
+ }
302
+
303
+ // Use server-side API
304
+ const result = await apiPost('/api/v1/admin/docs/trash/recover', { file });
305
+ const slug = result.data?.slug || file;
306
+ const lang = result.data?.language || 'zh';
307
+
308
+ outputSuccess('Document recovered successfully!', json, { slug, language: lang });
309
+ },
310
+
311
+ async reorder(args) {
312
+ const ordersFlag = extractArg(args, '--orders') || extractArg(args, '-o');
313
+ const json = hasFlag(args, '--json');
314
+
315
+ if (!ordersFlag) {
316
+ console.error('Error: --orders is required (e.g. --orders "slug1:0,slug2:1,slug3:2")');
317
+ process.exit(1);
318
+ }
319
+
320
+ const orders = ordersFlag.split(',').map(pair => {
321
+ const [slug, sort] = pair.split(':').map(s => s && s.trim());
322
+ const sortNum = Number(sort);
323
+ if (!slug || Number.isNaN(sortNum)) {
324
+ console.error(`Error: invalid pair "${pair}". Expected slug:sort.`);
325
+ process.exit(1);
326
+ }
327
+ return { slug, sort: sortNum };
328
+ });
329
+
330
+ await apiPut('/api/v1/admin/docs/reorder', { orders });
331
+ outputSuccess(`Reordered ${orders.length} documents`, json, { orders });
332
+ }
333
+ };
334
+
335
+ function printDocHelp() {
336
+ console.log(`
337
+ Usage: geo doc <action> [options]
338
+
339
+ Actions:
340
+ create Create a new document
341
+ list List documents
342
+ get Get document content
343
+ update Update a document
344
+ delete Delete a document (moves to trash, language-specific)
345
+ trash List or clear trash
346
+ recover Recover a document from trash
347
+ reorder Batch update sort order within a category
348
+
349
+ Options:
350
+ --sort NUM Document sort order (ascending, default 999)
351
+ --file,-f File path for create/update
352
+ --content Inline content for update
353
+ --content-only Update content only (skip frontmatter metadata)
354
+ --slug,-s Document slug
355
+ --category,-c Category slug
356
+ --lang,-l Language code (default: zh)
357
+ --title,-t Document title (create only)
358
+ --tags,-T Comma-separated tags
359
+ --description,-d Short description
360
+ --author,-a Author name (default: Agent)
361
+ --draft Save as draft (skip publishing)
362
+ --yes,-y Skip confirmation prompt (for scripting)
363
+ --json Machine-readable JSON output
364
+
365
+ Examples:
366
+ geo doc create --file ./product.md --category can-motion --sort 1
367
+ geo doc create --file ./draft.md --category new-cat --draft # save as draft
368
+ geo doc update --slug product-slug --sort 2
369
+ geo doc list --category can-motion
370
+ geo doc get --slug uim2040cm
371
+ geo doc update --slug uim2040cm --content "Updated content"
372
+ geo doc delete --slug old-doc --lang zh # moves to trash
373
+ geo doc delete --slug old-doc --lang zh --yes # skip confirmation
374
+ geo doc trash # list trash
375
+ geo doc trash --clear # empty trash (with confirmation)
376
+ geo doc trash --clear --yes # empty trash (skip confirmation)
377
+ geo doc recover --file old-doc-zh-123456.md # recover from trash
378
+ geo doc reorder --orders "slug1:0,slug2:1,slug3:2"
379
+ `);
380
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Draft management commands
3
+ * Usage: geo draft [list|get|publish|delete] [options]
4
+ */
5
+
6
+ import { extractArg, hasFlag } from '../utils/args.js';
7
+ import { apiGet, apiPost, 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 draft(args) {
12
+ const result = dispatch(args, actions, ['list', 'get', 'publish', 'delete'], printDraftHelp);
13
+ if (result) await actions[result.action](result.subArgs);
14
+ }
15
+
16
+ const actions = {
17
+ async list(args) {
18
+ const json = hasFlag(args, '--json');
19
+
20
+ const data = await apiGet('/api/v1/admin/drafts');
21
+ const drafts = data.data || [];
22
+
23
+ if (outputJson(drafts, json)) return;
24
+
25
+ console.log(`\nDrafts (${drafts.length} found):\n`);
26
+ drafts.forEach(d => {
27
+ const lang = d.language || d.lang || '?';
28
+ const status = d.status || 'pending';
29
+ console.log(` ${d.slug.padEnd(30)} | ${lang.padEnd(4)} | ${status.padEnd(10)} | ${d.title || ''}`);
30
+ });
31
+ },
32
+
33
+ async get(args) {
34
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
35
+ const json = hasFlag(args, '--json');
36
+
37
+ if (!slug) {
38
+ console.error('Error: --slug is required');
39
+ process.exit(1);
40
+ }
41
+
42
+ const data = await apiGet(`/api/v1/admin/drafts/${encodeURIComponent(slug)}`);
43
+ const draft = data.data || {};
44
+
45
+ if (outputJson(draft, json)) return;
46
+
47
+ console.log(`\nDraft: ${draft.title || slug}\n`);
48
+ console.log(` Slug: ${draft.slug}`);
49
+ console.log(` Language: ${draft.language || draft.lang || '?'}`);
50
+ console.log(` Status: ${draft.status || 'pending'}`);
51
+ if (draft.content) {
52
+ console.log(`\n--- Content ---\n`);
53
+ console.log(draft.content);
54
+ }
55
+ },
56
+
57
+ async publish(args) {
58
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
59
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
60
+ const json = hasFlag(args, '--json');
61
+
62
+ if (!slug) {
63
+ console.error('Error: --slug is required');
64
+ process.exit(1);
65
+ }
66
+
67
+ await apiPost(`/api/v1/admin/drafts/${encodeURIComponent(slug)}/publish`, { language: lang });
68
+ outputSuccess(`Draft published: ${slug}`, json, { slug, published: true, language: lang });
69
+ },
70
+
71
+ async delete(args) {
72
+ const slug = extractArg(args, '--slug') || extractArg(args, '-s');
73
+ const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
74
+ const json = hasFlag(args, '--json');
75
+
76
+ if (!slug) {
77
+ console.error('Error: --slug is required');
78
+ process.exit(1);
79
+ }
80
+
81
+ const confirmed = await confirmDelete(`Delete draft "${slug}"?`, args);
82
+ if (!confirmed) {
83
+ console.log('Cancelled.');
84
+ return;
85
+ }
86
+
87
+ await apiDelete(`/api/v1/admin/drafts/${encodeURIComponent(slug)}?lang=${lang}`);
88
+ outputSuccess(`Draft deleted: ${slug}`, json, { deleted: slug, language: lang });
89
+ }
90
+ };
91
+
92
+ function printDraftHelp() {
93
+ console.log(`
94
+ Usage: geo draft <action> [options]
95
+
96
+ Actions:
97
+ list List all drafts
98
+ get Get draft content
99
+ publish Publish draft to production
100
+ delete Delete/reject a draft
101
+
102
+ Options:
103
+ --slug,-s Draft slug (required for get/publish/delete)
104
+ --lang,-l Language code (default: zh)
105
+ --json Machine-readable JSON output
106
+
107
+ Examples:
108
+ geo draft list --json
109
+ geo draft get --slug my-doc-draft --json
110
+ geo draft publish --slug my-doc-draft
111
+ geo draft delete --slug my-doc-draft
112
+ `);
113
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Feedback management commands
3
+ * Usage: geo feedback [list|delete|promote] [options]
4
+ */
5
+
6
+ import { extractArg, hasFlag } from '../utils/args.js';
7
+ import { apiGet, apiPost, 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 feedback(args) {
12
+ const result = dispatch(args, actions, ['list', 'delete', 'promote'], printFeedbackHelp);
13
+ if (result) await actions[result.action](result.subArgs);
14
+ }
15
+
16
+ const actions = {
17
+ async list(args) {
18
+ const json = hasFlag(args, '--json');
19
+
20
+ const data = await apiGet('/api/v1/admin/feedback');
21
+ const items = data.data || [];
22
+
23
+ if (outputJson(items, json)) return;
24
+
25
+ console.log(`\nFeedback (${items.length} found):\n`);
26
+ items.forEach(f => {
27
+ const id = (f.id || f._id || '').toString().slice(0, 8);
28
+ const slug = f.docSlug || f.slug || '?';
29
+ console.log(` ${id.padEnd(10)} | ${slug.padEnd(30)} | rating=${(f.rating || '?').toString().padEnd(4)} | ${(f.comment || '').substring(0, 40)}`);
30
+ });
31
+ },
32
+
33
+ async delete(args) {
34
+ const id = extractArg(args, '--id');
35
+ const json = hasFlag(args, '--json');
36
+
37
+ if (!id) {
38
+ console.error('Error: --id is required');
39
+ process.exit(1);
40
+ }
41
+
42
+ const confirmed = await confirmDelete(`Delete feedback "${id}"?`, args);
43
+ if (!confirmed) {
44
+ console.log('Cancelled.');
45
+ return;
46
+ }
47
+
48
+ await apiDelete(`/api/v1/admin/feedback/${encodeURIComponent(id)}`);
49
+ outputSuccess(`Feedback deleted: ${id}`, json, { deleted: id });
50
+ },
51
+
52
+ async promote(args) {
53
+ const id = extractArg(args, '--id');
54
+ const json = hasFlag(args, '--json');
55
+
56
+ if (!id) {
57
+ console.error('Error: --id is required');
58
+ process.exit(1);
59
+ }
60
+
61
+ await apiPost(`/api/v1/admin/feedback/${encodeURIComponent(id)}/promote`);
62
+ outputSuccess(`Feedback promoted: ${id}`, json, { id, promoted: true });
63
+ }
64
+ };
65
+
66
+ function printFeedbackHelp() {
67
+ console.log(`
68
+ Usage: geo feedback <action> [options]
69
+
70
+ Actions:
71
+ list List all feedback entries
72
+ delete Delete a feedback entry
73
+ promote Promote feedback to document content or FAQ
74
+
75
+ Options:
76
+ --id Feedback ID (required for delete/promote)
77
+ --json Machine-readable JSON output
78
+
79
+ Examples:
80
+ geo feedback list --json
81
+ geo feedback delete --id "feedback-id"
82
+ geo feedback promote --id "feedback-id"
83
+ `);
84
+ }