aiexecode 1.0.75 → 1.0.81

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.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

@@ -6,226 +6,372 @@
6
6
  import React from 'react';
7
7
  import { Text, Box } from 'ink';
8
8
  import { theme } from '../design/themeColors.js';
9
+ import { colorizeCode } from './syntaxHighlighter.js';
10
+ import { GridRenderer } from './GridRenderer.js';
11
+ import { ProcessInlineText } from './InlineFormatter.js';
12
+
13
+ // 렌더링 상수
14
+ const SPACING = {
15
+ EMPTY_LINE: 1,
16
+ CODE_PADDING: 1,
17
+ LIST_PREFIX_PAD: 1,
18
+ LIST_TEXT_GROW: 1
19
+ };
9
20
 
10
21
  /**
11
22
  * 마크다운 텍스트를 파싱하여 Ink 컴포넌트로 변환
23
+ * @param {string} text - 마크다운 텍스트
24
+ * @param {Object} options - 렌더링 옵션
25
+ * @param {boolean} options.isPending - 스트리밍 중 여부
26
+ * @param {number} options.availableHeight - 사용 가능한 터미널 높이
27
+ * @param {number} options.terminalWidth - 터미널 너비 (기본값: 80)
28
+ * @returns {React.ReactNode}
12
29
  */
