@mandujs/cli 0.15.3 → 0.16.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -32,7 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "^0.13.2",
35
+ "@mandujs/core": "^0.16.0",
36
+ "@mandujs/ate": "0.16.0",
36
37
  "cfonts": "^3.3.0"
37
38
  },
38
39
  "engines": {
@@ -0,0 +1,64 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { theme } from "../terminal";
5
+
6
+ function run(cmd: string, args: string[], cwd: string): void {
7
+ const res = spawnSync(cmd, args, { cwd, stdio: "inherit" });
8
+ if (res.error) throw res.error;
9
+ if (res.status !== 0) throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
10
+ }
11
+
12
+ function runTry(cmd: string, args: string[], cwd: string): boolean {
13
+ try {
14
+ run(cmd, args, cwd);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ export async function addTest({ cwd = process.cwd() }: { cwd?: string } = {}): Promise<boolean> {
22
+ const pkgPath = join(cwd, "package.json");
23
+ if (!existsSync(pkgPath)) {
24
+ console.error(theme.error("package.json not found. Run inside a Mandu project."));
25
+ return false;
26
+ }
27
+
28
+ console.log(theme.heading("🥟 Mandu ATE: installing test automation deps..."));
29
+
30
+ // Install in project (no external SaaS deps)
31
+ // NOTE: @mandujs/ate might not be published yet. In that case, fallback to local file: install.
32
+ const installed = runTry("bun", ["add", "-d", "@mandujs/ate", "@playwright/test", "playwright"], cwd);
33
+ if (!installed) {
34
+ const localAtePath = join(cwd, "..", "mandu", "packages", "ate");
35
+ const localPkg = join(localAtePath, "package.json");
36
+ if (existsSync(localPkg)) {
37
+ console.warn(theme.warn(`@mandujs/ate not found on npm. Falling back to local install: file:${localAtePath}`));
38
+ run("bun", ["add", "-d", `file:${localAtePath}`, "@playwright/test", "playwright"], cwd);
39
+ } else {
40
+ console.error(theme.error("@mandujs/ate not found on npm, and local fallback path not found."));
41
+ console.error(theme.muted(`Expected: ${localPkg}`));
42
+ console.error(theme.muted("Tip: install manually with: bun add -d file:/abs/path/to/mandu/packages/ate"));
43
+ return false;
44
+ }
45
+ }
46
+
47
+ const browserInstalled = runTry("bunx", ["playwright", "install", "chromium"], cwd);
48
+ if (!browserInstalled) {
49
+ console.error(theme.error("Failed to install Playwright browsers. Try manually: bunx playwright install chromium"));
50
+ return false;
51
+ }
52
+
53
+ // Create directories and baseline config
54
+ const dirs = [
55
+ join(cwd, "tests", "e2e", "auto"),
56
+ join(cwd, "tests", "e2e", "manual"),
57
+ join(cwd, ".mandu", "scenarios"),
58
+ join(cwd, ".mandu", "reports"),
59
+ ];
60
+ for (const d of dirs) mkdirSync(d, { recursive: true });
61
+
62
+ console.log(theme.success("✅ ATE installed. Next: bunx mandu test:auto"));
63
+ return true;
64
+ }
@@ -348,6 +348,45 @@ registerCommand({
348
348
  },
349
349
  });
350
350
 
351
+ // ============================================================================
352
+ // ATE (Automation Test Engine)
353
+ // ============================================================================
354
+
355
+ registerCommand({
356
+ id: "add",
357
+ description: "프로젝트에 기능 추가",
358
+ subcommands: ["test"],
359
+ async run(ctx) {
360
+ const sub = ctx.args[1];
361
+ if (sub !== "test") return false;
362
+ const { addTest } = await import("./add");
363
+ return addTest({ cwd: process.cwd() });
364
+ },
365
+ });
366
+
367
+ registerCommand({
368
+ id: "test:auto",
369
+ description: "ATE 자동 E2E 생성/실행",
370
+ async run(ctx) {
371
+ const { testAuto } = await import("./test-auto");
372
+ return testAuto({
373
+ ci: ctx.options.ci === "true",
374
+ impact: ctx.options.impact === "true",
375
+ baseURL: ctx.options["base-url"] || ctx.options.baseURL || ctx.options.baseUrl,
376
+ });
377
+ },
378
+ });
379
+
380
+ registerCommand({
381
+ id: "test:heal",
382
+ description: "ATE healing 제안 생성(자동 커밋 금지)",
383
+ async run() {
384
+ const { testHeal } = await import("./test-heal");
385
+ return testHeal();
386
+ },
387
+ });
388
+
389
+
351
390
  // 레거시 명령어 (DEPRECATED)
352
391
  registerCommand({
353
392
  id: "spec-upsert",
@@ -0,0 +1,59 @@
1
+ import { theme } from "../terminal";
2
+ import { ateExtract, ateGenerate, ateRun, ateReport, ateImpact } from "@mandujs/ate";
3
+
4
+ function jsonOut(obj: unknown) {
5
+ console.log(JSON.stringify(obj, null, 2));
6
+ }
7
+
8
+ export async function testAuto(opts: { ci?: boolean; impact?: boolean; baseURL?: string } = {}): Promise<boolean> {
9
+ const repoRoot = process.cwd();
10
+ const oracleLevel = "L0" as const;
11
+
12
+ try {
13
+ // 1) extract
14
+ const extractRes = await ateExtract({ repoRoot });
15
+
16
+ // 2) impact subset (optional)
17
+ let onlyRoutes: string[] | undefined;
18
+ let impactInfo: any = { mode: "full", changedFiles: [], selectedRoutes: [] };
19
+ if (opts.impact) {
20
+ const impactRes = ateImpact({ repoRoot });
21
+ onlyRoutes = impactRes.selectedRoutes.length ? impactRes.selectedRoutes : undefined;
22
+ impactInfo = {
23
+ mode: onlyRoutes ? "subset" : "full",
24
+ changedFiles: impactRes.changedFiles,
25
+ selectedRoutes: impactRes.selectedRoutes,
26
+ };
27
+ }
28
+
29
+ // 3) generate
30
+ const genRes = ateGenerate({ repoRoot, oracleLevel, onlyRoutes });
31
+
32
+ // 4) run
33
+ const runRes = await ateRun({ repoRoot, ci: opts.ci, headless: opts.ci, baseURL: opts.baseURL });
34
+
35
+ // 5) report
36
+ const repRes = await ateReport({
37
+ repoRoot,
38
+ runId: runRes.runId,
39
+ startedAt: runRes.startedAt,
40
+ finishedAt: runRes.finishedAt,
41
+ exitCode: runRes.exitCode,
42
+ oracleLevel,
43
+ impact: impactInfo,
44
+ });
45
+
46
+ jsonOut({
47
+ ok: repRes.summary.ok,
48
+ extract: extractRes,
49
+ generate: genRes,
50
+ run: { runId: runRes.runId, exitCode: runRes.exitCode },
51
+ report: { summaryPath: repRes.summaryPath },
52
+ });
53
+
54
+ return repRes.summary.ok;
55
+ } catch (err) {
56
+ console.error(theme.error("ATE test:auto failed"), err);
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1,15 @@
1
+ import { theme } from "../terminal";
2
+ import { ateHeal } from "@mandujs/ate";
3
+
4
+ export async function testHeal(): Promise<boolean> {
5
+ const repoRoot = process.cwd();
6
+ try {
7
+ const runId = "latest"; // minimal skeleton
8
+ const res = ateHeal({ repoRoot, runId });
9
+ console.log(JSON.stringify(res, null, 2));
10
+ return true;
11
+ } catch (err) {
12
+ console.error(theme.error("ATE test:heal failed"), err);
13
+ return false;
14
+ }
15
+ }
package/src/main.ts CHANGED
@@ -60,6 +60,13 @@ Commands:
60
60
  lock --verify Lockfile 검증 (설정 무결성 확인)
61
61
  lock --diff Lockfile과 현재 설정 비교
62
62
 
63
+ add test ATE 설치 + Playwright 브라우저 준비
64
+ test:auto ATE extract→generate→run→report
65
+ test:auto --ci CI 모드(headless/아티팩트 강화)
66
+ test:auto --impact 변경 파일 기반 subset 실행
67
+ test:auto --base-url <url> 대상 서버 baseURL 지정 (기본: http://localhost:3333)
68
+ test:heal 최근 실패 기반 healing 제안 생성(자동 커밋 금지)
69
+
63
70
  Options:
64
71
  --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
65
72
  --template <name> init 템플릿: default, realtime-chat (기본: default)
@@ -8,6 +8,54 @@ import {
8
8
  type LockfileValidationResult,
9
9
  } from "@mandujs/core";
10
10
 
11
+ /**
12
+ * Lockfile command templates for consistent messaging
13
+ */
14
+ export const LOCKFILE_COMMANDS = {
15
+ update: "mandu lock",
16
+ diff: "mandu lock --diff",
17
+ safeDev: "mandu lock && mandu dev --watch",
18
+ } as const;
19
+
20
+ /**
21
+ * Formatted lockfile guidance lines with alternative commands
22
+ */
23
+ export const LOCKFILE_GUIDE_LINES = {
24
+ update: `${LOCKFILE_COMMANDS.update} (or bunx mandu lock)`,
25
+ diff: `${LOCKFILE_COMMANDS.diff} (or bunx mandu lock --diff)`,
26
+ safeDev: `${LOCKFILE_COMMANDS.safeDev} (or bun run dev:safe)`,
27
+ } as const;
28
+
29
+ /**
30
+ * Returns formatted lockfile guidance lines for display
31
+ *
32
+ * @returns Array of guidance messages with Korean labels
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const lines = getLockfileGuidanceLines();
37
+ * lines.forEach(line => console.log(` ↳ ${line}`));
38
+ * // Output:
39
+ * // ↳ lock 갱신: mandu lock (or bunx mandu lock)
40
+ * // ↳ 변경 확인: mandu lock --diff (or bunx mandu lock --diff)
41
+ * // ↳ 안정 실행: mandu lock && mandu dev --watch (or bun run dev:safe)
42
+ * ```
43
+ */
44
+ export function getLockfileGuidanceLines(): string[] {
45
+ return [
46
+ `lock 갱신: ${LOCKFILE_GUIDE_LINES.update}`,
47
+ `변경 확인: ${LOCKFILE_GUIDE_LINES.diff}`,
48
+ `안정 실행: ${LOCKFILE_GUIDE_LINES.safeDev}`,
49
+ ];
50
+ }
51
+
52
+ /**
53
+ * Validates runtime lockfile against current config
54
+ *
55
+ * @param config - Mandu configuration object
56
+ * @param rootDir - Project root directory
57
+ * @returns Validation result with lockfile, action, and bypass status
58
+ */
11
59
  export async function validateRuntimeLockfile(config: Record<string, unknown>, rootDir: string) {
12
60
  const lockfile = await readLockfile(rootDir);
13
61
 
@@ -30,17 +78,22 @@ export async function validateRuntimeLockfile(config: Record<string, unknown>, r
30
78
  return { lockfile, lockResult, action, bypassed };
31
79
  }
32
80
 
81
+ /**
82
+ * Handles blocked server start due to lockfile mismatch
83
+ *
84
+ * Exits process with error code 1 if action is "block"
85
+ *
86
+ * @param action - Policy action from lockfile validation
87
+ * @param lockResult - Validation result with details
88
+ */
33
89
  export function handleBlockedLockfile(action: "pass" | "warn" | "error" | "block", lockResult: LockfileValidationResult | null): void {
34
90
  if (action !== "block") return;
35
91
 
92
+ const guidance = getLockfileGuidanceLines();
36
93
  console.error("🛑 서버 시작 차단: Lockfile 불일치");
37
- console.error(" 설정이 변경되었습니다. 의도한 변경이라면 아래 중 하나를 실행하세요:");
38
- console.error(" $ mandu lock");
39
- console.error(" $ bunx mandu lock");
40
- console.error("");
41
- console.error(" 변경 사항 확인:");
42
- console.error(" $ mandu lock --diff");
43
- console.error(" $ bunx mandu lock --diff");
94
+ console.error(" 설정이 변경되었습니다. 의도한 변경이라면 아래를 실행하세요:");
95
+ console.error(` ${guidance[0]}`);
96
+ console.error(` ${guidance[1]}`);
44
97
  if (lockResult) {
45
98
  console.error("");
46
99
  console.error(formatValidationResult(lockResult));
@@ -48,6 +101,14 @@ export function handleBlockedLockfile(action: "pass" | "warn" | "error" | "block
48
101
  process.exit(1);
49
102
  }
50
103
 
104
+ /**
105
+ * Prints runtime lockfile validation status
106
+ *
107
+ * @param action - Policy action from lockfile validation
108
+ * @param bypassed - Whether validation was bypassed
109
+ * @param lockfile - Lockfile data (null if not found)
110
+ * @param lockResult - Validation result with hash and validity
111
+ */
51
112
  export function printRuntimeLockfileStatus(
52
113
  action: "pass" | "warn" | "error" | "block",
53
114
  bypassed: boolean,
@@ -56,11 +117,12 @@ export function printRuntimeLockfileStatus(
56
117
  ): void {
57
118
  if (action === "warn") {
58
119
  console.log(`⚠️ ${formatPolicyAction(action, bypassed)}`);
59
- console.log(` ↳ lock 갱신: mandu lock (or bunx mandu lock)`);
60
- console.log(` ↳ 변경 확인: mandu lock --diff (or bunx mandu lock --diff)`);
120
+ for (const line of getLockfileGuidanceLines()) {
121
+ console.log(` ↳ ${line}`);
122
+ }
61
123
  } else if (lockfile && lockResult?.valid) {
62
124
  console.log(`🔒 설정 무결성 확인됨 (${lockResult.currentHash?.slice(0, 8)})`);
63
125
  } else if (!lockfile) {
64
- console.log(`💡 Lockfile 없음 - 'mandu lock' 또는 'bunx mandu lock'으로 생성 권장`);
126
+ console.log(`💡 Lockfile 없음 - '${LOCKFILE_COMMANDS.update}'으로 생성 권장`);
65
127
  }
66
128
  }
@@ -7,7 +7,7 @@ import {
7
7
  openChatStream,
8
8
  sendChatMessage,
9
9
  type ChatStreamConnectionState,
10
- } from "./chat-api";
10
+ } from "@/client/features/chat/chat-api";
11
11
 
12
12
  export function useRealtimeChat() {
13
13
  const [messages, setMessages] = useState<ChatMessage[]>([]);