@mandujs/cli 0.9.24 → 0.9.42
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/commands/build.ts +22 -16
- package/src/commands/dev.ts +220 -191
- package/src/commands/guard-arch.ts +13 -7
- package/src/commands/init.ts +8 -7
- package/src/commands/routes.ts +11 -1
- package/src/errors/codes.ts +35 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/messages.ts +143 -0
- package/src/main.ts +28 -12
- package/src/util/bun.ts +6 -0
package/package.json
CHANGED
package/src/commands/build.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Hydration이 필요한 Island들을 번들링합니다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { loadManifest, buildClientBundles, printBundleStats } from "@mandujs/core";
|
|
7
|
+
import { loadManifest, buildClientBundles, printBundleStats, validateAndReport } from "@mandujs/core";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import fs from "fs/promises";
|
|
10
10
|
|
|
@@ -19,14 +19,20 @@ export interface BuildOptions {
|
|
|
19
19
|
outDir?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
const specPath = path.join(cwd, "spec", "routes.manifest.json");
|
|
25
|
-
|
|
26
|
-
console.log("📦 Mandu Build - Client Bundle Builder\n");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const specPath = path.join(cwd, "spec", "routes.manifest.json");
|
|
25
|
+
|
|
26
|
+
console.log("📦 Mandu Build - Client Bundle Builder\n");
|
|
27
|
+
|
|
28
|
+
const config = await validateAndReport(cwd);
|
|
29
|
+
if (!config) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const buildConfig = config.build ?? {};
|
|
33
|
+
|
|
34
|
+
// 1. Spec 로드
|
|
35
|
+
const specResult = await loadManifest(specPath);
|
|
30
36
|
if (!specResult.success) {
|
|
31
37
|
console.error("❌ Spec 로드 실패:");
|
|
32
38
|
for (const error of specResult.errors) {
|
|
@@ -58,13 +64,13 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
|
58
64
|
console.log(` - ${route.id} (${hydration.strategy}, ${hydration.priority || "visible"})`);
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
// 3. 번들 빌드
|
|
62
|
-
const startTime = performance.now();
|
|
63
|
-
const result = await buildClientBundles(manifest, cwd, {
|
|
64
|
-
minify: options.minify,
|
|
65
|
-
sourcemap: options.sourcemap,
|
|
66
|
-
outDir: options.outDir,
|
|
67
|
-
});
|
|
67
|
+
// 3. 번들 빌드
|
|
68
|
+
const startTime = performance.now();
|
|
69
|
+
const result = await buildClientBundles(manifest, cwd, {
|
|
70
|
+
minify: options.minify ?? buildConfig.minify,
|
|
71
|
+
sourcemap: options.sourcemap ?? buildConfig.sourcemap,
|
|
72
|
+
outDir: options.outDir ?? buildConfig.outDir,
|
|
73
|
+
});
|
|
68
74
|
|
|
69
75
|
// 4. 결과 출력
|
|
70
76
|
console.log("");
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
startServer,
|
|
3
|
-
registerApiHandler,
|
|
4
|
-
registerPageLoader,
|
|
5
|
-
registerPageHandler,
|
|
6
|
-
registerLayoutLoader,
|
|
1
|
+
import {
|
|
2
|
+
startServer,
|
|
3
|
+
registerApiHandler,
|
|
4
|
+
registerPageLoader,
|
|
5
|
+
registerPageHandler,
|
|
6
|
+
registerLayoutLoader,
|
|
7
7
|
startDevBundler,
|
|
8
8
|
createHMRServer,
|
|
9
9
|
needsHydration,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
formatReportForAgent,
|
|
18
18
|
formatReportAsAgentJSON,
|
|
19
19
|
getPreset,
|
|
20
|
+
validateAndReport,
|
|
20
21
|
type RoutesManifest,
|
|
21
22
|
type GuardConfig,
|
|
22
23
|
type GuardPreset,
|
|
@@ -24,8 +25,10 @@ import {
|
|
|
24
25
|
} from "@mandujs/core";
|
|
25
26
|
import { isDirectory, resolveFromCwd } from "../util/fs";
|
|
26
27
|
import { resolveOutputFormat, type OutputFormat } from "../util/output";
|
|
27
|
-
import
|
|
28
|
-
|
|
28
|
+
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
29
|
+
import { importFresh } from "../util/bun";
|
|
30
|
+
import path from "path";
|
|
31
|
+
|
|
29
32
|
export interface DevOptions {
|
|
30
33
|
port?: number;
|
|
31
34
|
/** HMR 비활성화 */
|
|
@@ -39,55 +42,64 @@ export interface DevOptions {
|
|
|
39
42
|
/** Guard 출력 형식 */
|
|
40
43
|
guardFormat?: OutputFormat;
|
|
41
44
|
}
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
44
47
|
const rootDir = resolveFromCwd(".");
|
|
48
|
+
const config = await validateAndReport(rootDir);
|
|
49
|
+
|
|
50
|
+
if (!config) {
|
|
51
|
+
printCLIError(CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const serverConfig = config.server ?? {};
|
|
56
|
+
const devConfig = config.dev ?? {};
|
|
57
|
+
const guardConfigFromFile = config.guard ?? {};
|
|
45
58
|
|
|
46
59
|
console.log(`🥟 Mandu Dev Server (FS Routes)`);
|
|
47
|
-
|
|
48
|
-
// .env 파일 로드
|
|
49
|
-
const envResult = await loadEnv({
|
|
50
|
-
rootDir,
|
|
51
|
-
env: "development",
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (envResult.loaded.length > 0) {
|
|
55
|
-
console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// FS Routes 스캔
|
|
59
|
-
console.log(`📂 app/ 폴더 스캔 중...`);
|
|
60
|
-
|
|
61
|
-
const result = await generateManifest(rootDir, {
|
|
62
|
-
skipLegacy: true,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (result.manifest.routes.length === 0) {
|
|
66
|
-
|
|
67
|
-
console.log("
|
|
68
|
-
console.log("");
|
|
69
|
-
console.log("
|
|
70
|
-
console.log("");
|
|
71
|
-
console.log(" app/
|
|
72
|
-
console.log("
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
60
|
+
|
|
61
|
+
// .env 파일 로드
|
|
62
|
+
const envResult = await loadEnv({
|
|
63
|
+
rootDir,
|
|
64
|
+
env: "development",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (envResult.loaded.length > 0) {
|
|
68
|
+
console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// FS Routes 스캔
|
|
72
|
+
console.log(`📂 app/ 폴더 스캔 중...`);
|
|
73
|
+
|
|
74
|
+
const result = await generateManifest(rootDir, {
|
|
75
|
+
skipLegacy: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (result.manifest.routes.length === 0) {
|
|
79
|
+
printCLIError(CLI_ERROR_CODES.DEV_NO_ROUTES);
|
|
80
|
+
console.log("💡 app/ 폴더에 page.tsx 파일을 생성하세요:");
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(" app/page.tsx → /");
|
|
83
|
+
console.log(" app/blog/page.tsx → /blog");
|
|
84
|
+
console.log(" app/api/users/route.ts → /api/users");
|
|
85
|
+
console.log("");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
let manifest = result.manifest;
|
|
79
90
|
console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
|
|
80
91
|
|
|
81
92
|
const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
|
|
82
|
-
const guardPreset = options.guardPreset || "mandu";
|
|
93
|
+
const guardPreset = options.guardPreset || guardConfigFromFile.preset || "mandu";
|
|
83
94
|
const guardFormat = resolveOutputFormat(options.guardFormat);
|
|
84
95
|
const guardConfig: GuardConfig | null =
|
|
85
96
|
options.guard === false
|
|
86
97
|
? null
|
|
87
98
|
: {
|
|
88
99
|
preset: guardPreset,
|
|
89
|
-
srcDir: "src",
|
|
90
|
-
realtime: true,
|
|
100
|
+
srcDir: guardConfigFromFile.srcDir || "src",
|
|
101
|
+
realtime: guardConfigFromFile.realtime ?? true,
|
|
102
|
+
exclude: guardConfigFromFile.exclude,
|
|
91
103
|
realtimeOutput: guardFormat,
|
|
92
104
|
fsRoutes: enableFsRoutes
|
|
93
105
|
? {
|
|
@@ -144,161 +156,178 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
144
156
|
|
|
145
157
|
// Layout 경로 추적 (중복 등록 방지)
|
|
146
158
|
const registeredLayouts = new Set<string>();
|
|
147
|
-
|
|
148
|
-
// 핸들러 등록 함수
|
|
149
|
-
const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
|
|
150
|
-
// 리로드 시 레이아웃 캐시 클리어
|
|
151
|
-
if (isReload) {
|
|
152
|
-
registeredLayouts.clear();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for (const route of manifest.routes) {
|
|
156
|
-
if (route.kind === "api") {
|
|
157
|
-
const modulePath = path.resolve(rootDir, route.module);
|
|
158
|
-
try {
|
|
159
|
-
// 캐시 무효화 (HMR용)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
159
|
+
|
|
160
|
+
// 핸들러 등록 함수
|
|
161
|
+
const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
|
|
162
|
+
// 리로드 시 레이아웃 캐시 클리어
|
|
163
|
+
if (isReload) {
|
|
164
|
+
registeredLayouts.clear();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const route of manifest.routes) {
|
|
168
|
+
if (route.kind === "api") {
|
|
169
|
+
const modulePath = path.resolve(rootDir, route.module);
|
|
170
|
+
try {
|
|
171
|
+
// 캐시 무효화 (HMR용)
|
|
172
|
+
const module = await importFresh(modulePath);
|
|
173
|
+
let handler = module.default || module.handler || module;
|
|
174
|
+
|
|
175
|
+
// ManduFilling 인스턴스를 핸들러 함수로 래핑
|
|
176
|
+
if (handler && typeof handler.handle === 'function') {
|
|
177
|
+
console.log(` 🔄 ManduFilling 래핑: ${route.id}`);
|
|
178
|
+
const filling = handler;
|
|
179
|
+
handler = async (req: Request, params?: Record<string, string>) => {
|
|
180
|
+
return filling.handle(req, params);
|
|
181
|
+
};
|
|
182
|
+
} else {
|
|
183
|
+
console.log(` ⚠️ 핸들러 타입: ${typeof handler}, handle: ${typeof handler?.handle}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
registerApiHandler(route.id, handler);
|
|
187
|
+
console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
|
|
190
|
+
}
|
|
191
|
+
} else if (route.kind === "page" && route.componentModule) {
|
|
192
|
+
const componentPath = path.resolve(rootDir, route.componentModule);
|
|
193
|
+
const isIsland = needsHydration(route);
|
|
194
|
+
const hasLayout = route.layoutChain && route.layoutChain.length > 0;
|
|
195
|
+
|
|
196
|
+
// Layout 로더 등록
|
|
197
|
+
if (route.layoutChain) {
|
|
198
|
+
for (const layoutPath of route.layoutChain) {
|
|
199
|
+
if (!registeredLayouts.has(layoutPath)) {
|
|
200
|
+
const absLayoutPath = path.resolve(rootDir, layoutPath);
|
|
201
|
+
registerLayoutLoader(layoutPath, async () => {
|
|
202
|
+
// 캐시 무효화 (HMR용)
|
|
203
|
+
return importFresh(absLayoutPath);
|
|
204
|
+
});
|
|
205
|
+
registeredLayouts.add(layoutPath);
|
|
206
|
+
console.log(` 🎨 Layout: ${layoutPath}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
|
|
212
|
+
if (route.slotModule) {
|
|
213
|
+
registerPageHandler(route.id, async () => {
|
|
214
|
+
const module = await importFresh(componentPath);
|
|
215
|
+
return module.default;
|
|
216
|
+
});
|
|
217
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
|
|
218
|
+
} else {
|
|
219
|
+
registerPageLoader(route.id, () => importFresh(componentPath));
|
|
220
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// 초기 핸들러 등록
|
|
227
|
+
await registerHandlers(manifest);
|
|
228
|
+
console.log("");
|
|
229
|
+
|
|
230
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
231
|
+
const port =
|
|
232
|
+
options.port ??
|
|
233
|
+
(envPort && Number.isFinite(envPort) ? envPort : undefined) ??
|
|
234
|
+
serverConfig.port ??
|
|
235
|
+
3333;
|
|
236
|
+
|
|
237
|
+
// HMR 서버 시작 (클라이언트 슬롯이 있는 경우)
|
|
238
|
+
let hmrServer: ReturnType<typeof createHMRServer> | null = null;
|
|
239
|
+
let devBundler: Awaited<ReturnType<typeof startDevBundler>> | null = null;
|
|
240
|
+
|
|
217
241
|
const hasIslands = manifest.routes.some(
|
|
218
242
|
(r) => r.kind === "page" && r.clientModule && needsHydration(r)
|
|
219
243
|
);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
244
|
+
|
|
245
|
+
const hmrEnabled = options.noHmr ? false : (devConfig.hmr ?? true);
|
|
246
|
+
if (hasIslands && hmrEnabled) {
|
|
247
|
+
// HMR 서버 시작
|
|
248
|
+
hmrServer = createHMRServer(port);
|
|
249
|
+
|
|
250
|
+
// Dev 번들러 시작 (파일 감시)
|
|
251
|
+
devBundler = await startDevBundler({
|
|
252
|
+
rootDir,
|
|
253
|
+
manifest,
|
|
254
|
+
watchDirs: devConfig.watchDirs,
|
|
255
|
+
onRebuild: (result) => {
|
|
256
|
+
if (result.success) {
|
|
257
|
+
if (result.routeId === "*") {
|
|
258
|
+
hmrServer?.broadcast({
|
|
259
|
+
type: "reload",
|
|
260
|
+
data: {
|
|
261
|
+
timestamp: Date.now(),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
hmrServer?.broadcast({
|
|
266
|
+
type: "island-update",
|
|
267
|
+
data: {
|
|
268
|
+
routeId: result.routeId,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
hmrServer?.broadcast({
|
|
275
|
+
type: "error",
|
|
276
|
+
data: {
|
|
277
|
+
routeId: result.routeId,
|
|
278
|
+
message: result.error,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
onError: (error, routeId) => {
|
|
284
|
+
hmrServer?.broadcast({
|
|
285
|
+
type: "error",
|
|
286
|
+
data: {
|
|
287
|
+
routeId,
|
|
288
|
+
message: error.message,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 메인 서버 시작
|
|
296
|
+
const server = startServer(manifest, {
|
|
297
|
+
port,
|
|
298
|
+
hostname: serverConfig.hostname,
|
|
299
|
+
rootDir,
|
|
300
|
+
isDev: true,
|
|
301
|
+
hmrPort: hmrServer ? port : undefined,
|
|
302
|
+
bundleManifest: devBundler?.initialBuild.manifest,
|
|
303
|
+
cors: serverConfig.cors,
|
|
304
|
+
streaming: serverConfig.streaming,
|
|
305
|
+
});
|
|
306
|
+
|
|
278
307
|
// FS Routes 실시간 감시
|
|
279
308
|
const routesWatcher = await watchFSRoutes(rootDir, {
|
|
280
309
|
skipLegacy: true,
|
|
281
310
|
onChange: async (result) => {
|
|
282
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
283
|
-
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
284
|
-
|
|
285
|
-
// 레지스트리 클리어 (layout 캐시 포함)
|
|
286
|
-
clearDefaultRegistry();
|
|
287
|
-
|
|
288
|
-
// 새 매니페스트로 서버 업데이트
|
|
289
|
-
manifest = result.manifest;
|
|
290
|
-
console.log(` 📋 라우트: ${manifest.routes.length}개`);
|
|
291
|
-
|
|
292
|
-
// 라우트 재등록 (isReload = true)
|
|
293
|
-
await registerHandlers(manifest, true);
|
|
294
|
-
|
|
295
|
-
// HMR 브로드캐스트 (전체 리로드)
|
|
296
|
-
if (hmrServer) {
|
|
297
|
-
hmrServer.broadcast({
|
|
298
|
-
type: "reload",
|
|
299
|
-
data: { timestamp: Date.now() },
|
|
300
|
-
});
|
|
301
|
-
}
|
|
311
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
312
|
+
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
313
|
+
|
|
314
|
+
// 레지스트리 클리어 (layout 캐시 포함)
|
|
315
|
+
clearDefaultRegistry();
|
|
316
|
+
|
|
317
|
+
// 새 매니페스트로 서버 업데이트
|
|
318
|
+
manifest = result.manifest;
|
|
319
|
+
console.log(` 📋 라우트: ${manifest.routes.length}개`);
|
|
320
|
+
|
|
321
|
+
// 라우트 재등록 (isReload = true)
|
|
322
|
+
await registerHandlers(manifest, true);
|
|
323
|
+
|
|
324
|
+
// HMR 브로드캐스트 (전체 리로드)
|
|
325
|
+
if (hmrServer) {
|
|
326
|
+
hmrServer.broadcast({
|
|
327
|
+
type: "reload",
|
|
328
|
+
data: { timestamp: Date.now() },
|
|
329
|
+
});
|
|
330
|
+
}
|
|
302
331
|
},
|
|
303
332
|
});
|
|
304
333
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
calculateLayerStatistics,
|
|
20
20
|
generateGuardMarkdownReport,
|
|
21
21
|
generateHTMLReport,
|
|
22
|
+
validateAndReport,
|
|
22
23
|
type GuardConfig,
|
|
23
24
|
type GuardPreset,
|
|
24
25
|
} from "@mandujs/core";
|
|
@@ -53,21 +54,18 @@ export interface GuardArchOptions {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
|
|
57
|
+
const rootDir = resolveFromCwd(".");
|
|
56
58
|
const {
|
|
57
|
-
preset = "mandu",
|
|
58
59
|
watch = false,
|
|
59
60
|
ci = false,
|
|
60
61
|
format,
|
|
61
62
|
quiet = false,
|
|
62
|
-
srcDir = "src",
|
|
63
63
|
listPresets: showPresets = false,
|
|
64
64
|
output,
|
|
65
65
|
reportFormat = "markdown",
|
|
66
66
|
saveStats = false,
|
|
67
67
|
showTrend = false,
|
|
68
68
|
} = options;
|
|
69
|
-
|
|
70
|
-
const rootDir = resolveFromCwd(".");
|
|
71
69
|
const resolvedFormat = resolveOutputFormat(format);
|
|
72
70
|
const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
|
|
73
71
|
|
|
@@ -90,6 +88,13 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
90
88
|
return true;
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
const fileConfig = await validateAndReport(rootDir);
|
|
92
|
+
if (!fileConfig) return false;
|
|
93
|
+
const guardConfigFromFile = fileConfig.guard ?? {};
|
|
94
|
+
|
|
95
|
+
const preset = options.preset ?? guardConfigFromFile.preset ?? "mandu";
|
|
96
|
+
const srcDir = options.srcDir ?? guardConfigFromFile.srcDir ?? "src";
|
|
97
|
+
|
|
93
98
|
if (resolvedFormat === "console") {
|
|
94
99
|
console.log("");
|
|
95
100
|
console.log("🛡️ Mandu Guard - Architecture Checker");
|
|
@@ -101,11 +106,12 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
// Guard 설정
|
|
104
|
-
const
|
|
109
|
+
const guardConfig: GuardConfig = {
|
|
105
110
|
preset,
|
|
106
111
|
srcDir,
|
|
107
112
|
realtime: watch,
|
|
108
113
|
realtimeOutput: resolvedFormat,
|
|
114
|
+
exclude: guardConfigFromFile.exclude,
|
|
109
115
|
fsRoutes: enableFsRoutes
|
|
110
116
|
? {
|
|
111
117
|
noPageToPage: true,
|
|
@@ -152,7 +158,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
const watcher = createGuardWatcher({
|
|
155
|
-
config,
|
|
161
|
+
config: guardConfig,
|
|
156
162
|
rootDir,
|
|
157
163
|
onViolation: (violation) => {
|
|
158
164
|
// 실시간 위반 출력은 watcher 내부에서 처리됨
|
|
@@ -185,7 +191,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
185
191
|
console.log("🔍 Scanning for architecture violations...\n");
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
const report = await checkDirectory(
|
|
194
|
+
const report = await checkDirectory(guardConfig, rootDir);
|
|
189
195
|
const presetDef = getPreset(preset);
|
|
190
196
|
|
|
191
197
|
// 출력 형식에 따른 리포트 출력
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
3
4
|
|
|
4
5
|
export type CSSFramework = "tailwind" | "panda" | "none";
|
|
5
6
|
export type UILibrary = "shadcn" | "ark" | "none";
|
|
@@ -162,8 +163,8 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
162
163
|
// Check if target directory exists
|
|
163
164
|
try {
|
|
164
165
|
await fs.access(targetDir);
|
|
165
|
-
|
|
166
|
-
return false;
|
|
166
|
+
printCLIError(CLI_ERROR_CODES.INIT_DIR_EXISTS, { path: targetDir });
|
|
167
|
+
return false;
|
|
167
168
|
} catch {
|
|
168
169
|
// Directory doesn't exist, good to proceed
|
|
169
170
|
}
|
|
@@ -175,9 +176,9 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
175
176
|
try {
|
|
176
177
|
await fs.access(templateDir);
|
|
177
178
|
} catch {
|
|
178
|
-
|
|
179
|
-
console.error(` 사용 가능한 템플릿: default`);
|
|
180
|
-
return false;
|
|
179
|
+
printCLIError(CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND, { template });
|
|
180
|
+
console.error(` 사용 가능한 템플릿: default`);
|
|
181
|
+
return false;
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
console.log(`📋 템플릿 복사 중...`);
|
package/src/commands/routes.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
generateManifest,
|
|
10
10
|
formatRoutesForCLI,
|
|
11
11
|
watchFSRoutes,
|
|
12
|
+
validateAndReport,
|
|
12
13
|
type GenerateOptions,
|
|
13
14
|
type FSScannerConfig,
|
|
14
15
|
} from "@mandujs/core";
|
|
@@ -46,11 +47,14 @@ export interface RoutesWatchOptions {
|
|
|
46
47
|
*/
|
|
47
48
|
export async function routesGenerate(options: RoutesGenerateOptions = {}): Promise<boolean> {
|
|
48
49
|
const rootDir = resolveFromCwd(".");
|
|
50
|
+
const config = await validateAndReport(rootDir);
|
|
51
|
+
if (!config) return false;
|
|
49
52
|
|
|
50
53
|
console.log("🥟 Mandu FS Routes Generate\n");
|
|
51
54
|
|
|
52
55
|
try {
|
|
53
56
|
const generateOptions: GenerateOptions = {
|
|
57
|
+
scanner: config.fsRoutes,
|
|
54
58
|
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
55
59
|
skipLegacy: true, // 레거시 병합 비활성화
|
|
56
60
|
};
|
|
@@ -93,11 +97,13 @@ export async function routesGenerate(options: RoutesGenerateOptions = {}): Promi
|
|
|
93
97
|
*/
|
|
94
98
|
export async function routesList(options: RoutesListOptions = {}): Promise<boolean> {
|
|
95
99
|
const rootDir = resolveFromCwd(".");
|
|
100
|
+
const config = await validateAndReport(rootDir);
|
|
101
|
+
if (!config) return false;
|
|
96
102
|
|
|
97
103
|
console.log("🥟 Mandu Routes List\n");
|
|
98
104
|
|
|
99
105
|
try {
|
|
100
|
-
const result = await scanRoutes(rootDir);
|
|
106
|
+
const result = await scanRoutes(rootDir, config.fsRoutes);
|
|
101
107
|
|
|
102
108
|
if (result.errors.length > 0) {
|
|
103
109
|
console.log("⚠️ 스캔 경고:");
|
|
@@ -164,6 +170,8 @@ export async function routesList(options: RoutesListOptions = {}): Promise<boole
|
|
|
164
170
|
*/
|
|
165
171
|
export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boolean> {
|
|
166
172
|
const rootDir = resolveFromCwd(".");
|
|
173
|
+
const config = await validateAndReport(rootDir);
|
|
174
|
+
if (!config) return false;
|
|
167
175
|
|
|
168
176
|
console.log("🥟 Mandu FS Routes Watch\n");
|
|
169
177
|
console.log("👀 라우트 변경 감시 중... (Ctrl+C로 종료)\n");
|
|
@@ -171,6 +179,7 @@ export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boo
|
|
|
171
179
|
try {
|
|
172
180
|
// 초기 스캔
|
|
173
181
|
const initialResult = await generateManifest(rootDir, {
|
|
182
|
+
scanner: config.fsRoutes,
|
|
174
183
|
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
175
184
|
});
|
|
176
185
|
|
|
@@ -178,6 +187,7 @@ export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boo
|
|
|
178
187
|
|
|
179
188
|
// 감시 시작
|
|
180
189
|
const watcher = await watchFSRoutes(rootDir, {
|
|
190
|
+
scanner: config.fsRoutes,
|
|
181
191
|
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
182
192
|
onChange: (result) => {
|
|
183
193
|
const timestamp = new Date().toLocaleTimeString();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI error codes
|
|
3
|
+
*/
|
|
4
|
+
export const CLI_ERROR_CODES = {
|
|
5
|
+
// Init errors (E001-E009)
|
|
6
|
+
INIT_DIR_EXISTS: "CLI_E001",
|
|
7
|
+
INIT_BUN_NOT_FOUND: "CLI_E002",
|
|
8
|
+
INIT_TEMPLATE_NOT_FOUND: "CLI_E003",
|
|
9
|
+
|
|
10
|
+
// Dev errors (E010-E019)
|
|
11
|
+
DEV_PORT_IN_USE: "CLI_E010",
|
|
12
|
+
DEV_MANIFEST_NOT_FOUND: "CLI_E011",
|
|
13
|
+
DEV_NO_ROUTES: "CLI_E012",
|
|
14
|
+
|
|
15
|
+
// Guard errors (E020-E029)
|
|
16
|
+
GUARD_CONFIG_INVALID: "CLI_E020",
|
|
17
|
+
GUARD_PRESET_NOT_FOUND: "CLI_E021",
|
|
18
|
+
GUARD_VIOLATION_FOUND: "CLI_E022",
|
|
19
|
+
|
|
20
|
+
// Build errors (E030-E039)
|
|
21
|
+
BUILD_ENTRY_NOT_FOUND: "CLI_E030",
|
|
22
|
+
BUILD_BUNDLE_FAILED: "CLI_E031",
|
|
23
|
+
BUILD_OUTDIR_NOT_WRITABLE: "CLI_E032",
|
|
24
|
+
|
|
25
|
+
// Config errors (E040-E049)
|
|
26
|
+
CONFIG_PARSE_FAILED: "CLI_E040",
|
|
27
|
+
CONFIG_VALIDATION_FAILED: "CLI_E041",
|
|
28
|
+
|
|
29
|
+
// CLI usage errors (E100+)
|
|
30
|
+
UNKNOWN_COMMAND: "CLI_E100",
|
|
31
|
+
UNKNOWN_SUBCOMMAND: "CLI_E101",
|
|
32
|
+
MISSING_ARGUMENT: "CLI_E102",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export type CLIErrorCode = typeof CLI_ERROR_CODES[keyof typeof CLI_ERROR_CODES];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { CLI_ERROR_CODES, type CLIErrorCode } from "./codes";
|
|
2
|
+
|
|
3
|
+
interface ErrorInfo {
|
|
4
|
+
message: string;
|
|
5
|
+
suggestion?: string;
|
|
6
|
+
docLink?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ERROR_MESSAGES: Record<CLIErrorCode, ErrorInfo> = {
|
|
10
|
+
[CLI_ERROR_CODES.INIT_DIR_EXISTS]: {
|
|
11
|
+
message: "Directory already exists: {path}",
|
|
12
|
+
suggestion: "Choose a different project name or remove the existing directory.",
|
|
13
|
+
},
|
|
14
|
+
[CLI_ERROR_CODES.INIT_BUN_NOT_FOUND]: {
|
|
15
|
+
message: "Bun runtime not found.",
|
|
16
|
+
suggestion: "Install Bun and ensure it is available in your PATH.",
|
|
17
|
+
},
|
|
18
|
+
[CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND]: {
|
|
19
|
+
message: "Template not found: {template}",
|
|
20
|
+
suggestion: "Use a valid template name (default).",
|
|
21
|
+
},
|
|
22
|
+
[CLI_ERROR_CODES.DEV_PORT_IN_USE]: {
|
|
23
|
+
message: "Port {port} is already in use.",
|
|
24
|
+
suggestion: "Use --port to pick a different port or stop the process using this port.",
|
|
25
|
+
},
|
|
26
|
+
[CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND]: {
|
|
27
|
+
message: "Routes manifest not found.",
|
|
28
|
+
suggestion: "Run `mandu routes generate` or create app/ routes before dev.",
|
|
29
|
+
},
|
|
30
|
+
[CLI_ERROR_CODES.DEV_NO_ROUTES]: {
|
|
31
|
+
message: "No routes were found in app/.",
|
|
32
|
+
suggestion: "Create app/page.tsx or app/api/*/route.ts to get started.",
|
|
33
|
+
},
|
|
34
|
+
[CLI_ERROR_CODES.GUARD_CONFIG_INVALID]: {
|
|
35
|
+
message: "Invalid guard configuration.",
|
|
36
|
+
suggestion: "Check your mandu.config and guard settings.",
|
|
37
|
+
},
|
|
38
|
+
[CLI_ERROR_CODES.GUARD_PRESET_NOT_FOUND]: {
|
|
39
|
+
message: "Unknown architecture preset: {preset}",
|
|
40
|
+
suggestion: "Available presets: mandu, fsd, clean, hexagonal, atomic.",
|
|
41
|
+
},
|
|
42
|
+
[CLI_ERROR_CODES.GUARD_VIOLATION_FOUND]: {
|
|
43
|
+
message: "{count} architecture violation(s) found.",
|
|
44
|
+
suggestion: "Fix violations above or run with --format agent for AI-friendly output.",
|
|
45
|
+
},
|
|
46
|
+
[CLI_ERROR_CODES.BUILD_ENTRY_NOT_FOUND]: {
|
|
47
|
+
message: "Build entry not found: {entry}",
|
|
48
|
+
suggestion: "Check your routes manifest or build inputs.",
|
|
49
|
+
},
|
|
50
|
+
[CLI_ERROR_CODES.BUILD_BUNDLE_FAILED]: {
|
|
51
|
+
message: "Bundle build failed for '{target}'.",
|
|
52
|
+
suggestion: "Review build errors above for missing deps or syntax errors.",
|
|
53
|
+
},
|
|
54
|
+
[CLI_ERROR_CODES.BUILD_OUTDIR_NOT_WRITABLE]: {
|
|
55
|
+
message: "Output directory is not writable: {path}",
|
|
56
|
+
suggestion: "Ensure the directory exists and you have write permissions.",
|
|
57
|
+
},
|
|
58
|
+
[CLI_ERROR_CODES.CONFIG_PARSE_FAILED]: {
|
|
59
|
+
message: "Failed to parse mandu.config.",
|
|
60
|
+
suggestion: "Fix syntax errors in the config file.",
|
|
61
|
+
},
|
|
62
|
+
[CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED]: {
|
|
63
|
+
message: "Configuration validation failed.",
|
|
64
|
+
suggestion: "Review validation errors above and fix your config.",
|
|
65
|
+
},
|
|
66
|
+
[CLI_ERROR_CODES.UNKNOWN_COMMAND]: {
|
|
67
|
+
message: "Unknown command: {command}",
|
|
68
|
+
suggestion: "Run with --help to see available commands.",
|
|
69
|
+
},
|
|
70
|
+
[CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND]: {
|
|
71
|
+
message: "Unknown subcommand '{subcommand}' for {command}.",
|
|
72
|
+
suggestion: "Run the command with --help to see available subcommands.",
|
|
73
|
+
},
|
|
74
|
+
[CLI_ERROR_CODES.MISSING_ARGUMENT]: {
|
|
75
|
+
message: "Missing required argument: {argument}",
|
|
76
|
+
suggestion: "Provide the required argument and try again.",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function interpolate(text: string, context?: Record<string, string | number>): string {
|
|
81
|
+
if (!context) return text;
|
|
82
|
+
let result = text;
|
|
83
|
+
for (const [key, value] of Object.entries(context)) {
|
|
84
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), String(value));
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatCLIError(
|
|
90
|
+
code: CLIErrorCode,
|
|
91
|
+
context?: Record<string, string | number>
|
|
92
|
+
): string {
|
|
93
|
+
const info = ERROR_MESSAGES[code];
|
|
94
|
+
const message = interpolate(info?.message ?? "Unknown error", context);
|
|
95
|
+
const suggestion = info?.suggestion ? interpolate(info.suggestion, context) : undefined;
|
|
96
|
+
|
|
97
|
+
const lines = ["", `❌ Error [${code}]`, ` ${message}`];
|
|
98
|
+
if (suggestion) {
|
|
99
|
+
lines.push("", `💡 ${suggestion}`);
|
|
100
|
+
}
|
|
101
|
+
if (info?.docLink) {
|
|
102
|
+
lines.push(`📖 ${info.docLink}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class CLIError extends Error {
|
|
109
|
+
readonly code: CLIErrorCode;
|
|
110
|
+
readonly context?: Record<string, string | number>;
|
|
111
|
+
|
|
112
|
+
constructor(code: CLIErrorCode, context?: Record<string, string | number>) {
|
|
113
|
+
super(formatCLIError(code, context));
|
|
114
|
+
this.code = code;
|
|
115
|
+
this.context = context;
|
|
116
|
+
this.name = "CLIError";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function printCLIError(
|
|
121
|
+
code: CLIErrorCode,
|
|
122
|
+
context?: Record<string, string | number>
|
|
123
|
+
): void {
|
|
124
|
+
console.error(formatCLIError(code, context));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function handleCLIError(error: unknown): never {
|
|
128
|
+
if (error instanceof CLIError) {
|
|
129
|
+
console.error(error.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
console.error(`\n❌ Unexpected error: ${error.message}\n`);
|
|
135
|
+
if (process.env.DEBUG) {
|
|
136
|
+
console.error(error.stack);
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.error("\n❌ Unknown error occurred\n");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { watch } from "./commands/watch";
|
|
|
23
23
|
import { brainSetup, brainStatus } from "./commands/brain";
|
|
24
24
|
import { routesGenerate, routesList, routesWatch } from "./commands/routes";
|
|
25
25
|
import { monitor } from "./commands/monitor";
|
|
26
|
+
import { CLI_ERROR_CODES, handleCLIError, printCLIError } from "./errors";
|
|
26
27
|
|
|
27
28
|
const HELP_TEXT = `
|
|
28
29
|
🥟 Mandu CLI - Agent-Native Fullstack Framework
|
|
@@ -265,7 +266,10 @@ async function main(): Promise<void> {
|
|
|
265
266
|
break;
|
|
266
267
|
default:
|
|
267
268
|
if (hasSubCommand) {
|
|
268
|
-
|
|
269
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
270
|
+
command: "guard",
|
|
271
|
+
subcommand,
|
|
272
|
+
});
|
|
269
273
|
console.log("\nUsage: bunx mandu guard <arch|legacy>");
|
|
270
274
|
process.exit(1);
|
|
271
275
|
}
|
|
@@ -320,7 +324,10 @@ async function main(): Promise<void> {
|
|
|
320
324
|
verbose: options.verbose === "true",
|
|
321
325
|
});
|
|
322
326
|
} else {
|
|
323
|
-
|
|
327
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
328
|
+
command: "routes",
|
|
329
|
+
subcommand,
|
|
330
|
+
});
|
|
324
331
|
console.log("\nUsage: bunx mandu routes <generate|list|watch>");
|
|
325
332
|
process.exit(1);
|
|
326
333
|
}
|
|
@@ -334,7 +341,7 @@ async function main(): Promise<void> {
|
|
|
334
341
|
case "create": {
|
|
335
342
|
const routeId = args[2] || options._positional;
|
|
336
343
|
if (!routeId) {
|
|
337
|
-
|
|
344
|
+
printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, { argument: "routeId" });
|
|
338
345
|
console.log("\nUsage: bunx mandu contract create <routeId>");
|
|
339
346
|
process.exit(1);
|
|
340
347
|
}
|
|
@@ -356,7 +363,10 @@ async function main(): Promise<void> {
|
|
|
356
363
|
});
|
|
357
364
|
break;
|
|
358
365
|
default:
|
|
359
|
-
|
|
366
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
367
|
+
command: "contract",
|
|
368
|
+
subcommand,
|
|
369
|
+
});
|
|
360
370
|
console.log("\nUsage: bunx mandu contract <create|validate|build|diff>");
|
|
361
371
|
process.exit(1);
|
|
362
372
|
}
|
|
@@ -379,7 +389,10 @@ async function main(): Promise<void> {
|
|
|
379
389
|
});
|
|
380
390
|
break;
|
|
381
391
|
default:
|
|
382
|
-
|
|
392
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
393
|
+
command: "openapi",
|
|
394
|
+
subcommand,
|
|
395
|
+
});
|
|
383
396
|
console.log("\nUsage: bunx mandu openapi <generate|serve>");
|
|
384
397
|
process.exit(1);
|
|
385
398
|
}
|
|
@@ -410,7 +423,10 @@ async function main(): Promise<void> {
|
|
|
410
423
|
});
|
|
411
424
|
break;
|
|
412
425
|
default:
|
|
413
|
-
|
|
426
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
427
|
+
command: "change",
|
|
428
|
+
subcommand,
|
|
429
|
+
});
|
|
414
430
|
console.log(`\nUsage: bunx mandu change <begin|commit|rollback|status|list|prune>`);
|
|
415
431
|
process.exit(1);
|
|
416
432
|
}
|
|
@@ -458,7 +474,10 @@ async function main(): Promise<void> {
|
|
|
458
474
|
});
|
|
459
475
|
break;
|
|
460
476
|
default:
|
|
461
|
-
|
|
477
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
478
|
+
command: "brain",
|
|
479
|
+
subcommand,
|
|
480
|
+
});
|
|
462
481
|
console.log("\nUsage: bunx mandu brain <setup|status>");
|
|
463
482
|
process.exit(1);
|
|
464
483
|
}
|
|
@@ -466,7 +485,7 @@ async function main(): Promise<void> {
|
|
|
466
485
|
}
|
|
467
486
|
|
|
468
487
|
default:
|
|
469
|
-
|
|
488
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
|
|
470
489
|
console.log(HELP_TEXT);
|
|
471
490
|
process.exit(1);
|
|
472
491
|
}
|
|
@@ -476,7 +495,4 @@ async function main(): Promise<void> {
|
|
|
476
495
|
}
|
|
477
496
|
}
|
|
478
497
|
|
|
479
|
-
main().catch((error) =>
|
|
480
|
-
console.error("❌ 예상치 못한 오류:", error);
|
|
481
|
-
process.exit(1);
|
|
482
|
-
});
|
|
498
|
+
main().catch((error) => handleCLIError(error));
|
package/src/util/bun.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function importFresh<T = unknown>(modulePath: string): Promise<T> {
|
|
2
|
+
const url = Bun.pathToFileURL(modulePath);
|
|
3
|
+
const cacheBusted = new URL(url.href);
|
|
4
|
+
cacheBusted.searchParams.set("t", Date.now().toString());
|
|
5
|
+
return import(cacheBusted.href) as Promise<T>;
|
|
6
|
+
}
|