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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GEO status and manifest commands
|
|
3
|
+
* Usage: geo geo [status|manifest|llms|sitemap] [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { hasFlag } from '../utils/args.js';
|
|
7
|
+
import { apiGet, apiGetRaw, apiPost, getBaseUrl } from '../utils/api.js';
|
|
8
|
+
import { outputJson, outputSuccess } from '../utils/output.js';
|
|
9
|
+
|
|
10
|
+
const subActions = { status, manifest, llms, sitemap, report, rebuild };
|
|
11
|
+
|
|
12
|
+
export async function geo(args) {
|
|
13
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
14
|
+
printGeoHelp();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const [action, ...subArgs] = args;
|
|
19
|
+
|
|
20
|
+
if (subArgs.includes('--help') || subArgs.includes('-h')) {
|
|
21
|
+
printSubHelp(action);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!subActions[action]) {
|
|
26
|
+
console.error(`Unknown action: ${action}`);
|
|
27
|
+
printGeoHelp();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await subActions[action](subArgs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printGeoHelp() {
|
|
35
|
+
console.log(`
|
|
36
|
+
Usage: geo geo <action> [options]
|
|
37
|
+
|
|
38
|
+
Actions:
|
|
39
|
+
status Show GEO status and coverage
|
|
40
|
+
manifest Show site manifest (JSON)
|
|
41
|
+
llms Show llms.txt content
|
|
42
|
+
sitemap Show sitemap.xml content
|
|
43
|
+
report Show GEO analysis report
|
|
44
|
+
rebuild Rebuild GEO files (llms.txt, sitemap.xml)
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--json Machine-readable JSON output
|
|
48
|
+
--help Show help
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
geo geo status
|
|
52
|
+
geo geo rebuild
|
|
53
|
+
geo geo report --json`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printSubHelp(action) {
|
|
57
|
+
const helps = {
|
|
58
|
+
status: `Show GEO status and coverage.
|
|
59
|
+
|
|
60
|
+
Usage: geo geo status [--json]`,
|
|
61
|
+
manifest: `Show site manifest as JSON.
|
|
62
|
+
|
|
63
|
+
Usage: geo geo manifest`,
|
|
64
|
+
llms: `Display llms.txt content (AI crawler feed).
|
|
65
|
+
|
|
66
|
+
Usage: geo geo llms`,
|
|
67
|
+
sitemap: `Display sitemap.xml content.
|
|
68
|
+
|
|
69
|
+
Usage: geo geo sitemap`,
|
|
70
|
+
report: `Show GEO analysis report.
|
|
71
|
+
|
|
72
|
+
Usage: geo geo report [--json]`,
|
|
73
|
+
rebuild: `Rebuild GEO files (llms.txt, sitemap.xml).
|
|
74
|
+
|
|
75
|
+
Usage: geo geo rebuild [--json]`
|
|
76
|
+
};
|
|
77
|
+
console.log(helps[action] || `Unknown action: ${action}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function status(args = []) {
|
|
81
|
+
const json = hasFlag(args, '--json');
|
|
82
|
+
const baseUrl = getBaseUrl();
|
|
83
|
+
|
|
84
|
+
const data = await apiGet('/api/v1/geo/manifest');
|
|
85
|
+
const geoData = data.data || {};
|
|
86
|
+
// Server returns counts.totalDocs (nested), also check root level for compatibility
|
|
87
|
+
const counts = geoData.counts || {};
|
|
88
|
+
const total = counts.totalDocs || geoData.totalDocs || 0;
|
|
89
|
+
|
|
90
|
+
// Check AI compatibility features
|
|
91
|
+
const aiCompat = geoData.aiCompatibility || {};
|
|
92
|
+
const jsonLdSupported = aiCompat.jsonLdSupported || false;
|
|
93
|
+
const llmsTxtPath = aiCompat.llmsTxtPath || null;
|
|
94
|
+
|
|
95
|
+
// Calculate coverage based on categories with docs
|
|
96
|
+
const categories = geoData.categories || [];
|
|
97
|
+
const catsWithDocs = categories.filter(c => c.docCount > 0).length;
|
|
98
|
+
const coverage = categories.length > 0 ? ((catsWithDocs / categories.length) * 100).toFixed(1) : '0.0';
|
|
99
|
+
|
|
100
|
+
if (outputJson({ baseUrl, ...geoData, coverage: `${coverage}%` }, json)) return;
|
|
101
|
+
|
|
102
|
+
console.log('\n=== GEO Status ===\n');
|
|
103
|
+
console.log(`Base URL: ${baseUrl}`);
|
|
104
|
+
console.log(`Total Docs: ${total}`);
|
|
105
|
+
console.log(`Categories: ${categories.length}`);
|
|
106
|
+
console.log(`JSON-LD Supported: ${jsonLdSupported ? 'Yes' : 'No'}`);
|
|
107
|
+
console.log(`llms.txt: ${llmsTxtPath || 'Not configured'}`);
|
|
108
|
+
if (categories.length > 0) console.log(`\nGEO Coverage: ${coverage}% (${catsWithDocs}/${categories.length} categories with docs)`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function manifest(_args) {
|
|
112
|
+
const data = await apiGet('/api/v1/geo/manifest');
|
|
113
|
+
outputJson(data.data, true); // always JSON format
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function llms() {
|
|
117
|
+
const text = await apiGetRaw('/api/v1/llms.txt');
|
|
118
|
+
console.log(text);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function sitemap() {
|
|
122
|
+
const text = await apiGetRaw('/api/v1/geo/sitemap.xml');
|
|
123
|
+
console.log(text);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function report(args = []) {
|
|
127
|
+
const json = args.includes('--json');
|
|
128
|
+
|
|
129
|
+
const data = await apiGet('/api/v1/admin/geo/report');
|
|
130
|
+
const reportData = data.data || {};
|
|
131
|
+
|
|
132
|
+
if (outputJson(reportData, json)) return;
|
|
133
|
+
|
|
134
|
+
console.log('\n=== GEO Report ===\n');
|
|
135
|
+
console.log(JSON.stringify(reportData, null, 2));
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function rebuild(args = []) {
|
|
140
|
+
const json = args.includes('--json');
|
|
141
|
+
console.log('Rebuilding GEO files (llms.txt, sitemap.xml)...');
|
|
142
|
+
const data = await apiPost('/api/v1/geo/rebuild', {});
|
|
143
|
+
outputSuccess('GEO files rebuilt', json, data.data);
|
|
144
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guestbook management commands
|
|
3
|
+
* Usage: geo guestbook [list|toggle|update|delete] [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { extractArg, hasFlag } from '../utils/args.js';
|
|
7
|
+
import { apiGet, 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 guestbook(args) {
|
|
12
|
+
const result = dispatch(args, actions, ['list', 'toggle', 'update', 'delete'], printGuestbookHelp);
|
|
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/guestbook');
|
|
21
|
+
const entries = data.data || [];
|
|
22
|
+
|
|
23
|
+
if (outputJson(entries, json)) return;
|
|
24
|
+
|
|
25
|
+
console.log(`\nGuestbook Entries (${entries.length} found):\n`);
|
|
26
|
+
entries.forEach(e => {
|
|
27
|
+
const id = (e.id || e._id || '').toString().slice(0, 8);
|
|
28
|
+
const status = e.status || 'pending';
|
|
29
|
+
console.log(` ${id.padEnd(10)} | ${status.padEnd(10)} | ${(e.name || '').padEnd(15)} | ${(e.message || '').substring(0, 40)}`);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async toggle(args) {
|
|
34
|
+
const enabled = extractArg(args, '--enabled');
|
|
35
|
+
const json = hasFlag(args, '--json');
|
|
36
|
+
|
|
37
|
+
if (enabled === null || enabled === undefined) {
|
|
38
|
+
console.error('Error: --enabled is required (true or false)');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const enabledBool = enabled === 'true' || enabled === '1';
|
|
43
|
+
|
|
44
|
+
await apiPut('/api/v1/admin/guestbook-toggle', { enabled: enabledBool });
|
|
45
|
+
outputSuccess(`Guestbook ${enabledBool ? 'enabled' : 'disabled'}`, json, { enabled: enabledBool });
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async update(args) {
|
|
49
|
+
const id = extractArg(args, '--id');
|
|
50
|
+
const status = extractArg(args, '--status');
|
|
51
|
+
const json = hasFlag(args, '--json');
|
|
52
|
+
|
|
53
|
+
if (!id) {
|
|
54
|
+
console.error('Error: --id is required');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!status) {
|
|
59
|
+
console.error('Error: --status is required (approved or rejected)');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!['approved', 'rejected'].includes(status)) {
|
|
64
|
+
console.error('Error: --status must be "approved" or "rejected"');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await apiPut(`/api/v1/admin/guestbook/${encodeURIComponent(id)}`, { status });
|
|
69
|
+
outputSuccess(`Guestbook entry ${status}: ${id}`, json, { id, status });
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async delete(args) {
|
|
73
|
+
const id = extractArg(args, '--id');
|
|
74
|
+
const json = hasFlag(args, '--json');
|
|
75
|
+
|
|
76
|
+
if (!id) {
|
|
77
|
+
console.error('Error: --id is required');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const confirmed = await confirmDelete(`Delete guestbook entry "${id}"?`, args);
|
|
82
|
+
if (!confirmed) {
|
|
83
|
+
console.log('Cancelled.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await apiDelete(`/api/v1/admin/guestbook/${encodeURIComponent(id)}`);
|
|
88
|
+
outputSuccess(`Guestbook entry deleted: ${id}`, json, { deleted: id });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function printGuestbookHelp() {
|
|
93
|
+
console.log(`
|
|
94
|
+
Usage: geo guestbook <action> [options]
|
|
95
|
+
|
|
96
|
+
Actions:
|
|
97
|
+
list List all guestbook entries
|
|
98
|
+
toggle Enable or disable guestbook
|
|
99
|
+
update Update entry status (approve/reject)
|
|
100
|
+
delete Delete a guestbook entry
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
--id Entry ID (required for update/delete)
|
|
104
|
+
--status New status: approved, rejected (required for update)
|
|
105
|
+
--enabled Enable/disable guestbook: true, false (required for toggle)
|
|
106
|
+
--json Machine-readable JSON output
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
geo guestbook list --json
|
|
110
|
+
geo guestbook toggle --enabled true
|
|
111
|
+
geo guestbook update --id "entry-id" --status approved
|
|
112
|
+
geo guestbook delete --id "entry-id"
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login command
|
|
3
|
+
* Usage:
|
|
4
|
+
* geo login --url URL --user USER --pass PASS (password login)
|
|
5
|
+
* geo login --url URL --token TOKEN (token login)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractArg } from '../utils/args.js';
|
|
9
|
+
import { config } from '../utils/config.js';
|
|
10
|
+
import readline from 'readline';
|
|
11
|
+
|
|
12
|
+
const COOKIE_NAME = 'geo_wiki_token';
|
|
13
|
+
const CSRF_COOKIE_NAME = 'XSRF-TOKEN';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read password from stdin without echoing to terminal.
|
|
17
|
+
*/
|
|
18
|
+
async function readPasswordFromStdin() {
|
|
19
|
+
// If stdin is not a TTY, read from pipe
|
|
20
|
+
if (!process.stdin.isTTY) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
let data = '';
|
|
23
|
+
process.stdin.setEncoding('utf-8');
|
|
24
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
25
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// Interactive prompt
|
|
29
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
rl.question('Password: ', (answer) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
resolve(answer);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse the geo_wiki_token cookie pair out of a Set-Cookie header.
|
|
40
|
+
* Exported for unit testing.
|
|
41
|
+
*/
|
|
42
|
+
export function extractCookiePair(setCookieHeader) {
|
|
43
|
+
if (!setCookieHeader) return null;
|
|
44
|
+
const entries = String(setCookieHeader).split(/,(?=\s*[A-Za-z0-9_-]+=)/);
|
|
45
|
+
const cookies = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const pair = entry.split(';')[0].trim();
|
|
48
|
+
if (pair.startsWith(`${COOKIE_NAME}=`) || pair.startsWith(`${CSRF_COOKIE_NAME}=`)) {
|
|
49
|
+
cookies.push(pair);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return cookies.length > 0 ? cookies.join('; ') : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function login(args) {
|
|
56
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
57
|
+
printLoginHelp();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const url = extractArg(args, '--url') || extractArg(args, '-u');
|
|
62
|
+
const token = extractArg(args, '--token') || extractArg(args, '-t');
|
|
63
|
+
const user = extractArg(args, '--user') || extractArg(args, '-U');
|
|
64
|
+
const pass = extractArg(args, '--pass') || extractArg(args, '-p');
|
|
65
|
+
|
|
66
|
+
if (!url) {
|
|
67
|
+
printLoginHelp();
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Warn about HTTP (credentials sent in plaintext)
|
|
72
|
+
if (url.startsWith('http://') && !url.includes('localhost') && !url.includes('127.0.0.1')) {
|
|
73
|
+
console.warn('⚠️ Warning: Using HTTP — credentials will be transmitted in plaintext. Use HTTPS for production.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Token login
|
|
77
|
+
if (token) {
|
|
78
|
+
console.log(`Connecting to ${url}...`);
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(`${url}/api/v1/auth/me`, {
|
|
81
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
82
|
+
});
|
|
83
|
+
const data = await res.json().catch(() => ({}));
|
|
84
|
+
if (!res.ok || !data.success) {
|
|
85
|
+
console.error(`Token login failed: ${data.message || res.statusText}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
config.setBaseUrl(url);
|
|
89
|
+
config.setApiToken(token);
|
|
90
|
+
const u = data.data?.user;
|
|
91
|
+
if (u && u.username) {
|
|
92
|
+
console.log('Login successful!');
|
|
93
|
+
console.log(`User: ${u.username} (${u.role || 'unknown'})`);
|
|
94
|
+
} else {
|
|
95
|
+
console.log('Login successful!');
|
|
96
|
+
console.log('Token accepted (user info will be shown on next status check)');
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(`Login failed: ${e.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Password login
|
|
106
|
+
if (!user) {
|
|
107
|
+
printLoginHelp();
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Read password from stdin if --pass is '-' or not provided
|
|
112
|
+
let password = pass;
|
|
113
|
+
if (!password || password === '-') {
|
|
114
|
+
password = await readPasswordFromStdin();
|
|
115
|
+
if (!password) {
|
|
116
|
+
console.error('Error: password is required (use --pass or pipe via stdin)');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`Logging in to ${url}...`);
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(`${url}/api/v1/auth/login`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
'X-Client': 'geowiki-cli'
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ username: user, password: password })
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const data = await res.json().catch(() => ({}));
|
|
133
|
+
|
|
134
|
+
if (!res.ok || !data.success) {
|
|
135
|
+
console.error(`Login failed: ${data.message || data.error || res.statusText}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const setCookie = res.headers.get('set-cookie');
|
|
140
|
+
const cookiePair = extractCookiePair(setCookie);
|
|
141
|
+
if (!cookiePair) {
|
|
142
|
+
console.error('Login failed: server did not return geo_wiki_token cookie.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
config.setBaseUrl(url);
|
|
147
|
+
config.setCookie(cookiePair);
|
|
148
|
+
config.clearApiToken(); // Clear any stale API token when logging in with password
|
|
149
|
+
|
|
150
|
+
const u = data.data?.user || {};
|
|
151
|
+
console.log('Login successful!');
|
|
152
|
+
console.log(`User: ${u.username || user} (${u.role || 'unknown'})`);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`Login failed: ${e.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printLoginHelp() {
|
|
160
|
+
console.log(`
|
|
161
|
+
Usage:
|
|
162
|
+
geo login --url URL --user USER --pass PASS (password login)
|
|
163
|
+
geo login --url URL --user USER (prompt for password)
|
|
164
|
+
echo "pass" | geo login --url URL --user USER (password from stdin)
|
|
165
|
+
geo login --url URL --token TOKEN (token login)
|
|
166
|
+
|
|
167
|
+
Options:
|
|
168
|
+
--url, -u Wiki base URL (e.g., http://your-site:9002)
|
|
169
|
+
--user, -U Username (password login)
|
|
170
|
+
--pass, -p Password (password login). Use '-' to read from stdin.
|
|
171
|
+
--token, -t API Token (token login)
|
|
172
|
+
|
|
173
|
+
Security tip: Avoid --pass in shell history. Use prompt or stdin instead.
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
geo login --url http://your-site:9002 --user admin --pass <password>
|
|
177
|
+
geo login --url http://your-site:9002 --user admin
|
|
178
|
+
echo "mypassword" | geo login --url http://your-site:9002 --user admin --pass -
|
|
179
|
+
geo login --url http://your-site:9002 --token geo_xxxxx
|
|
180
|
+
`);
|
|
181
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media management commands
|
|
3
|
+
* Usage: geo media [upload|list|delete|tree|mkdir|rmdir|move] [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { extractArg, hasFlag } from '../utils/args.js';
|
|
7
|
+
import { apiGet, apiPost, apiDelete, apiUpload, 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
|
+
|
|
12
|
+
export async function media(args) {
|
|
13
|
+
const result = dispatch(args, actions, ['upload', 'list', 'delete', 'tree', 'mkdir', 'rmdir', 'move'], printMediaHelp);
|
|
14
|
+
if (result) await actions[result.action](result.subArgs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const actions = {
|
|
18
|
+
async upload(args) {
|
|
19
|
+
const file = extractArg(args, '--file') || extractArg(args, '-f');
|
|
20
|
+
const directory = extractArg(args, '--directory') || extractArg(args, '-d');
|
|
21
|
+
const json = hasFlag(args, '--json');
|
|
22
|
+
|
|
23
|
+
if (!file) {
|
|
24
|
+
console.error('Error: --file is required');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(file)) {
|
|
29
|
+
console.error(`File not found: ${file}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!json) console.log(`Uploading: ${file}${directory ? ` -> ${directory}/` : ''}...`);
|
|
34
|
+
|
|
35
|
+
const data = await apiUpload('/api/v1/media/upload', file, { directory });
|
|
36
|
+
const baseUrl = getBaseUrl();
|
|
37
|
+
const payload = data.data || {};
|
|
38
|
+
|
|
39
|
+
if (outputJson({ ...payload, fullUrl: `${baseUrl}${payload.url || ''}` }, json)) return;
|
|
40
|
+
|
|
41
|
+
console.log('Uploaded successfully!');
|
|
42
|
+
console.log(`File: ${payload.filename}`);
|
|
43
|
+
console.log(`URL: ${baseUrl}${payload.url}`);
|
|
44
|
+
console.log(`\nTip: To delete this file later, use:\n geo media delete --file ${payload.filename}`);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async list(args) {
|
|
48
|
+
const dir = extractArg(args, '--dir') || '';
|
|
49
|
+
const json = hasFlag(args, '--json');
|
|
50
|
+
|
|
51
|
+
const qs = dir ? `?dir=${encodeURIComponent(dir)}` : '';
|
|
52
|
+
const data = await apiGet(`/api/v1/media${qs}`);
|
|
53
|
+
const files = data.data?.files || data.data || [];
|
|
54
|
+
|
|
55
|
+
if (outputJson(files, json)) return;
|
|
56
|
+
|
|
57
|
+
console.log(`\nMedia files (${files.length} found)${dir ? ` in ${dir}` : ''}:\n`);
|
|
58
|
+
files.forEach(f => {
|
|
59
|
+
const size = f.size > 1024 * 1024 ? `${(f.size / 1024 / 1024).toFixed(1)}MB` : `${(f.size / 1024).toFixed(1)}KB`;
|
|
60
|
+
console.log(` ${(f.originalFilename || f.filename || '').padEnd(40)} | ${size.padEnd(10)} | ${f.uploadedBy || '—'}`);
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async tree(args) {
|
|
65
|
+
const json = hasFlag(args, '--json');
|
|
66
|
+
|
|
67
|
+
const data = await apiGet('/api/v1/media?tree=1');
|
|
68
|
+
const tree = data.data?.tree || [];
|
|
69
|
+
|
|
70
|
+
if (outputJson(tree, json)) return;
|
|
71
|
+
|
|
72
|
+
console.log('\nMedia directory tree:\n');
|
|
73
|
+
printTree(tree, '');
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async mkdir(args) {
|
|
77
|
+
const name = extractArg(args, '--name') || extractArg(args, '-n');
|
|
78
|
+
const parent = extractArg(args, '--parent') || extractArg(args, '-p') || '';
|
|
79
|
+
const json = hasFlag(args, '--json');
|
|
80
|
+
|
|
81
|
+
if (!name) {
|
|
82
|
+
console.error('Error: --name is required');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!json) console.log(`Creating folder: ${parent ? parent + '/' : ''}${name}...`);
|
|
87
|
+
|
|
88
|
+
const data = await apiPost('/api/v1/media/folder', { name, parentPath: parent });
|
|
89
|
+
outputSuccess(`Created: ${data.data?.path || name}`, json, data.data);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async rmdir(args) {
|
|
93
|
+
const path = extractArg(args, '--path') || extractArg(args, '-p');
|
|
94
|
+
const json = hasFlag(args, '--json');
|
|
95
|
+
|
|
96
|
+
if (!path) {
|
|
97
|
+
console.error('Error: --path is required');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!json) console.log(`Deleting folder: ${path}...`);
|
|
102
|
+
|
|
103
|
+
await apiDelete('/api/v1/media/folder', { folderPath: path });
|
|
104
|
+
outputSuccess(`Deleted folder: ${path}`, json, { deleted: path });
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async move(args) {
|
|
108
|
+
const source = extractArg(args, '--source') || extractArg(args, '-s');
|
|
109
|
+
const target = extractArg(args, '--target') || extractArg(args, '-t') || '';
|
|
110
|
+
const json = hasFlag(args, '--json');
|
|
111
|
+
|
|
112
|
+
if (!source) {
|
|
113
|
+
console.error('Error: --source is required');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!json) console.log(`Moving: ${source} -> ${target || '(root)'}...`);
|
|
118
|
+
|
|
119
|
+
const data = await apiPost('/api/v1/media/move', { sourcePath: source, targetDir: target });
|
|
120
|
+
outputSuccess(`Moved: ${source}`, json, data.data);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async delete(args) {
|
|
124
|
+
const filename = extractArg(args, '--file') || extractArg(args, '-f');
|
|
125
|
+
const json = hasFlag(args, '--json');
|
|
126
|
+
|
|
127
|
+
if (!filename) {
|
|
128
|
+
console.error('Error: --file is required');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!json) console.log(`Deleting: ${filename}...`);
|
|
133
|
+
|
|
134
|
+
await apiDelete(`/api/v1/media/${encodeURIComponent(filename)}`);
|
|
135
|
+
outputSuccess(`Deleted: ${filename}`, json, { deleted: filename });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function printTree(nodes, prefix) {
|
|
140
|
+
nodes.forEach((node, i) => {
|
|
141
|
+
const isLast = i === nodes.length - 1;
|
|
142
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
143
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
144
|
+
console.log(`${prefix}${connector}${node.name}/`);
|
|
145
|
+
if (node.children && node.children.length) {
|
|
146
|
+
printTree(node.children, prefix + childPrefix);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printMediaHelp() {
|
|
152
|
+
console.log(`
|
|
153
|
+
Usage: geo media <action> [options]
|
|
154
|
+
|
|
155
|
+
Actions:
|
|
156
|
+
upload Upload a file (image / video / 3D model / document)
|
|
157
|
+
list List media files in a directory
|
|
158
|
+
tree Show directory tree
|
|
159
|
+
mkdir Create a folder
|
|
160
|
+
rmdir Delete a folder
|
|
161
|
+
move Move a file to another directory
|
|
162
|
+
delete Delete a media file (admin)
|
|
163
|
+
|
|
164
|
+
Options:
|
|
165
|
+
--file, -f Local file path (upload) or filename (delete)
|
|
166
|
+
--directory, -d Sub-directory for upload (e.g. "products/eva-kit")
|
|
167
|
+
--dir Directory to list (e.g. "products")
|
|
168
|
+
--name, -n Folder name (mkdir)
|
|
169
|
+
--parent, -p Parent directory (mkdir)
|
|
170
|
+
--path, -p Folder path to delete (rmdir)
|
|
171
|
+
--source, -s Source file path (move, e.g. "test/image.png")
|
|
172
|
+
--target, -t Target directory (move, empty = root)
|
|
173
|
+
--json Machine-readable JSON output
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
geo media upload --file ./diagram.jpg
|
|
177
|
+
geo media upload --file ./model.step --directory models
|
|
178
|
+
geo media list --dir products
|
|
179
|
+
geo media tree
|
|
180
|
+
geo media mkdir --name products
|
|
181
|
+
geo media mkdir --name images --parent products
|
|
182
|
+
geo media rmdir --path old-folder
|
|
183
|
+
geo media move --source test/img.png --target products
|
|
184
|
+
geo media move --source products/img.png # move to root
|
|
185
|
+
geo media delete --file old-image.jpg
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search documents
|
|
3
|
+
* Usage: geo search <query> [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { extractArg, hasFlag } from '../utils/args.js';
|
|
7
|
+
import { apiGet, getBaseUrl } from '../utils/api.js';
|
|
8
|
+
import { outputJson } from '../utils/output.js';
|
|
9
|
+
|
|
10
|
+
export async function search(args) {
|
|
11
|
+
const query = args[0];
|
|
12
|
+
|
|
13
|
+
if (!query || query === '--help' || query === '-h') {
|
|
14
|
+
printSearchHelp();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const category = extractArg(args, '--category') || extractArg(args, '-c');
|
|
19
|
+
const lang = extractArg(args, '--lang') || extractArg(args, '-l') || 'zh';
|
|
20
|
+
const limit = extractArg(args, '--limit') || '20';
|
|
21
|
+
const json = hasFlag(args, '--json');
|
|
22
|
+
|
|
23
|
+
let url = `/api/v1/docs/search?q=${encodeURIComponent(query)}&lang=${lang}&limit=${limit}`;
|
|
24
|
+
if (category) url += `&category=${encodeURIComponent(category)}`;
|
|
25
|
+
|
|
26
|
+
const data = await apiGet(url);
|
|
27
|
+
const results = data.data || [];
|
|
28
|
+
|
|
29
|
+
if (outputJson(results, json)) return;
|
|
30
|
+
|
|
31
|
+
console.log(`\nFound ${results.length} result(s):\n`);
|
|
32
|
+
|
|
33
|
+
if (results.length === 0) {
|
|
34
|
+
console.log(' No results found.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const baseUrl = getBaseUrl();
|
|
39
|
+
results.forEach((r, i) => {
|
|
40
|
+
console.log(`${i + 1}. ${r.title || r.slug}`);
|
|
41
|
+
console.log(` URL: ${baseUrl}/docs/${r.slug}`);
|
|
42
|
+
if (r.description) {
|
|
43
|
+
console.log(` ${r.description.substring(0, 80)}...`);
|
|
44
|
+
}
|
|
45
|
+
console.log('');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printSearchHelp() {
|
|
50
|
+
console.log(`
|
|
51
|
+
Usage: geo search <query> [options]
|
|
52
|
+
|
|
53
|
+
Arguments:
|
|
54
|
+
query Search query string
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--category, -c Filter by category
|
|
58
|
+
--lang, -l Language (zh/en), default: zh
|
|
59
|
+
--limit Max results, default: 20
|
|
60
|
+
--json Machine-readable JSON output
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
geo search "CAN bus"
|
|
64
|
+
geo search "motor" --category can-motion
|
|
65
|
+
geo search "Arduino" --lang en
|
|
66
|
+
`);
|
|
67
|
+
}
|