13
- export function renderMarkdown(text) {
30
+ export function renderMarkdown(text, options = {}) {
14
31
  if (!text) return null;
15
32
 
16
- const lines = text.split('\n');
17
- const elements = [];
18
- let inCodeBlock = false;
19
- let codeLines = [];
20
- let codeLanguage = '';
21
-
22
- for (let i = 0; i < lines.length; i++) {
23
- const line = lines[i];
24
-
25
- // Code block 처리
26
- if (line.trim().startsWith('```')) {
27
- if (inCodeBlock) {
28
- // Code block
29
- if (codeLines.length > 0) {
30
- elements.push(
31
- React.createElement(Box, {
32
- key: `code-${i}`,
33
- flexDirection: 'column',
34
- borderStyle: 'round',
35
- borderColor: theme.text.secondary,
36
- paddingX: 1,
37
- marginY: 0
38
- },
39
- React.createElement(Text, { color: theme.text.secondary }, codeLines.join('\n'))
40
- )
41
- );
42
- }
43
- codeLines = [];
44
- codeLanguage = '';
45
- inCodeBlock = false;
33
+ const {
34
+ isPending = false,
35
+ availableHeight,
36
+ terminalWidth = 80
37
+ } = options;
38
+
39
+ const lineArray = text.split(/\r?\n/);
40
+
41
+ // 정규식 패턴
42
+ const patterns = {
43
+ header: /^ *(#{1,4}) +(.*)/,
44
+ codeFence: /^ *(`{3,}|~{3,}) *(\w*?) *$/,
45
+ unorderedList: /^([ \t]*)([-*+]) +(.*)/,
46
+ orderedList: /^([ \t]*)(\d+)\. +(.*)/,
47
+ horizontalRule: /^ *([-*_] *){3,} *$/,
48
+ tableRow: /^\s*\|(.+)\|\s*$/,
49
+ tableSeparator: /^\s*\|?\s*(:?-+:?)\s*(\|\s*(:?-+:?)\s*)+\|?\s*$/
50
+ };
51
+
52
+ const blocks = [];
53
+ let previousLineWasEmpty = true;
54
+
55
+ // 상태 변수
56
+ let codeBlockActive = false;
57
+ let codeBlockLines = [];
58
+ let codeBlockLanguage = null;
59
+ let codeBlockFence = '';
60
+
61
+ let tableActive = false;
62
+ let tableHeaderCells = [];
63
+ let tableDataRows = [];
64
+
65
+ function appendBlock(block) {
66
+ if (block) {
67
+ blocks.push(block);
68
+ previousLineWasEmpty = false;
69
+ }
70
+ }
71
+
72
+ lineArray.forEach((currentLine, lineIndex) => {
73
+ const lineKey = `ln-${lineIndex}`;
74
+
75
+ // 코드 블록 내부 처리
76
+ if (codeBlockActive) {
77
+ const fenceMatch = currentLine.match(patterns.codeFence);
78
+ if (fenceMatch &&
79
+ fenceMatch[1].startsWith(codeBlockFence[0]) &&
80
+ fenceMatch[1].length >= codeBlockFence.length) {
81
+ appendBlock(
82
+ React.createElement(BuildCodeBlock, {
83
+ key: lineKey,
84
+ lines: codeBlockLines,
85
+ language: codeBlockLanguage,
86
+ isPending,
87
+ availableHeight,
88
+ terminalWidth
89
+ })
90
+ );
91
+ codeBlockActive = false;
92
+ codeBlockLines = [];
93
+ codeBlockLanguage = null;
94
+ codeBlockFence = '';
46
95
  } else {
47
- // Code block 시작
48
- codeLanguage = line.trim().slice(3).trim();
49
- inCodeBlock = true;
96
+ codeBlockLines.push(currentLine);
50
97
  }
51
- continue;
98
+ return;
52
99
  }
53
100
 
54
- if (inCodeBlock) {
55
- codeLines.push(line);
56
- continue;
57
- }
101
+ // 패턴 매칭
102
+ const fenceMatch = currentLine.match(patterns.codeFence);
103
+ const headerMatch = currentLine.match(patterns.header);
104
+ const ulMatch = currentLine.match(patterns.unorderedList);
105
+ const olMatch = currentLine.match(patterns.orderedList);
106
+ const hrMatch = currentLine.match(patterns.horizontalRule);
107
+ const tableRowMatch = currentLine.match(patterns.tableRow);
108
+ const tableSepMatch = currentLine.match(patterns.tableSeparator);
58
109
 
59
- //
60
- if (line.trim() === '') {
61
- elements.push(React.createElement(Text, { key: `empty-${i}` }, ''));
62
- continue;
110
+ // 코드 블록 시작
111
+ if (fenceMatch) {
112
+ codeBlockActive = true;
113
+ codeBlockFence = fenceMatch[1];
114
+ codeBlockLanguage = fenceMatch[2] || null;
115
+ }
116
+ // 테이블 시작 감지
117
+ else if (tableRowMatch && !tableActive) {
118
+ if (lineIndex + 1 < lineArray.length &&
119
+ lineArray[lineIndex + 1].match(patterns.tableSeparator)) {
120
+ tableActive = true;
121
+ tableHeaderCells = tableRowMatch[1].split('|').map(cell => cell.trim());
122
+ tableDataRows = [];
123
+ } else {
124
+ appendBlock(
125
+ React.createElement(Box, { key: lineKey },
126
+ React.createElement(Text, { wrap: 'wrap' },
127
+ React.createElement(ProcessInlineText, { content: currentLine })
128
+ )
129
+ )
130
+ );
131
+ }
132
+ }
133
+ // 테이블 구분선 스킵
134
+ else if (tableActive && tableSepMatch) {
135
+ // 구분선은 무시
136
+ }
137
+ // 테이블 데이터 행
138
+ else if (tableActive && tableRowMatch) {
139
+ const cells = tableRowMatch[1].split('|').map(cell => cell.trim());
140
+ while (cells.length < tableHeaderCells.length) cells.push('');
141
+ if (cells.length > tableHeaderCells.length) cells.length = tableHeaderCells.length;
142
+ tableDataRows.push(cells);
63
143
  }
144
+ // 테이블 종료
145
+ else if (tableActive && !tableRowMatch) {
146
+ if (tableHeaderCells.length > 0 && tableDataRows.length > 0) {
147
+ appendBlock(
148
+ React.createElement(BuildTable, {
149
+ key: `table-${blocks.length}`,
150
+ columnHeaders: tableHeaderCells,
151
+ dataRows: tableDataRows,
152
+ maxWidth: terminalWidth
153
+ })
154
+ );
155
+ }
156
+ tableActive = false;
157
+ tableDataRows = [];
158
+ tableHeaderCells = [];
64
159
 
65
- // Heading 처리 (### 먼저 검사 - 긴 것부터)
66
- if (line.startsWith('### ')) {
67
- elements.push(
68
- React.createElement(Box, {
69
- key: `h3-${i}`,
70
- flexDirection: 'row'
71
- },
72
- React.createElement(Text, {
73
- color: theme.text.accent,
74
- bold: true
75
- }, renderInlineMarkdown(line.slice(4)))
76
- )
77
- );
78
- continue;
160
+ // 현재 처리
161
+ if (currentLine.trim().length > 0) {
162
+ appendBlock(
163
+ React.createElement(Box, { key: lineKey },
164
+ React.createElement(Text, { wrap: 'wrap' },
165
+ React.createElement(ProcessInlineText, { content: currentLine })
166
+ )
167
+ )
168
+ );
169
+ }
79
170
  }
80
- if (line.startsWith('## ')) {
81
- elements.push(
82
- React.createElement(Box, {
83
- key: `h2-${i}`,
84
- flexDirection: 'row'
85
- },
86
- React.createElement(Text, {
87
- color: theme.text.accent,
88
- bold: true
89
- }, renderInlineMarkdown(line.slice(3)))
171
+ // 수평선
172
+ else if (hrMatch) {
173
+ appendBlock(
174
+ React.createElement(Box, { key: lineKey },
175
+ React.createElement(Text, { dimColor: true }, '---')
90
176
  )
91
177
  );
92
- continue;
93
178
  }
94
- if (line.startsWith('# ')) {
95
- elements.push(
96
- React.createElement(Box, {
97
- key: `h1-${i}`,
98
- flexDirection: 'row'
99
- },
100
- React.createElement(Text, {
101
- color: theme.text.accent,
102
- bold: true
103
- }, renderInlineMarkdown(line.slice(2)))
104
- )
179
+ // 헤더
180
+ else if (headerMatch) {
181
+ const level = headerMatch[1].length;
182
+ const headerText = headerMatch[2];
183
+ let headerElement = null;
184
+
185
+ switch (level) {
186
+ case 1:
187
+ headerElement = React.createElement(Text, { bold: true, color: theme.text.link },
188
+ React.createElement(ProcessInlineText, { content: headerText })
189
+ );
190
+ break;
191
+ case 2:
192
+ headerElement = React.createElement(Text, { bold: true, color: theme.text.link },
193
+ React.createElement(ProcessInlineText, { content: headerText })
194
+ );
195
+ break;
196
+ case 3:
197
+ headerElement = React.createElement(Text, { bold: true, color: theme.text.primary },
198
+ React.createElement(ProcessInlineText, { content: headerText })
199
+ );
200
+ break;
201
+ case 4:
202
+ headerElement = React.createElement(Text, { italic: true, color: theme.text.secondary },
203
+ React.createElement(ProcessInlineText, { content: headerText })
204
+ );
205
+ break;
206
+ default:
207
+ headerElement = React.createElement(Text, { color: theme.text.primary },
208
+ React.createElement(ProcessInlineText, { content: headerText })
209
+ );
210
+ }
211
+ if (headerElement) {
212
+ appendBlock(React.createElement(Box, { key: lineKey }, headerElement));
213
+ }
214
+ }
215
+ // 순서 없는 리스트
216
+ else if (ulMatch) {
217
+ const [, indent, marker, content] = ulMatch;
218
+ appendBlock(
219
+ React.createElement(BuildListItem, {
220
+ key: lineKey,
221
+ itemText: content,
222
+ listType: 'ul',
223
+ marker: marker,
224
+ indentation: indent
225
+ })
105
226
  );
106
- continue;
107
227
  }
108
-
109
- // 리스트 항목 처리
110
- const listMatch = line.match(/^(\s*)([-*•])\s+(.+)$/);
111
- if (listMatch) {
112
- const [, indent, bullet, content] = listMatch;
113
- elements.push(
114
- React.createElement(Box, {
115
- key: `list-${i}`,
116
- flexDirection: 'row',
117
- marginLeft: indent.length
118
- },
119
- React.createElement(Text, { color: theme.text.accent }, bullet + ' '),
120
- React.createElement(Text, null, renderInlineMarkdown(content))
121
- )
228
+ // 순서 있는 리스트
229
+ else if (olMatch) {
230
+ const [, indent, marker, content] = olMatch;
231
+ appendBlock(
232
+ React.createElement(BuildListItem, {
233
+ key: lineKey,
234
+ itemText: content,
235
+ listType: 'ol',
236
+ marker: marker,
237
+ indentation: indent
238
+ })
122
239
  );
123
- continue;
124
240
  }
241
+ // 빈 줄 또는 일반 텍스트
242
+ else {
243
+ if (currentLine.trim().length === 0 && !codeBlockActive) {
244
+ if (!previousLineWasEmpty) {
245
+ blocks.push(
246
+ React.createElement(Box, {
247
+ key: `space-${lineIndex}`,
248
+ height: SPACING.EMPTY_LINE
249
+ })
250
+ );
251
+ previousLineWasEmpty = true;
252
+ }
253
+ } else {
254
+ appendBlock(
255
+ React.createElement(Box, { key: lineKey },
256
+ React.createElement(Text, { wrap: 'wrap', color: theme.text.primary },
257
+ React.createElement(ProcessInlineText, { content: currentLine })
258
+ )
259
+ )
260
+ );
261
+ }
262
+ }
263
+ });
125
264
 
126
- // 일반 텍스트 (인라인 마크다운 처리)
127
- elements.push(
128
- React.createElement(Box, {
129
- key: `line-${i}`,
130
- flexDirection: 'row'
131
- },
132
- React.createElement(Text, null, renderInlineMarkdown(line))
133
- )
265
+ // 닫히지 않은 코드 블록 처리
266
+ if (codeBlockActive) {
267
+ appendBlock(
268
+ React.createElement(BuildCodeBlock, {
269
+ key: 'eof-code',
270
+ lines: codeBlockLines,
271
+ language: codeBlockLanguage,
272
+ isPending,
273
+ availableHeight,
274
+ terminalWidth
275
+ })
134
276
  );
135
277
  }
136
278
 
137
- return React.createElement(Box, { flexDirection: 'column' }, elements);
279
+ // 닫히지 않은 테이블 처리
280
+ if (tableActive && tableHeaderCells.length > 0 && tableDataRows.length > 0) {
281
+ appendBlock(
282
+ React.createElement(BuildTable, {
283
+ key: `table-${blocks.length}`,
284
+ columnHeaders: tableHeaderCells,
285
+ dataRows: tableDataRows,
286
+ maxWidth: terminalWidth
287
+ })
288
+ );
289
+ }
290
+
291
+ return React.createElement(React.Fragment, null, blocks);
138
292
  }
139
293
 
140
294
  /**
141
- * 인라인 마크다운 처리 (**, `, 등)
295
+ * 코드 블록 렌더링 컴포넌트
142
296
  */
143
- function renderInlineMarkdown(text) {
144
- const parts = [];
145
- let currentIndex = 0;
146
- let partKey = 0;
147
-
148
- // 모든 마크다운 패턴 찾기 (정규식 재생성하여 lastIndex 문제 방지)
149
- const patterns = [];
150
-
151
- // **bold** 패턴 찾기
152
- let boldRegex = /\*\*([^*]+)\*\*/g;
153
- let match;
154
- while ((match = boldRegex.exec(text)) !== null) {
155
- patterns.push({
156
- type: 'bold',
157
- start: match.index,
158
- end: match.index + match[0].length,
159
- content: match[1]
160
- });
161
- }
162
-
163
- // `code` 패턴 찾기
164
- let codeRegex = /`([^`]+)`/g;
165
- while ((match = codeRegex.exec(text)) !== null) {
166
- patterns.push({
167
- type: 'code',
168
- start: match.index,
169
- end: match.index + match[0].length,
170
- content: match[1]
171
- });
172
- }
297
+ const BuildCodeBlockInternal = ({ lines, language, isPending, availableHeight, terminalWidth }) => {
298
+ const MIN_LINES_BEFORE_MSG = 1;
299
+ const RESERVED_LINES = 2;
173
300
 
174
- // 시작 위치로 정렬
175
- patterns.sort((a, b) => a.start - b.start);
301
+ if (isPending && availableHeight !== undefined) {
302
+ const maxLines = Math.max(0, availableHeight - RESERVED_LINES);
176
303
 
177
- // 겹치는 패턴 제거 (먼저 나온 패턴 우선)
178
- const filteredPatterns = [];
179
- let lastEnd = 0;
180
- for (const pattern of patterns) {
181
- if (pattern.start >= lastEnd) {
182
- filteredPatterns.push(pattern);
183
- lastEnd = pattern.end;
184
- }
185
- }
186
-
187
- // 패턴 적용
188
- for (const pattern of filteredPatterns) {
189
- // 이전 텍스트 추가
190
- if (currentIndex < pattern.start) {
191
- const plainText = text.slice(currentIndex, pattern.start);
192
- if (plainText) {
193
- parts.push(
194
- React.createElement(Text, { key: `text-${partKey++}` }, plainText)
304
+ if (lines.length > maxLines) {
305
+ if (maxLines < MIN_LINES_BEFORE_MSG) {
306
+ return React.createElement(Box, { paddingLeft: SPACING.CODE_PADDING },
307
+ React.createElement(Text, { color: theme.text.secondary },
308
+ '... code is being written ...'
309
+ )
195
310
  );
196
311
  }
197
- }
198
312
 
199
- // 패턴 적용된 텍스트 추가
200
- if (pattern.type === 'bold') {
201
- parts.push(
202
- React.createElement(Text, {
203
- key: `bold-${partKey++}`,
204
- bold: true,
205
- color: theme.text.primary
206
- }, pattern.content)
207
- );
208
- } else if (pattern.type === 'code') {
209
- parts.push(
210
- React.createElement(Text, {
211
- key: `code-${partKey++}`,
212
- color: theme.status.warning
213
- }, pattern.content)
214
- );
215
- }
216
-
217
- currentIndex = pattern.end;
218
- }
313
+ const truncatedLines = lines.slice(0, maxLines);
314
+ const truncatedCode = colorizeCode(truncatedLines.join('\n'), language, true);
219
315
 
220
- // 남은 텍스트 추가
221
- if (currentIndex < text.length) {
222
- const remainingText = text.slice(currentIndex);
223
- if (remainingText) {
224
- parts.push(
225
- React.createElement(Text, { key: `text-${partKey++}` }, remainingText)
316
+ return React.createElement(Box, {
317
+ paddingLeft: SPACING.CODE_PADDING,
318
+ flexDirection: 'column'
319
+ },
320
+ truncatedCode,
321
+ React.createElement(Text, { color: theme.text.secondary },
322
+ '... generating more ...'
323
+ )
226
324
  );
227
325
  }
228
326
  }
229
327
 
230
- return parts.length > 0 ? parts : text;
231
- }
328
+ const fullCode = lines.join('\n');
329
+ const highlightedCode = colorizeCode(fullCode, language, true);
330
+
331
+ return React.createElement(Box, {
332
+ paddingLeft: SPACING.CODE_PADDING,
333
+ flexDirection: 'column',
334
+ width: terminalWidth,
335
+ flexShrink: 0
336
+ }, highlightedCode);
337
+ };
338
+
339
+ const BuildCodeBlock = React.memo(BuildCodeBlockInternal);
340
+
341
+ /**
342
+ * 리스트 항목 렌더링 컴포넌트
343
+ */
344
+ const BuildListItemInternal = ({ itemText, listType, marker, indentation = '' }) => {
345
+ const displayPrefix = listType === 'ol' ? `${marker}. ` : `${marker} `;
346
+ const prefixLength = displayPrefix.length;
347
+ const indentAmount = indentation.length;
348
+
349
+ return React.createElement(Box, {
350
+ paddingLeft: indentAmount + SPACING.LIST_PREFIX_PAD,
351
+ flexDirection: 'row'
352
+ },
353
+ React.createElement(Box, { width: prefixLength },
354
+ React.createElement(Text, { color: theme.text.primary }, displayPrefix)
355
+ ),
356
+ React.createElement(Box, { flexGrow: SPACING.LIST_TEXT_GROW },
357
+ React.createElement(Text, { wrap: 'wrap', color: theme.text.primary },
358
+ React.createElement(ProcessInlineText, { content: itemText })
359
+ )
360
+ )
361
+ );
362
+ };
363
+
364
+ const BuildListItem = React.memo(BuildListItemInternal);
365
+
366
+ /**
367
+ * 테이블 렌더링 컴포넌트
368
+ */
369
+ const BuildTableInternal = ({ columnHeaders, dataRows, maxWidth }) => {
370
+ return React.createElement(GridRenderer, {
371
+ columnHeaders,
372
+ dataRows,
373
+ maxWidth
374
+ });
375
+ };
376
+
377
+ const BuildTable = React.memo(BuildTableInternal);
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Version check utility
3
+ * Compares local version with remote version from GitHub
4
+ */
5
+
6
+ import { createDebugLogger } from './debug_log.js';
7
+ import https from 'https';
8
+
9
+ const debugLog = createDebugLogger('version_check.log', 'version_check');
10
+
11
+ const REMOTE_PACKAGE_JSON_URL = 'https://raw.githubusercontent.com/kstost/aiexecode/refs/heads/main/package.json';
12
+ const TIMEOUT_MS = 5000; // 5 seconds timeout
13
+
14
+ /**
15
+ * Fetch remote package.json from GitHub
16
+ * @returns {Promise<Object|null>} Remote package.json object or null on error
17
+ */
18
+ async function fetchRemotePackageJson() {
19
+ return new Promise((resolve) => {
20
+ const req = https.get(REMOTE_PACKAGE_JSON_URL, { timeout: TIMEOUT_MS }, (res) => {
21
+ let data = '';
22
+
23
+ res.on('data', (chunk) => {
24
+ data += chunk;
25
+ });
26
+
27
+ res.on('end', () => {
28
+ if (res.statusCode === 200) {
29
+ try {
30
+ const packageJson = JSON.parse(data);
31
+ resolve(packageJson);
32
+ } catch (err) {
33
+ debugLog(`Failed to parse remote package.json: ${err.message}`);
34
+ resolve(null);
35
+ }
36
+ } else {
37
+ debugLog(`HTTP ${res.statusCode} when fetching remote package.json`);
38
+ resolve(null);
39
+ }
40
+ });
41
+ });
42
+
43
+ req.on('error', (err) => {
44
+ debugLog(`Network error when fetching remote package.json: ${err.message}`);
45
+ resolve(null);
46
+ });
47
+
48
+ req.on('timeout', () => {
49
+ debugLog('Timeout when fetching remote package.json');
50
+ req.destroy();
51
+ resolve(null);
52
+ });
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Compare two semantic versions
58
+ * @param {string} version1 - First version (e.g., "1.0.76")
59
+ * @param {string} version2 - Second version (e.g., "1.0.77")
60
+ * @returns {number} -1 if version1 < version2, 0 if equal, 1 if version1 > version2
61
+ */
62
+ function compareVersions(version1, version2) {
63
+ const v1parts = version1.split('.').map(Number);
64
+ const v2parts = version2.split('.').map(Number);
65
+
66
+ for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
67
+ const v1 = v1parts[i] || 0;
68
+ const v2 = v2parts[i] || 0;
69
+
70
+ if (v1 < v2) return -1;
71
+ if (v1 > v2) return 1;
72
+ }
73
+
74
+ return 0;
75
+ }
76
+
77
+ /**
78
+ * Check for available updates
79
+ * @param {string} currentVersion - Current local version
80
+ * @returns {Promise<Object>} Object with { hasUpdate: boolean, remoteVersion: string|null, updateAvailable: boolean }
81
+ */
82
+ export async function checkForUpdates(currentVersion) {
83
+ debugLog(`Checking for updates... Current version: ${currentVersion}`);
84
+
85
+ const remotePackageJson = await fetchRemotePackageJson();
86
+
87
+ if (!remotePackageJson || !remotePackageJson.version) {
88
+ debugLog('Could not fetch remote version');
89
+ return {
90
+ hasUpdate: false,
91
+ remoteVersion: null,
92
+ updateAvailable: false
93
+ };
94
+ }
95
+
96
+ const remoteVersion = remotePackageJson.version;
97
+ debugLog(`Remote version: ${remoteVersion}`);
98
+
99
+ const comparison = compareVersions(currentVersion, remoteVersion);
100
+
101
+ if (comparison < 0) {
102
+ debugLog(`Update available: ${currentVersion} → ${remoteVersion}`);
103
+ return {
104
+ hasUpdate: true,
105
+ remoteVersion,
106
+ updateAvailable: true
107
+ };
108
+ } else {
109
+ debugLog('No update available (local version is up to date or newer)');
110
+ return {
111
+ hasUpdate: false,
112
+ remoteVersion,
113
+ updateAvailable: false
114
+ };
115
+ }
116
+ }