aiexecode 1.0.157
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/LICENSE +68 -0
- package/README.md +347 -0
- package/config_template/mcp_config.json +3 -0
- package/config_template/package_name_store.json +5 -0
- package/config_template/settings.json +5 -0
- package/index.js +879 -0
- package/mcp-agent-lib/example/01-basic-usage.js +82 -0
- package/mcp-agent-lib/example/02-quick-start.js +52 -0
- package/mcp-agent-lib/example/03-http-server.js +76 -0
- package/mcp-agent-lib/example/04-multiple-servers.js +117 -0
- package/mcp-agent-lib/example/05-error-handling.js +116 -0
- package/mcp-agent-lib/example/06-resources-and-prompts.js +174 -0
- package/mcp-agent-lib/example/07-advanced-configuration.js +191 -0
- package/mcp-agent-lib/example/08-real-world-chatbot.js +331 -0
- package/mcp-agent-lib/example/README.md +346 -0
- package/mcp-agent-lib/index.js +19 -0
- package/mcp-agent-lib/init.sh +3 -0
- package/mcp-agent-lib/package-lock.json +1216 -0
- package/mcp-agent-lib/package.json +53 -0
- package/mcp-agent-lib/sampleFastMCPClient/client.py +25 -0
- package/mcp-agent-lib/sampleFastMCPClient/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServer/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServer/server.py +12 -0
- package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/server.py +43 -0
- package/mcp-agent-lib/sampleFastMCPServerRootsRequest/server.py +63 -0
- package/mcp-agent-lib/sampleMCPHost/index.js +386 -0
- package/mcp-agent-lib/sampleMCPHost/mcp_config.json +24 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/elicitation.js +151 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/index.js +166 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/roots.js +197 -0
- package/mcp-agent-lib/src/mcp_client.js +1860 -0
- package/mcp-agent-lib/src/mcp_message_logger.js +517 -0
- package/package.json +72 -0
- package/payload_viewer/out/404/index.html +1 -0
- package/payload_viewer/out/404.html +1 -0
- package/payload_viewer/out/_next/static/chunks/060f9a97930f3d04.js +1 -0
- package/payload_viewer/out/_next/static/chunks/103c802c8f4a5ea1.js +1 -0
- package/payload_viewer/out/_next/static/chunks/16474fd6c6910c45.js +1 -0
- package/payload_viewer/out/_next/static/chunks/17722e3ac4e00587.js +1 -0
- package/payload_viewer/out/_next/static/chunks/305b077a9873cf54.js +1 -0
- package/payload_viewer/out/_next/static/chunks/4c1d05c6741c2bdd.js +5 -0
- package/payload_viewer/out/_next/static/chunks/538cc02e54714b23.js +1 -0
- package/payload_viewer/out/_next/static/chunks/6251fa5907d2b226.js +5 -0
- package/payload_viewer/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- package/payload_viewer/out/_next/static/chunks/b6c0459f3789d25c.js +1 -0
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/_next/static/chunks/bd2dcf98c9b362f6.js +1 -0
- package/payload_viewer/out/_next/static/chunks/c8a542ae21335479.js +1 -0
- package/payload_viewer/out/_next/static/chunks/cdd12d5c1a5a6064.js +1 -0
- package/payload_viewer/out/_next/static/chunks/e411019f55d87c42.js +1 -0
- package/payload_viewer/out/_next/static/chunks/e60ef129113f6e24.js +1 -0
- package/payload_viewer/out/_next/static/chunks/f1ac9047ac4a3fde.js +1 -0
- package/payload_viewer/out/_next/static/chunks/turbopack-0ac29803ce3c3c7a.js +3 -0
- package/payload_viewer/out/_next/static/chunks/turbopack-89db4c64206a73e4.js +3 -0
- package/payload_viewer/out/_next/static/chunks/turbopack-a5b8235fa59d7119.js +3 -0
- package/payload_viewer/out/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/favicon.0b3bf435.ico +0 -0
- package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_buildManifest.js +14 -0
- package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_clientMiddlewareManifest.json +1 -0
- package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_ssgManifest.js +1 -0
- package/payload_viewer/out/favicon.ico +0 -0
- package/payload_viewer/out/file.svg +1 -0
- package/payload_viewer/out/globe.svg +1 -0
- package/payload_viewer/out/index.html +1 -0
- package/payload_viewer/out/index.txt +23 -0
- package/payload_viewer/out/next.svg +1 -0
- package/payload_viewer/out/vercel.svg +1 -0
- package/payload_viewer/out/window.svg +1 -0
- package/payload_viewer/web_server.js +861 -0
- package/prompts/completion_judge.txt +128 -0
- package/prompts/orchestrator.txt +1213 -0
- package/src/LLMClient/client.js +1375 -0
- package/src/LLMClient/converters/input-normalizer.js +238 -0
- package/src/LLMClient/converters/responses-to-claude.js +503 -0
- package/src/LLMClient/converters/responses-to-gemini.js +648 -0
- package/src/LLMClient/converters/responses-to-ollama.js +348 -0
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +398 -0
- package/src/LLMClient/index.js +36 -0
- package/src/ai_based/completion_judge.js +421 -0
- package/src/ai_based/orchestrator.js +527 -0
- package/src/ai_based/pip_package_installer.js +173 -0
- package/src/ai_based/pip_package_lookup.js +197 -0
- package/src/cli/mcp_cli.js +70 -0
- package/src/cli/mcp_commands.js +255 -0
- package/src/commands/agents.js +18 -0
- package/src/commands/apikey.js +55 -0
- package/src/commands/bg.js +140 -0
- package/src/commands/commands.js +56 -0
- package/src/commands/debug.js +54 -0
- package/src/commands/exit.js +19 -0
- package/src/commands/help.js +35 -0
- package/src/commands/mcp.js +128 -0
- package/src/commands/model.js +176 -0
- package/src/commands/setup.js +13 -0
- package/src/commands/skills.js +51 -0
- package/src/commands/tools.js +165 -0
- package/src/commands/viewer.js +147 -0
- package/src/config/ai_models.js +312 -0
- package/src/config/config.js +10 -0
- package/src/config/constants.js +71 -0
- package/src/config/feature_flags.js +15 -0
- package/src/frontend/App.js +1263 -0
- package/src/frontend/README.md +81 -0
- package/src/frontend/components/AutocompleteMenu.js +47 -0
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/BlankLine.js +62 -0
- package/src/frontend/components/ConversationItem.js +893 -0
- package/src/frontend/components/CurrentModelView.js +43 -0
- package/src/frontend/components/FileDiffViewer.js +616 -0
- package/src/frontend/components/Footer.js +25 -0
- package/src/frontend/components/Header.js +42 -0
- package/src/frontend/components/HelpView.js +154 -0
- package/src/frontend/components/Input.js +344 -0
- package/src/frontend/components/LoadingIndicator.js +31 -0
- package/src/frontend/components/ModelListView.js +49 -0
- package/src/frontend/components/ModelUpdatedView.js +22 -0
- package/src/frontend/components/SessionSpinner.js +66 -0
- package/src/frontend/components/SetupWizard.js +242 -0
- package/src/frontend/components/StreamOutput.js +34 -0
- package/src/frontend/components/TodoList.js +56 -0
- package/src/frontend/components/ToolApprovalPrompt.js +452 -0
- package/src/frontend/design/themeColors.js +42 -0
- package/src/frontend/hooks/useCompletion.js +84 -0
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/hooks/useKeypress.js +145 -0
- package/src/frontend/index.js +65 -0
- package/src/frontend/utils/GridRenderer.js +140 -0
- package/src/frontend/utils/InlineFormatter.js +156 -0
- package/src/frontend/utils/diffUtils.js +235 -0
- package/src/frontend/utils/inputBuffer.js +441 -0
- package/src/frontend/utils/markdownParser.js +377 -0
- package/src/frontend/utils/outputRedirector.js +47 -0
- package/src/frontend/utils/renderInkComponent.js +42 -0
- package/src/frontend/utils/syntaxHighlighter.js +149 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +170 -0
- package/src/system/ai_request.js +737 -0
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +1233 -0
- package/src/system/command_loader.js +40 -0
- package/src/system/command_parser.js +133 -0
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +265 -0
- package/src/system/custom_command_loader.js +395 -0
- package/src/system/file_integrity.js +466 -0
- package/src/system/import_analyzer.py +174 -0
- package/src/system/log.js +82 -0
- package/src/system/mcp_integration.js +304 -0
- package/src/system/output_helper.js +89 -0
- package/src/system/session.js +1393 -0
- package/src/system/session_memory.js +481 -0
- package/src/system/skill_loader.js +324 -0
- package/src/system/system_info.js +483 -0
- package/src/system/tool_approval.js +160 -0
- package/src/system/tool_registry.js +184 -0
- package/src/system/ui_events.js +279 -0
- package/src/tools/code_editor.js +792 -0
- package/src/tools/file_reader.js +385 -0
- package/src/tools/glob.js +263 -0
- package/src/tools/response_message.js +30 -0
- package/src/tools/ripgrep.js +554 -0
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/todo_write.js +182 -0
- package/src/tools/web_download.py +74 -0
- package/src/tools/web_downloader.js +83 -0
- package/src/util/clone.js +174 -0
- package/src/util/config.js +203 -0
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +49 -0
- package/src/util/exit_handler.js +53 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/mcp_config_manager.js +159 -0
- package/src/util/output_formatter.js +50 -0
- package/src/util/path_helper.js +27 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +184 -0
- package/src/util/rag_helper.js +101 -0
- package/src/util/safe_fs.js +645 -0
- package/src/util/setup_wizard.js +62 -0
- package/src/util/text_formatter.js +33 -0
- package/src/util/version_check.js +116 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe File System Operations Wrapper
|
|
3
|
+
*
|
|
4
|
+
* 이 모듈은 파일 시스템의 파괴적 작업들을 안전하게 수행하기 위한 wrapper 함수들을 제공합니다.
|
|
5
|
+
* 모든 파괴적 작업은 검증과 에러 핸들링을 포함합니다.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
// 허용된 디렉토리들
|
|
14
|
+
const ALLOWED_DIRECTORIES = [
|
|
15
|
+
process.cwd(), // current working directory
|
|
16
|
+
path.join(homedir(), '.aiexe') // ~/.aiexe
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// 설정 파일 경로 (config.js와 동일하게 계산, 순환 참조 방지)
|
|
20
|
+
const SETTINGS_FILE = path.join(homedir(), '.aiexe', 'settings.json');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 디버그 모드 활성화 여부 확인 (순환 참조 방지를 위해 직접 구현)
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isDebugEnabled() {
|
|
27
|
+
try {
|
|
28
|
+
const data = readFileSync(SETTINGS_FILE, 'utf8');
|
|
29
|
+
const parsed = JSON.parse(data);
|
|
30
|
+
return parsed?.SHOW_API_PAYLOAD === true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 파일/디렉토리 존재 여부 확인 (비동기)
|
|
38
|
+
* @param {string} filePath - 확인할 경로
|
|
39
|
+
* @returns {Promise<boolean>}
|
|
40
|
+
*/
|
|
41
|
+
async function exists(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(filePath);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function debugLog(str) {
|
|
51
|
+
// 디버그 모드가 꺼져있으면 로그를 기록하지 않음
|
|
52
|
+
if (!isDebugEnabled()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const logDir = path.join(homedir(), '.aiexe', 'debuglog');
|
|
57
|
+
const logFile = path.join(logDir, 'safe_fs.log');
|
|
58
|
+
|
|
59
|
+
if (!(await exists(logDir))) {
|
|
60
|
+
await fs.mkdir(logDir, { recursive: true }).catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await fs.appendFile(logFile, str).catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 경로를 절대경로로 변환
|
|
69
|
+
* @param {string} filePath - 변환할 파일 경로
|
|
70
|
+
* @returns {string} 절대 경로
|
|
71
|
+
*/
|
|
72
|
+
function toAbsolutePath(filePath) {
|
|
73
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
74
|
+
return filePath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 이미 절대 경로이면 그대로 반환
|
|
78
|
+
if (path.isAbsolute(filePath)) {
|
|
79
|
+
return path.normalize(filePath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 상대 경로를 절대 경로로 변환
|
|
83
|
+
return path.resolve(process.cwd(), filePath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 경로 유효성 검증
|
|
88
|
+
* @param {string} filePath - 검증할 파일 경로
|
|
89
|
+
* @returns {boolean} 유효한 경로인지 여부
|
|
90
|
+
*/
|
|
91
|
+
function validatePath(filePath) {
|
|
92
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 경로가 허용된 디렉토리 내부에 있는지 확인 (symlink 공격 방지)
|
|
101
|
+
* @param {string} filePath - 확인할 파일 경로
|
|
102
|
+
* @returns {Promise<boolean>} 허용된 디렉토리 내부에 있는지 여부
|
|
103
|
+
*/
|
|
104
|
+
async function isWithinAllowedDirectoryAsync(filePath) {
|
|
105
|
+
try {
|
|
106
|
+
// 실제 경로로 해석하여 symlink 공격 방지
|
|
107
|
+
const realPath = await fs.realpath(path.resolve(filePath));
|
|
108
|
+
|
|
109
|
+
return ALLOWED_DIRECTORIES.some(allowedDir => {
|
|
110
|
+
const normalizedAllowedDir = path.normalize(path.resolve(allowedDir));
|
|
111
|
+
return realPath.startsWith(normalizedAllowedDir + path.sep) ||
|
|
112
|
+
realPath === normalizedAllowedDir;
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// 파일이 존재하지 않는 경우, 부모 디렉토리로 검증
|
|
116
|
+
// 새 파일 생성 시에는 파일이 아직 존재하지 않을 수 있음
|
|
117
|
+
const parentDir = path.dirname(path.resolve(filePath));
|
|
118
|
+
try {
|
|
119
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
120
|
+
return ALLOWED_DIRECTORIES.some(allowedDir => {
|
|
121
|
+
const normalizedAllowedDir = path.normalize(path.resolve(allowedDir));
|
|
122
|
+
return realParentPath.startsWith(normalizedAllowedDir + path.sep) ||
|
|
123
|
+
realParentPath === normalizedAllowedDir;
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
// 부모 디렉토리도 존재하지 않는 경우 normalize된 경로로 검증
|
|
127
|
+
const normalizedPath = path.normalize(path.resolve(filePath));
|
|
128
|
+
return ALLOWED_DIRECTORIES.some(allowedDir => {
|
|
129
|
+
const normalizedAllowedDir = path.normalize(path.resolve(allowedDir));
|
|
130
|
+
return normalizedPath.startsWith(normalizedAllowedDir + path.sep) ||
|
|
131
|
+
normalizedPath === normalizedAllowedDir;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 경로가 허용된 디렉토리 내부에 있는지 확인 (동기 버전, 후방 호환성용)
|
|
139
|
+
* @param {string} filePath - 확인할 파일 경로
|
|
140
|
+
* @returns {boolean} 허용된 디렉토리 내부에 있는지 여부
|
|
141
|
+
*/
|
|
142
|
+
function isWithinAllowedDirectory(filePath) {
|
|
143
|
+
const normalizedPath = path.normalize(path.resolve(filePath));
|
|
144
|
+
|
|
145
|
+
return ALLOWED_DIRECTORIES.some(allowedDir => {
|
|
146
|
+
const normalizedAllowedDir = path.normalize(path.resolve(allowedDir));
|
|
147
|
+
return normalizedPath.startsWith(normalizedAllowedDir + path.sep) ||
|
|
148
|
+
normalizedPath === normalizedAllowedDir;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 보호된 경로인지 확인 (deprecated - isWithinAllowedDirectory로 대체됨)
|
|
154
|
+
* @param {string} filePath - 확인할 파일 경로
|
|
155
|
+
* @returns {boolean} 보호된 경로인지 여부
|
|
156
|
+
*/
|
|
157
|
+
function isProtectedPath(filePath) {
|
|
158
|
+
// 허용된 디렉토리가 아니면 보호된 경로로 간주
|
|
159
|
+
return !isWithinAllowedDirectory(filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 안전한 파일 쓰기 (symlink 공격 방지 포함)
|
|
164
|
+
* @param {string} filePath - 쓸 파일 경로
|
|
165
|
+
* @param {string|Buffer} content - 파일 내용
|
|
166
|
+
* @param {string} encoding - 인코딩 (기본값: 'utf8')
|
|
167
|
+
* @returns {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
export async function safeWriteFile(filePath, content, encoding = 'utf8') {
|
|
170
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
171
|
+
|
|
172
|
+
if (!validatePath(absolutePath)) {
|
|
173
|
+
const error = `Invalid file path: ${filePath}`;
|
|
174
|
+
await debugLog(`[ERROR] safeWriteFile: ${error}\n`);
|
|
175
|
+
throw new Error(error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// symlink 공격 방지를 위해 비동기 검증 사용
|
|
179
|
+
const isAllowed = await isWithinAllowedDirectoryAsync(absolutePath);
|
|
180
|
+
if (!isAllowed) {
|
|
181
|
+
const error = `Protected path cannot be modified: ${absolutePath}`;
|
|
182
|
+
await debugLog(`[ERROR] safeWriteFile: ${error}\n`);
|
|
183
|
+
throw new Error(error);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await fs.writeFile(absolutePath, content, encoding);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const errorMsg = `Failed to write file ${absolutePath}: ${error.message}`;
|
|
190
|
+
await debugLog(`[ERROR] safeWriteFile: ${errorMsg}\n`);
|
|
191
|
+
throw new Error(errorMsg);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 안전한 파일 삭제
|
|
197
|
+
* @param {string} filePath - 삭제할 파일 경로
|
|
198
|
+
* @returns {Promise<void>}
|
|
199
|
+
*/
|
|
200
|
+
export async function safeUnlink(filePath) {
|
|
201
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
202
|
+
|
|
203
|
+
if (!validatePath(absolutePath)) {
|
|
204
|
+
const error = `Invalid file path: ${filePath}`;
|
|
205
|
+
await debugLog(`[ERROR] safeUnlink: ${error}\n`);
|
|
206
|
+
throw new Error(error);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isProtectedPath(absolutePath)) {
|
|
210
|
+
const error = `Protected path cannot be deleted: ${absolutePath}`;
|
|
211
|
+
await debugLog(`[ERROR] safeUnlink: ${error}\n`);
|
|
212
|
+
throw new Error(error);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
if (!(await exists(absolutePath))) {
|
|
217
|
+
return; // 파일이 없으면 조용히 반환
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await fs.unlink(absolutePath);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const errorMsg = `Failed to delete file ${absolutePath}: ${error.message}`;
|
|
223
|
+
await debugLog(`[ERROR] safeUnlink: ${errorMsg}\n`);
|
|
224
|
+
throw new Error(errorMsg);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 안전한 파일 이름 변경
|
|
230
|
+
* @param {string} oldPath - 기존 파일 경로
|
|
231
|
+
* @param {string} newPath - 새 파일 경로
|
|
232
|
+
* @returns {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
export async function safeRename(oldPath, newPath) {
|
|
235
|
+
const absoluteOldPath = toAbsolutePath(oldPath);
|
|
236
|
+
const absoluteNewPath = toAbsolutePath(newPath);
|
|
237
|
+
|
|
238
|
+
if (!validatePath(absoluteOldPath) || !validatePath(absoluteNewPath)) {
|
|
239
|
+
const error = `Invalid file path: ${oldPath} -> ${newPath}`;
|
|
240
|
+
await debugLog(`[ERROR] safeRename: ${error}\n`);
|
|
241
|
+
throw new Error(error);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (isProtectedPath(absoluteOldPath) || isProtectedPath(absoluteNewPath)) {
|
|
245
|
+
const error = `Protected path cannot be modified`;
|
|
246
|
+
await debugLog(`[ERROR] safeRename: ${error} (${absoluteOldPath} -> ${absoluteNewPath})\n`);
|
|
247
|
+
throw new Error(error);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
if (!(await exists(absoluteOldPath))) {
|
|
252
|
+
const errorMsg = `Source file does not exist: ${absoluteOldPath}`;
|
|
253
|
+
await debugLog(`[ERROR] safeRename: ${errorMsg}\n`);
|
|
254
|
+
throw new Error(errorMsg);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await fs.rename(absoluteOldPath, absoluteNewPath);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const errorMsg = `Failed to rename file ${absoluteOldPath} -> ${absoluteNewPath}: ${error.message}`;
|
|
260
|
+
await debugLog(`[ERROR] safeRename: ${errorMsg}\n`);
|
|
261
|
+
throw new Error(errorMsg);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 안전한 파일 절삭 (내용 비우기)
|
|
267
|
+
* @param {string} filePath - 절삭할 파일 경로
|
|
268
|
+
* @param {number} length - 절삭할 길이 (기본값: 0 - 파일 비우기)
|
|
269
|
+
* @returns {Promise<void>}
|
|
270
|
+
*/
|
|
271
|
+
export async function safeTruncate(filePath, length = 0) {
|
|
272
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
273
|
+
|
|
274
|
+
if (!validatePath(absolutePath)) {
|
|
275
|
+
const error = `Invalid file path: ${filePath}`;
|
|
276
|
+
await debugLog(`[ERROR] safeTruncate: ${error}\n`);
|
|
277
|
+
throw new Error(error);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (isProtectedPath(absolutePath)) {
|
|
281
|
+
const error = `Protected path cannot be modified: ${absolutePath}`;
|
|
282
|
+
await debugLog(`[ERROR] safeTruncate: ${error}\n`);
|
|
283
|
+
throw new Error(error);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
if (!(await exists(absolutePath))) {
|
|
288
|
+
const errorMsg = `File does not exist: ${absolutePath}`;
|
|
289
|
+
await debugLog(`[ERROR] safeTruncate: ${errorMsg}\n`);
|
|
290
|
+
throw new Error(errorMsg);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await fs.truncate(absolutePath, length);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const errorMsg = `Failed to truncate file ${absolutePath}: ${error.message}`;
|
|
296
|
+
await debugLog(`[ERROR] safeTruncate: ${errorMsg}\n`);
|
|
297
|
+
throw new Error(errorMsg);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 안전한 디렉토리 삭제
|
|
303
|
+
* @param {string} dirPath - 삭제할 디렉토리 경로
|
|
304
|
+
* @param {Object} options - 추가 옵션
|
|
305
|
+
* @param {boolean} options.recursive - 재귀적으로 삭제 여부
|
|
306
|
+
* @returns {Promise<void>}
|
|
307
|
+
*/
|
|
308
|
+
export async function safeRmdir(dirPath, options = {}) {
|
|
309
|
+
const absolutePath = toAbsolutePath(dirPath);
|
|
310
|
+
|
|
311
|
+
if (!validatePath(absolutePath)) {
|
|
312
|
+
const error = `Invalid directory path: ${dirPath}`;
|
|
313
|
+
await debugLog(`[ERROR] safeRmdir: ${error}\n`);
|
|
314
|
+
throw new Error(error);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isProtectedPath(absolutePath)) {
|
|
318
|
+
const error = `Protected path cannot be deleted: ${absolutePath}`;
|
|
319
|
+
await debugLog(`[ERROR] safeRmdir: ${error}\n`);
|
|
320
|
+
throw new Error(error);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
if (!(await exists(absolutePath))) {
|
|
325
|
+
return; // 디렉토리가 없으면 조용히 반환
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await fs.rmdir(absolutePath, options);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const errorMsg = `Failed to delete directory ${absolutePath}: ${error.message}`;
|
|
331
|
+
await debugLog(`[ERROR] safeRmdir: ${errorMsg}\n`);
|
|
332
|
+
throw new Error(errorMsg);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 안전한 재귀적 삭제 (파일 또는 디렉토리)
|
|
338
|
+
* @param {string} targetPath - 삭제할 경로
|
|
339
|
+
* @param {Object} options - 추가 옵션
|
|
340
|
+
* @returns {Promise<void>}
|
|
341
|
+
*/
|
|
342
|
+
export async function safeRm(targetPath, options = {}) {
|
|
343
|
+
const absolutePath = toAbsolutePath(targetPath);
|
|
344
|
+
|
|
345
|
+
if (!validatePath(absolutePath)) {
|
|
346
|
+
const error = `Invalid path: ${targetPath}`;
|
|
347
|
+
await debugLog(`[ERROR] safeRm: ${error}\n`);
|
|
348
|
+
throw new Error(error);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isProtectedPath(absolutePath)) {
|
|
352
|
+
const error = `Protected path cannot be deleted: ${absolutePath}`;
|
|
353
|
+
await debugLog(`[ERROR] safeRm: ${error}\n`);
|
|
354
|
+
throw new Error(error);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
if (!(await exists(absolutePath))) {
|
|
359
|
+
return; // 경로가 없으면 조용히 반환
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await fs.rm(absolutePath, { ...options, recursive: true, force: true });
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const errorMsg = `Failed to remove ${absolutePath}: ${error.message}`;
|
|
365
|
+
await debugLog(`[ERROR] safeRm: ${errorMsg}\n`);
|
|
366
|
+
throw new Error(errorMsg);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================
|
|
371
|
+
// 읽기 전용 작업 (Read-only operations)
|
|
372
|
+
// ============================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 파일 읽기
|
|
376
|
+
* @param {string} filePath - 읽을 파일 경로
|
|
377
|
+
* @param {string} encoding - 인코딩 (기본값: 'utf8')
|
|
378
|
+
* @returns {Promise<string|Buffer>}
|
|
379
|
+
*/
|
|
380
|
+
export async function safeReadFile(filePath, encoding = 'utf8') {
|
|
381
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
return await fs.readFile(absolutePath, encoding);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const errorMsg = `Failed to read file ${absolutePath}: ${error.message}`;
|
|
387
|
+
await debugLog(`[ERROR] safeReadFile: ${errorMsg}\n`);
|
|
388
|
+
throw new Error(errorMsg);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 파일 존재 여부 확인 (비동기)
|
|
394
|
+
* @param {string} filePath - 확인할 파일 경로
|
|
395
|
+
* @returns {Promise<boolean>}
|
|
396
|
+
*/
|
|
397
|
+
export async function safeExists(filePath) {
|
|
398
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
399
|
+
return await exists(absolutePath);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 디렉토리 읽기
|
|
404
|
+
* @param {string} dirPath - 읽을 디렉토리 경로
|
|
405
|
+
* @param {Object} options - 추가 옵션
|
|
406
|
+
* @returns {Promise<string[]>}
|
|
407
|
+
*/
|
|
408
|
+
export async function safeReaddir(dirPath, options = {}) {
|
|
409
|
+
const absolutePath = toAbsolutePath(dirPath);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
return await fs.readdir(absolutePath, options);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const errorMsg = `Failed to read directory ${absolutePath}: ${error.message}`;
|
|
415
|
+
await debugLog(`[ERROR] safeReaddir: ${errorMsg}\n`);
|
|
416
|
+
throw new Error(errorMsg);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 파일/디렉토리 상태 정보 가져오기
|
|
422
|
+
* @param {string} targetPath - 확인할 경로
|
|
423
|
+
* @returns {Promise<import('fs').Stats>}
|
|
424
|
+
*/
|
|
425
|
+
export async function safeStat(targetPath) {
|
|
426
|
+
const absolutePath = toAbsolutePath(targetPath);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
return await fs.stat(absolutePath);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const errorMsg = `Failed to stat ${absolutePath}: ${error.message}`;
|
|
432
|
+
await debugLog(`[ERROR] safeStat: ${errorMsg}\n`);
|
|
433
|
+
throw new Error(errorMsg);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 파일/디렉토리 상태 정보 가져오기 (심볼릭 링크를 따라가지 않음)
|
|
439
|
+
* @param {string} targetPath - 확인할 경로
|
|
440
|
+
* @returns {Promise<import('fs').Stats>}
|
|
441
|
+
*/
|
|
442
|
+
export async function safeLstat(targetPath) {
|
|
443
|
+
const absolutePath = toAbsolutePath(targetPath);
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
return await fs.lstat(absolutePath);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
const errorMsg = `Failed to lstat ${absolutePath}: ${error.message}`;
|
|
449
|
+
await debugLog(`[ERROR] safeLstat: ${errorMsg}\n`);
|
|
450
|
+
throw new Error(errorMsg);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 파일 접근 권한 확인
|
|
456
|
+
* @param {string} filePath - 확인할 파일 경로
|
|
457
|
+
* @param {number} mode - 접근 모드 (fs.constants.F_OK, R_OK, W_OK, X_OK)
|
|
458
|
+
* @returns {Promise<void>}
|
|
459
|
+
*/
|
|
460
|
+
export async function safeAccess(filePath, mode) {
|
|
461
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
return await fs.access(absolutePath, mode);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
const errorMsg = `Failed to access ${absolutePath}: ${error.message}`;
|
|
467
|
+
await debugLog(`[ERROR] safeAccess: ${errorMsg}\n`);
|
|
468
|
+
throw new Error(errorMsg);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 디렉토리 생성
|
|
474
|
+
* @param {string} dirPath - 생성할 디렉토리 경로
|
|
475
|
+
* @param {Object} options - 추가 옵션
|
|
476
|
+
* @param {boolean} options.recursive - 재귀적으로 생성 여부
|
|
477
|
+
* @returns {Promise<string|undefined>}
|
|
478
|
+
*/
|
|
479
|
+
export async function safeMkdir(dirPath, options = {}) {
|
|
480
|
+
const absolutePath = toAbsolutePath(dirPath);
|
|
481
|
+
|
|
482
|
+
if (!validatePath(absolutePath)) {
|
|
483
|
+
const error = `Invalid directory path: ${dirPath}`;
|
|
484
|
+
await debugLog(`[ERROR] safeMkdir: ${error}\n`);
|
|
485
|
+
throw new Error(error);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isProtectedPath(absolutePath)) {
|
|
489
|
+
const error = `Protected path cannot be modified: ${absolutePath}`;
|
|
490
|
+
await debugLog(`[ERROR] safeMkdir: ${error}\n`);
|
|
491
|
+
throw new Error(error);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
return await fs.mkdir(absolutePath, options);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
// recursive: true 일 때 이미 존재하는 디렉토리는 에러가 아님
|
|
498
|
+
if (error.code === 'EEXIST' && options.recursive) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const errorMsg = `Failed to create directory ${absolutePath}: ${error.message}`;
|
|
502
|
+
await debugLog(`[ERROR] safeMkdir: ${errorMsg}\n`);
|
|
503
|
+
throw new Error(errorMsg);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 실제 경로 찾기 (심볼릭 링크 해석)
|
|
509
|
+
* @param {string} targetPath - 확인할 경로
|
|
510
|
+
* @returns {Promise<string>}
|
|
511
|
+
*/
|
|
512
|
+
export async function safeRealpath(targetPath) {
|
|
513
|
+
const absolutePath = toAbsolutePath(targetPath);
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
return await fs.realpath(absolutePath);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const errorMsg = `Failed to resolve realpath ${absolutePath}: ${error.message}`;
|
|
519
|
+
await debugLog(`[ERROR] safeRealpath: ${errorMsg}\n`);
|
|
520
|
+
throw new Error(errorMsg);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 파일 복사
|
|
526
|
+
* @param {string} src - 원본 파일 경로
|
|
527
|
+
* @param {string} dest - 대상 파일 경로
|
|
528
|
+
* @param {number} mode - 복사 모드
|
|
529
|
+
* @returns {Promise<void>}
|
|
530
|
+
*/
|
|
531
|
+
export async function safeCopyFile(src, dest, mode) {
|
|
532
|
+
const absoluteSrc = toAbsolutePath(src);
|
|
533
|
+
const absoluteDest = toAbsolutePath(dest);
|
|
534
|
+
|
|
535
|
+
if (!validatePath(absoluteSrc) || !validatePath(absoluteDest)) {
|
|
536
|
+
const error = `Invalid path: ${src} -> ${dest}`;
|
|
537
|
+
await debugLog(`[ERROR] safeCopyFile: ${error}\n`);
|
|
538
|
+
throw new Error(error);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (isProtectedPath(absoluteDest)) {
|
|
542
|
+
const error = `Protected path cannot be modified: ${absoluteDest}`;
|
|
543
|
+
await debugLog(`[ERROR] safeCopyFile: ${error}\n`);
|
|
544
|
+
throw new Error(error);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
return await fs.copyFile(absoluteSrc, absoluteDest, mode);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
const errorMsg = `Failed to copy file ${absoluteSrc} -> ${absoluteDest}: ${error.message}`;
|
|
551
|
+
await debugLog(`[ERROR] safeCopyFile: ${errorMsg}\n`);
|
|
552
|
+
throw new Error(errorMsg);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 파일에 내용 추가
|
|
558
|
+
* @param {string} filePath - 파일 경로
|
|
559
|
+
* @param {string|Buffer} data - 추가할 데이터
|
|
560
|
+
* @param {string} encoding - 인코딩 (기본값: 'utf8')
|
|
561
|
+
* @returns {Promise<void>}
|
|
562
|
+
*/
|
|
563
|
+
export async function safeAppendFile(filePath, data, encoding = 'utf8') {
|
|
564
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
565
|
+
|
|
566
|
+
if (!validatePath(absolutePath)) {
|
|
567
|
+
const error = `Invalid file path: ${filePath}`;
|
|
568
|
+
await debugLog(`[ERROR] safeAppendFile: ${error}\n`);
|
|
569
|
+
throw new Error(error);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (isProtectedPath(absolutePath)) {
|
|
573
|
+
const error = `Protected path cannot be modified: ${absolutePath}`;
|
|
574
|
+
await debugLog(`[ERROR] safeAppendFile: ${error}\n`);
|
|
575
|
+
throw new Error(error);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
return await fs.appendFile(absolutePath, data, encoding);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const errorMsg = `Failed to append to file ${absolutePath}: ${error.message}`;
|
|
582
|
+
await debugLog(`[ERROR] safeAppendFile: ${errorMsg}\n`);
|
|
583
|
+
throw new Error(errorMsg);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ============================================================
|
|
588
|
+
// 동기 작업 (Synchronous operations)
|
|
589
|
+
// ============================================================
|
|
590
|
+
|
|
591
|
+
import { mkdirSync, appendFileSync } from 'fs';
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 디렉토리 생성 (동기)
|
|
595
|
+
* @param {string} dirPath - 생성할 디렉토리 경로
|
|
596
|
+
* @param {Object} options - 추가 옵션
|
|
597
|
+
* @param {boolean} options.recursive - 재귀적으로 생성 여부
|
|
598
|
+
* @returns {string|undefined}
|
|
599
|
+
*/
|
|
600
|
+
export function safeMkdirSync(dirPath, options = {}) {
|
|
601
|
+
const absolutePath = toAbsolutePath(dirPath);
|
|
602
|
+
|
|
603
|
+
if (!validatePath(absolutePath)) {
|
|
604
|
+
throw new Error(`Invalid directory path: ${dirPath}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (isProtectedPath(absolutePath)) {
|
|
608
|
+
throw new Error(`Protected path cannot be modified: ${absolutePath}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
return mkdirSync(absolutePath, options);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
// recursive: true 일 때 이미 존재하는 디렉토리는 에러가 아님
|
|
615
|
+
if (error.code === 'EEXIST' && options.recursive) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
throw new Error(`Failed to create directory ${absolutePath}: ${error.message}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 파일에 내용 추가 (동기)
|
|
624
|
+
* @param {string} filePath - 파일 경로
|
|
625
|
+
* @param {string|Buffer} data - 추가할 데이터
|
|
626
|
+
* @param {string} encoding - 인코딩 (기본값: 'utf8')
|
|
627
|
+
* @returns {void}
|
|
628
|
+
*/
|
|
629
|
+
export function safeAppendFileSync(filePath, data, encoding = 'utf8') {
|
|
630
|
+
const absolutePath = toAbsolutePath(filePath);
|
|
631
|
+
|
|
632
|
+
if (!validatePath(absolutePath)) {
|
|
633
|
+
throw new Error(`Invalid file path: ${filePath}`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (isProtectedPath(absolutePath)) {
|
|
637
|
+
throw new Error(`Protected path cannot be modified: ${absolutePath}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
return appendFileSync(absolutePath, data, encoding);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
throw new Error(`Failed to append to file ${absolutePath}: ${error.message}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// 초기 설정 마법사 - 사용자에게 API 키 및 모델 설정을 안내합니다.
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { SetupWizard } from '../frontend/components/SetupWizard.js';
|
|
5
|
+
import { loadSettings, saveSettings, SETTINGS_FILE } from './config.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 초기 설정 마법사를 실행합니다 (Ink UI 버전).
|
|
10
|
+
* @returns {Promise<boolean>} 설정 완료 여부
|
|
11
|
+
*/
|
|
12
|
+
export async function runSetupWizard() {
|
|
13
|
+
// 화면 클리어하고 시작
|
|
14
|
+
console.clear();
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
let result = false;
|
|
18
|
+
|
|
19
|
+
const handleComplete = async (settings) => {
|
|
20
|
+
// 설정 저장
|
|
21
|
+
await saveSettings(settings);
|
|
22
|
+
|
|
23
|
+
result = true;
|
|
24
|
+
instance.unmount();
|
|
25
|
+
|
|
26
|
+
// 화면 클리어
|
|
27
|
+
console.clear();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleCancel = () => {
|
|
31
|
+
result = false;
|
|
32
|
+
instance.unmount();
|
|
33
|
+
|
|
34
|
+
// 화면 클리어
|
|
35
|
+
console.clear();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const instance = render(
|
|
39
|
+
React.createElement(SetupWizard, {
|
|
40
|
+
onComplete: handleComplete,
|
|
41
|
+
onCancel: handleCancel
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
exitOnCtrlC: false,
|
|
45
|
+
debug: false
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
instance.waitUntilExit().then(() => {
|
|
50
|
+
resolve(result);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 설정이 완료되었는지 확인합니다.
|
|
57
|
+
* @returns {Promise<boolean>} 설정 완료 여부
|
|
58
|
+
*/
|
|
59
|
+
export async function isConfigured() {
|
|
60
|
+
const settings = await loadSettings();
|
|
61
|
+
return Boolean(settings.API_KEY && settings.API_KEY.trim());
|
|
62
|
+
}
|