@mandujs/core 0.9.46 → 0.10.0
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/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +8 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/filling/filling.ts +88 -6
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +2 -0
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
package/src/index.ts
CHANGED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Lockfile 생성 🔐
|
|
3
|
+
*
|
|
4
|
+
* 설정 파일에서 lockfile 생성
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computeConfigHash, normalizeForHash } from "../utils/hasher.js";
|
|
8
|
+
import {
|
|
9
|
+
type ManduLockfile,
|
|
10
|
+
type LockfileGenerateOptions,
|
|
11
|
+
LOCKFILE_SCHEMA_VERSION,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// Lockfile 생성
|
|
16
|
+
// ============================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 설정에서 Lockfile 생성
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const config = await loadConfig();
|
|
24
|
+
* const lockfile = generateLockfile(config, {
|
|
25
|
+
* manduVersion: "0.9.46",
|
|
26
|
+
* includeSnapshot: true,
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function generateLockfile(
|
|
31
|
+
config: Record<string, unknown>,
|
|
32
|
+
options: LockfileGenerateOptions = {},
|
|
33
|
+
mcpConfig?: Record<string, unknown> | null
|
|
34
|
+
): ManduLockfile {
|
|
35
|
+
const {
|
|
36
|
+
manduVersion = getManduVersion(),
|
|
37
|
+
environment = detectEnvironment(),
|
|
38
|
+
includeSnapshot = false,
|
|
39
|
+
includeMcpServerHashes = true,
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
// 설정 해시 계산
|
|
43
|
+
const configHash = computeConfigHash(config);
|
|
44
|
+
|
|
45
|
+
// MCP 설정 해시 (있는 경우)
|
|
46
|
+
const { mcpHashSource, mcpServers } = resolveMcpSources(config, mcpConfig);
|
|
47
|
+
let mcpConfigHash: string | undefined;
|
|
48
|
+
let mcpServerHashes: ManduLockfile["mcpServers"];
|
|
49
|
+
|
|
50
|
+
if (mcpHashSource && Object.keys(mcpHashSource).length > 0) {
|
|
51
|
+
mcpConfigHash = computeConfigHash(mcpHashSource);
|
|
52
|
+
|
|
53
|
+
if (includeMcpServerHashes && mcpServers) {
|
|
54
|
+
mcpServerHashes = {};
|
|
55
|
+
for (const [name, serverConfig] of Object.entries(mcpServers)) {
|
|
56
|
+
mcpServerHashes[name] = {
|
|
57
|
+
hash: computeConfigHash(serverConfig),
|
|
58
|
+
version: extractServerVersion(serverConfig),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Lockfile 생성
|
|
65
|
+
const lockfile: ManduLockfile = {
|
|
66
|
+
schemaVersion: LOCKFILE_SCHEMA_VERSION,
|
|
67
|
+
manduVersion,
|
|
68
|
+
configHash,
|
|
69
|
+
generatedAt: new Date().toISOString(),
|
|
70
|
+
environment,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// 선택적 필드 추가
|
|
74
|
+
if (mcpConfigHash) {
|
|
75
|
+
lockfile.mcpConfigHash = mcpConfigHash;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (mcpServerHashes) {
|
|
79
|
+
lockfile.mcpServers = mcpServerHashes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (includeSnapshot) {
|
|
83
|
+
const normalized = normalizeForHash(config);
|
|
84
|
+
const snapshotConfig =
|
|
85
|
+
normalized && typeof normalized === "object"
|
|
86
|
+
? (normalized as Record<string, unknown>)
|
|
87
|
+
: {};
|
|
88
|
+
|
|
89
|
+
if (mcpServers) {
|
|
90
|
+
const normalizedMcp = normalizeForHash(mcpServers);
|
|
91
|
+
if (normalizedMcp !== undefined) {
|
|
92
|
+
snapshotConfig.mcpServers = normalizedMcp;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lockfile.snapshot = {
|
|
97
|
+
config: snapshotConfig,
|
|
98
|
+
environment: environment ?? "development",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return lockfile;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* MCP 설정에서 별도 Lockfile 데이터 생성
|
|
107
|
+
*/
|
|
108
|
+
export function generateMcpLockData(
|
|
109
|
+
mcpConfig: Record<string, unknown>
|
|
110
|
+
): { hash: string; servers: ManduLockfile["mcpServers"] } {
|
|
111
|
+
const { mcpHashSource, mcpServers } = resolveMcpSources({}, mcpConfig);
|
|
112
|
+
const hash = mcpHashSource ? computeConfigHash(mcpHashSource) : computeConfigHash(mcpConfig);
|
|
113
|
+
const servers: ManduLockfile["mcpServers"] = {};
|
|
114
|
+
|
|
115
|
+
if (mcpServers) {
|
|
116
|
+
for (const [name, serverConfig] of Object.entries(mcpServers)) {
|
|
117
|
+
servers[name] = {
|
|
118
|
+
hash: computeConfigHash(serverConfig),
|
|
119
|
+
version: extractServerVersion(serverConfig),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { hash, servers };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================
|
|
128
|
+
// 유틸리티
|
|
129
|
+
// ============================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* mandu 버전 가져오기
|
|
133
|
+
*/
|
|
134
|
+
function getManduVersion(): string {
|
|
135
|
+
// 실제 구현에서는 package.json에서 읽거나 빌드 시 주입
|
|
136
|
+
try {
|
|
137
|
+
// @ts-ignore - 빌드 시 주입되는 값
|
|
138
|
+
if (typeof __MANDU_VERSION__ !== "undefined") {
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
return __MANDU_VERSION__;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// ignore
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 기본값
|
|
147
|
+
return "0.0.0";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 현재 환경 감지
|
|
152
|
+
*/
|
|
153
|
+
function detectEnvironment(): "development" | "production" | "ci" {
|
|
154
|
+
// CI 환경 감지
|
|
155
|
+
if (
|
|
156
|
+
process.env.CI === "true" ||
|
|
157
|
+
process.env.GITHUB_ACTIONS === "true" ||
|
|
158
|
+
process.env.GITLAB_CI === "true" ||
|
|
159
|
+
process.env.JENKINS_URL
|
|
160
|
+
) {
|
|
161
|
+
return "ci";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// NODE_ENV 기반
|
|
165
|
+
if (process.env.NODE_ENV === "production") {
|
|
166
|
+
return "production";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return "development";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 서버 설정에서 버전 추출
|
|
174
|
+
*/
|
|
175
|
+
function extractServerVersion(
|
|
176
|
+
serverConfig: unknown
|
|
177
|
+
): string | undefined {
|
|
178
|
+
if (typeof serverConfig !== "object" || serverConfig === null) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const config = serverConfig as Record<string, unknown>;
|
|
183
|
+
|
|
184
|
+
// version 필드 직접 확인
|
|
185
|
+
if (typeof config.version === "string") {
|
|
186
|
+
return config.version;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// args에서 버전 패턴 추출 시도 (예: @package/name@1.2.3)
|
|
190
|
+
if (Array.isArray(config.args)) {
|
|
191
|
+
for (const arg of config.args) {
|
|
192
|
+
if (typeof arg === "string") {
|
|
193
|
+
const match = arg.match(/@[\w-]+\/[\w-]+@([\d.]+)/);
|
|
194
|
+
if (match) {
|
|
195
|
+
return match[1];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================
|
|
205
|
+
// 해시 재계산
|
|
206
|
+
// ============================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 현재 설정의 해시만 빠르게 계산
|
|
210
|
+
*/
|
|
211
|
+
export function computeCurrentHashes(
|
|
212
|
+
config: Record<string, unknown>,
|
|
213
|
+
mcpConfig?: Record<string, unknown> | null
|
|
214
|
+
): { configHash: string; mcpConfigHash?: string } {
|
|
215
|
+
const configHash = computeConfigHash(config);
|
|
216
|
+
|
|
217
|
+
const { mcpHashSource } = resolveMcpSources(config, mcpConfig);
|
|
218
|
+
const mcpConfigHash = mcpHashSource && Object.keys(mcpHashSource).length > 0
|
|
219
|
+
? computeConfigHash(mcpHashSource)
|
|
220
|
+
: undefined;
|
|
221
|
+
|
|
222
|
+
return { configHash, mcpConfigHash };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================
|
|
226
|
+
// MCP 설정 해석
|
|
227
|
+
// ============================================
|
|
228
|
+
|
|
229
|
+
export function resolveMcpSources(
|
|
230
|
+
config: Record<string, unknown>,
|
|
231
|
+
mcpConfig?: Record<string, unknown> | null
|
|
232
|
+
): {
|
|
233
|
+
mcpHashSource?: Record<string, unknown>;
|
|
234
|
+
mcpServers?: Record<string, unknown>;
|
|
235
|
+
} {
|
|
236
|
+
if (mcpConfig && typeof mcpConfig === "object") {
|
|
237
|
+
const mcpServers = (mcpConfig as Record<string, unknown>).mcpServers;
|
|
238
|
+
if (mcpServers && typeof mcpServers === "object" && !Array.isArray(mcpServers)) {
|
|
239
|
+
return {
|
|
240
|
+
mcpHashSource: mcpConfig,
|
|
241
|
+
mcpServers: mcpServers as Record<string, unknown>,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
mcpHashSource: mcpConfig,
|
|
246
|
+
mcpServers: mcpConfig,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const configServers = config.mcpServers as Record<string, unknown> | undefined;
|
|
251
|
+
if (configServers && typeof configServers === "object") {
|
|
252
|
+
return {
|
|
253
|
+
mcpHashSource: configServers,
|
|
254
|
+
mcpServers: configServers,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Lockfile I/O 📁
|
|
3
|
+
*
|
|
4
|
+
* Lockfile 읽기/쓰기 및 공개 API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
type ManduLockfile,
|
|
11
|
+
type LockfileError,
|
|
12
|
+
LOCKFILE_PATH,
|
|
13
|
+
LOCKFILE_DIR,
|
|
14
|
+
LOCKFILE_SCHEMA_VERSION,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// 읽기
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Lockfile 읽기
|
|
23
|
+
*
|
|
24
|
+
* @param projectRoot 프로젝트 루트 디렉토리
|
|
25
|
+
* @returns Lockfile 또는 null (없는 경우)
|
|
26
|
+
* @throws 파싱 오류 시
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const lockfile = await readLockfile(process.cwd());
|
|
31
|
+
* if (lockfile) {
|
|
32
|
+
* console.log(`Config hash: ${lockfile.configHash}`);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export async function readLockfile(
|
|
37
|
+
projectRoot: string
|
|
38
|
+
): Promise<ManduLockfile | null> {
|
|
39
|
+
const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const file = Bun.file(lockfilePath);
|
|
43
|
+
const exists = await file.exists();
|
|
44
|
+
|
|
45
|
+
if (!exists) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const content = await file.text();
|
|
50
|
+
const data = JSON.parse(content) as ManduLockfile;
|
|
51
|
+
|
|
52
|
+
// 스키마 버전 체크
|
|
53
|
+
if (data.schemaVersion !== LOCKFILE_SCHEMA_VERSION) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[Mandu] Lockfile schema version mismatch: expected ${LOCKFILE_SCHEMA_VERSION}, got ${data.schemaVersion}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return data;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof SyntaxError) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to parse lockfile at ${lockfilePath}: ${error.message}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* MCP 설정 읽기 (.mcp.json)
|
|
72
|
+
*/
|
|
73
|
+
export async function readMcpConfig(
|
|
74
|
+
projectRoot: string
|
|
75
|
+
): Promise<Record<string, unknown> | null> {
|
|
76
|
+
const mcpPath = path.join(projectRoot, ".mcp.json");
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const file = Bun.file(mcpPath);
|
|
80
|
+
const exists = await file.exists();
|
|
81
|
+
if (!exists) return null;
|
|
82
|
+
|
|
83
|
+
const content = await file.text();
|
|
84
|
+
const data = JSON.parse(content) as Record<string, unknown>;
|
|
85
|
+
return data ?? null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof SyntaxError) {
|
|
88
|
+
throw new Error(`Failed to parse .mcp.json at ${mcpPath}: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lockfile 존재 여부 확인
|
|
96
|
+
*/
|
|
97
|
+
export async function lockfileExists(projectRoot: string): Promise<boolean> {
|
|
98
|
+
const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
|
|
99
|
+
const file = Bun.file(lockfilePath);
|
|
100
|
+
return file.exists();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// 쓰기
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lockfile 쓰기
|
|
109
|
+
*
|
|
110
|
+
* @param projectRoot 프로젝트 루트 디렉토리
|
|
111
|
+
* @param lockfile Lockfile 데이터
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const lockfile = generateLockfile(config);
|
|
116
|
+
* await writeLockfile(process.cwd(), lockfile);
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export async function writeLockfile(
|
|
120
|
+
projectRoot: string,
|
|
121
|
+
lockfile: ManduLockfile
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const lockfileDir = path.join(projectRoot, LOCKFILE_DIR);
|
|
124
|
+
const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
|
|
125
|
+
|
|
126
|
+
// 디렉토리 생성
|
|
127
|
+
await mkdir(lockfileDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
// JSON 포맷팅 (가독성)
|
|
130
|
+
const content = JSON.stringify(lockfile, null, 2);
|
|
131
|
+
|
|
132
|
+
// 쓰기
|
|
133
|
+
await Bun.write(lockfilePath, content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Lockfile 삭제
|
|
138
|
+
*/
|
|
139
|
+
export async function deleteLockfile(projectRoot: string): Promise<boolean> {
|
|
140
|
+
const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const file = Bun.file(lockfilePath);
|
|
144
|
+
const exists = await file.exists();
|
|
145
|
+
|
|
146
|
+
if (!exists) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { unlink } = await import("node:fs/promises");
|
|
151
|
+
await unlink(lockfilePath);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================
|
|
159
|
+
// 유틸리티
|
|
160
|
+
// ============================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Lockfile 경로 가져오기
|
|
164
|
+
*/
|
|
165
|
+
export function getLockfilePath(projectRoot: string): string {
|
|
166
|
+
return path.join(projectRoot, LOCKFILE_PATH);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Lockfile 오류 생성 헬퍼
|
|
171
|
+
*/
|
|
172
|
+
export function createLockfileError(
|
|
173
|
+
code: LockfileError["code"],
|
|
174
|
+
message: string,
|
|
175
|
+
details?: Record<string, unknown>
|
|
176
|
+
): LockfileError {
|
|
177
|
+
return { code, message, details };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================
|
|
181
|
+
// Re-exports
|
|
182
|
+
// ============================================
|
|
183
|
+
|
|
184
|
+
export * from "./types.js";
|
|
185
|
+
export * from "./generate.js";
|
|
186
|
+
export * from "./validate.js";
|