@mandujs/cli 0.9.24 β 0.9.43
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 +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/commands/build.ts +50 -22
- package/src/commands/check.ts +16 -18
- package/src/commands/contract.ts +50 -42
- package/src/commands/dev.ts +294 -212
- package/src/commands/doctor.ts +27 -25
- package/src/commands/guard-arch.ts +25 -10
- package/src/commands/init.ts +8 -7
- package/src/commands/monitor.ts +2 -3
- package/src/commands/openapi.ts +107 -48
- 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 +103 -157
- package/src/util/bun.ts +6 -0
- package/src/util/manifest.ts +52 -0
- package/src/util/port.ts +71 -0
- package/templates/default/AGENTS.md +96 -0
- package/templates/default/app/globals.css +45 -33
- package/templates/default/package.json +15 -12
- package/templates/default/postcss.config.js +0 -6
- package/templates/default/tailwind.config.ts +0 -64
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
|
|
@@ -30,30 +31,25 @@ const HELP_TEXT = `
|
|
|
30
31
|
Usage: bunx mandu <command> [options]
|
|
31
32
|
|
|
32
33
|
Commands:
|
|
33
|
-
init
|
|
34
|
-
check
|
|
35
|
-
routes generate
|
|
36
|
-
routes list
|
|
37
|
-
routes watch
|
|
38
|
-
dev
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
guard
|
|
42
|
-
guard
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
watch μ€μκ° νμΌ κ°μ - κ²½κ³ λ§ (Brain)
|
|
53
|
-
monitor MCP Activity Monitor λ‘κ·Έ μ€νΈλ¦Ό
|
|
54
|
-
|
|
55
|
-
brain setup sLLM μ€μ (μ ν)
|
|
56
|
-
brain status Brain μν νμΈ
|
|
34
|
+
init μ νλ‘μ νΈ μμ± (Tailwind + shadcn/ui κΈ°λ³Έ ν¬ν¨)
|
|
35
|
+
check FS Routes + Guard ν΅ν© κ²μ¬
|
|
36
|
+
routes generate FS Routes μ€μΊ λ° λ§€λνμ€νΈ μμ±
|
|
37
|
+
routes list νμ¬ λΌμ°νΈ λͺ©λ‘ μΆλ ₯
|
|
38
|
+
routes watch μ€μκ° λΌμ°νΈ κ°μ
|
|
39
|
+
dev κ°λ° μλ² μ€ν (FS Routes + Guard κΈ°λ³Έ)
|
|
40
|
+
build ν΄λΌμ΄μΈνΈ λ²λ€ λΉλ (Hydration)
|
|
41
|
+
guard μν€ν
μ² μλ° κ²μ¬ (κΈ°λ³Έ)
|
|
42
|
+
guard arch μν€ν
μ² μλ° κ²μ¬ (FSD/Clean/Hexagonal)
|
|
43
|
+
guard legacy λ κ±°μ Spec Guard κ²μ¬
|
|
44
|
+
spec-upsert Spec νμΌ κ²μ¦ λ° lock κ°±μ (λ κ±°μ)
|
|
45
|
+
generate Specμμ μ½λ μμ± (λ κ±°μ)
|
|
46
|
+
|
|
47
|
+
doctor Guard μ€ν¨ λΆμ + ν¨μΉ μ μ (Brain)
|
|
48
|
+
watch μ€μκ° νμΌ κ°μ - κ²½κ³ λ§ (Brain)
|
|
49
|
+
monitor MCP Activity Monitor λ‘κ·Έ μ€νΈλ¦Ό
|
|
50
|
+
|
|
51
|
+
brain setup sLLM μ€μ (μ ν)
|
|
52
|
+
brain status Brain μν νμΈ
|
|
57
53
|
|
|
58
54
|
contract create <routeId> λΌμ°νΈμ λν Contract μμ±
|
|
59
55
|
contract validate Contract-Slot μΌκ΄μ± κ²μ¦
|
|
@@ -63,79 +59,68 @@ Commands:
|
|
|
63
59
|
openapi generate OpenAPI 3.0 μ€ν μμ±
|
|
64
60
|
openapi serve Swagger UI λ‘컬 μλ² μ€ν
|
|
65
61
|
|
|
66
|
-
change begin
|
|
67
|
-
change commit
|
|
68
|
-
change rollback
|
|
69
|
-
change status
|
|
70
|
-
change list
|
|
71
|
-
change prune
|
|
62
|
+
change begin λ³κ²½ νΈλμμ
μμ (μ€λ
μ· μμ±)
|
|
63
|
+
change commit λ³κ²½ νμ
|
|
64
|
+
change rollback μ€λ
μ·μΌλ‘ 볡μ
|
|
65
|
+
change status νμ¬ νΈλμμ
μν
|
|
66
|
+
change list λ³κ²½ μ΄λ ₯ μ‘°ν
|
|
67
|
+
change prune μ€λλ μ€λ
μ· μ 리
|
|
72
68
|
|
|
73
69
|
Options:
|
|
74
|
-
--name <name>
|
|
75
|
-
--css <framework>
|
|
76
|
-
--ui <library>
|
|
77
|
-
--theme
|
|
78
|
-
--minimal
|
|
79
|
-
--file <path>
|
|
80
|
-
--
|
|
81
|
-
--guard
|
|
82
|
-
--
|
|
83
|
-
--
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--watch
|
|
96
|
-
--
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
--format <fmt> doctor μΆλ ₯ νμ: console, json, markdown (κΈ°λ³Έ: console)
|
|
109
|
-
--no-llm doctorμμ LLM μ¬μ© μ ν¨ (ν
νλ¦Ώ λͺ¨λ)
|
|
110
|
-
--model <name> brain setup μ λͺ¨λΈ μ΄λ¦ (κΈ°λ³Έ: llama3.2)
|
|
111
|
-
--url <url> brain setup μ Ollama URL
|
|
112
|
-
--verbose μμΈ μΆλ ₯
|
|
113
|
-
--help, -h λμλ§ νμ
|
|
70
|
+
--name <name> init μ νλ‘μ νΈ μ΄λ¦ (κΈ°λ³Έ: my-mandu-app)
|
|
71
|
+
--css <framework> init μ CSS νλ μμν¬: tailwind, panda, none (κΈ°λ³Έ: tailwind)
|
|
72
|
+
--ui <library> init μ UI λΌμ΄λΈλ¬λ¦¬: shadcn, ark, none (κΈ°λ³Έ: shadcn)
|
|
73
|
+
--theme init μ λ€ν¬λͺ¨λ ν
λ§ μμ€ν
μΆκ°
|
|
74
|
+
--minimal init μ CSS/UI μμ΄ μ΅μ ν
νλ¦Ώ μμ± (--css none --ui none)
|
|
75
|
+
--file <path> spec-upsert spec νμΌ/monitor λ‘κ·Έ νμΌ κ²½λ‘
|
|
76
|
+
--watch build/guard arch νμΌ κ°μ λͺ¨λ
|
|
77
|
+
--output <path> routes/openapi/doctor/contract/guard μΆλ ₯ κ²½λ‘
|
|
78
|
+
--verbose routes list/watch, contract validate, brain status μμΈ μΆλ ₯
|
|
79
|
+
--from <path> contract diff κΈ°μ€ λ μ§μ€νΈλ¦¬ κ²½λ‘
|
|
80
|
+
--to <path> contract diff λμ λ μ§μ€νΈλ¦¬ κ²½λ‘
|
|
81
|
+
--json contract diff κ²°κ³Ό JSON μΆλ ₯
|
|
82
|
+
--title <title> openapi generate title
|
|
83
|
+
--version <ver> openapi generate version
|
|
84
|
+
--summary monitor μμ½ μΆλ ₯ (JSON λ‘κ·Έμμλ§)
|
|
85
|
+
--since <duration> monitor μμ½ κΈ°κ° (μ: 5m, 30s, 1h)
|
|
86
|
+
--follow <bool> monitor follow λͺ¨λ (κΈ°λ³Έ: true)
|
|
87
|
+
--message <msg> change begin μ μ€λͺ
λ©μμ§
|
|
88
|
+
--id <id> change rollback μ νΉμ λ³κ²½ ID
|
|
89
|
+
--keep <n> change prune μ μ μ§ν μ€λ
μ· μ (κΈ°λ³Έ: 5)
|
|
90
|
+
--no-llm doctorμμ LLM μ¬μ© μ ν¨ (ν
νλ¦Ώ λͺ¨λ)
|
|
91
|
+
--status watch μνλ§ μΆλ ₯
|
|
92
|
+
--debounce <ms> watch debounce (ms)
|
|
93
|
+
--model <name> brain setup μ λͺ¨λΈ μ΄λ¦ (κΈ°λ³Έ: llama3.2)
|
|
94
|
+
--url <url> brain setup μ Ollama URL
|
|
95
|
+
--skip-check brain setup μ λͺ¨λΈ/μλ² μ²΄ν¬ κ±΄λλ
|
|
96
|
+
--help, -h λμλ§ νμ
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
- μΆλ ₯ ν¬λ§·μ νκ²½μ λ°λΌ μλ κ²°μ λ©λλ€ (TTY/CI/MANDU_OUTPUT).
|
|
100
|
+
- doctor μΆλ ₯μ .jsonμ΄λ©΄ JSON, κ·Έ μΈλ markdownμΌλ‘ μ μ₯λ©λλ€.
|
|
101
|
+
- guard arch 리ν¬νΈλ .json/.html/.md νμ₯μλ₯Ό μλ μΆλ‘ ν©λλ€.
|
|
102
|
+
- ν¬νΈλ PORT νκ²½λ³μ λλ mandu.configμ server.portλ‘ μ€μ ν©λλ€.
|
|
103
|
+
- ν¬νΈ μΆ©λ μ λ€μ μ¬μ© κ°λ₯ν ν¬νΈλ‘ μλ λ³κ²½λ©λλ€.
|
|
114
104
|
|
|
115
105
|
Examples:
|
|
116
106
|
bunx mandu init --name my-app # Tailwind + shadcn/ui κΈ°λ³Έ
|
|
117
107
|
bunx mandu init my-app --minimal # CSS/UI μμ΄ μ΅μ ν
νλ¦Ώ
|
|
118
|
-
bunx mandu
|
|
119
|
-
bunx mandu
|
|
120
|
-
bunx mandu check
|
|
121
|
-
bunx mandu routes list
|
|
122
|
-
bunx mandu routes generate
|
|
123
|
-
bunx mandu dev --port 3000
|
|
124
|
-
bunx mandu dev --no-guard
|
|
125
|
-
bunx mandu build --minify
|
|
108
|
+
bunx mandu dev
|
|
109
|
+
bunx mandu build --watch
|
|
126
110
|
bunx mandu guard
|
|
127
|
-
bunx mandu guard arch --preset fsd
|
|
128
111
|
bunx mandu guard arch --watch
|
|
129
|
-
bunx mandu guard arch --
|
|
130
|
-
bunx mandu
|
|
131
|
-
bunx mandu
|
|
112
|
+
bunx mandu guard arch --output guard-report.md
|
|
113
|
+
bunx mandu check
|
|
114
|
+
bunx mandu routes list --verbose
|
|
115
|
+
bunx mandu contract create users
|
|
116
|
+
bunx mandu contract validate --verbose
|
|
117
|
+
bunx mandu contract build --output .mandu/contracts.json
|
|
118
|
+
bunx mandu contract diff --json
|
|
119
|
+
bunx mandu openapi generate --output docs/openapi.json
|
|
120
|
+
bunx mandu openapi serve
|
|
132
121
|
bunx mandu monitor --summary --since 5m
|
|
133
|
-
bunx mandu doctor
|
|
122
|
+
bunx mandu doctor --output reports/doctor.json
|
|
134
123
|
bunx mandu brain setup --model codellama
|
|
135
|
-
bunx mandu contract create users
|
|
136
|
-
bunx mandu contract build
|
|
137
|
-
bunx mandu contract diff
|
|
138
|
-
bunx mandu openapi generate --output docs/api.json
|
|
139
124
|
bunx mandu change begin --message "Add new route"
|
|
140
125
|
|
|
141
126
|
FS Routes Workflow (κΆμ₯):
|
|
@@ -172,31 +157,6 @@ function parseArgs(args: string[]): { command: string; options: Record<string, s
|
|
|
172
157
|
return { command, options };
|
|
173
158
|
}
|
|
174
159
|
|
|
175
|
-
/**
|
|
176
|
-
* ν¬νΈ μ΅μ
μμ νκ² νμ±
|
|
177
|
-
* - μ«μκ° μλλ©΄ undefined λ°ν (κΈ°λ³Έκ° μ¬μ©)
|
|
178
|
-
* - μ ν¨ λ²μ: 1-65535
|
|
179
|
-
*/
|
|
180
|
-
function parsePort(value: string | undefined, optionName = "port"): number | undefined {
|
|
181
|
-
if (!value || value === "true") {
|
|
182
|
-
return undefined; // κΈ°λ³Έκ° μ¬μ©
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const port = Number(value);
|
|
186
|
-
|
|
187
|
-
if (Number.isNaN(port)) {
|
|
188
|
-
console.warn(`β οΈ Invalid --${optionName} value: "${value}" (using default)`);
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
193
|
-
console.warn(`β οΈ Invalid --${optionName} range: ${port} (must be 1-65535, using default)`);
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return port;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
160
|
async function main(): Promise<void> {
|
|
201
161
|
const args = process.argv.slice(2);
|
|
202
162
|
const { command, options } = parseArgs(args);
|
|
@@ -228,30 +188,15 @@ async function main(): Promise<void> {
|
|
|
228
188
|
break;
|
|
229
189
|
|
|
230
190
|
case "check":
|
|
231
|
-
success = await check(
|
|
232
|
-
preset: options.preset as any,
|
|
233
|
-
format: options.format as any,
|
|
234
|
-
ci: options.ci === "true",
|
|
235
|
-
quiet: options.quiet === "true",
|
|
236
|
-
legacy: options.legacy === "true",
|
|
237
|
-
});
|
|
191
|
+
success = await check();
|
|
238
192
|
break;
|
|
239
193
|
|
|
240
194
|
case "guard": {
|
|
241
195
|
const subCommand = args[1];
|
|
242
196
|
const hasSubCommand = subCommand && !subCommand.startsWith("--");
|
|
243
197
|
const guardArchOptions = {
|
|
244
|
-
preset: options.preset as any,
|
|
245
198
|
watch: options.watch === "true",
|
|
246
|
-
ci: options.ci === "true",
|
|
247
|
-
format: options.format as any,
|
|
248
|
-
quiet: options.quiet === "true",
|
|
249
|
-
srcDir: options["src-dir"],
|
|
250
|
-
listPresets: options["list-presets"] === "true",
|
|
251
199
|
output: options.output,
|
|
252
|
-
reportFormat: (options["report-format"] as any) || "markdown",
|
|
253
|
-
saveStats: options["save-stats"] === "true",
|
|
254
|
-
showTrend: options["show-trend"] === "true",
|
|
255
200
|
};
|
|
256
201
|
switch (subCommand) {
|
|
257
202
|
case "arch":
|
|
@@ -259,13 +204,14 @@ async function main(): Promise<void> {
|
|
|
259
204
|
break;
|
|
260
205
|
case "legacy":
|
|
261
206
|
case "spec":
|
|
262
|
-
success = await guardCheck(
|
|
263
|
-
autoCorrect: options["no-auto-correct"] !== "true",
|
|
264
|
-
});
|
|
207
|
+
success = await guardCheck();
|
|
265
208
|
break;
|
|
266
209
|
default:
|
|
267
210
|
if (hasSubCommand) {
|
|
268
|
-
|
|
211
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
212
|
+
command: "guard",
|
|
213
|
+
subcommand,
|
|
214
|
+
});
|
|
269
215
|
console.log("\nUsage: bunx mandu guard <arch|legacy>");
|
|
270
216
|
process.exit(1);
|
|
271
217
|
}
|
|
@@ -277,20 +223,12 @@ async function main(): Promise<void> {
|
|
|
277
223
|
|
|
278
224
|
case "build":
|
|
279
225
|
success = await build({
|
|
280
|
-
minify: options.minify === "true",
|
|
281
|
-
sourcemap: options.sourcemap === "true",
|
|
282
226
|
watch: options.watch === "true",
|
|
283
227
|
});
|
|
284
228
|
break;
|
|
285
229
|
|
|
286
230
|
case "dev":
|
|
287
|
-
await dev(
|
|
288
|
-
port: parsePort(options.port),
|
|
289
|
-
guard: options["no-guard"] === "true" ? false : options.guard !== "false",
|
|
290
|
-
guardPreset: options["guard-preset"] as any,
|
|
291
|
-
guardFormat: options["guard-format"] as any,
|
|
292
|
-
legacy: options.legacy === "true",
|
|
293
|
-
});
|
|
231
|
+
await dev();
|
|
294
232
|
break;
|
|
295
233
|
|
|
296
234
|
case "routes": {
|
|
@@ -320,7 +258,10 @@ async function main(): Promise<void> {
|
|
|
320
258
|
verbose: options.verbose === "true",
|
|
321
259
|
});
|
|
322
260
|
} else {
|
|
323
|
-
|
|
261
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
262
|
+
command: "routes",
|
|
263
|
+
subcommand,
|
|
264
|
+
});
|
|
324
265
|
console.log("\nUsage: bunx mandu routes <generate|list|watch>");
|
|
325
266
|
process.exit(1);
|
|
326
267
|
}
|
|
@@ -334,7 +275,7 @@ async function main(): Promise<void> {
|
|
|
334
275
|
case "create": {
|
|
335
276
|
const routeId = args[2] || options._positional;
|
|
336
277
|
if (!routeId) {
|
|
337
|
-
|
|
278
|
+
printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, { argument: "routeId" });
|
|
338
279
|
console.log("\nUsage: bunx mandu contract create <routeId>");
|
|
339
280
|
process.exit(1);
|
|
340
281
|
}
|
|
@@ -356,7 +297,10 @@ async function main(): Promise<void> {
|
|
|
356
297
|
});
|
|
357
298
|
break;
|
|
358
299
|
default:
|
|
359
|
-
|
|
300
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
301
|
+
command: "contract",
|
|
302
|
+
subcommand,
|
|
303
|
+
});
|
|
360
304
|
console.log("\nUsage: bunx mandu contract <create|validate|build|diff>");
|
|
361
305
|
process.exit(1);
|
|
362
306
|
}
|
|
@@ -374,12 +318,13 @@ async function main(): Promise<void> {
|
|
|
374
318
|
});
|
|
375
319
|
break;
|
|
376
320
|
case "serve":
|
|
377
|
-
success = await openAPIServe(
|
|
378
|
-
port: parsePort(options.port),
|
|
379
|
-
});
|
|
321
|
+
success = await openAPIServe();
|
|
380
322
|
break;
|
|
381
323
|
default:
|
|
382
|
-
|
|
324
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
325
|
+
command: "openapi",
|
|
326
|
+
subcommand,
|
|
327
|
+
});
|
|
383
328
|
console.log("\nUsage: bunx mandu openapi <generate|serve>");
|
|
384
329
|
process.exit(1);
|
|
385
330
|
}
|
|
@@ -410,7 +355,10 @@ async function main(): Promise<void> {
|
|
|
410
355
|
});
|
|
411
356
|
break;
|
|
412
357
|
default:
|
|
413
|
-
|
|
358
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
359
|
+
command: "change",
|
|
360
|
+
subcommand,
|
|
361
|
+
});
|
|
414
362
|
console.log(`\nUsage: bunx mandu change <begin|commit|rollback|status|list|prune>`);
|
|
415
363
|
process.exit(1);
|
|
416
364
|
}
|
|
@@ -419,7 +367,6 @@ async function main(): Promise<void> {
|
|
|
419
367
|
|
|
420
368
|
case "doctor":
|
|
421
369
|
success = await doctor({
|
|
422
|
-
format: (options.format as "console" | "json" | "markdown") || "console",
|
|
423
370
|
useLLM: options["no-llm"] !== "true",
|
|
424
371
|
output: options.output,
|
|
425
372
|
});
|
|
@@ -434,7 +381,6 @@ async function main(): Promise<void> {
|
|
|
434
381
|
|
|
435
382
|
case "monitor":
|
|
436
383
|
success = await monitor({
|
|
437
|
-
format: options.format as any,
|
|
438
384
|
summary: options.summary === "true",
|
|
439
385
|
since: options.since,
|
|
440
386
|
follow: options.follow === "false" ? false : true,
|
|
@@ -458,7 +404,10 @@ async function main(): Promise<void> {
|
|
|
458
404
|
});
|
|
459
405
|
break;
|
|
460
406
|
default:
|
|
461
|
-
|
|
407
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
408
|
+
command: "brain",
|
|
409
|
+
subcommand,
|
|
410
|
+
});
|
|
462
411
|
console.log("\nUsage: bunx mandu brain <setup|status>");
|
|
463
412
|
process.exit(1);
|
|
464
413
|
}
|
|
@@ -466,7 +415,7 @@ async function main(): Promise<void> {
|
|
|
466
415
|
}
|
|
467
416
|
|
|
468
417
|
default:
|
|
469
|
-
|
|
418
|
+
printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
|
|
470
419
|
console.log(HELP_TEXT);
|
|
471
420
|
process.exit(1);
|
|
472
421
|
}
|
|
@@ -476,7 +425,4 @@ async function main(): Promise<void> {
|
|
|
476
425
|
}
|
|
477
426
|
}
|
|
478
427
|
|
|
479
|
-
main().catch((error) =>
|
|
480
|
-
console.error("β μμμΉ λͺ»ν μ€λ₯:", error);
|
|
481
|
-
process.exit(1);
|
|
482
|
-
});
|
|
428
|
+
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import {
|
|
3
|
+
generateManifest,
|
|
4
|
+
loadManifest,
|
|
5
|
+
type RoutesManifest,
|
|
6
|
+
type FSScannerConfig,
|
|
7
|
+
} from "@mandujs/core";
|
|
8
|
+
import { isDirectory } from "./fs";
|
|
9
|
+
|
|
10
|
+
export type ManifestSource = "fs" | "spec";
|
|
11
|
+
|
|
12
|
+
export interface ResolvedManifest {
|
|
13
|
+
manifest: RoutesManifest;
|
|
14
|
+
source: ManifestSource;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveManifest(
|
|
19
|
+
rootDir: string,
|
|
20
|
+
options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
|
|
21
|
+
): Promise<ResolvedManifest> {
|
|
22
|
+
const appDir = path.resolve(rootDir, "app");
|
|
23
|
+
const hasApp = await isDirectory(appDir);
|
|
24
|
+
|
|
25
|
+
if (hasApp) {
|
|
26
|
+
const result = await generateManifest(rootDir, {
|
|
27
|
+
scanner: options.fsRoutes,
|
|
28
|
+
outputPath: options.outputPath,
|
|
29
|
+
skipLegacy: true,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
manifest: result.manifest,
|
|
33
|
+
source: "fs",
|
|
34
|
+
warnings: result.warnings,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const specPath = path.join(rootDir, "spec", "routes.manifest.json");
|
|
39
|
+
if (await Bun.file(specPath).exists()) {
|
|
40
|
+
const result = await loadManifest(specPath);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
manifest: result.data!,
|
|
46
|
+
source: "spec",
|
|
47
|
+
warnings: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
|
|
52
|
+
}
|
package/src/util/port.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createServer } from "net";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
4
|
+
|
|
5
|
+
function isPortUsable(error: unknown): boolean {
|
|
6
|
+
if (!error || typeof error !== "object") return false;
|
|
7
|
+
const code = (error as { code?: string }).code;
|
|
8
|
+
return code === "EADDRINUSE" || code === "EACCES";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function isPortAvailable(port: number, hostname?: string): Promise<boolean> {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const server = createServer();
|
|
14
|
+
|
|
15
|
+
server.once("error", (error) => {
|
|
16
|
+
if (isPortUsable(error)) {
|
|
17
|
+
resolve(false);
|
|
18
|
+
} else {
|
|
19
|
+
resolve(false);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
server.once("listening", () => {
|
|
24
|
+
server.close(() => resolve(true));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
server.listen(port, hostname);
|
|
29
|
+
server.unref();
|
|
30
|
+
} catch {
|
|
31
|
+
resolve(false);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function resolveAvailablePort(
|
|
37
|
+
startPort: number,
|
|
38
|
+
options: {
|
|
39
|
+
hostname?: string;
|
|
40
|
+
offsets?: number[];
|
|
41
|
+
maxAttempts?: number;
|
|
42
|
+
} = {}
|
|
43
|
+
): Promise<{ port: number; attempts: number }> {
|
|
44
|
+
const offsets = options.offsets && options.offsets.length > 0 ? options.offsets : [0];
|
|
45
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
46
|
+
|
|
47
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
48
|
+
const candidate = startPort + attempt;
|
|
49
|
+
if (candidate < 1 || candidate > 65535) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const targets = offsets
|
|
54
|
+
.map((offset) => candidate + offset)
|
|
55
|
+
.filter((port) => port >= 1 && port <= 65535);
|
|
56
|
+
|
|
57
|
+
if (targets.length !== offsets.length) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = await Promise.all(
|
|
62
|
+
targets.map((port) => isPortAvailable(port, options.hostname))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (results.every(Boolean)) {
|
|
66
|
+
return { port: candidate, attempts: attempt };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`No available port found starting at ${startPort}`);
|
|
71
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# AI Agent Instructions for Mandu Project
|
|
2
|
+
|
|
3
|
+
μ΄ νλ‘μ νΈλ **Mandu Framework**λ‘ κ΅¬μΆλμμ΅λλ€. AI μμ΄μ νΈκ° μ΄ νλ‘μ νΈλ₯Ό λ€λ£° λ μλ μ§μΉ¨μ λ°λΌμ£ΌμΈμ.
|
|
4
|
+
|
|
5
|
+
## ν¨ν€μ§ λ§€λμ : Bun (νμ)
|
|
6
|
+
|
|
7
|
+
**β οΈ μ€μ: μ΄ νλ‘μ νΈλ Bunλ§ μ¬μ©ν©λλ€. npm/yarn/pnpmμ μ¬μ©νμ§ λ§μΈμ.**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# β
μ¬λ°λ₯Έ λͺ
λ Ήμ΄
|
|
11
|
+
bun install # μμ‘΄μ± μ€μΉ
|
|
12
|
+
bun add <package> # ν¨ν€μ§ μΆκ°
|
|
13
|
+
bun remove <package> # ν¨ν€μ§ μ κ±°
|
|
14
|
+
bun run dev # κ°λ° μλ² μμ
|
|
15
|
+
bun run build # νλ‘λμ
λΉλ
|
|
16
|
+
bun test # ν
μ€νΈ μ€ν
|
|
17
|
+
|
|
18
|
+
# β μ¬μ© κΈμ§
|
|
19
|
+
npm install / yarn install / pnpm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## νλ‘μ νΈ κ΅¬μ‘°
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
βββ app/ # FS κΈ°λ° λΌμ°ν
(νμ΄μ§, API)
|
|
26
|
+
β βββ page.tsx # / λΌμ°νΈ
|
|
27
|
+
β βββ layout.tsx # λ£¨νΈ λ μ΄μμ
|
|
28
|
+
β βββ globals.css # Tailwind CSS (v4)
|
|
29
|
+
β βββ api/ # API λΌμ°νΈ
|
|
30
|
+
βββ src/
|
|
31
|
+
β βββ client/ # ν΄λΌμ΄μΈνΈ μ½λ (FSD ꡬ쑰)
|
|
32
|
+
β β βββ shared/ # κ³΅μ© UI, μ νΈλ¦¬ν°
|
|
33
|
+
β β βββ entities/ # μν°ν° μ»΄ν¬λνΈ
|
|
34
|
+
β β βββ features/ # κΈ°λ₯ μ»΄ν¬λνΈ
|
|
35
|
+
β β βββ widgets/ # μμ ―/Island μ»΄ν¬λνΈ
|
|
36
|
+
β βββ server/ # μλ² μ½λ (Clean Architecture)
|
|
37
|
+
β β βββ domain/ # λλ©μΈ λͺ¨λΈ
|
|
38
|
+
β β βββ application/ # λΉμ¦λμ€ λ‘μ§
|
|
39
|
+
β β βββ infra/ # μΈνλΌ/DB
|
|
40
|
+
β βββ shared/ # ν΄λΌμ΄μΈνΈ-μλ² κ³΅μ μ½λ
|
|
41
|
+
β βββ contracts/ # API κ³μ½ νμ
|
|
42
|
+
β βββ types/ # κ³΅μ© νμ
|
|
43
|
+
βββ mandu.config.ts # Mandu μ€μ (μ ν)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## μ£Όμ κ·μΉ
|
|
47
|
+
|
|
48
|
+
### 1. Island μ»΄ν¬λνΈ
|
|
49
|
+
ν΄λΌμ΄μΈνΈ μνΈμμ©μ΄ νμν μ»΄ν¬λνΈλ `*.island.tsx`λ‘ λͺ
λͺ
:
|
|
50
|
+
```tsx
|
|
51
|
+
// src/client/widgets/counter/Counter.island.tsx
|
|
52
|
+
"use client";
|
|
53
|
+
export function CounterIsland() { ... }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. API λΌμ°νΈ
|
|
57
|
+
`app/api/` ν΄λμ `route.ts` νμΌλ‘ μ μ:
|
|
58
|
+
```typescript
|
|
59
|
+
// app/api/users/route.ts
|
|
60
|
+
import { Mandu } from "@mandujs/core";
|
|
61
|
+
export default Mandu.filling()
|
|
62
|
+
.get((ctx) => ctx.ok({ users: [] }))
|
|
63
|
+
.post(async (ctx) => { ... });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Tailwind CSS v4
|
|
67
|
+
CSS-first μ€μ μ¬μ© (`tailwind.config.ts` μμ):
|
|
68
|
+
```css
|
|
69
|
+
/* app/globals.css */
|
|
70
|
+
@import "tailwindcss";
|
|
71
|
+
@theme {
|
|
72
|
+
--color-primary: hsl(222.2 47.4% 11.2%);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Import Alias
|
|
77
|
+
`@/` = `src/` κ²½λ‘:
|
|
78
|
+
```typescript
|
|
79
|
+
import { Button } from "@/client/shared/ui/button";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## μ€ν λ°©λ²
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bun install # μ΅μ΄ μ€μΉ
|
|
86
|
+
bun run dev # κ°λ° μλ² (http://localhost:4000)
|
|
87
|
+
bun run build # νλ‘λμ
λΉλ
|
|
88
|
+
bun run guard # μν€ν
μ² κ²μ¦
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## κΈ°μ μ€ν
|
|
92
|
+
|
|
93
|
+
- **Runtime**: Bun 1.x
|
|
94
|
+
- **Framework**: Mandu (React 19 + Bun native)
|
|
95
|
+
- **Styling**: Tailwind CSS v4
|
|
96
|
+
- **Language**: TypeScript 5.x
|