gencow 0.1.75 → 0.1.76
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/bin/gencow.mjs +169 -356
- package/package.json +1 -1
package/bin/gencow.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { resolve, dirname, basename } from "path";
|
|
|
23
23
|
import { homedir } from "os";
|
|
24
24
|
import { fileURLToPath } from "url";
|
|
25
25
|
import { createRequire } from "module";
|
|
26
|
+
import { buildReadmeMarkdown, extractComponentsBlock } from "../lib/readme-codegen.mjs";
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
|
|
@@ -416,316 +417,21 @@ process.exit(0);
|
|
|
416
417
|
// Pure logic lives in lib/readme-codegen.mjs (testable).
|
|
417
418
|
// This wrapper handles only file I/O.
|
|
418
419
|
|
|
420
|
+
|
|
419
421
|
function generateReadmeMd(config, apiObj) {
|
|
420
422
|
const absoluteFunctions = resolve(process.cwd(), config.functionsDir);
|
|
421
423
|
const readmePath = resolve(absoluteFunctions, "README.md");
|
|
422
424
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const namespaces = Object.keys(apiObj);
|
|
426
|
-
|
|
427
|
-
let md = `# Gencow API Guide\n`;
|
|
428
|
-
md += `> ⚡ Auto-generated by Gencow CLI — do not edit manually.\n`;
|
|
429
|
-
md += `> Last updated: ${now}\n`;
|
|
430
|
-
md += `> 📚 Gencow 공식 문서 (전체 참조): https://docs.gencow.com/llms-full.txt\n\n`;
|
|
431
|
-
md += `---\n\n`;
|
|
432
|
-
|
|
433
|
-
// ── 1. API Reference Table ─────────────────────────────
|
|
434
|
-
md += `## 📦 Available APIs\n\n`;
|
|
435
|
-
for (const [ns, fns] of Object.entries(apiObj)) {
|
|
436
|
-
md += `### \`${ns}\`\n\n`;
|
|
437
|
-
md += `| Function | Type | Description |\n`;
|
|
438
|
-
md += `| :--- | :--- | :--- |\n`;
|
|
439
|
-
for (const q of fns.queries) {
|
|
440
|
-
md += `| \`api.${ns}.${q}\` | \`query\` | Fetch ${ns} data |\n`;
|
|
441
|
-
}
|
|
442
|
-
for (const m of fns.mutations) {
|
|
443
|
-
md += `| \`api.${ns}.${m}\` | \`mutation\` | Modify ${ns} data |\n`;
|
|
444
|
-
}
|
|
445
|
-
md += `\n`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ── 1-1. React 사용법 (Primary — 최상단 배치) ────────────
|
|
449
|
-
md += `---\n\n## ⚡ 데이터 사용법 (필수 — 반드시 이 방식을 사용하세요)\n\n`;
|
|
450
|
-
md += `> ⚠️ **\`useQuery\` / \`useMutation\` Hook을 반드시 사용하세요.**\n`;
|
|
451
|
-
md += `> Gencow는 WebSocket 실시간 동기화를 내장하고 있어, Hook을 사용하면 데이터가 **자동으로 동기화**됩니다.\n`;
|
|
452
|
-
md += `> \`fetch()\`를 직접 호출하거나 \`apiPost()\` 같은 래퍼를 만들지 마세요.\n\n`;
|
|
453
|
-
md += `\`\`\`typescript\n`;
|
|
454
|
-
md += `import { api } from "@/gencow/api"; // 자동 생성됨\n`;
|
|
455
|
-
md += `import { useQuery, useMutation } from "@gencow/react";\n\n`;
|
|
456
|
-
for (const [ns, fns] of Object.entries(apiObj)) {
|
|
457
|
-
if (fns.queries.length > 0) {
|
|
458
|
-
md += `// ${ns} 데이터 조회 (실시간 자동 갱신)\n`;
|
|
459
|
-
md += `const ${ns} = useQuery(api.${ns}.${fns.queries[0]}); // ${ns.charAt(0).toUpperCase() + ns.slice(1)}[] | undefined\n`;
|
|
460
|
-
}
|
|
461
|
-
if (fns.mutations.length > 0) {
|
|
462
|
-
md += `// ${ns} 데이터 변경 (로딩/에러 자동 관리)\n`;
|
|
463
|
-
md += `const [${fns.mutations[0]}${ns.charAt(0).toUpperCase() + ns.slice(1)}, isPending] = useMutation(api.${ns}.${fns.mutations[0]});\n`;
|
|
464
|
-
}
|
|
465
|
-
md += `\n`;
|
|
466
|
-
}
|
|
467
|
-
md += `\`\`\`\n\n`;
|
|
468
|
-
md += `### ❌ 절대 하지 마세요\n\n`;
|
|
469
|
-
md += `\`\`\`typescript\n`;
|
|
470
|
-
md += `// ❌ fetch()로 직접 API 호출하지 마세요\n`;
|
|
471
|
-
md += `fetch("/api/query", { body: JSON.stringify({ name: "tasks.create", args: {...} }) })\n`;
|
|
472
|
-
md += `apiPost("tasks/create", { ... }) // ❌ 래퍼도 만들지 마세요\n\n`;
|
|
473
|
-
md += `// ✅ useMutation을 사용하세요 — 실시간 동기화 + 로딩 상태 자동 관리\n`;
|
|
474
|
-
md += `const [create] = useMutation(api.tasks.create);\n`;
|
|
475
|
-
md += `await create({ title: "새 태스크" });\n`;
|
|
476
|
-
md += `// → 서버가 WebSocket으로 useQuery를 자동 갱신 — fetchTasks() 같은 수동 리페치 불필요!\n`;
|
|
477
|
-
md += `\`\`\`\n\n`;
|
|
478
|
-
md += `### 조건부 쿼리 (skip/enabled)\n\n`;
|
|
479
|
-
md += `선택된 항목이 없을 때 등 조건부로 쿼리를 건너뛰어야 할 때:\n\n`;
|
|
480
|
-
md += `\`\`\`typescript\n`;
|
|
481
|
-
md += `// 방법 A: "skip" 토큰 (Convex 스타일) — 추천\n`;
|
|
482
|
-
md += `const messages = useQuery(api.chat.getMessages,\n`;
|
|
483
|
-
md += ` conversationId ? { conversationId } : "skip"\n`;
|
|
484
|
-
md += `);\n\n`;
|
|
485
|
-
md += `// 방법 B: enabled 옵션 (TanStack Query 스타일)\n`;
|
|
486
|
-
md += `const messages = useQuery(api.chat.getMessages,\n`;
|
|
487
|
-
md += ` { conversationId },\n`;
|
|
488
|
-
md += ` { enabled: !!conversationId }\n`;
|
|
489
|
-
md += `);\n`;
|
|
490
|
-
md += `// → skip 상태에서는 API 호출 없이 undefined 반환\n`;
|
|
491
|
-
md += `\`\`\`\n\n`;
|
|
492
|
-
|
|
493
|
-
// ── 1-2. Auth ────────────────────────────────────────────
|
|
494
|
-
md += `---\n\n## 🔐 인증 (better-auth, 세션 기반)\n\n`;
|
|
495
|
-
md += `| 엔드포인트 | 메서드 | 설명 |\n`;
|
|
496
|
-
md += `| :--- | :--- | :--- |\n`;
|
|
497
|
-
md += `| \`/api/auth/sign-up/email\` | POST | 회원가입 (\`{ email, password, name }\`) |\n`;
|
|
498
|
-
md += `| \`/api/auth/sign-in/email\` | POST | 로그인 (\`{ email, password }\`) |\n`;
|
|
499
|
-
md += `| \`/api/auth/sign-out\` | POST | 로그아웃 |\n\n`;
|
|
500
|
-
md += `> ⚠️ 모든 API 요청에 \`credentials: "include"\`를 포함해야 합니다 (세션 쿠키 전송)\n`;
|
|
501
|
-
md += `> ⚠️ JWT 토큰이나 \`/auth/register\` 같은 커스텀 경로를 만들지 마세요.\n\n`;
|
|
502
|
-
md += `### Next.js 프론트엔드 연동 (필수)\n\n`;
|
|
503
|
-
md += `프론트엔드(localhost:3000)와 백엔드(localhost:5456)가 다른 포트에서 실행되므로,\n`;
|
|
504
|
-
md += `\`next.config.ts\`에 rewrites 프록시를 설정해야 인증이 작동합니다:\n\n`;
|
|
505
|
-
md += `\`\`\`typescript\n`;
|
|
506
|
-
md += `// next.config.ts\n`;
|
|
507
|
-
md += `const nextConfig = {\n`;
|
|
508
|
-
md += ` async rewrites() {\n`;
|
|
509
|
-
md += ` return [\n`;
|
|
510
|
-
md += ` { source: "/api/:path*", destination: "http://localhost:5456/api/:path*" },\n`;
|
|
511
|
-
md += ` { source: "/ws", destination: "http://localhost:5456/ws" },\n`;
|
|
512
|
-
md += ` ];\n`;
|
|
513
|
-
md += ` },\n`;
|
|
514
|
-
md += `};\n`;
|
|
515
|
-
md += `\`\`\`\n\n`;
|
|
516
|
-
md += `또는 \`gencowAuth()\`에 명시적으로 baseUrl을 전달하세요:\n\n`;
|
|
517
|
-
md += `\`\`\`typescript\n`;
|
|
518
|
-
md += `const { signIn, useAuth } = gencowAuth(import.meta.env.VITE_API_URL || "http://localhost:5456");\n`;
|
|
519
|
-
md += `\`\`\`\n\n`;
|
|
520
|
-
|
|
521
|
-
// ── 1-3. RPC 직접 호출 (접힘 — 비-React 환경용) ──────────
|
|
522
|
-
md += `---\n\n`;
|
|
523
|
-
md += `<details>\n`;
|
|
524
|
-
md += `<summary>📡 RPC 직접 호출 (Node.js / cURL / 비-React 환경)</summary>\n\n`;
|
|
525
|
-
md += `> ⚠️ React에서는 이 방법을 사용하지 마세요. 위의 \`useQuery\`/\`useMutation\`을 사용하세요.\n\n`;
|
|
526
|
-
md += `\`\`\`typescript\n`;
|
|
527
|
-
md += `// Query 호출 (self-fetch: 서버 내부에서 다른 함수 호출 시)\n`;
|
|
528
|
-
md += `const baseUrl = process.env.GENCOW_INTERNAL_URL || "http://localhost:5456";\n`;
|
|
529
|
-
md += `const res = await fetch(\`\${baseUrl}/api/query\`, {\n`;
|
|
530
|
-
md += ` method: "POST",\n`;
|
|
531
|
-
md += ` headers: { "Content-Type": "application/json" },\n`;
|
|
532
|
-
md += ` credentials: "include",\n`;
|
|
533
|
-
if (namespaces.length > 0 && Object.values(apiObj)[0]?.queries?.[0]) {
|
|
534
|
-
md += ` body: JSON.stringify({ name: "${namespaces[0]}.${Object.values(apiObj)[0].queries[0]}", args: {} }),\n`;
|
|
535
|
-
} else {
|
|
536
|
-
md += ` body: JSON.stringify({ name: "namespace.functionName", args: {} }),\n`;
|
|
537
|
-
}
|
|
538
|
-
md += `});\n\n`;
|
|
539
|
-
if (Object.values(apiObj)[0]?.mutations?.length > 0) {
|
|
540
|
-
const firstMut = Object.values(apiObj)[0].mutations[0];
|
|
541
|
-
md += `// Mutation 호출\n`;
|
|
542
|
-
md += `const res = await fetch(\`\${baseUrl}/api/mutation\`, {\n`;
|
|
543
|
-
md += ` method: "POST",\n`;
|
|
544
|
-
md += ` headers: { "Content-Type": "application/json" },\n`;
|
|
545
|
-
md += ` credentials: "include",\n`;
|
|
546
|
-
md += ` body: JSON.stringify({ name: "${namespaces[0]}.${firstMut}", args: { /* ... */ } }),\n`;
|
|
547
|
-
md += `});\n`;
|
|
548
|
-
}
|
|
549
|
-
md += `\`\`\`\n\n`;
|
|
550
|
-
md += `</details>\n\n`;
|
|
551
|
-
|
|
552
|
-
// ── 3. Copy-Paste AI Prompt ────────────────────────────
|
|
553
|
-
md += `---\n\n## 🤖 AI Vibe-Coding Prompt (복사해서 AI에게 전달)\n\n`;
|
|
554
|
-
md += `> 아래 프롬프트를 Cursor / Claude / ChatGPT에 붙여넣으세요.\n\n`;
|
|
555
|
-
md += `\`\`\`\n`;
|
|
556
|
-
md += `다음은 현재 Gencow 백엔드에 등록된 API 구조야:\n\n`;
|
|
557
|
-
for (const [ns, fns] of Object.entries(apiObj)) {
|
|
558
|
-
md += `[${ns}]\n`;
|
|
559
|
-
for (const q of fns.queries) md += ` - api.${ns}.${q} (query)\n`;
|
|
560
|
-
for (const m of fns.mutations) md += ` - api.${ns}.${m} (mutation)\n`;
|
|
561
|
-
}
|
|
562
|
-
md += `\n`;
|
|
563
|
-
md += `React Hook 사용법:\n`;
|
|
564
|
-
md += ` useQuery(api.namespace.fnName) // 데이터 구독 (실시간 자동 갱신)\n`;
|
|
565
|
-
md += ` useMutation(api.namespace.fnName) // 데이터 변경 (로딩/에러 자동 관리)\n`;
|
|
566
|
-
md += `\n`;
|
|
567
|
-
md += `⚠️ 중요 규칙:\n`;
|
|
568
|
-
md += `- 반드시 useQuery와 useMutation을 사용해서 데이터와 연결해줘.\n`;
|
|
569
|
-
md += `- fetch()를 직접 호출하거나 apiPost() 같은 래퍼를 만들지 마.\n`;
|
|
570
|
-
md += `- gencow/api.ts는 자동 생성된 파일이야. 수동으로 만들지 마.\n`;
|
|
571
|
-
md += `- gencow/index.ts의 re-export는 export * as moduleName from "./moduleName" 패턴을 써.\n`;
|
|
572
|
-
md += ` Module, Mod 같은 접미사를 붙이지 마.\n`;
|
|
573
|
-
md += `\n`;
|
|
574
|
-
md += `⚠️ mutation 제한:\n`;
|
|
575
|
-
md += `- mutation은 10초 이내에 완료되어야 해. 외부 API나 LLM 호출이 길면 단계별로 분리해.\n`;
|
|
576
|
-
md += `- 긴 작업은 ctx.scheduler.runAfter(0, "module.nextStep", { sessionId }) 로 다음 단계를 예약.\n`;
|
|
577
|
-
md += `- 예: 크롤링(Step1) → 필터링(Step2) → 요약(Step3) 각각 별도 mutation으로 분리.\n`;
|
|
578
|
-
md += `- 같은 서버의 다른 모듈 함수를 호출할 때는 fetch()가 아닌 직접 import해서 호출해.\n`;
|
|
579
|
-
md += ` 예: import { fetchNews } from "./naverApi"; → const result = await fetchNews.handler(ctx, { keyword });\n`;
|
|
580
|
-
md += ` HTTP self-fetch (fetch("/api/mutation")) 패턴은 불필요한 네트워크 우회이므로 사용하지 마.\n`;
|
|
581
|
-
md += `\n`;
|
|
582
|
-
md += `크론 잡 (예약 작업):\n`;
|
|
583
|
-
md += ` gencow/crons.ts에서 cronJobs()로 선언\n`;
|
|
584
|
-
md += ` crons.interval("name", { minutes: N }, "module.mutation")\n`;
|
|
585
|
-
md += ` crons.daily("name", { hour: H }, "module.mutation")\n`;
|
|
586
|
-
md += ` crons.weekly("name", { dayOfWeek: D, hour: H }, "module.mutation")\n`;
|
|
587
|
-
md += ` crons.cron("name", "cron-expression", "module.mutation")\n`;
|
|
588
|
-
md += ` 반드시 export default crons 필요\n`;
|
|
589
|
-
md += `\n`;
|
|
590
|
-
md += `위 API를 기반으로 Next.js + Tailwind CSS UI 컴포넌트를 만들어줘.\n`;
|
|
591
|
-
md += `- TypeScript 타입을 최대한 활용하고, 로딩/에러 상태도 처리해줘.\n`;
|
|
592
|
-
md += `- ctx.ai.chat()을 사용해서 AI를 호출하고, OpenAI SDK를 직접 설치하지 마.\n`;
|
|
593
|
-
md += `\n`;
|
|
594
|
-
md += `배포 규칙:\n`;
|
|
595
|
-
md += `- 백엔드: \`npx gencow deploy\` (gencow/ 폴더만 배포됨. 프론트엔드는 포함 안 됨)\n`;
|
|
596
|
-
md += `- 프론트엔드: VITE_API_URL=https://{앱ID}.gencow.app npm run build 후 \`npx gencow deploy --static dist/\`\n`;
|
|
597
|
-
md += `- gencow deploy는 프론트엔드를 포함하지 않아. 반드시 별도로 --static 배포해.\n`;
|
|
598
|
-
md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 클라우드에 설정해.\n`;
|
|
599
|
-
md += `- .env 파일은 로컬 개발 전용이야. 클라우드에는 gencow env push로 올려.\n`;
|
|
600
|
-
md += `- 로컬 dev 서버에 설정하려면 \`gencow env set --local KEY=VALUE\`를 써.\n`;
|
|
601
|
-
md += `\`\`\`\n\n`;
|
|
602
|
-
|
|
603
|
-
// ── 4. AI 사용법 ────────────────────────────────────
|
|
604
|
-
md += `---\n\n## 🤖 AI 사용법 (ctx.ai)\n\n`;
|
|
605
|
-
md += `> Gencow는 OpenAI GPT-4o/4o-mini를 내장 지원합니다.\n`;
|
|
606
|
-
md += `> API 키 관리, 과금, 보안을 플랫폼이 자동 처리합니다.\n\n`;
|
|
607
|
-
md += `### 백엔드에서 AI 호출\n\n`;
|
|
608
|
-
md += `\`\`\`typescript\n`;
|
|
609
|
-
md += `// gencow/ai.ts\n`;
|
|
610
|
-
md += `import { mutation, v } from "@gencow/core";\n\n`;
|
|
611
|
-
md += `export const chat = mutation("ai.chat", {\n`;
|
|
612
|
-
md += ` args: { message: v.string() },\n`;
|
|
613
|
-
md += ` handler: async (ctx, args) => {\n`;
|
|
614
|
-
md += ` const result = await ctx.ai.chat({\n`;
|
|
615
|
-
md += ` model: "gpt-4o-mini", // 또는 "gpt-4o"\n`;
|
|
616
|
-
md += ` messages: [{ role: "user", content: args.message }],\n`;
|
|
617
|
-
md += ` });\n`;
|
|
618
|
-
md += ` return result.text;\n`;
|
|
619
|
-
md += ` },\n`;
|
|
620
|
-
md += `});\n`;
|
|
621
|
-
md += `\`\`\`\n\n`;
|
|
622
|
-
md += `### 로컬 개발 설정\n\n`;
|
|
623
|
-
md += `로컬에서 AI 기능을 테스트하려면 \`.env\`에 키를 추가하세요:\n\n`;
|
|
624
|
-
md += `\`\`\`\n# .env\nOPENAI_API_KEY=sk-your-key-here\n\`\`\`\n\n`;
|
|
625
|
-
md += `> ⚠️ 배포 후에는 플랫폼 키가 자동 사용됩니다. .env의 키는 로컬 전용입니다.\n\n`;
|
|
626
|
-
md += `### ❌ 하지 마세요\n\n`;
|
|
627
|
-
md += `\`\`\`typescript\n`;
|
|
628
|
-
md += `// ❌ OpenAI SDK를 직접 설치하지 마세요\n`;
|
|
629
|
-
md += `import OpenAI from "openai";\n`;
|
|
630
|
-
md += `const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n`;
|
|
631
|
-
md += `// ✅ ctx.ai.chat()을 사용하세요 — 키 관리 + 과금 자동\n`;
|
|
632
|
-
md += `const result = await ctx.ai.chat({ model: "gpt-4o-mini", messages: [...] });\n`;
|
|
633
|
-
md += `\`\`\`\n\n`;
|
|
634
|
-
|
|
635
|
-
// ── 5. 배포 가이드 ───────────────────────────────────
|
|
636
|
-
md += `---\n\n## 🚀 배포\n\n`;
|
|
637
|
-
md += `### 클라우드 배포 (3단계)\n\n`;
|
|
638
|
-
md += `\`\`\`bash\n`;
|
|
639
|
-
md += `# 1. 로그인 (최초 1회)\n`;
|
|
640
|
-
md += `npx gencow login\n\n`;
|
|
641
|
-
md += `# 2. 배포\n`;
|
|
642
|
-
md += `npx gencow deploy\n\n`;
|
|
643
|
-
md += `# 3. 환경변수 설정 (필요 시)\n`;
|
|
644
|
-
md += `npx gencow env set DATABASE_URL=postgres://...\n`;
|
|
645
|
-
md += `npx gencow env list\n`;
|
|
646
|
-
md += `\`\`\`\n\n`;
|
|
647
|
-
md += `> 배포 후 앱은 \`https://{앱이름}.gencow.app\`에서 접근 가능\n\n`;
|
|
648
|
-
md += `### 환경변수 관리\n\n`;
|
|
649
|
-
md += `| 명령어 | 설명 |\n`;
|
|
650
|
-
md += `| :--- | :--- |\n`;
|
|
651
|
-
md += `| \`gencow env list\` | 클라우드 환경변수 목록 |\n`;
|
|
652
|
-
md += `| \`gencow env set KEY=VALUE\` | 클라우드 환경변수 추가/수정 |\n`;
|
|
653
|
-
md += `| \`gencow env unset KEY\` | 클라우드 환경변수 삭제 |\n`;
|
|
654
|
-
md += `| \`gencow env push\` | .env를 클라우드에 일괄 업로드 |\n`;
|
|
655
|
-
md += `| \`--local\` 옵션 | 로컬 dev 서버 대상 (예: \`env set --local K=V\`) |\n\n`;
|
|
656
|
-
|
|
657
|
-
// ── 6. Cron Jobs ──────────────────────────────────────
|
|
658
|
-
md += `---\n\n## ⏰ Cron Jobs (예약 작업)\n\n`;
|
|
659
|
-
md += `\`gencow/crons.ts\` 파일에 선언하면 서버 시작 시 자동 등록됩니다.\n\n`;
|
|
660
|
-
md += `\`\`\`typescript\n`;
|
|
661
|
-
md += `import { cronJobs } from "@gencow/core";\n\n`;
|
|
662
|
-
md += `const crons = cronJobs();\n\n`;
|
|
663
|
-
md += `// 매 30분마다 실행\n`;
|
|
664
|
-
md += `crons.interval("sync", { minutes: 30 }, "data.sync");\n\n`;
|
|
665
|
-
md += `// 매일 오전 9시 실행 (서버 시간)\n`;
|
|
666
|
-
md += `crons.daily("report", { hour: 9 }, "reports.generate");\n\n`;
|
|
667
|
-
md += `// 매주 월요일 오전 10시\n`;
|
|
668
|
-
md += `crons.weekly("weekly", { dayOfWeek: 1, hour: 10 }, "reports.weekly");\n\n`;
|
|
669
|
-
md += `// 커스텀 cron 표현식\n`;
|
|
670
|
-
md += `crons.cron("custom", "0 */2 * * *", "custom.handler");\n\n`;
|
|
671
|
-
md += `export default crons; // ← 필수!\n`;
|
|
672
|
-
md += `\`\`\`\n\n`;
|
|
673
|
-
md += `> ⚠️ \`export default crons\`가 없으면 서버가 cron을 인식하지 않습니다.\n`;
|
|
674
|
-
md += `> ⚠️ action 문자열은 기존 mutation 이름과 정확히 매칭되어야 합니다.\n\n`;
|
|
675
|
-
md += `> 💡 cron 핸들러에서 mutation을 호출하려면 self-fetch를 사용하세요:\n`;
|
|
676
|
-
md += `> \`\`\`typescript\n`;
|
|
677
|
-
md += `> const baseUrl = process.env.GENCOW_INTERNAL_URL || "http://localhost:5456";\n`;
|
|
678
|
-
md += `> await fetch(\`\${baseUrl}/api/mutation\`, { method: "POST", ... });\n`;
|
|
679
|
-
md += `> \`\`\`\n\n`;
|
|
680
|
-
|
|
681
|
-
// ── 7. 배포 ──────────────────────────────────────────────
|
|
682
|
-
md += `---\n\n## 🚀 배포\n\n`;
|
|
683
|
-
md += `### 백엔드 API 배포\n`;
|
|
684
|
-
md += `\`\`\`bash\n`;
|
|
685
|
-
md += `gencow deploy # gencow/ 폴더 → 클라우드 서버 배포\n`;
|
|
686
|
-
md += `\`\`\`\n\n`;
|
|
687
|
-
md += `### 프론트엔드 배포 (frontend/ 있는 경우)\n`;
|
|
688
|
-
md += `\`\`\`bash\n`;
|
|
689
|
-
md += `# 1. 백엔드 URL을 환경변수로 빌드\n`;
|
|
690
|
-
md += `cd frontend\n`;
|
|
691
|
-
md += `VITE_API_URL=https://{앱ID}.gencow.app npm run build\n\n`;
|
|
692
|
-
md += `# 2. 빌드 결과물 정적 배포\n`;
|
|
693
|
-
md += `gencow deploy --static dist/\n`;
|
|
694
|
-
md += `\`\`\`\n\n`;
|
|
695
|
-
md += `### 정적 사이트 전용 (API 없는 경우)\n`;
|
|
696
|
-
md += `\`\`\`bash\n`;
|
|
697
|
-
md += `gencow deploy --static dist/ # 순수 HTML/CSS/JS만 배포\n`;
|
|
698
|
-
md += `\`\`\`\n\n`;
|
|
699
|
-
md += `> ⚠️ 프론트엔드에서 API를 호출하려면 빌드 시 \`VITE_API_URL\`을 반드시 설정하세요.\n\n`;
|
|
700
|
-
md += `### CORS 설정\n\n`;
|
|
701
|
-
md += `- \`*.gencow.app\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
|
|
702
|
-
md += `- 커스텀 도메인에서 API를 호출하려면 환경변수를 설정하세요:\n\n`;
|
|
703
|
-
md += `\`\`\`bash\n`;
|
|
704
|
-
md += `gencow env set CORS_ORIGINS=https://myapp.com,https://www.myapp.com\n`;
|
|
705
|
-
md += `\`\`\`\n\n`;
|
|
706
|
-
|
|
707
|
-
// ── 8. Dev Tips ────────────────────────────────────────
|
|
708
|
-
md += `---\n\n## 💡 개발 팁\n\n`;
|
|
709
|
-
md += `- \`gencow/\` 폴더 내 파일을 수정하면 \`api.ts\`와 이 README가 **자동으로 재생성**됩니다.\n`;
|
|
710
|
-
md += `- 스키마 변경 후 \`gencow db:push\`를 실행하면 DB가 즉시 동기화됩니다.\n`;
|
|
711
|
-
md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
|
|
712
|
-
md += `- 로컬 개발: \`gencow dev\` → \`http://localhost:5456\`\n`;
|
|
713
|
-
md += `- Self-fetch: \`process.env.GENCOW_INTERNAL_URL\` — cron/mutation에서 다른 함수 호출 시 사용\n`;
|
|
714
|
-
md += `- .env 파일은 로컬 전용. 클라우드에는 \`gencow env push\`로 올리세요. (로컬 서버는 \`--local\`)\n`;
|
|
715
|
-
|
|
716
|
-
// ── 5. 기존 컴포넌트 섹션 보존 (gencow add로 추가된 부분) ──
|
|
717
|
-
const COMP_START = "<!-- gencow-components-start -->";
|
|
718
|
-
const COMP_END = "<!-- gencow-components-end -->";
|
|
719
|
-
|
|
425
|
+
// Preserve existing component block (added by `gencow add`)
|
|
426
|
+
let existingComponentsBlock;
|
|
720
427
|
try {
|
|
721
428
|
const existingReadme = readFileSync(readmePath, "utf-8");
|
|
722
|
-
|
|
723
|
-
const startIdx = existingReadme.indexOf(COMP_START);
|
|
724
|
-
const endIdx = existingReadme.indexOf(COMP_END) + COMP_END.length;
|
|
725
|
-
md += "\n" + existingReadme.slice(startIdx, endIdx) + "\n";
|
|
726
|
-
}
|
|
429
|
+
existingComponentsBlock = extractComponentsBlock(existingReadme);
|
|
727
430
|
} catch { /* README doesn't exist yet */ }
|
|
728
431
|
|
|
432
|
+
// Generate README content using the canonical pure function
|
|
433
|
+
const md = buildReadmeMarkdown(apiObj, { existingComponentsBlock });
|
|
434
|
+
|
|
729
435
|
writeFileSync(readmePath, md);
|
|
730
436
|
success(`Generated ${config.functionsDir}/README.md (AI vibe-coding guide)`);
|
|
731
437
|
}
|
|
@@ -1950,11 +1656,13 @@ ${BOLD}Examples:${RESET}
|
|
|
1950
1656
|
let staticDir = null; // --static 옵션
|
|
1951
1657
|
let isStatic = false;
|
|
1952
1658
|
let forceDeploy = false; // --force: skip dependency audit
|
|
1659
|
+
let noBackend = false; // --no-backend: skip backend auto-deploy in --static mode
|
|
1953
1660
|
|
|
1954
1661
|
for (let i = 0; i < deployArgs.length; i++) {
|
|
1955
1662
|
const a = deployArgs[i];
|
|
1956
1663
|
if (a === "--prod") envTarget = "prod";
|
|
1957
1664
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
1665
|
+
else if (a === "--no-backend") noBackend = true;
|
|
1958
1666
|
else if (a === "--app" || a === "-a") appId = deployArgs[++i];
|
|
1959
1667
|
else if (a === "--static") {
|
|
1960
1668
|
isStatic = true;
|
|
@@ -1985,7 +1693,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1985
1693
|
|
|
1986
1694
|
// ── Static Deploy 분기 ────────────────────────────────
|
|
1987
1695
|
if (isStatic) {
|
|
1988
|
-
return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath);
|
|
1696
|
+
return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
|
|
1989
1697
|
}
|
|
1990
1698
|
|
|
1991
1699
|
log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
|
|
@@ -2200,7 +1908,9 @@ ${BOLD}Examples:${RESET}
|
|
|
2200
1908
|
},
|
|
2201
1909
|
|
|
2202
1910
|
// ── deploy --static 내부 헬퍼 ─────────────────────────
|
|
2203
|
-
async _deployStatic(creds, appId, displayName, staticDirArg, gencowJsonPath) {
|
|
1911
|
+
async _deployStatic(creds, appId, displayName, staticDirArg, gencowJsonPath, opts = {}) {
|
|
1912
|
+
const { forceDeploy = false, noBackend = false, envTarget = "dev" } = opts;
|
|
1913
|
+
|
|
2204
1914
|
// 정적 폴더 자동 감지
|
|
2205
1915
|
const AUTO_DETECT = ["dist", "out", "build", ".next/out"];
|
|
2206
1916
|
let targetDir = staticDirArg;
|
|
@@ -2222,8 +1932,24 @@ ${BOLD}Examples:${RESET}
|
|
|
2222
1932
|
process.exit(1);
|
|
2223
1933
|
}
|
|
2224
1934
|
|
|
2225
|
-
|
|
2226
|
-
|
|
1935
|
+
// ── 백엔드 감지: CWD 또는 상위 디렉토리에 gencow/ 존재 여부 ──
|
|
1936
|
+
const parentDir = resolve(process.cwd(), "..");
|
|
1937
|
+
const hasCwdBackend = existsSync(resolve(process.cwd(), "gencow"))
|
|
1938
|
+
&& existsSync(resolve(process.cwd(), "package.json"));
|
|
1939
|
+
const hasParentBackend = existsSync(resolve(parentDir, "gencow"))
|
|
1940
|
+
&& (existsSync(resolve(parentDir, "gencow.config.ts")) || existsSync(resolve(parentDir, "package.json")));
|
|
1941
|
+
const detectedBackend = hasCwdBackend || hasParentBackend;
|
|
1942
|
+
const backendRoot = hasCwdBackend ? process.cwd() : (hasParentBackend ? parentDir : null);
|
|
1943
|
+
const shouldDeployBackend = detectedBackend && !noBackend;
|
|
1944
|
+
|
|
1945
|
+
if (shouldDeployBackend) {
|
|
1946
|
+
log(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
|
|
1947
|
+
info(`백엔드 감지: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (자동 배포)`);
|
|
1948
|
+
info(`프론트엔드: ${targetDir}/`);
|
|
1949
|
+
} else {
|
|
1950
|
+
log(`\n${BOLD}${CYAN}Gencow Static Deploy${RESET}\n`);
|
|
1951
|
+
info(`폴더: ${targetDir}/`);
|
|
1952
|
+
}
|
|
2227
1953
|
if (appId) {
|
|
2228
1954
|
info(`앱 ID: ${appId}`);
|
|
2229
1955
|
} else {
|
|
@@ -2231,65 +1957,152 @@ ${BOLD}Examples:${RESET}
|
|
|
2231
1957
|
}
|
|
2232
1958
|
log("");
|
|
2233
1959
|
|
|
2234
|
-
// ──
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
const
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
1960
|
+
// ── 가드레일: 빌드 결과물 API 참조 스캔 ───────────────
|
|
1961
|
+
// 백엔드 자동 배포 시에는 API 서버가 존재하므로 경고 불필요
|
|
1962
|
+
if (!shouldDeployBackend) {
|
|
1963
|
+
try {
|
|
1964
|
+
const fullTargetDir = resolve(process.cwd(), targetDir);
|
|
1965
|
+
const scanDir = (dir) => {
|
|
1966
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1967
|
+
const files = [];
|
|
1968
|
+
for (const e of entries) {
|
|
1969
|
+
const p = resolve(dir, e.name);
|
|
1970
|
+
if (e.isDirectory() && e.name !== "node_modules") {
|
|
1971
|
+
files.push(...scanDir(p));
|
|
1972
|
+
} else if (e.isFile() && e.name.endsWith(".js")) {
|
|
1973
|
+
files.push(p);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return files;
|
|
1977
|
+
};
|
|
1978
|
+
const jsFiles = scanDir(fullTargetDir);
|
|
1979
|
+
const apiRefFiles = [];
|
|
1980
|
+
for (const f of jsFiles) {
|
|
1981
|
+
const content = readFileSync(f, "utf8");
|
|
1982
|
+
if (content.includes("/api/query") || content.includes("/api/mutation")) {
|
|
1983
|
+
apiRefFiles.push(f.replace(fullTargetDir + "/", ""));
|
|
2246
1984
|
}
|
|
2247
1985
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
1986
|
+
if (apiRefFiles.length > 0) {
|
|
1987
|
+
log("");
|
|
1988
|
+
warn(`빌드 파일에서 API 참조가 발견되었습니다:`);
|
|
1989
|
+
for (const f of apiRefFiles.slice(0, 5)) log(` ${DIM}- ${f}${RESET}`);
|
|
1990
|
+
if (apiRefFiles.length > 5) log(` ${DIM}... 외 ${apiRefFiles.length - 5}개${RESET}`);
|
|
1991
|
+
log("");
|
|
1992
|
+
warn(`정적 호스팅에는 API 서버가 없어 404가 발생합니다.`);
|
|
1993
|
+
info(`💡 백엔드가 필요하다면: ${CYAN}VITE_API_URL=https://{backend}.gencow.app npm run build${RESET} 후 배포`);
|
|
1994
|
+
info(`💡 별도 백엔드를 사용한다면: ${CYAN}VITE_API_URL${RESET} 환경변수로 빌드 대상을 지정하세요.`);
|
|
1995
|
+
log("");
|
|
1996
|
+
|
|
1997
|
+
const { createInterface } = await import("readline");
|
|
1998
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1999
|
+
const answer = await new Promise(resolve => {
|
|
2000
|
+
rl.question(` ${YELLOW}⚠${RESET} 그래도 정적 배포를 계속하시겠습니까? (y/N) `, resolve);
|
|
2001
|
+
});
|
|
2002
|
+
rl.close();
|
|
2003
|
+
if (answer.toLowerCase() !== "y") {
|
|
2004
|
+
info("배포 취소됨.");
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
log("");
|
|
2008
|
+
}
|
|
2009
|
+
} catch { /* 스캔 실패 — 무시하고 진행 */ }
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// ── 백엔드 자동 배포 (감지된 경우) ────────────────────
|
|
2013
|
+
if (shouldDeployBackend) {
|
|
2014
|
+
log(` ${BOLD}── 백엔드 배포 ──────────────────────${RESET}\n`);
|
|
2015
|
+
|
|
2016
|
+
// 1-0. Pre-deploy dependency audit
|
|
2017
|
+
if (!forceDeploy) {
|
|
2018
|
+
try {
|
|
2019
|
+
const { auditDeployDependencies, formatAuditError } = await import("../lib/deploy-auditor.mjs");
|
|
2020
|
+
const entryPoint = resolve(backendRoot, "gencow", "index.ts");
|
|
2021
|
+
if (existsSync(entryPoint)) {
|
|
2022
|
+
const auditResult = await auditDeployDependencies(entryPoint);
|
|
2023
|
+
if (!auditResult.passed) {
|
|
2024
|
+
log(formatAuditError(auditResult));
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
} catch (auditErr) {
|
|
2029
|
+
warn(`의존성 검사 스킵: ${auditErr.message}`);
|
|
2256
2030
|
}
|
|
2257
2031
|
}
|
|
2258
|
-
if (apiRefFiles.length > 0) {
|
|
2259
|
-
log("");
|
|
2260
|
-
warn(`빌드 파일에서 API 참조가 발견되었습니다:`);
|
|
2261
|
-
for (const f of apiRefFiles.slice(0, 5)) log(` ${DIM}- ${f}${RESET}`);
|
|
2262
|
-
if (apiRefFiles.length > 5) log(` ${DIM}... 외 ${apiRefFiles.length - 5}개${RESET}`);
|
|
2263
|
-
log("");
|
|
2264
|
-
warn(`정적 호스팅에는 API 서버가 없어 404가 발생합니다.`);
|
|
2265
|
-
info(`💡 백엔드가 필요하다면: ${CYAN}VITE_API_URL=https://{backend}.gencow.app npm run build${RESET} 후 배포`);
|
|
2266
|
-
info(`💡 별도 백엔드를 사용한다면: ${CYAN}VITE_API_URL${RESET} 환경변수로 빌드 대상을 지정하세요.`);
|
|
2267
|
-
log("");
|
|
2268
2032
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2033
|
+
// 1. tar.gz 패키징 (백엔드)
|
|
2034
|
+
info("백엔드 패키징 중...");
|
|
2035
|
+
const { execSync: exec } = await import("child_process");
|
|
2036
|
+
const tmpBackendBundle = resolve(backendRoot, ".gencow", "deploy-bundle.tar.gz");
|
|
2037
|
+
mkdirSync(dirname(tmpBackendBundle), { recursive: true });
|
|
2038
|
+
|
|
2039
|
+
const backendFiles = ["gencow/", "package.json"];
|
|
2040
|
+
if (existsSync(resolve(backendRoot, "bun.lockb"))) backendFiles.push("bun.lockb");
|
|
2041
|
+
if (existsSync(resolve(backendRoot, "package-lock.json"))) backendFiles.push("package-lock.json");
|
|
2042
|
+
if (existsSync(resolve(backendRoot, "tsconfig.json"))) backendFiles.push("tsconfig.json");
|
|
2043
|
+
|
|
2044
|
+
try {
|
|
2045
|
+
exec(`tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
|
|
2046
|
+
} catch (e) {
|
|
2047
|
+
error(`백엔드 패키징 실패: ${e.message}`);
|
|
2048
|
+
process.exit(1);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
const backendBundleSize = statSync(tmpBackendBundle).size;
|
|
2052
|
+
success(`백엔드 번들 생성: ${(backendBundleSize / 1024).toFixed(1)} KB`);
|
|
2053
|
+
|
|
2054
|
+
// 2. 앱이 없으면 생성
|
|
2055
|
+
if (!appId) {
|
|
2056
|
+
info("앱 생성 중 (ID 자동 생성)...");
|
|
2057
|
+
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2058
|
+
if (!createRes.ok) {
|
|
2059
|
+
const createErr = await createRes.json().catch(() => ({}));
|
|
2060
|
+
error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
|
|
2061
|
+
process.exit(1);
|
|
2278
2062
|
}
|
|
2279
|
-
|
|
2063
|
+
const createData = await createRes.json();
|
|
2064
|
+
appId = createData.appId || createData.name;
|
|
2065
|
+
if (!appId) {
|
|
2066
|
+
error("앱 생성 응답에 appId가 없습니다.");
|
|
2067
|
+
process.exit(1);
|
|
2068
|
+
}
|
|
2069
|
+
success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
|
|
2070
|
+
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
2071
|
+
appId, displayName, platformUrl: creds.platformUrl,
|
|
2072
|
+
}, null, 2));
|
|
2073
|
+
info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
|
|
2074
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2280
2075
|
}
|
|
2281
|
-
} catch { /* 스캔 실패 — 무시하고 진행 */ }
|
|
2282
2076
|
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2077
|
+
// 3. 백엔드 업로드
|
|
2078
|
+
info("백엔드 배포 중...");
|
|
2079
|
+
const backendBuffer = readFileSync(tmpBackendBundle);
|
|
2080
|
+
const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
2083
|
+
body: backendBuffer,
|
|
2084
|
+
});
|
|
2085
|
+
try { unlinkSync(tmpBackendBundle); } catch { }
|
|
2086
|
+
|
|
2087
|
+
if (!backendDeployRes.ok) {
|
|
2088
|
+
const errData = await backendDeployRes.json().catch(() => ({}));
|
|
2089
|
+
error(`백엔드 배포 실패: ${errData.error || backendDeployRes.statusText}`);
|
|
2090
|
+
if (errData.crashLogs?.length) {
|
|
2091
|
+
log("");
|
|
2092
|
+
warn("서버 시작 실패 원인:");
|
|
2093
|
+
for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2094
|
+
}
|
|
2095
|
+
error("백엔드 배포 실패로 프론트엔드 배포를 건너뜁니다.");
|
|
2096
|
+
process.exit(1);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
const backendData = await backendDeployRes.json();
|
|
2100
|
+
success(`백엔드 배포 완료!`);
|
|
2101
|
+
info(`URL: ${backendData.url}`);
|
|
2102
|
+
info(`Hash: ${backendData.bundleHash}`);
|
|
2103
|
+
updateEnvLocalUrl(backendData.url);
|
|
2292
2104
|
log("");
|
|
2105
|
+
log(` ${BOLD}── 프론트엔드 배포 ──────────────────${RESET}\n`);
|
|
2293
2106
|
}
|
|
2294
2107
|
|
|
2295
2108
|
// 1. tar.gz 패키징
|