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,197 @@
1
+ /**
2
+ * Unified API client for CLI commands
3
+ * Auto-injects baseUrl, cookie auth, CSRF token, and error handling.
4
+ *
5
+ * Round 2 migration: server uses httpOnly Cookie (geo_wiki_token).
6
+ * We send the persisted cookie pair via the `Cookie` header on every call.
7
+ * CSRF token is extracted from the cookie and sent in x-xsrf-token header.
8
+ */
9
+
10
+ import path from 'path';
11
+ import { config } from './config.js';
12
+
13
+ const REQUEST_TIMEOUT = 30000; // 30 seconds
14
+
15
+ function fail(msg) {
16
+ console.error(msg);
17
+ process.exit(1);
18
+ }
19
+
20
+ function extractCsrfToken(cookie) {
21
+ if (!cookie) return '';
22
+ const match = cookie.match(/XSRF-TOKEN=([^;]+)/);
23
+ return match ? match[1] : '';
24
+ }
25
+
26
+ function authHeader(extra = {}) {
27
+ // API token takes precedence
28
+ const apiToken = config.getApiToken();
29
+ if (apiToken) {
30
+ return { ...extra, 'Authorization': `Bearer ${apiToken}` };
31
+ }
32
+ // Fall back to cookie auth
33
+ const cookie = config.getCookie();
34
+ if (!cookie) return extra;
35
+ const csrfToken = extractCsrfToken(cookie);
36
+ const headers = { ...extra, Cookie: cookie };
37
+ if (csrfToken) {
38
+ headers['x-xsrf-token'] = csrfToken;
39
+ }
40
+ return headers;
41
+ }
42
+
43
+ async function handleResponse(res, errorMessage) {
44
+ const data = await res.json().catch(() => ({}));
45
+
46
+ if (!res.ok || (data.success === false)) {
47
+ fail(`${errorMessage}: ${data.message || data.error || res.statusText}`);
48
+ }
49
+
50
+ return data;
51
+ }
52
+
53
+ export async function apiGet(path) {
54
+ const baseUrl = config.getBaseUrl();
55
+ try {
56
+ const res = await fetch(`${baseUrl}${path}`, {
57
+ headers: authHeader(),
58
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT)
59
+ });
60
+ return await handleResponse(res, `Request failed`);
61
+ } catch (e) {
62
+ fail(`Error: ${e.message}`);
63
+ }
64
+ }
65
+
66
+ export async function apiGetRaw(path) {
67
+ const baseUrl = config.getBaseUrl();
68
+ try {
69
+ const res = await fetch(`${baseUrl}${path}`, {
70
+ headers: authHeader(),
71
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT)
72
+ });
73
+ if (!res.ok) fail(`Request failed: ${res.statusText}`);
74
+ return await res.text();
75
+ } catch (e) {
76
+ fail(`Error: ${e.message}`);
77
+ }
78
+ }
79
+
80
+ export async function apiPost(path, body) {
81
+ const baseUrl = config.getBaseUrl();
82
+ try {
83
+ const res = await fetch(`${baseUrl}${path}`, {
84
+ method: 'POST',
85
+ headers: authHeader({ 'Content-Type': 'application/json' }),
86
+ body: JSON.stringify(body),
87
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT)
88
+ });
89
+ return await handleResponse(res, `Request failed`);
90
+ } catch (e) {
91
+ fail(`Error: ${e.message}`);
92
+ }
93
+ }
94
+
95
+ export async function apiPut(path, body) {
96
+ const baseUrl = config.getBaseUrl();
97
+ try {
98
+ const res = await fetch(`${baseUrl}${path}`, {
99
+ method: 'PUT',
100
+ headers: authHeader({ 'Content-Type': 'application/json' }),
101
+ body: JSON.stringify(body),
102
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT)
103
+ });
104
+ return await handleResponse(res, `Request failed`);
105
+ } catch (e) {
106
+ fail(`Error: ${e.message}`);
107
+ }
108
+ }
109
+
110
+ export async function apiDelete(path, body) {
111
+ const baseUrl = config.getBaseUrl();
112
+ try {
113
+ const opts = {
114
+ method: 'DELETE',
115
+ headers: authHeader({ 'Content-Type': 'application/json' }),
116
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT)
117
+ };
118
+ if (body) opts.body = JSON.stringify(body);
119
+ const res = await fetch(`${baseUrl}${path}`, opts);
120
+ if (!res.ok) {
121
+ const data = await res.json().catch(() => ({}));
122
+ fail(`Failed to delete: ${data.message || data.error || res.statusText}`);
123
+ }
124
+ return true;
125
+ } catch (e) {
126
+ fail(`Error: ${e.message}`);
127
+ }
128
+ }
129
+
130
+ const MIME_BY_EXT = {
131
+ '.jpg': 'image/jpeg',
132
+ '.jpeg': 'image/jpeg',
133
+ '.png': 'image/png',
134
+ '.gif': 'image/gif',
135
+ '.webp': 'image/webp',
136
+ '.mp4': 'video/mp4',
137
+ '.webm': 'video/webm',
138
+ '.mp3': 'audio/mpeg',
139
+ '.wav': 'audio/wav',
140
+ '.ogg': 'audio/ogg',
141
+ '.pdf': 'application/pdf',
142
+ '.doc': 'application/msword',
143
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
144
+ '.txt': 'text/plain',
145
+ '.md': 'text/markdown',
146
+ '.step': 'application/step',
147
+ '.stp': 'application/stp',
148
+ '.igs': 'application/iges',
149
+ '.iges': 'application/iges'
150
+ };
151
+
152
+ function guessMime(filePath) {
153
+ const ext = path.extname(filePath).toLowerCase();
154
+ return MIME_BY_EXT[ext] || 'application/octet-stream';
155
+ }
156
+
157
+ // Exported for unit testing; mirrors the server's ALLOWED_TYPES table.
158
+ export const __test = { guessMime, MIME_BY_EXT };
159
+
160
+ /**
161
+ * Upload a file as raw binary body (server expects req.headers + raw bytes).
162
+ * @param {string} apiPath Always '/api/v1/media/upload'.
163
+ * @param {string} file Local file path.
164
+ * @param {object} [opts]
165
+ * @param {string} [opts.directory] Optional sub-directory under public/media.
166
+ */
167
+ export async function apiUpload(apiPath, file, opts = {}) {
168
+ const baseUrl = config.getBaseUrl();
169
+ const fs = await import('fs');
170
+ const buffer = fs.readFileSync(file);
171
+ const fileName = path.basename(file);
172
+ const mime = guessMime(file);
173
+
174
+ const headers = authHeader({
175
+ 'Content-Type': mime,
176
+ 'X-Original-Filename': encodeURIComponent(fileName)
177
+ });
178
+ if (opts.directory) {
179
+ headers['X-Media-Directory'] = opts.directory;
180
+ }
181
+
182
+ try {
183
+ const res = await fetch(`${baseUrl}${apiPath}`, {
184
+ method: 'POST',
185
+ headers,
186
+ body: buffer,
187
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT * 3) // Uploads can be larger
188
+ });
189
+ return await handleResponse(res, `Upload failed`);
190
+ } catch (e) {
191
+ fail(`Error: ${e.message}`);
192
+ }
193
+ }
194
+
195
+ export function getBaseUrl() {
196
+ return config.getBaseUrl();
197
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Argument parsing helpers for CLI commands
3
+ */
4
+
5
+ export function extractArg(args, flag) {
6
+ // Support --flag=value syntax
7
+ for (const arg of args) {
8
+ if (arg.startsWith(flag + '=')) {
9
+ return arg.slice(flag.length + 1);
10
+ }
11
+ }
12
+ // Support --flag value syntax
13
+ const idx = args.indexOf(flag);
14
+ if (idx < 0) return null;
15
+ const val = args[idx + 1];
16
+ // Reject if next token is itself a flag
17
+ if (val !== undefined && !val.startsWith('-')) {
18
+ return val;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ export function hasFlag(args, flag) {
24
+ return args.includes(flag);
25
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Config manager for GEO Wiki CLI
3
+ * Stores cookie and base URL in user's home directory.
4
+ *
5
+ * Round 2 migration: server uses httpOnly Cookie (geo_wiki_token) for auth.
6
+ * The CLI persists the raw cookie pair (e.g. "geo_wiki_token=eyJ...") and
7
+ * sends it back via the `Cookie` request header on subsequent calls.
8
+ */
9
+
10
+ import os from 'os';
11
+ import path from 'path';
12
+ import fs from 'fs';
13
+
14
+ const CONFIG_DIR = path.join(os.homedir(), '.geowiki');
15
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
16
+
17
+ function load() {
18
+ try {
19
+ if (fs.existsSync(CONFIG_FILE)) {
20
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
21
+ }
22
+ } catch (e) {}
23
+ return { cookie: null, baseUrl: null, apiToken: null };
24
+ }
25
+
26
+ function save(data) {
27
+ if (!fs.existsSync(CONFIG_DIR)) {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ }
30
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
31
+ }
32
+
33
+ export const config = {
34
+ getCookie() {
35
+ return load().cookie;
36
+ },
37
+
38
+ setCookie(cookie) {
39
+ const data = load();
40
+ data.cookie = cookie;
41
+ save(data);
42
+ },
43
+
44
+ clearCookie() {
45
+ const data = load();
46
+ data.cookie = null;
47
+ save(data);
48
+ },
49
+
50
+ // API Token support
51
+ getApiToken() {
52
+ return load().apiToken || null;
53
+ },
54
+
55
+ setApiToken(token) {
56
+ const data = load();
57
+ data.apiToken = token;
58
+ save(data);
59
+ },
60
+
61
+ clearApiToken() {
62
+ const data = load();
63
+ data.apiToken = null;
64
+ save(data);
65
+ },
66
+
67
+ // Backwards-compatible aliases
68
+ getToken() {
69
+ const data = load();
70
+ return data.cookie || data.apiToken;
71
+ },
72
+ clearToken() {
73
+ const data = load();
74
+ data.cookie = null;
75
+ data.apiToken = null;
76
+ save(data);
77
+ },
78
+
79
+ getBaseUrl() {
80
+ return load().baseUrl;
81
+ },
82
+
83
+ setBaseUrl(baseUrl) {
84
+ const data = load();
85
+ data.baseUrl = baseUrl;
86
+ save(data);
87
+ },
88
+
89
+ clearAll() {
90
+ if (fs.existsSync(CONFIG_FILE)) {
91
+ fs.unlinkSync(CONFIG_FILE);
92
+ }
93
+ }
94
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Generic action dispatch for CLI subcommands
3
+ */
4
+
5
+ export function dispatch(args, actions, validActions, printHelp) {
6
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
7
+ printHelp();
8
+ return null;
9
+ }
10
+
11
+ const [action, ...subArgs] = args;
12
+
13
+ if (!validActions.includes(action)) {
14
+ console.error(`Unknown action: ${action}`);
15
+ printHelp();
16
+ process.exit(1);
17
+ }
18
+
19
+ return { action, subArgs };
20
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Output helpers for CLI commands
3
+ * Supports --json flag for agent-friendly structured output
4
+ */
5
+
6
+ import readline from 'readline';
7
+
8
+ export function outputJson(data, jsonFlag) {
9
+ if (jsonFlag) {
10
+ console.log(JSON.stringify(data, null, 2));
11
+ return true;
12
+ }
13
+ return false;
14
+ }
15
+
16
+ export function outputSuccess(message, jsonFlag, data = {}) {
17
+ if (jsonFlag) {
18
+ console.log(JSON.stringify({ success: true, ...data }, null, 2));
19
+ return;
20
+ }
21
+ console.log(message);
22
+ }
23
+
24
+ /**
25
+ * Prompt user for confirmation. Returns true if confirmed.
26
+ * Skips prompt if --yes/-y flag is present.
27
+ * @param {string} message - Confirmation message
28
+ * @param {string[]} args - Command args to check for --yes/-y
29
+ * @returns {Promise<boolean>}
30
+ */
31
+ export async function confirmDelete(message, args = []) {
32
+ if (args.includes('--yes') || args.includes('-y')) return true;
33
+
34
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
35
+ return new Promise((resolve) => {
36
+ rl.question(`${message} (y/N) `, (answer) => {
37
+ rl.close();
38
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
39
+ });
40
+ });
41
+ }
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "geowiki-cli",
3
+ "version": "1.0.1",
4
+ "description": "GEO Wiki Pro - AI-powered knowledge base with GEO optimization",
5
+ "type": "module",
6
+ "bin": "bin/geo.js",
7
+ "files": [
8
+ "bin/",
9
+ "cli/commands/",
10
+ "cli/utils/",
11
+ "cli/index.js",
12
+ "cli/package.json"
13
+ ],
14
+ "scripts": {
15
+ "dev": "vite",
16
+ "build": "vite build",
17
+ "preview": "vite preview",
18
+ "server": "node server/index.js",
19
+ "dev:full": "concurrently \"npm run dev\" \"npm run server\"",
20
+ "import:data": "node scripts/import-data.js",
21
+ "lint": "eslint src --ext .vue,.js",
22
+ "test": "vitest run",
23
+ "test:e2e": "node tests/e2e/setup.js && playwright test",
24
+ "test:e2e:ui": "playwright test --ui",
25
+ "test:e2e:headed": "playwright test --headed",
26
+ "prepare": "node -e \"try{require('husky')}catch{}\""
27
+ },
28
+ "dependencies": {
29
+ "@codemirror/autocomplete": "^6.20.2",
30
+ "@codemirror/commands": "^6.10.3",
31
+ "@codemirror/lang-markdown": "^6.5.0",
32
+ "@codemirror/language": "^6.12.3",
33
+ "@codemirror/language-data": "^6.5.2",
34
+ "@codemirror/search": "^6.7.0",
35
+ "@codemirror/state": "^6.6.0",
36
+ "@codemirror/theme-one-dark": "^6.1.3",
37
+ "@codemirror/view": "^6.42.1",
38
+ "bcryptjs": "^3.0.3",
39
+ "compression": "^1.8.1",
40
+ "cors": "^2.8.0",
41
+ "dompurify": "^3.4.5",
42
+ "express": "^4.18.0",
43
+ "fuse.js": "^7.0.0",
44
+ "gray-matter": "^4.0.3",
45
+ "helmet": "^8.0.0",
46
+ "highlight.js": "^11.9.0",
47
+ "jsonwebtoken": "^9.0.0",
48
+ "markdown-it": "^14.1.1",
49
+ "markdown-it-anchor": "^8.6.7",
50
+ "markdown-it-container": "^3.0.0",
51
+ "markdown-it-emoji": "^2.0.2",
52
+ "markdown-it-footnote": "^3.0.3",
53
+ "markdown-it-mark": "^3.0.1",
54
+ "markdown-it-task-lists": "^1.4.1",
55
+ "pinia": "^2.1.0",
56
+ "pino": "^10.3.1",
57
+ "pino-pretty": "^13.1.3",
58
+ "sql.js": "^1.14.1",
59
+ "vue": "^3.4.0",
60
+ "vue-router": "^4.2.0"
61
+ },
62
+ "devDependencies": {
63
+ "@playwright/test": "^1.44.0",
64
+ "@vitejs/plugin-vue": "^5.0.0",
65
+ "@vue/test-utils": "^2.4.10",
66
+ "autoprefixer": "^10.4.0",
67
+ "concurrently": "^8.2.0",
68
+ "eslint": "^8.57.0",
69
+ "eslint-plugin-vue": "^9.33.0",
70
+ "husky": "^9.1.7",
71
+ "jsdom": "^29.1.1",
72
+ "lint-staged": "^16.4.0",
73
+ "postcss": "^8.4.0",
74
+ "tailwindcss": "^3.4.0",
75
+ "vite": "^5.0.0",
76
+ "vitest": "^1.2.0",
77
+ "vue-eslint-parser": "^9.4.3"
78
+ },
79
+ "keywords": [
80
+ "geo",
81
+ "wiki",
82
+ "knowledge-base",
83
+ "cli",
84
+ "ai",
85
+ "seo",
86
+ "documentation"
87
+ ],
88
+ "author": "GEO Wiki Team",
89
+ "license": "MIT",
90
+ "repository": {
91
+ "type": "git",
92
+ "url": "git+https://github.com/ai-littleyao/geo-wiki-framework.git"
93
+ },
94
+ "lint-staged": {
95
+ "*.{vue,js}": "eslint --fix",
96
+ "*.{json,md,yml}": "prettier --write"
97
+ }
98
+ }