draftgo-cli 4.0.22 → 4.0.24

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.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/bin/draftgo.js +8 -8
  3. package/package.json +72 -72
  4. package/resources/custom-service-sdk/auth_test.go +56 -0
  5. package/resources/custom-service-sdk/manifest.json +14 -9
  6. package/resources/custom-service-sdk/platform.go +19 -27
  7. package/resources/custom-service-sdk/resources.go +1 -0
  8. package/resources/custom-service-sdk/resources_scope_test.go +10 -5
  9. package/resources/custom-service-sdk/sdk.go +6 -5
  10. package/resources/skill/SKILL.md +1 -1
  11. package/resources/skill/manifest.json +1 -1
  12. package/resources/skill/references/aihub.md +74 -74
  13. package/resources/skill/references/app-api.md +78 -78
  14. package/resources/skill/references/architecture.md +40 -40
  15. package/resources/skill/references/checkout.md +105 -105
  16. package/resources/skill/references/custom-services.md +6 -6
  17. package/resources/skill/references/data.md +168 -168
  18. package/resources/skill/references/methods.md +3 -0
  19. package/resources/skill/references/modules.md +48 -48
  20. package/resources/skill/references/runtime.md +95 -96
  21. package/resources/skill/story/SKILL.md +264 -264
  22. package/src/commands/help.js +72 -72
  23. package/src/commands/listTargets.js +12 -12
  24. package/src/commands/status.js +2 -2
  25. package/src/commands/uninstall.js +45 -45
  26. package/src/commands/update.js +20 -20
  27. package/src/customServices.js +5 -4
  28. package/src/detect.js +14 -14
  29. package/src/fsx.js +67 -67
  30. package/src/index.js +25 -25
  31. package/src/localRuntime/detect.js +76 -76
  32. package/src/localRuntime/mysqlClient.js +138 -138
  33. package/src/logger.js +37 -37
  34. package/src/mcp/client.js +586 -595
  35. package/src/mcp/hosts.js +520 -520
  36. package/src/mcp/protocol.js +184 -164
  37. package/src/prompt.js +94 -94
  38. package/src/updateCheck.js +16 -16
@@ -1,174 +1,194 @@
1
- 'use strict';
2
-
1
+ 'use strict';
2
+
3
3
  const DEFAULT_PROTOCOL_VERSION = '2026-07-28';
