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
|
@@ -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
|
+
}
|