aiexecode 1.0.94 → 1.0.98
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.
- package/README.md +198 -88
- package/index.js +43 -9
- package/package.json +4 -4
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +3 -3
- package/payload_viewer/web_server.js +361 -0
- package/src/LLMClient/client.js +392 -16
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +608 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +35 -4
- package/src/ai_based/orchestrator.js +146 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +42 -7
- package/src/commands/reasoning_effort.js +2 -2
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +106 -6
- package/src/config/constants.js +71 -0
- package/src/frontend/App.js +8 -0
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/CurrentModelView.js +2 -2
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/Input.js +33 -11
- package/src/frontend/components/ModelListView.js +1 -1
- package/src/frontend/components/SetupWizard.js +51 -8
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +175 -12
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/session.js +59 -35
- package/src/system/skill_loader.js +318 -0
- package/src/system/tool_approval.js +10 -0
- package/src/tools/file_reader.js +49 -9
- package/src/tools/glob.js +0 -3
- package/src/tools/ripgrep.js +5 -7
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/web_downloader.js +0 -3
- package/src/util/clone.js +174 -0
- package/src/util/config.js +38 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +68 -1
- package/src/util/safe_fs.js +43 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 깊은 복사 유틸리티
|
|
3
|
+
* JSON.parse(JSON.stringify()) 패턴을 대체하는 안전한 깊은 복사 함수들을 제공합니다.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 값을 깊은 복사합니다.
|
|
8
|
+
* - 순환 참조를 감지하고 처리합니다.
|
|
9
|
+
* - Date, RegExp 등의 특수 객체를 올바르게 복사합니다.
|
|
10
|
+
* - undefined, function, Symbol은 복사되지 않습니다 (JSON.stringify와 동일).
|
|
11
|
+
*
|
|
12
|
+
* @param {*} value - 복사할 값
|
|
13
|
+
* @param {WeakMap} [visited] - 순환 참조 감지용 (내부 사용)
|
|
14
|
+
* @returns {*} 깊은 복사된 값
|
|
15
|
+
*/
|
|
16
|
+
export function deepClone(value, visited = new WeakMap()) {
|
|
17
|
+
// 기본 타입은 그대로 반환
|
|
18
|
+
if (value === null || typeof value !== 'object') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 순환 참조 감지
|
|
23
|
+
if (visited.has(value)) {
|
|
24
|
+
return visited.get(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Date 객체 처리
|
|
28
|
+
if (value instanceof Date) {
|
|
29
|
+
return new Date(value.getTime());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// RegExp 객체 처리
|
|
33
|
+
if (value instanceof RegExp) {
|
|
34
|
+
return new RegExp(value.source, value.flags);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Map 객체 처리
|
|
38
|
+
if (value instanceof Map) {
|
|
39
|
+
const clonedMap = new Map();
|
|
40
|
+
visited.set(value, clonedMap);
|
|
41
|
+
for (const [key, val] of value) {
|
|
42
|
+
clonedMap.set(deepClone(key, visited), deepClone(val, visited));
|
|
43
|
+
}
|
|
44
|
+
return clonedMap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Set 객체 처리
|
|
48
|
+
if (value instanceof Set) {
|
|
49
|
+
const clonedSet = new Set();
|
|
50
|
+
visited.set(value, clonedSet);
|
|
51
|
+
for (const val of value) {
|
|
52
|
+
clonedSet.add(deepClone(val, visited));
|
|
53
|
+
}
|
|
54
|
+
return clonedSet;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 배열 처리
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
const clonedArray = [];
|
|
60
|
+
visited.set(value, clonedArray);
|
|
61
|
+
for (let i = 0; i < value.length; i++) {
|
|
62
|
+
clonedArray[i] = deepClone(value[i], visited);
|
|
63
|
+
}
|
|
64
|
+
return clonedArray;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 일반 객체 처리
|
|
68
|
+
const clonedObj = {};
|
|
69
|
+
visited.set(value, clonedObj);
|
|
70
|
+
|
|
71
|
+
for (const key of Object.keys(value)) {
|
|
72
|
+
const val = value[key];
|
|
73
|
+
// undefined와 function은 스킵 (JSON.stringify 동작과 일치)
|
|
74
|
+
if (val !== undefined && typeof val !== 'function') {
|
|
75
|
+
clonedObj[key] = deepClone(val, visited);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return clonedObj;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* JSON 직렬화 가능한 값만 깊은 복사합니다.
|
|
84
|
+
* JSON.parse(JSON.stringify())와 동일한 동작이지만 에러 처리가 추가되었습니다.
|
|
85
|
+
*
|
|
86
|
+
* @param {*} value - 복사할 값
|
|
87
|
+
* @param {*} [fallback] - 복사 실패 시 반환할 기본값 (기본: null)
|
|
88
|
+
* @returns {*} 깊은 복사된 값 또는 fallback
|
|
89
|
+
*/
|
|
90
|
+
export function jsonClone(value, fallback = null) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(JSON.stringify(value));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 얕은 복사를 수행합니다.
|
|
100
|
+
* 첫 번째 레벨만 복사하고 중첩된 객체는 참조를 유지합니다.
|
|
101
|
+
*
|
|
102
|
+
* @param {*} value - 복사할 값
|
|
103
|
+
* @returns {*} 얕은 복사된 값
|
|
104
|
+
*/
|
|
105
|
+
export function shallowClone(value) {
|
|
106
|
+
if (value === null || typeof value !== 'object') {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return [...value];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ...value };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 두 객체를 깊은 병합합니다.
|
|
119
|
+
* target에 source의 값을 병합합니다 (source가 우선).
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} target - 대상 객체
|
|
122
|
+
* @param {Object} source - 소스 객체
|
|
123
|
+
* @returns {Object} 병합된 새 객체
|
|
124
|
+
*/
|
|
125
|
+
export function deepMerge(target, source) {
|
|
126
|
+
if (source === null || typeof source !== 'object') {
|
|
127
|
+
return source;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (target === null || typeof target !== 'object') {
|
|
131
|
+
return deepClone(source);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(source)) {
|
|
135
|
+
return deepClone(source);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = { ...target };
|
|
139
|
+
|
|
140
|
+
for (const key of Object.keys(source)) {
|
|
141
|
+
const sourceValue = source[key];
|
|
142
|
+
const targetValue = target[key];
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
sourceValue !== null &&
|
|
146
|
+
typeof sourceValue === 'object' &&
|
|
147
|
+
!Array.isArray(sourceValue) &&
|
|
148
|
+
targetValue !== null &&
|
|
149
|
+
typeof targetValue === 'object' &&
|
|
150
|
+
!Array.isArray(targetValue)
|
|
151
|
+
) {
|
|
152
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
153
|
+
} else {
|
|
154
|
+
result[key] = deepClone(sourceValue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 객체가 깊은 복사 가능한지 (JSON 직렬화 가능한지) 검사합니다.
|
|
163
|
+
*
|
|
164
|
+
* @param {*} value - 검사할 값
|
|
165
|
+
* @returns {boolean} JSON 직렬화 가능하면 true
|
|
166
|
+
*/
|
|
167
|
+
export function isCloneable(value) {
|
|
168
|
+
try {
|
|
169
|
+
JSON.stringify(value);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/util/config.js
CHANGED
|
@@ -41,10 +41,20 @@ export const PAYLOAD_LOG_DIR = join(CONFIG_DIR, 'payload_log');
|
|
|
41
41
|
export const PAYLOAD_LLM_LOG_DIR = join(CONFIG_DIR, 'payload_LLM_log');
|
|
42
42
|
export const DEBUG_LOG_DIR = join(CONFIG_DIR, 'debuglog');
|
|
43
43
|
export const DEBUG_LOG_FILE = join(CONFIG_DIR, 'debug.txt'); // Deprecated: 호환성을 위해 유지
|
|
44
|
+
// Skill 시스템 경로
|
|
45
|
+
export const PERSONAL_SKILLS_DIR = join(CONFIG_DIR, 'skills'); // ~/.aiexe/skills/
|
|
46
|
+
export const PROJECT_SKILLS_DIR = '.aiexe/skills'; // CWD/.aiexe/skills/ (상대경로)
|
|
47
|
+
|
|
48
|
+
// Custom Commands 시스템 경로 (Claude Code 스타일)
|
|
49
|
+
export const PERSONAL_COMMANDS_DIR = join(CONFIG_DIR, 'commands'); // ~/.aiexe/commands/
|
|
50
|
+
export const PROJECT_COMMANDS_DIR = '.aiexe/commands'; // CWD/.aiexe/commands/ (상대경로)
|
|
44
51
|
const DEFAULT_SETTINGS = {
|
|
45
52
|
API_KEY: '',
|
|
46
53
|
MODEL: DEFAULT_MODEL,
|
|
54
|
+
BASE_URL: '', // 커스텀 API 엔드포인트 (비어있으면 기본값 사용)
|
|
47
55
|
REASONING_EFFORT: 'medium', // 'minimal', 'low', 'medium', 'high'
|
|
56
|
+
// API 요청/응답 내용 화면 표시 여부
|
|
57
|
+
SHOW_API_PAYLOAD: false, // true: 요청/응답 내용을 화면에 표시, false: 표시하지 않음
|
|
48
58
|
// 도구 활성화 옵션
|
|
49
59
|
TOOLS_ENABLED: {
|
|
50
60
|
edit_file_range: false, // 기본적으로 비활성화 (edit_file_replace 사용 권장)
|
|
@@ -74,7 +84,7 @@ export async function ensureConfigDirectory() {
|
|
|
74
84
|
if (!templateDir) return;
|
|
75
85
|
|
|
76
86
|
const templateFiles = await safeReaddir(templateDir);
|
|
77
|
-
await Promise.
|
|
87
|
+
const copyResults = await Promise.allSettled(
|
|
78
88
|
templateFiles.map(async (fileName) => {
|
|
79
89
|
const sourcePath = join(templateDir, fileName);
|
|
80
90
|
const stat = await safeStat(sourcePath);
|
|
@@ -82,8 +92,15 @@ export async function ensureConfigDirectory() {
|
|
|
82
92
|
const destPath = join(CONFIG_DIR, fileName);
|
|
83
93
|
await safeCopyFile(sourcePath, destPath);
|
|
84
94
|
}
|
|
95
|
+
return fileName;
|
|
85
96
|
})
|
|
86
97
|
);
|
|
98
|
+
// 실패한 복사 작업은 조용히 무시 (설정 디렉토리 초기화 실패가 앱 전체를 막으면 안됨)
|
|
99
|
+
const failedCopies = copyResults.filter(r => r.status === 'rejected');
|
|
100
|
+
if (failedCopies.length > 0) {
|
|
101
|
+
// 로깅만 하고 에러는 던지지 않음
|
|
102
|
+
console.error(`[config] ${failedCopies.length} template file(s) failed to copy`);
|
|
103
|
+
}
|
|
87
104
|
} catch (error) {
|
|
88
105
|
// Failed to ensure config directory - silently ignore
|
|
89
106
|
throw error;
|
|
@@ -131,7 +148,7 @@ export async function saveSettings(settings) {
|
|
|
131
148
|
/**
|
|
132
149
|
* API 키의 접두어를 기반으로 발급처를 판단합니다.
|
|
133
150
|
* @param {string} apiKey - API 키 문자열
|
|
134
|
-
* @returns {string|null} 발급처 이름 ("Google", "OpenAI", "Anthropic") 또는 null (알 수 없는 경우)
|
|
151
|
+
* @returns {string|null} 발급처 이름 ("Google", "OpenAI", "Anthropic", "Z.AI") 또는 null (알 수 없는 경우)
|
|
135
152
|
*/
|
|
136
153
|
export function APIKeyIssuedFrom(apiKey) {
|
|
137
154
|
if (!apiKey || typeof apiKey !== 'string') {
|
|
@@ -144,7 +161,26 @@ export function APIKeyIssuedFrom(apiKey) {
|
|
|
144
161
|
return 'OpenAI';
|
|
145
162
|
} else if (apiKey.startsWith('sk-ant-')) {
|
|
146
163
|
return 'Anthropic';
|
|
164
|
+
} else if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
|
|
165
|
+
// Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
|
|
166
|
+
return 'Z.AI';
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
return null;
|
|
150
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Provider별 기본 BASE_URL 반환
|
|
174
|
+
* @param {string} provider - 프로바이더 이름 ('openai', 'claude', 'zai', 'gemini')
|
|
175
|
+
* @returns {string|null} 기본 BASE_URL 또는 null
|
|
176
|
+
*/
|
|
177
|
+
export function getDefaultBaseUrlForProvider(provider) {
|
|
178
|
+
const baseUrls = {
|
|
179
|
+
'zai': 'https://api.z.ai/api/anthropic',
|
|
180
|
+
// 다른 프로바이더는 SDK 기본값 사용
|
|
181
|
+
'openai': null,
|
|
182
|
+
'claude': null,
|
|
183
|
+
'gemini': null
|
|
184
|
+
};
|
|
185
|
+
return baseUrls[provider] || null;
|
|
186
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Migration Utility
|
|
3
|
+
* 설정 버전 관리 및 자동 마이그레이션을 제공합니다.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { safeReadFile, safeWriteFile, safeExists } from './safe_fs.js';
|
|
7
|
+
import { CONFIG_DIR, SETTINGS_FILE } from './config.js';
|
|
8
|
+
import { deepMerge } from './clone.js';
|
|
9
|
+
|
|
10
|
+
/** 현재 설정 버전 */
|
|
11
|
+
export const CURRENT_CONFIG_VERSION = 2;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 설정 마이그레이션 정의
|
|
15
|
+
* 각 버전에서 다음 버전으로의 마이그레이션 함수를 정의합니다.
|
|
16
|
+
*/
|
|
17
|
+
const migrations = {
|
|
18
|
+
/**
|
|
19
|
+
* 버전 1 → 2 마이그레이션
|
|
20
|
+
* - TOOLS_ENABLED 구조 추가
|
|
21
|
+
* - REASONING_EFFORT 기본값 추가
|
|
22
|
+
*/
|
|
23
|
+
1: (settings) => {
|
|
24
|
+
const migrated = { ...settings };
|
|
25
|
+
|
|
26
|
+
// TOOLS_ENABLED가 없으면 추가
|
|
27
|
+
if (!migrated.TOOLS_ENABLED) {
|
|
28
|
+
migrated.TOOLS_ENABLED = {
|
|
29
|
+
edit_file_range: false,
|
|
30
|
+
edit_file_replace: true,
|
|
31
|
+
write_file: true,
|
|
32
|
+
read_file: true,
|
|
33
|
+
read_file_range: true,
|
|
34
|
+
bash: true,
|
|
35
|
+
run_python_code: false,
|
|
36
|
+
fetch_web_page: true,
|
|
37
|
+
response_message: true,
|
|
38
|
+
ripgrep: true,
|
|
39
|
+
glob_search: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// REASONING_EFFORT가 없으면 추가
|
|
44
|
+
if (!migrated.REASONING_EFFORT) {
|
|
45
|
+
migrated.REASONING_EFFORT = 'medium';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
migrated._config_version = 2;
|
|
49
|
+
return migrated;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 설정 버전을 확인합니다.
|
|
55
|
+
* @param {Object} settings - 설정 객체
|
|
56
|
+
* @returns {number} 설정 버전 (없으면 1 반환)
|
|
57
|
+
*/
|
|
58
|
+
export function getConfigVersion(settings) {
|
|
59
|
+
return settings?._config_version || 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 설정이 마이그레이션이 필요한지 확인합니다.
|
|
64
|
+
* @param {Object} settings - 설정 객체
|
|
65
|
+
* @returns {boolean} 마이그레이션이 필요하면 true
|
|
66
|
+
*/
|
|
67
|
+
export function needsMigration(settings) {
|
|
68
|
+
const version = getConfigVersion(settings);
|
|
69
|
+
return version < CURRENT_CONFIG_VERSION;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 설정을 최신 버전으로 마이그레이션합니다.
|
|
74
|
+
* @param {Object} settings - 설정 객체
|
|
75
|
+
* @returns {{settings: Object, migrated: boolean, fromVersion: number, toVersion: number}}
|
|
76
|
+
*/
|
|
77
|
+
export function migrateSettings(settings) {
|
|
78
|
+
const fromVersion = getConfigVersion(settings);
|
|
79
|
+
let currentSettings = { ...settings };
|
|
80
|
+
let currentVersion = fromVersion;
|
|
81
|
+
|
|
82
|
+
// 순차적으로 마이그레이션 적용
|
|
83
|
+
while (currentVersion < CURRENT_CONFIG_VERSION) {
|
|
84
|
+
const migrationFn = migrations[currentVersion];
|
|
85
|
+
if (migrationFn) {
|
|
86
|
+
currentSettings = migrationFn(currentSettings);
|
|
87
|
+
currentVersion++;
|
|
88
|
+
} else {
|
|
89
|
+
// 마이그레이션 함수가 없으면 버전만 업데이트
|
|
90
|
+
currentSettings._config_version = currentVersion + 1;
|
|
91
|
+
currentVersion++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
settings: currentSettings,
|
|
97
|
+
migrated: fromVersion !== currentVersion,
|
|
98
|
+
fromVersion,
|
|
99
|
+
toVersion: currentVersion
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 설정 파일을 마이그레이션하고 저장합니다.
|
|
105
|
+
* @param {string} settingsPath - 설정 파일 경로 (선택, 기본값: SETTINGS_FILE)
|
|
106
|
+
* @returns {Promise<{migrated: boolean, fromVersion: number, toVersion: number}>}
|
|
107
|
+
*/
|
|
108
|
+
export async function migrateSettingsFile(settingsPath = SETTINGS_FILE) {
|
|
109
|
+
try {
|
|
110
|
+
// 설정 파일 존재 확인
|
|
111
|
+
if (!(await safeExists(settingsPath))) {
|
|
112
|
+
return { migrated: false, fromVersion: CURRENT_CONFIG_VERSION, toVersion: CURRENT_CONFIG_VERSION };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 설정 파일 읽기
|
|
116
|
+
const content = await safeReadFile(settingsPath, 'utf8');
|
|
117
|
+
const settings = JSON.parse(content);
|
|
118
|
+
|
|
119
|
+
// 마이그레이션 필요 여부 확인
|
|
120
|
+
if (!needsMigration(settings)) {
|
|
121
|
+
return { migrated: false, fromVersion: getConfigVersion(settings), toVersion: CURRENT_CONFIG_VERSION };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 마이그레이션 수행
|
|
125
|
+
const result = migrateSettings(settings);
|
|
126
|
+
|
|
127
|
+
// 마이그레이션된 설정 저장
|
|
128
|
+
await safeWriteFile(settingsPath, JSON.stringify(result.settings, null, 2), 'utf8');
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
migrated: result.migrated,
|
|
132
|
+
fromVersion: result.fromVersion,
|
|
133
|
+
toVersion: result.toVersion
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`[config_migration] Migration failed: ${error.message}`);
|
|
137
|
+
return { migrated: false, fromVersion: 0, toVersion: 0, error: error.message };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 새 설정 객체를 기본값과 병합합니다.
|
|
143
|
+
* @param {Object} settings - 사용자 설정
|
|
144
|
+
* @param {Object} defaults - 기본 설정
|
|
145
|
+
* @returns {Object} 병합된 설정
|
|
146
|
+
*/
|
|
147
|
+
export function mergeWithDefaults(settings, defaults) {
|
|
148
|
+
const merged = deepMerge(defaults, settings);
|
|
149
|
+
merged._config_version = CURRENT_CONFIG_VERSION;
|
|
150
|
+
return merged;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 설정 백업을 생성합니다.
|
|
155
|
+
* @param {string} settingsPath - 설정 파일 경로
|
|
156
|
+
* @returns {Promise<string>} 백업 파일 경로
|
|
157
|
+
*/
|
|
158
|
+
export async function createSettingsBackup(settingsPath = SETTINGS_FILE) {
|
|
159
|
+
try {
|
|
160
|
+
if (!(await safeExists(settingsPath))) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = await safeReadFile(settingsPath, 'utf8');
|
|
165
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
166
|
+
const backupPath = settingsPath.replace('.json', `.backup-${timestamp}.json`);
|
|
167
|
+
|
|
168
|
+
await safeWriteFile(backupPath, content, 'utf8');
|
|
169
|
+
return backupPath;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`[config_migration] Backup failed: ${error.message}`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Reference Parser
|
|
3
|
+
*
|
|
4
|
+
* 메시지에서 @경로 형태의 파일/디렉토리 참조를 파싱하고 변환합니다.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { safeStat, safeExists } from './safe_fs.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 메시지에서 @참조를 파싱하여 파일/디렉토리 목록과 변환된 메시지를 반환합니다.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} message - 사용자 입력 메시지
|
|
14
|
+
* @returns {Promise<Object>} 파싱 결과
|
|
15
|
+
* @example
|
|
16
|
+
* // 입력: "@src/index.js @package.json 분석해줘"
|
|
17
|
+
* // 출력: {
|
|
18
|
+
* // hasReferences: true,
|
|
19
|
+
* // files: ['src/index.js', 'package.json'],
|
|
20
|
+
* // directories: [],
|
|
21
|
+
* // transformedMessage: "참조된 파일:\n- src/index.js (파일)\n- package.json (파일)\n\n분석해줘"
|
|
22
|
+
* // }
|
|
23
|
+
*/
|
|
24
|
+
export async function parseFileReferences(message) {
|
|
25
|
+
if (!message || typeof message !== 'string') {
|
|
26
|
+
return {
|
|
27
|
+
hasReferences: false,
|
|
28
|
+
files: [],
|
|
29
|
+
directories: [],
|
|
30
|
+
transformedMessage: message || ''
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// @ 뒤에 경로 문자열이 오는 패턴 매칭
|
|
35
|
+
// 경로: 알파벳, 숫자, /, ., _, -, ~로 구성
|
|
36
|
+
const atPattern = /@([^\s@]+)/g;
|
|
37
|
+
const matches = [...message.matchAll(atPattern)];
|
|
38
|
+
|
|
39
|
+
if (matches.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
hasReferences: false,
|
|
42
|
+
files: [],
|
|
43
|
+
directories: [],
|
|
44
|
+
transformedMessage: message
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const files = [];
|
|
49
|
+
const directories = [];
|
|
50
|
+
const validReferences = [];
|
|
51
|
+
|
|
52
|
+
// 각 @참조 검증
|
|
53
|
+
for (const match of matches) {
|
|
54
|
+
const refPath = match[1];
|
|
55
|
+
|
|
56
|
+
// 경로 유효성 검사 (최소한의 검증)
|
|
57
|
+
if (!refPath || refPath.length === 0) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 절대 경로로 변환
|
|
62
|
+
const absolutePath = resolve(process.cwd(), refPath);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const exists = await safeExists(absolutePath);
|
|
66
|
+
if (!exists) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stat = await safeStat(absolutePath);
|
|
71
|
+
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
directories.push(refPath);
|
|
74
|
+
validReferences.push({
|
|
75
|
+
original: match[0],
|
|
76
|
+
path: refPath,
|
|
77
|
+
type: 'directory'
|
|
78
|
+
});
|
|
79
|
+
} else if (stat.isFile()) {
|
|
80
|
+
files.push(refPath);
|
|
81
|
+
validReferences.push({
|
|
82
|
+
original: match[0],
|
|
83
|
+
path: refPath,
|
|
84
|
+
type: 'file'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// 존재하지 않거나 접근 불가능한 경로는 무시
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 유효한 참조가 없으면 원본 메시지 반환
|
|
94
|
+
if (validReferences.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
hasReferences: false,
|
|
97
|
+
files: [],
|
|
98
|
+
directories: [],
|
|
99
|
+
transformedMessage: message
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 메시지에서 @참조 제거
|
|
104
|
+
let cleanedMessage = message;
|
|
105
|
+
for (const ref of validReferences) {
|
|
106
|
+
cleanedMessage = cleanedMessage.replace(ref.original, '').trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 연속된 공백 정리
|
|
110
|
+
cleanedMessage = cleanedMessage.replace(/\s+/g, ' ').trim();
|
|
111
|
+
|
|
112
|
+
// 변환된 메시지 생성
|
|
113
|
+
const referenceLines = validReferences.map(ref => {
|
|
114
|
+
const suffix = ref.type === 'directory' ? '/' : '';
|
|
115
|
+
const typeLabel = ref.type === 'directory' ? '디렉토리' : '파일';
|
|
116
|
+
return `- ${ref.path}${suffix} (${typeLabel})`;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const transformedMessage = [
|
|
120
|
+
'참조된 파일:',
|
|
121
|
+
...referenceLines,
|
|
122
|
+
'',
|
|
123
|
+
cleanedMessage
|
|
124
|
+
].join('\n');
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
hasReferences: true,
|
|
128
|
+
files,
|
|
129
|
+
directories,
|
|
130
|
+
transformedMessage
|
|
131
|
+
};
|
|
132
|
+
}
|