4
-
5
- class McpHttpError extends Error {
6
- constructor(message, { status = 0, rpc = null, code = null } = {}) {
7
- super(message);
8
- this.name = 'McpHttpError';
9
- this.status = status;
10
- this.rpc = rpc;
11
- this.code = code;
12
- }
13
- }
14
-
15
- function redactText(value, secrets = []) {
16
- let text = String(value == null ? '' : value);
17
- for (const secret of secrets.filter(Boolean)) text = text.split(String(secret)).join('[REDACTED]');
18
- text = text.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s,;"']+/ig, '$1[REDACTED]');
19
- text = text.replace(/(\"?(?:token|sat)\"?\s*[:=]\s*\"?)[^\s,;"'}]+/ig, '$1[REDACTED]');
20
- return text;
21
- }
22
-
23
- function endpointFor(config) {
24
- const server = new URL(String(config.server || config.mcp_url));
25
- const serverPath = server.pathname.replace(/\/+$/, '');
26
- const endpoint = config.mcp_url
27
- ? new URL(String(config.mcp_url), server)
28
- : new URL(`${serverPath}/mcp`, server);
29
- if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
30
- throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
31
- }
32
- const allowedOrigins = new Set([
33
- server.origin,
34
- ...((config.mcp_allowed_origins || []).map((value) => new URL(value).origin)),
35
- ]);
36
- if (!allowedOrigins.has(endpoint.origin)) {
4
+
5
+ class McpHttpError extends Error {
6
+ constructor(message, { status = 0, rpc = null, code = null } = {}) {
7
+ super(message);
8
+ this.name = 'McpHttpError';
9
+ this.status = status;
10
+ this.rpc = rpc;
11
+ this.code = code;
12
+ }
13
+ }
14
+
15
+ function redactText(value, secrets = []) {
16
+ let text = String(value == null ? '' : value);
17
+ for (const secret of secrets.filter(Boolean)) text = text.split(String(secret)).join('[REDACTED]');
18
+ text = text.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s,;"']+/ig, '$1[REDACTED]');
19
+ text = text.replace(/(\"?(?:token|sat)\"?\s*[:=]\s*\"?)[^\s,;"'}]+/ig, '$1[REDACTED]');
20
+ return text;
21
+ }
22
+
23
+ function endpointFor(config) {
24
+ const server = new URL(String(config.server || config.mcp_url));
25
+ const serverPath = server.pathname.replace(/\/+$/, '');
26
+ const endpoint = config.mcp_url
27
+ ? new URL(String(config.mcp_url), server)
28
+ : new URL(`${serverPath}/mcp`, server);
29
+ if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
30
+ throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
31
+ }
32
+ const allowedOrigins = new Set([
33
+ server.origin,
34
+ ...((config.mcp_allowed_origins || []).map((value) => new URL(value).origin)),
35
+ ]);
36
+ if (!allowedOrigins.has(endpoint.origin)) {
37
37
  throw new McpHttpError('DraftGo MCP endpoint origin is not allowlisted; refusing to send the API Key.');
38
- }
39
- return endpoint.toString();
40
- }
41
-
38
+ }
39
+ return endpoint.toString();
40
+ }
41
+
42
42
  function jsonRpcMessages(value) {
43
- if (Array.isArray(value)) return value.filter((item) => item && typeof item === 'object');
44
- return value && typeof value === 'object' ? [value] : [];
43
+ if (Array.isArray(value)) return value.filter((item) => item && typeof item === 'object');
44
+ return value && typeof value === 'object' ? [value] : [];
45
45
  }
46
46
 
47
- async function parseEventStream(body, onMessage) {
48
- if (!body) return [];
49
- const reader = body.getReader();
50
- const decoder = new TextDecoder();
51
- let pending = '';
52
- let dataLines = [];
53
- const messages = [];
54
- const consume = () => {
55
- if (!dataLines.length) return;
56
- const data = dataLines.join('\n');
57
- dataLines = [];
58
- if (!data || data === '[DONE]') return;
59
- let parsed;
60
- try { parsed = JSON.parse(data); } catch {
61
- throw new McpHttpError('DraftGo MCP returned invalid JSON in an event stream.');
62
- }
63
- for (const message of jsonRpcMessages(parsed)) {
64
- messages.push(message);
65
- if (onMessage) onMessage(message);
66
- }
67
- };
68
- while (true) {
69
- const { done, value } = await reader.read();
70
- pending += decoder.decode(value || new Uint8Array(), { stream: !done });
71
- const lines = pending.split(/\r?\n/);
72
- pending = lines.pop() || '';
73
- for (const line of lines) {
74
- if (!line) consume();
75
- else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
76
- }
77
- if (done) break;
47
+ function routingHeaders(message) {
48
+ const messages = jsonRpcMessages(message);
49
+ if (messages.length !== 1 || typeof messages[0].method !== 'string' || !messages[0].method) return {};
50
+ const request = messages[0];
51
+ const headers = { 'Mcp-Method': request.method };
52
+ const params = request.params && typeof request.params === 'object' && !Array.isArray(request.params)
53
+ ? request.params
54
+ : {};
55
+ if (request.method === 'tools/call' && typeof params.name === 'string' && params.name) {
56
+ headers['Mcp-Name'] = params.name;
57
+ } else if (request.method === 'prompts/get' && typeof params.name === 'string' && params.name) {
58
+ headers['Mcp-Name'] = params.name;
59
+ } else if (request.method === 'resources/read' && typeof params.uri === 'string' && params.uri) {
60
+ headers['Mcp-Name'] = params.uri;
78
61
  }
79
- if (pending.startsWith('data:')) dataLines.push(pending.slice(5).replace(/^ /, ''));
80
- consume();
81
- return messages;
62
+ return headers;
82
63
  }
83
-
84
- async function readLimitedText(response, maxBytes = 65536) {
85
- if (!response.body) return '';
86
- const reader = response.body.getReader();
87
- const chunks = [];
88
- let size = 0;
89
- while (size < maxBytes) {
90
- const { done, value } = await reader.read();
91
- if (done) break;
92
- const chunk = Buffer.from(value);
93
- chunks.push(chunk.subarray(0, Math.max(0, maxBytes - size)));
94
- size += chunk.length;
95
- }
96
- return Buffer.concat(chunks).toString('utf8');
97
- }
98
-
99
- async function postJsonRpc(config, message, options = {}) {
100
- const token = String(config.token || config.sat || '');
101
- const controller = new AbortController();
102
- const timeoutMs = Number(options.timeoutMs || config.mcp_timeout_ms || 60000);
103
- const timer = setTimeout(() => controller.abort(new Error('MCP request timed out')), timeoutMs);
104
- const externalSignal = options.signal;
105
- const abort = () => controller.abort(externalSignal.reason);
106
- if (externalSignal) {
107
- if (externalSignal.aborted) abort();
108
- else externalSignal.addEventListener('abort', abort, { once: true });
109
- }
110
-
64
+
65
+ async function parseEventStream(body, onMessage) {
66
+ if (!body) return [];
67
+ const reader = body.getReader();
68
+ const decoder = new TextDecoder();
69
+ let pending = '';
70
+ let dataLines = [];
71
+ const messages = [];
72
+ const consume = () => {
73
+ if (!dataLines.length) return;
74
+ const data = dataLines.join('\n');
75
+ dataLines = [];
76
+ if (!data || data === '[DONE]') return;
77
+ let parsed;
78
+ try { parsed = JSON.parse(data); } catch {
79
+ throw new McpHttpError('DraftGo MCP returned invalid JSON in an event stream.');
80
+ }
81
+ for (const message of jsonRpcMessages(parsed)) {
82
+ messages.push(message);
83
+ if (onMessage) onMessage(message);
84
+ }
85
+ };
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+ pending += decoder.decode(value || new Uint8Array(), { stream: !done });
89
+ const lines = pending.split(/\r?\n/);
90
+ pending = lines.pop() || '';
91
+ for (const line of lines) {
92
+ if (!line) consume();
93
+ else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
94
+ }
95
+ if (done) break;
96
+ }
97
+ if (pending.startsWith('data:')) dataLines.push(pending.slice(5).replace(/^ /, ''));
98
+ consume();
99
+ return messages;
100
+ }
101
+
102
+ async function readLimitedText(response, maxBytes = 65536) {
103
+ if (!response.body) return '';
104
+ const reader = response.body.getReader();
105
+ const chunks = [];
106
+ let size = 0;
107
+ while (size < maxBytes) {
108
+ const { done, value } = await reader.read();
109
+ if (done) break;
110
+ const chunk = Buffer.from(value);
111
+ chunks.push(chunk.subarray(0, Math.max(0, maxBytes - size)));
112
+ size += chunk.length;
113
+ }
114
+ return Buffer.concat(chunks).toString('utf8');
115
+ }
116
+
117
+ async function postJsonRpc(config, message, options = {}) {
118
+ const token = String(config.token || config.sat || '');
119
+ const controller = new AbortController();
120
+ const timeoutMs = Number(options.timeoutMs || config.mcp_timeout_ms || 60000);
121
+ const timer = setTimeout(() => controller.abort(new Error('MCP request timed out')), timeoutMs);
122
+ const externalSignal = options.signal;
123
+ const abort = () => controller.abort(externalSignal.reason);
124
+ if (externalSignal) {
125
+ if (externalSignal.aborted) abort();
126
+ else externalSignal.addEventListener('abort', abort, { once: true });
127
+ }
128
+
111
129
  const headers = {
112
- Authorization: `Bearer ${token}`,
113
- Accept: 'application/json, text/event-stream',
114
- 'Content-Type': 'application/json',
130
+ Authorization: `Bearer ${token}`,
131
+ Accept: 'application/json, text/event-stream',
132
+ 'Content-Type': 'application/json',
115
133
  'MCP-Protocol-Version': options.protocolVersion || DEFAULT_PROTOCOL_VERSION,
116
- };
117
- if (options.sessionId) headers['Mcp-Session-Id'] = options.sessionId;
118
-
119
- let response;
120
- try {
121
- response = await fetch(endpointFor(config), {
122
- method: 'POST',
123
- headers,
124
- body: JSON.stringify(message),
125
- signal: controller.signal,
126
- });
127
- const sessionId = response.headers.get('mcp-session-id');
128
- if (sessionId && options.onSession) options.onSession(sessionId);
129
- if (response.status === 202 || response.status === 204) return [];
130
-
131
- const contentType = String(response.headers.get('content-type') || '').toLowerCase();
132
- if (!response.ok) {
133
- const bodyText = await readLimitedText(response);
134
- let rpc = null;
135
- try {
136
- const parsed = JSON.parse(bodyText);
137
- rpc = jsonRpcMessages(parsed)[0] || null;
138
- } catch { /* retain a bounded text error */ }
139
- const structured = rpc && rpc.error && (rpc.error.message || rpc.error.code);
140
- throw new McpHttpError(
141
- redactText(structured || bodyText || `MCP HTTP ${response.status}`, [token]),
142
- { status: response.status, rpc, code: rpc && rpc.error && rpc.error.code },
143
- );
144
- }
145
-
146
- if (contentType.includes('text/event-stream')) {
147
- return await parseEventStream(response.body, options.onMessage);
148
- }
149
- const text = await response.text();
150
- if (!text.trim()) return [];
151
- let parsed;
152
- try { parsed = JSON.parse(text); } catch {
153
- throw new McpHttpError('DraftGo MCP returned invalid JSON.', { status: response.status });
154
- }
155
- const messages = jsonRpcMessages(parsed);
156
- if (options.onMessage) messages.forEach(options.onMessage);
157
- return messages;
158
- } catch (error) {
159
- if (error instanceof McpHttpError || controller.signal.aborted) throw error;
160
- throw new McpHttpError(redactText(error.message || error, [token]), { status: response && response.status });
161
- } finally {
162
- clearTimeout(timer);
163
- if (externalSignal) externalSignal.removeEventListener('abort', abort);
164
- }
165
- }
166
-
167
- module.exports = {
168
- DEFAULT_PROTOCOL_VERSION,
169
- McpHttpError,
170
- redactText,
134
+ ...routingHeaders(message),
135
+ };
136
+ if (options.sessionId) headers['Mcp-Session-Id'] = options.sessionId;
137
+
138
+ let response;
139
+ try {
140
+ response = await fetch(endpointFor(config), {
141
+ method: 'POST',
142
+ headers,
143
+ body: JSON.stringify(message),
144
+ signal: controller.signal,
145
+ });
146
+ const sessionId = response.headers.get('mcp-session-id');
147
+ if (sessionId && options.onSession) options.onSession(sessionId);
148
+ if (response.status === 202 || response.status === 204) return [];
149
+
150
+ const contentType = String(response.headers.get('content-type') || '').toLowerCase();
151
+ if (!response.ok) {
152
+ const bodyText = await readLimitedText(response);
153
+ let rpc = null;
154
+ try {
155
+ const parsed = JSON.parse(bodyText);
156
+ rpc = jsonRpcMessages(parsed)[0] || null;
157
+ } catch { /* retain a bounded text error */ }
158
+ const structured = rpc && rpc.error && (rpc.error.message || rpc.error.code);
159
+ throw new McpHttpError(
160
+ redactText(structured || bodyText || `MCP HTTP ${response.status}`, [token]),
161
+ { status: response.status, rpc, code: rpc && rpc.error && rpc.error.code },
162
+ );
163
+ }
164
+
165
+ if (contentType.includes('text/event-stream')) {
166
+ return await parseEventStream(response.body, options.onMessage);
167
+ }
168
+ const text = await response.text();
169
+ if (!text.trim()) return [];
170
+ let parsed;
171
+ try { parsed = JSON.parse(text); } catch {
172
+ throw new McpHttpError('DraftGo MCP returned invalid JSON.', { status: response.status });
173
+ }
174
+ const messages = jsonRpcMessages(parsed);
175
+ if (options.onMessage) messages.forEach(options.onMessage);
176
+ return messages;
177
+ } catch (error) {
178
+ if (error instanceof McpHttpError || controller.signal.aborted) throw error;
179
+ throw new McpHttpError(redactText(error.message || error, [token]), { status: response && response.status });
180
+ } finally {
181
+ clearTimeout(timer);
182
+ if (externalSignal) externalSignal.removeEventListener('abort', abort);
183
+ }
184
+ }
185
+
186
+ module.exports = {
187
+ DEFAULT_PROTOCOL_VERSION,
188
+ McpHttpError,
189
+ redactText,
171
190
  endpointFor,
172
- postJsonRpc,
173
- parseEventStream,
174
- };
191
+ routingHeaders,
192
+ postJsonRpc,
193
+ parseEventStream,
194
+ };
package/src/prompt.js CHANGED
@@ -1,94 +1,94 @@
1
- 'use strict';
2
-
3
- // Tiny interactive prompt helpers built on Node's readline.
4
- // No external deps. All functions return Promises.
5
-
6
- const readline = require('readline');
7
-
8
- function makeRL() {
9
- return readline.createInterface({
10
- input: process.stdin,
11
- output: process.stdout,
12
- terminal: true,
13
- });
14
- }
15
-
16
- function ask(question, { default: def } = {}) {
17
- return new Promise((resolve) => {
18
- const rl = makeRL();
19
- const hint = def !== undefined && def !== '' ? ` [${def}]` : '';
20
- rl.question(`${question}${hint} `, (answer) => {
21
- rl.close();
22
- const v = (answer || '').trim();
23
- resolve(v === '' && def !== undefined ? String(def) : v);
24
- });
25
- });
26
- }
27
-
28
- async function askRequired(question, { default: def, validate } = {}) {
29
- // Loop until user supplies a non-empty value (or default is used).
30
- // If `validate(v)` returns a string, treat as error message and re-prompt.
31
- while (true) {
32
- const v = await ask(question, { default: def });
33
- if (!v) {
34
- console.log(' (不能为空,请重新输入)');
35
- continue;
36
- }
37
- if (typeof validate === 'function') {
38
- const err = validate(v);
39
- if (typeof err === 'string' && err) {
40
- console.log(` ${err}`);
41
- continue;
42
- }
43
- }
44
- return v;
45
- }
46
- }
47
-
48
- async function confirm(question, { default: def = false } = {}) {
49
- const yn = def ? 'Y/n' : 'y/N';
50
- const v = (await ask(`${question} (${yn})`)).toLowerCase();
51
- if (!v) return def;
52
- return v === 'y' || v === 'yes';
53
- }
54
-
55
- async function askPassword(question, { default: def } = {}) {
56
- // Best-effort masked input. Falls back to plain input if stdin is not a TTY.
57
- if (!process.stdin.isTTY) return ask(question, { default: def });
58
- return new Promise((resolve) => {
59
- const stdin = process.stdin;
60
- const out = process.stdout;
61
- const hint = def ? ' [使用上次保存的值]' : '';
62
- out.write(`${question}${hint} `);
63
- let buf = '';
64
- const onData = (ch) => {
65
- const s = ch.toString('utf8');
66
- for (const c of s) {
67
- if (c === '\n' || c === '\r' || c === '\u0004') {
68
- stdin.setRawMode(false);
69
- stdin.pause();
70
- stdin.removeListener('data', onData);
71
- out.write('\n');
72
- const v = buf;
73
- resolve(v === '' && def !== undefined ? String(def) : v);
74
- return;
75
- } else if (c === '\u0003') { // Ctrl+C
76
- process.exit(130);
77
- } else if (c === '\u007f' || c === '\b') {
78
- if (buf.length > 0) {
79
- buf = buf.slice(0, -1);
80
- out.write('\b \b');
81
- }
82
- } else {
83
- buf += c;
84
- out.write('*');
85
- }
86
- }
87
- };
88
- stdin.setRawMode(true);
89
- stdin.resume();
90
- stdin.on('data', onData);
91
- });
92
- }
93
-
94
- module.exports = { ask, askRequired, confirm, askPassword };
1
+ 'use strict';
2
+
3
+ // Tiny interactive prompt helpers built on Node's readline.
4
+ // No external deps. All functions return Promises.
5
+
6
+ const readline = require('readline');
7
+
8
+ function makeRL() {
9
+ return readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ terminal: true,
13
+ });
14
+ }
15
+
16
+ function ask(question, { default: def } = {}) {
17
+ return new Promise((resolve) => {
18
+ const rl = makeRL();
19
+ const hint = def !== undefined && def !== '' ? ` [${def}]` : '';
20
+ rl.question(`${question}${hint} `, (answer) => {
21
+ rl.close();
22
+ const v = (answer || '').trim();
23
+ resolve(v === '' && def !== undefined ? String(def) : v);
24
+ });
25
+ });
26
+ }
27
+
28
+ async function askRequired(question, { default: def, validate } = {}) {
29
+ // Loop until user supplies a non-empty value (or default is used).
30
+ // If `validate(v)` returns a string, treat as error message and re-prompt.
31
+ while (true) {
32
+ const v = await ask(question, { default: def });
33
+ if (!v) {
34
+ console.log(' (不能为空,请重新输入)');
35
+ continue;
36
+ }
37
+ if (typeof validate === 'function') {
38
+ const err = validate(v);
39
+ if (typeof err === 'string' && err) {
40
+ console.log(` ${err}`);
41
+ continue;
42
+ }
43
+ }
44
+ return v;
45
+ }
46
+ }
47
+
48
+ async function confirm(question, { default: def = false } = {}) {
49
+ const yn = def ? 'Y/n' : 'y/N';
50
+ const v = (await ask(`${question} (${yn})`)).toLowerCase();
51
+ if (!v) return def;
52
+ return v === 'y' || v === 'yes';
53
+ }
54
+
55
+ async function askPassword(question, { default: def } = {}) {
56
+ // Best-effort masked input. Falls back to plain input if stdin is not a TTY.
57
+ if (!process.stdin.isTTY) return ask(question, { default: def });
58
+ return new Promise((resolve) => {
59
+ const stdin = process.stdin;
60
+ const out = process.stdout;
61
+ const hint = def ? ' [使用上次保存的值]' : '';
62
+ out.write(`${question}${hint} `);
63
+ let buf = '';
64
+ const onData = (ch) => {
65
+ const s = ch.toString('utf8');
66
+ for (const c of s) {
67
+ if (c === '\n' || c === '\r' || c === '\u0004') {
68
+ stdin.setRawMode(false);
69
+ stdin.pause();
70
+ stdin.removeListener('data', onData);
71
+ out.write('\n');
72
+ const v = buf;
73
+ resolve(v === '' && def !== undefined ? String(def) : v);
74
+ return;
75
+ } else if (c === '\u0003') { // Ctrl+C
76
+ process.exit(130);
77
+ } else if (c === '\u007f' || c === '\b') {
78
+ if (buf.length > 0) {
79
+ buf = buf.slice(0, -1);
80
+ out.write('\b \b');
81
+ }
82
+ } else {
83
+ buf += c;
84
+ out.write('*');
85
+ }
86
+ }
87
+ };
88
+ stdin.setRawMode(true);
89
+ stdin.resume();
90
+ stdin.on('data', onData);
91
+ });
92
+ }
93
+
94
+ module.exports = { ask, askRequired, confirm, askPassword };
@@ -1,14 +1,14 @@
1
- 'use strict';
2
-
1
+ 'use strict';
2
+
3
3
  // Fetch the latest draftgo-cli version from the DraftGo release endpoint.
