@mandujs/core 0.3.2 → 0.3.3
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/README.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +4 -2
- package/src/change/history.ts +145 -0
- package/src/change/index.ts +40 -0
- package/src/change/integrity.ts +81 -0
- package/src/change/snapshot.ts +233 -0
- package/src/change/transaction.ts +293 -0
- package/src/change/types.ts +102 -0
- package/src/error/classifier.ts +314 -0
- package/src/error/formatter.ts +237 -0
- package/src/error/index.ts +25 -0
- package/src/error/stack-analyzer.ts +295 -0
- package/src/error/types.ts +140 -0
- package/src/filling/context.ts +228 -219
- package/src/filling/filling.ts +256 -234
- package/src/filling/index.ts +7 -7
- package/src/generator/generate.ts +85 -3
- package/src/generator/index.ts +2 -2
- package/src/guard/auto-correct.ts +257 -203
- package/src/index.ts +2 -0
- package/src/report/index.ts +1 -1
- package/src/runtime/index.ts +3 -3
- package/src/runtime/router.ts +65 -65
- package/src/runtime/server.ts +189 -139
- package/src/runtime/ssr.ts +38 -38
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
package/README.md
CHANGED
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/konamgil/mandu/main/mandu_only_simbol.png" alt="Mandu" width="200" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<h1 align="center">@mandujs/core</h1>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<strong>Mandu Framework Core</strong><br/>
|
|
9
|
-
Spec, Generator, Guard, Runtime, Filling
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
<p align="center">
|
|
13
|
-
English | <a href="./README.ko.md"><strong>한국어</strong></a>
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
## Installation
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
bun add @mandujs/core
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
> Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
|
|
23
|
-
|
|
24
|
-
## Module Structure
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
@mandujs/core
|
|
28
|
-
├── spec/ # Spec schema and loading
|
|
29
|
-
├── generator/ # Code generation
|
|
30
|
-
├── guard/ # Architecture checking and auto-correction
|
|
31
|
-
├── runtime/ # Server and router
|
|
32
|
-
└── report/ # Guard report generation
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Spec Module
|
|
36
|
-
|
|
37
|
-
Route manifest schema definition and loading.
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
import { loadManifest, RoutesManifest, RouteSpec } from "@mandujs/core";
|
|
41
|
-
|
|
42
|
-
// Load and validate manifest
|
|
43
|
-
const result = await loadManifest("spec/routes.manifest.json");
|
|
44
|
-
|
|
45
|
-
if (result.success && result.data) {
|
|
46
|
-
const manifest: RoutesManifest = result.data;
|
|
47
|
-
manifest.routes.forEach((route: RouteSpec) => {
|
|
48
|
-
console.log(route.id, route.pattern, route.kind);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Lock File
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { writeLock, readLock } from "@mandujs/core";
|
|
57
|
-
|
|
58
|
-
// Write lock file
|
|
59
|
-
const lock = await writeLock("spec/spec.lock.json", manifest);
|
|
60
|
-
console.log(lock.routesHash);
|
|
61
|
-
|
|
62
|
-
// Read lock file
|
|
63
|
-
const existing = await readLock("spec/spec.lock.json");
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Generator Module
|
|
67
|
-
|
|
68
|
-
Spec-based code generation.
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
import { generateRoutes, GenerateResult } from "@mandujs/core";
|
|
72
|
-
|
|
73
|
-
const result: GenerateResult = await generateRoutes(manifest, "./");
|
|
74
|
-
|
|
75
|
-
console.log("Created:", result.created);
|
|
76
|
-
console.log("Skipped:", result.skipped); // Existing slot files
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Template Functions
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
import {
|
|
83
|
-
generateApiHandler,
|
|
84
|
-
generateApiHandlerWithSlot,
|
|
85
|
-
generateSlotLogic,
|
|
86
|
-
generatePageComponent
|
|
87
|
-
} from "@mandujs/core";
|
|
88
|
-
|
|
89
|
-
// Generate API handler
|
|
90
|
-
const code = generateApiHandler(route);
|
|
91
|
-
|
|
92
|
-
// API handler with slot
|
|
93
|
-
const codeWithSlot = generateApiHandlerWithSlot(route);
|
|
94
|
-
|
|
95
|
-
// Slot logic file
|
|
96
|
-
const slotCode = generateSlotLogic(route);
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Guard Module
|
|
100
|
-
|
|
101
|
-
Architecture rule checking and auto-correction.
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
import {
|
|
105
|
-
runGuardCheck,
|
|
106
|
-
runAutoCorrect,
|
|
107
|
-
GuardResult,
|
|
108
|
-
GuardViolation
|
|
109
|
-
} from "@mandujs/core";
|
|
110
|
-
|
|
111
|
-
// Run check
|
|
112
|
-
const result: GuardResult = await runGuardCheck(manifest, "./");
|
|
113
|
-
|
|
114
|
-
if (!result.passed) {
|
|
115
|
-
result.violations.forEach((v: GuardViolation) => {
|
|
116
|
-
console.log(`${v.rule}: ${v.message}`);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Run auto-correction
|
|
120
|
-
const corrected = await runAutoCorrect(result.violations, manifest, "./");
|
|
121
|
-
console.log("Fixed:", corrected.steps);
|
|
122
|
-
console.log("Remaining violations:", corrected.remainingViolations);
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Guard Rules
|
|
127
|
-
|
|
128
|
-
| Rule ID | Description | Auto-correctable |
|
|
129
|
-
|---------|-------------|------------------|
|
|
130
|
-
| `SPEC_HASH_MISMATCH` | Spec and lock hash mismatch | ✅ |
|
|
131
|
-
| `GENERATED_MANUAL_EDIT` | Manual edit to generated file | ✅ |
|
|
132
|
-
| `HANDLER_NOT_FOUND` | Handler file not found | ❌ |
|
|
133
|
-
| `COMPONENT_NOT_FOUND` | Component file not found | ❌ |
|
|
134
|
-
| `SLOT_NOT_FOUND` | Slot file not found | ✅ |
|
|
135
|
-
|
|
136
|
-
## Runtime Module
|
|
137
|
-
|
|
138
|
-
Server startup and routing.
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import {
|
|
142
|
-
startServer,
|
|
143
|
-
registerApiHandler,
|
|
144
|
-
registerPageLoader
|
|
145
|
-
} from "@mandujs/core";
|
|
146
|
-
|
|
147
|
-
// Register API handler
|
|
148
|
-
registerApiHandler("getUsers", async (req) => {
|
|
149
|
-
return { users: [] };
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Register page loader
|
|
153
|
-
registerPageLoader("homePage", () => import("./pages/Home"));
|
|
154
|
-
|
|
155
|
-
// Start server
|
|
156
|
-
const server = startServer(manifest, { port: 3000 });
|
|
157
|
-
|
|
158
|
-
// Stop
|
|
159
|
-
server.stop();
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Report Module
|
|
163
|
-
|
|
164
|
-
Guard result report generation.
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import { buildGuardReport } from "@mandujs/core";
|
|
168
|
-
|
|
169
|
-
const report = buildGuardReport(guardResult, lockPath);
|
|
170
|
-
console.log(report); // Formatted text report
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Types
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
import type {
|
|
177
|
-
RoutesManifest,
|
|
178
|
-
RouteSpec,
|
|
179
|
-
RouteKind,
|
|
180
|
-
SpecLock,
|
|
181
|
-
GuardResult,
|
|
182
|
-
GuardViolation,
|
|
183
|
-
GenerateResult,
|
|
184
|
-
AutoCorrectResult,
|
|
185
|
-
} from "@mandujs/core";
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Requirements
|
|
189
|
-
|
|
190
|
-
- Bun >= 1.0.0
|
|
191
|
-
- React >= 18.0.0
|
|
192
|
-
- Zod >= 3.0.0
|
|
193
|
-
|
|
194
|
-
## Related Packages
|
|
195
|
-
|
|
196
|
-
- [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
|
|
197
|
-
|
|
198
|
-
## License
|
|
199
|
-
|
|
200
|
-
MIT
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/konamgil/mandu/main/mandu_only_simbol.png" alt="Mandu" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@mandujs/core</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Mandu Framework Core</strong><br/>
|
|
9
|
+
Spec, Generator, Guard, Runtime, Filling
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
English | <a href="./README.ko.md"><strong>한국어</strong></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @mandujs/core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
|
|
23
|
+
|
|
24
|
+
## Module Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
@mandujs/core
|
|
28
|
+
├── spec/ # Spec schema and loading
|
|
29
|
+
├── generator/ # Code generation
|
|
30
|
+
├── guard/ # Architecture checking and auto-correction
|
|
31
|
+
├── runtime/ # Server and router
|
|
32
|
+
└── report/ # Guard report generation
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Spec Module
|
|
36
|
+
|
|
37
|
+
Route manifest schema definition and loading.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { loadManifest, RoutesManifest, RouteSpec } from "@mandujs/core";
|
|
41
|
+
|
|
42
|
+
// Load and validate manifest
|
|
43
|
+
const result = await loadManifest("spec/routes.manifest.json");
|
|
44
|
+
|
|
45
|
+
if (result.success && result.data) {
|
|
46
|
+
const manifest: RoutesManifest = result.data;
|
|
47
|
+
manifest.routes.forEach((route: RouteSpec) => {
|
|
48
|
+
console.log(route.id, route.pattern, route.kind);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Lock File
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { writeLock, readLock } from "@mandujs/core";
|
|
57
|
+
|
|
58
|
+
// Write lock file
|
|
59
|
+
const lock = await writeLock("spec/spec.lock.json", manifest);
|
|
60
|
+
console.log(lock.routesHash);
|
|
61
|
+
|
|
62
|
+
// Read lock file
|
|
63
|
+
const existing = await readLock("spec/spec.lock.json");
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Generator Module
|
|
67
|
+
|
|
68
|
+
Spec-based code generation.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { generateRoutes, GenerateResult } from "@mandujs/core";
|
|
72
|
+
|
|
73
|
+
const result: GenerateResult = await generateRoutes(manifest, "./");
|
|
74
|
+
|
|
75
|
+
console.log("Created:", result.created);
|
|
76
|
+
console.log("Skipped:", result.skipped); // Existing slot files
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Template Functions
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import {
|
|
83
|
+
generateApiHandler,
|
|
84
|
+
generateApiHandlerWithSlot,
|
|
85
|
+
generateSlotLogic,
|
|
86
|
+
generatePageComponent
|
|
87
|
+
} from "@mandujs/core";
|
|
88
|
+
|
|
89
|
+
// Generate API handler
|
|
90
|
+
const code = generateApiHandler(route);
|
|
91
|
+
|
|
92
|
+
// API handler with slot
|
|
93
|
+
const codeWithSlot = generateApiHandlerWithSlot(route);
|
|
94
|
+
|
|
95
|
+
// Slot logic file
|
|
96
|
+
const slotCode = generateSlotLogic(route);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Guard Module
|
|
100
|
+
|
|
101
|
+
Architecture rule checking and auto-correction.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import {
|
|
105
|
+
runGuardCheck,
|
|
106
|
+
runAutoCorrect,
|
|
107
|
+
GuardResult,
|
|
108
|
+
GuardViolation
|
|
109
|
+
} from "@mandujs/core";
|
|
110
|
+
|
|
111
|
+
// Run check
|
|
112
|
+
const result: GuardResult = await runGuardCheck(manifest, "./");
|
|
113
|
+
|
|
114
|
+
if (!result.passed) {
|
|
115
|
+
result.violations.forEach((v: GuardViolation) => {
|
|
116
|
+
console.log(`${v.rule}: ${v.message}`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Run auto-correction
|
|
120
|
+
const corrected = await runAutoCorrect(result.violations, manifest, "./");
|
|
121
|
+
console.log("Fixed:", corrected.steps);
|
|
122
|
+
console.log("Remaining violations:", corrected.remainingViolations);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Guard Rules
|
|
127
|
+
|
|
128
|
+
| Rule ID | Description | Auto-correctable |
|
|
129
|
+
|---------|-------------|------------------|
|
|
130
|
+
| `SPEC_HASH_MISMATCH` | Spec and lock hash mismatch | ✅ |
|
|
131
|
+
| `GENERATED_MANUAL_EDIT` | Manual edit to generated file | ✅ |
|
|
132
|
+
| `HANDLER_NOT_FOUND` | Handler file not found | ❌ |
|
|
133
|
+
| `COMPONENT_NOT_FOUND` | Component file not found | ❌ |
|
|
134
|
+
| `SLOT_NOT_FOUND` | Slot file not found | ✅ |
|
|
135
|
+
|
|
136
|
+
## Runtime Module
|
|
137
|
+
|
|
138
|
+
Server startup and routing.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import {
|
|
142
|
+
startServer,
|
|
143
|
+
registerApiHandler,
|
|
144
|
+
registerPageLoader
|
|
145
|
+
} from "@mandujs/core";
|
|
146
|
+
|
|
147
|
+
// Register API handler
|
|
148
|
+
registerApiHandler("getUsers", async (req) => {
|
|
149
|
+
return { users: [] };
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Register page loader
|
|
153
|
+
registerPageLoader("homePage", () => import("./pages/Home"));
|
|
154
|
+
|
|
155
|
+
// Start server
|
|
156
|
+
const server = startServer(manifest, { port: 3000 });
|
|
157
|
+
|
|
158
|
+
// Stop
|
|
159
|
+
server.stop();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Report Module
|
|
163
|
+
|
|
164
|
+
Guard result report generation.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { buildGuardReport } from "@mandujs/core";
|
|
168
|
+
|
|
169
|
+
const report = buildGuardReport(guardResult, lockPath);
|
|
170
|
+
console.log(report); // Formatted text report
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Types
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import type {
|
|
177
|
+
RoutesManifest,
|
|
178
|
+
RouteSpec,
|
|
179
|
+
RouteKind,
|
|
180
|
+
SpecLock,
|
|
181
|
+
GuardResult,
|
|
182
|
+
GuardViolation,
|
|
183
|
+
GenerateResult,
|
|
184
|
+
AutoCorrectResult,
|
|
185
|
+
} from "@mandujs/core";
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Requirements
|
|
189
|
+
|
|
190
|
+
- Bun >= 1.0.0
|
|
191
|
+
- React >= 18.0.0
|
|
192
|
+
- Zod >= 3.0.0
|
|
193
|
+
|
|
194
|
+
## Related Packages
|
|
195
|
+
|
|
196
|
+
- [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"bun": ">=1.0.0"
|
|
34
|
+
},
|
|
32
35
|
"peerDependencies": {
|
|
33
|
-
"bun": ">=1.0.0",
|
|
34
36
|
"react": ">=18.0.0",
|
|
35
37
|
"react-dom": ">=18.0.0",
|
|
36
38
|
"zod": ">=3.0.0"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import type { ChangeRecord, HistoryConfig } from "./types";
|
|
3
|
+
import { deleteSnapshot, listSnapshotIds } from "./snapshot";
|
|
4
|
+
import { DEFAULT_HISTORY_CONFIG } from "./types";
|
|
5
|
+
|
|
6
|
+
const SPEC_DIR = "spec";
|
|
7
|
+
const HISTORY_DIR = "history";
|
|
8
|
+
const CHANGES_FILE = "changes.json";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Changes 파일 경로
|
|
12
|
+
*/
|
|
13
|
+
function getChangesPath(rootDir: string): string {
|
|
14
|
+
return path.join(rootDir, SPEC_DIR, HISTORY_DIR, CHANGES_FILE);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 모든 변경 기록 조회
|
|
19
|
+
*/
|
|
20
|
+
export async function listChanges(rootDir: string): Promise<ChangeRecord[]> {
|
|
21
|
+
const changesPath = getChangesPath(rootDir);
|
|
22
|
+
try {
|
|
23
|
+
const file = Bun.file(changesPath);
|
|
24
|
+
if (!(await file.exists())) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return await file.json();
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 특정 변경 기록 조회
|
|
35
|
+
*/
|
|
36
|
+
export async function getChange(rootDir: string, id: string): Promise<ChangeRecord | null> {
|
|
37
|
+
const changes = await listChanges(rootDir);
|
|
38
|
+
return changes.find((c) => c.id === id) || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 변경 기록 저장
|
|
43
|
+
*/
|
|
44
|
+
async function writeChanges(rootDir: string, changes: ChangeRecord[]): Promise<void> {
|
|
45
|
+
const changesPath = getChangesPath(rootDir);
|
|
46
|
+
const historyDir = path.join(rootDir, SPEC_DIR, HISTORY_DIR);
|
|
47
|
+
|
|
48
|
+
// 디렉토리 확보
|
|
49
|
+
await Bun.write(path.join(historyDir, ".gitkeep"), "");
|
|
50
|
+
|
|
51
|
+
await Bun.write(changesPath, JSON.stringify(changes, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 오래된 스냅샷 정리
|
|
56
|
+
* @param rootDir 프로젝트 루트 디렉토리
|
|
57
|
+
* @param keepCount 유지할 스냅샷 수 (기본: 5)
|
|
58
|
+
* @returns 삭제된 스냅샷 ID 목록
|
|
59
|
+
*/
|
|
60
|
+
export async function pruneHistory(
|
|
61
|
+
rootDir: string,
|
|
62
|
+
keepCount: number = DEFAULT_HISTORY_CONFIG.maxSnapshots
|
|
63
|
+
): Promise<string[]> {
|
|
64
|
+
const deletedIds: string[] = [];
|
|
65
|
+
|
|
66
|
+
// 모든 스냅샷 ID 조회 (최신 순으로 정렬됨)
|
|
67
|
+
const snapshotIds = await listSnapshotIds(rootDir);
|
|
68
|
+
|
|
69
|
+
if (snapshotIds.length <= keepCount) {
|
|
70
|
+
return deletedIds;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 변경 기록 조회
|
|
74
|
+
const changes = await listChanges(rootDir);
|
|
75
|
+
|
|
76
|
+
// 활성 트랜잭션의 스냅샷 ID 수집 (삭제 불가)
|
|
77
|
+
const activeSnapshotIds = new Set(
|
|
78
|
+
changes.filter((c) => c.status === "active").map((c) => c.snapshotId)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 유지할 스냅샷 외의 것들 삭제
|
|
82
|
+
const toDelete = snapshotIds.slice(keepCount);
|
|
83
|
+
|
|
84
|
+
for (const snapshotId of toDelete) {
|
|
85
|
+
// 활성 트랜잭션의 스냅샷은 삭제하지 않음
|
|
86
|
+
if (activeSnapshotIds.has(snapshotId)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const deleted = await deleteSnapshot(rootDir, snapshotId);
|
|
91
|
+
if (deleted) {
|
|
92
|
+
deletedIds.push(snapshotId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 삭제된 스냅샷에 연결된 변경 기록도 정리
|
|
97
|
+
if (deletedIds.length > 0) {
|
|
98
|
+
const deletedSet = new Set(deletedIds);
|
|
99
|
+
const remainingChanges = changes.filter((c) => {
|
|
100
|
+
// 활성 상태는 유지
|
|
101
|
+
if (c.status === "active") {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// 스냅샷이 삭제되지 않은 것만 유지
|
|
105
|
+
return !deletedSet.has(c.snapshotId);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await writeChanges(rootDir, remainingChanges);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return deletedIds;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* History 설정 로드 (향후 mandu.config.json에서 로드)
|
|
116
|
+
*/
|
|
117
|
+
export async function loadHistoryConfig(rootDir: string): Promise<HistoryConfig> {
|
|
118
|
+
// 향후 mandu.config.json에서 로드하도록 확장
|
|
119
|
+
// 현재는 기본값 반환
|
|
120
|
+
return DEFAULT_HISTORY_CONFIG;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 변경 통계 조회
|
|
125
|
+
*/
|
|
126
|
+
export async function getChangeStats(
|
|
127
|
+
rootDir: string
|
|
128
|
+
): Promise<{
|
|
129
|
+
total: number;
|
|
130
|
+
active: number;
|
|
131
|
+
committed: number;
|
|
132
|
+
rolledBack: number;
|
|
133
|
+
snapshotCount: number;
|
|
134
|
+
}> {
|
|
135
|
+
const changes = await listChanges(rootDir);
|
|
136
|
+
const snapshotIds = await listSnapshotIds(rootDir);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
total: changes.length,
|
|
140
|
+
active: changes.filter((c) => c.status === "active").length,
|
|
141
|
+
committed: changes.filter((c) => c.status === "committed").length,
|
|
142
|
+
rolledBack: changes.filter((c) => c.status === "rolled_back").length,
|
|
143
|
+
snapshotCount: snapshotIds.length,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
ChangeRecord,
|
|
4
|
+
Snapshot,
|
|
5
|
+
TransactionState,
|
|
6
|
+
HistoryConfig,
|
|
7
|
+
RestoreResult,
|
|
8
|
+
CommitResult,
|
|
9
|
+
RollbackResult,
|
|
10
|
+
BeginChangeOptions,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export { DEFAULT_HISTORY_CONFIG } from "./types";
|
|
14
|
+
|
|
15
|
+
// Integrity
|
|
16
|
+
export { computeFileHash, collectFileHashes, collectFilePaths } from "./integrity";
|
|
17
|
+
|
|
18
|
+
// Snapshot
|
|
19
|
+
export {
|
|
20
|
+
createSnapshot,
|
|
21
|
+
readSnapshot,
|
|
22
|
+
writeSnapshot,
|
|
23
|
+
readSnapshotById,
|
|
24
|
+
restoreSnapshot,
|
|
25
|
+
deleteSnapshot,
|
|
26
|
+
listSnapshotIds,
|
|
27
|
+
} from "./snapshot";
|
|
28
|
+
|
|
29
|
+
// Transaction
|
|
30
|
+
export {
|
|
31
|
+
beginChange,
|
|
32
|
+
commitChange,
|
|
33
|
+
rollbackChange,
|
|
34
|
+
hasActiveTransaction,
|
|
35
|
+
getActiveTransaction,
|
|
36
|
+
getTransactionStatus,
|
|
37
|
+
} from "./transaction";
|
|
38
|
+
|
|
39
|
+
// History
|
|
40
|
+
export { listChanges, getChange, pruneHistory, loadHistoryConfig, getChangeStats } from "./history";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 파일의 SHA-256 해시를 계산
|
|
6
|
+
*/
|
|
7
|
+
export async function computeFileHash(filePath: string): Promise<string> {
|
|
8
|
+
try {
|
|
9
|
+
const file = Bun.file(filePath);
|
|
10
|
+
const exists = await file.exists();
|
|
11
|
+
|
|
12
|
+
if (!exists) {
|
|
13
|
+
throw new Error(`File not found: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const content = await file.text();
|
|
17
|
+
return createHash("sha256").update(content).digest("hex");
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error instanceof Error && error.message.startsWith("File not found")) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Failed to compute hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 여러 파일의 해시를 수집
|
|
30
|
+
* @param rootDir 프로젝트 루트 디렉토리
|
|
31
|
+
* @param relativePaths 상대 경로 배열
|
|
32
|
+
* @returns 상대 경로 → 해시 맵
|
|
33
|
+
*/
|
|
34
|
+
export async function collectFileHashes(
|
|
35
|
+
rootDir: string,
|
|
36
|
+
relativePaths: string[]
|
|
37
|
+
): Promise<Record<string, string>> {
|
|
38
|
+
const hashes: Record<string, string> = {};
|
|
39
|
+
|
|
40
|
+
await Promise.all(
|
|
41
|
+
relativePaths.map(async (relativePath) => {
|
|
42
|
+
const absolutePath = path.join(rootDir, relativePath);
|
|
43
|
+
try {
|
|
44
|
+
const hash = await computeFileHash(absolutePath);
|
|
45
|
+
hashes[relativePath] = hash;
|
|
46
|
+
} catch {
|
|
47
|
+
// 파일이 없는 경우 무시 (스냅샷 시점에 없었을 수 있음)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return hashes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 디렉토리 내 모든 파일 경로를 재귀적으로 수집
|
|
57
|
+
*/
|
|
58
|
+
export async function collectFilePaths(
|
|
59
|
+
dirPath: string,
|
|
60
|
+
basePath: string = dirPath
|
|
61
|
+
): Promise<string[]> {
|
|
62
|
+
const paths: string[] = [];
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const entries = await Array.fromAsync(
|
|
66
|
+
new Bun.Glob("**/*").scan({
|
|
67
|
+
cwd: dirPath,
|
|
68
|
+
onlyFiles: true,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const relativePath = path.relative(basePath, path.join(dirPath, entry));
|
|
74
|
+
paths.push(relativePath);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// 디렉토리가 없는 경우 빈 배열 반환
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return paths;
|
|
81
|
+
}
|