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