4
4
  // Results are cached for 24 hours so update stays fast and deterministic.
5
-
5
+
6
6
  const http = require('http');
7
7
  const https = require('https');
8
8
  const fs = require('fs');
9
9
  const os = require('os');
10
10
  const path = require('path');
11
-
11
+
12
12
  const REPO = process.env.DRAFTGO_REPO || 'draftgo/draftgo-cli';
13
13
  const RELEASE_BASE_URL = process.env.DRAFTGO_INSTALL_BASE_URL || 'https://draftgo.cn/draftgo-cli';
14
14
  const RELEASE_URLS = [
@@ -85,16 +85,16 @@ async function fetchLatestVersion(timeoutMs = 3000, opts = {}) {
85
85
  }
86
86
  return null;
87
87
  }
88
-
89
- // Compare 'a.b.c' ignoring prerelease tags. Returns 1 / 0 / -1.
90
- function cmpSemver(a, b) {
91
- const pa = String(a).split('-')[0].split('.').map((x) => Number(x) || 0);
92
- const pb = String(b).split('-')[0].split('.').map((x) => Number(x) || 0);
93
- for (let i = 0; i < 3; i++) {
94
- const d = (pa[i] || 0) - (pb[i] || 0);
95
- if (d !== 0) return d > 0 ? 1 : -1;
96
- }
97
- return 0;
98
- }
99
-
88
+
89
+ // Compare 'a.b.c' ignoring prerelease tags. Returns 1 / 0 / -1.
90
+ function cmpSemver(a, b) {
91
+ const pa = String(a).split('-')[0].split('.').map((x) => Number(x) || 0);
92
+ const pb = String(b).split('-')[0].split('.').map((x) => Number(x) || 0);
93
+ for (let i = 0; i < 3; i++) {
94
+ const d = (pa[i] || 0) - (pb[i] || 0);
95
+ if (d !== 0) return d > 0 ? 1 : -1;
96
+ }
97
+ return 0;
98
+ }
99
+
100
100
  module.exports = { fetchLatestVersion, cmpSemver, readCache, requestVersion, CACHE_TTL_MS };