gencow 0.1.76 → 0.1.78

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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * .env File Parser — Pure function extracted from gencow.mjs buildEnv()
3
+ *
4
+ * Parses .env file content into a key-value object.
5
+ * Handles comments, blank lines, quoted values, and inline comments.
6
+ */
7
+
8
+ /**
9
+ * Parse .env file content string into key-value pairs.
10
+ *
11
+ * Rules:
12
+ * - Lines starting with # are comments (ignored)
13
+ * - Blank lines are ignored
14
+ * - Format: KEY=VALUE
15
+ * - Quoted values: KEY="value" or KEY='value' → strips quotes
16
+ * - Inline comments after unquoted values are stripped
17
+ *
18
+ * @param {string} content - Raw .env file content
19
+ * @returns {Record<string, string>} Parsed key-value object
20
+ */
21
+ export function parseEnvFile(content) {
22
+ const env = {};
23
+ if (!content) return env;
24
+
25
+ for (const line of content.split("\n")) {
26
+ const trimmed = line.trim();
27
+
28
+ // Skip empty lines and comments
29
+ if (!trimmed || trimmed.startsWith("#")) continue;
30
+
31
+ // Find first = sign
32
+ const eqIdx = trimmed.indexOf("=");
33
+ if (eqIdx === -1) continue;
34
+
35
+ const key = trimmed.slice(0, eqIdx).trim();
36
+ let val = trimmed.slice(eqIdx + 1).trim();
37
+
38
+ // Strip surrounding quotes
39
+ if (
40
+ (val.startsWith('"') && val.endsWith('"')) ||
41
+ (val.startsWith("'") && val.endsWith("'"))
42
+ ) {
43
+ val = val.slice(1, -1);
44
+ }
45
+
46
+ // Skip empty keys
47
+ if (!key) continue;
48
+
49
+ env[key] = val;
50
+ }
51
+
52
+ return env;
53
+ }
54
+
55
+ /**
56
+ * Validate environment variable key format.
57
+ * Only allows alphanumeric + underscores, must start with letter or underscore.
58
+ *
59
+ * @param {string} key
60
+ * @returns {boolean}
61
+ */
62
+ export function isValidEnvKey(key) {
63
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
64
+ }
65
+
66
+ /**
67
+ * Serialize env object back to .env file format.
68
+ *
69
+ * @param {Record<string, string>} env
70
+ * @returns {string}
71
+ */
72
+ export function serializeEnvFile(env) {
73
+ return Object.entries(env)
74
+ .map(([key, val]) => {
75
+ // Quote values containing spaces, #, or special chars
76
+ if (/[\s#"']/.test(val)) {
77
+ return `${key}="${val}"`;
78
+ }
79
+ return `${key}=${val}`;
80
+ })
81
+ .join("\n") + "\n";
82
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Project Validator — Pure function extracted from gencow.mjs
3
+ *
4
+ * Validates gencow project structure before init/deploy.
5
+ */
6
+
7
+ /**
8
+ * Validate project structure for deploy readiness.
9
+ *
10
+ * @param {object} opts
11
+ * @param {string[]} opts.files - List of relative file paths in the project
12
+ * @param {string} opts.functionsDir - Functions directory name (e.g. "gencow")
13
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
14
+ */
15
+ export function validateProjectStructure({ files, functionsDir = "gencow" }) {
16
+ const errors = [];
17
+ const warnings = [];
18
+
19
+ // Required: index.ts or index.js in functions dir
20
+ const hasIndex = files.some(
21
+ (f) =>
22
+ f === `${functionsDir}/index.ts` ||
23
+ f === `${functionsDir}/index.js`
24
+ );
25
+ if (!hasIndex) {
26
+ errors.push(`${functionsDir}/index.ts が見つかりません (必須)`);
27
+ }
28
+
29
+ // Required: schema.ts
30
+ const hasSchema = files.some(
31
+ (f) =>
32
+ f === `${functionsDir}/schema.ts` ||
33
+ f === `${functionsDir}/schema.js`
34
+ );
35
+ if (!hasSchema) {
36
+ errors.push(`${functionsDir}/schema.ts が見つかりません (必須)`);
37
+ }
38
+
39
+ // Warning: no package.json
40
+ const hasPackageJson = files.some((f) => f === "package.json");
41
+ if (!hasPackageJson) {
42
+ warnings.push("package.json が見つかりません");
43
+ }
44
+
45
+ // Warning: crons.ts exists but not exported from index
46
+ const hasCrons = files.some(
47
+ (f) =>
48
+ f === `${functionsDir}/crons.ts` ||
49
+ f === `${functionsDir}/crons.js`
50
+ );
51
+ if (hasCrons) {
52
+ // Can't check content here (pure function), but flag for awareness
53
+ warnings.push(
54
+ `${functionsDir}/crons.ts が見つかりました — export default crons を忘れないでください`
55
+ );
56
+ }
57
+
58
+ return {
59
+ valid: errors.length === 0,
60
+ errors,
61
+ warnings,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Check if a file should be excluded from deploy bundle.
67
+ *
68
+ * @param {string} filePath - Relative file path
69
+ * @returns {boolean} true if file should be excluded
70
+ */
71
+ export function shouldExcludeFromDeploy(filePath) {
72
+ const excludePatterns = [
73
+ /node_modules\//,
74
+ /\.git\//,
75
+ /\.gencow\//,
76
+ /\.env$/,
77
+ /\.env\.local$/,
78
+ /\.DS_Store$/,
79
+ /dist\//,
80
+ /\.next\//,
81
+ /\.turbo\//,
82
+ /coverage\//,
83
+ /__tests__\//,
84
+ /\.test\.(ts|js|tsx|jsx)$/,
85
+ /\.spec\.(ts|js|tsx|jsx)$/,
86
+ ];
87
+
88
+ return excludePatterns.some((pattern) => pattern.test(filePath));
89
+ }
@@ -0,0 +1,510 @@
1
+ /**
2
+ * README Codegen — Pure function extracted from gencow.mjs generateReadmeMd()
3
+ *
4
+ * Generates gencow/README.md content for AI vibe-coding.
5
+ * This module is purely functional: takes API object, returns markdown string.
6
+ * File I/O remains in gencow.mjs.
7
+ */
8
+
9
+ /**
10
+ * @typedef {{ queries: string[], mutations: string[] }} NamespaceFns
11
+ * @typedef {Record<string, NamespaceFns>} ApiObj
12
+ * @typedef {{ timestamp?: string, existingComponentsBlock?: string }} ReadmeOpts
13
+ */
14
+
15
+ /**
16
+ * Build the full README.md markdown content for a Gencow project.
17
+ *
18
+ * @param {ApiObj} apiObj - API namespace map (e.g. { tasks: { queries: ['list'], mutations: ['create'] } })
19
+ * @param {ReadmeOpts} [opts] - Optional overrides
20
+ * @returns {string} Complete README.md content
21
+ */
22
+ export function buildReadmeMarkdown(apiObj, opts = {}) {
23
+ const now = opts.timestamp || new Date().toLocaleString("ko-KR");
24
+ const namespaces = Object.keys(apiObj);
25
+
26
+ let md = "";
27
+
28
+ // ── Header ────────────────────────────────────────────
29
+ md += `# Gencow API Guide\n`;
30
+ md += `> ⚡ Auto-generated by Gencow CLI — do not edit manually.\n`;
31
+ md += `> Last updated: ${now}\n`;
32
+ md += `> 📚 Gencow 공식 문서 (전체 참조): https://docs.gencow.com/llms-full.txt\n\n`;
33
+ md += `---\n\n`;
34
+
35
+ // ── 1. API Reference Table ────────────────────────────
36
+ md += buildApiTable(apiObj);
37
+
38
+ // ── 2. React 사용법 ──────────────────────────────────
39
+ md += buildReactUsage(apiObj);
40
+
41
+ // ── 2.5. 프론트엔드 초기 설정 ────────────────────────
42
+ md += buildFrontendSetup();
43
+
44
+ // ── 3. Auth ──────────────────────────────────────────
45
+ md += buildAuthSection();
46
+
47
+ // ── 4. RPC 직접 호출 ─────────────────────────────────
48
+ md += buildRpcSection(apiObj, namespaces);
49
+
50
+ // ── 5. AI Vibe-Coding Prompt ─────────────────────────
51
+ md += buildAiPrompt(apiObj, namespaces);
52
+
53
+ // ── 6. AI 사용법 ─────────────────────────────────────
54
+ md += buildAiUsageSection();
55
+
56
+ // ── 7. 배포 가이드 (요약 + 상세) ─────────────────────
57
+ md += buildDeploySection();
58
+
59
+ // ── 8. Cron Jobs ─────────────────────────────────────
60
+ md += buildCronSection();
61
+
62
+ // ── 9. Dev Tips ──────────────────────────────────────
63
+ md += buildDevTips();
64
+
65
+ // ── 기존 컴포넌트 섹션 보존 ──────────────────────────
66
+ if (opts.existingComponentsBlock) {
67
+ md += "\n" + opts.existingComponentsBlock + "\n";
68
+ }
69
+
70
+ return md;
71
+ }
72
+
73
+ // ─── Section Builders (all pure) ──────────────────────────
74
+
75
+ /** @param {ApiObj} apiObj */
76
+ export function buildApiTable(apiObj) {
77
+ let md = `## 📦 Available APIs\n\n`;
78
+ for (const [ns, fns] of Object.entries(apiObj)) {
79
+ md += `### \`${ns}\`\n\n`;
80
+ md += `| Function | Type | Description |\n`;
81
+ md += `| :--- | :--- | :--- |\n`;
82
+ for (const q of fns.queries) {
83
+ md += `| \`api.${ns}.${q}\` | \`query\` | Fetch ${ns} data |\n`;
84
+ }
85
+ for (const m of fns.mutations) {
86
+ md += `| \`api.${ns}.${m}\` | \`mutation\` | Modify ${ns} data |\n`;
87
+ }
88
+ md += `\n`;
89
+ }
90
+ return md;
91
+ }
92
+
93
+ /** @param {ApiObj} apiObj */
94
+ export function buildReactUsage(apiObj) {
95
+ let md = `---\n\n## ⚡ 데이터 사용법 (필수 — 반드시 이 방식을 사용하세요)\n\n`;
96
+ md += `> ⚠️ **\`useQuery\` / \`useMutation\` Hook을 반드시 사용하세요.**\n`;
97
+ md += `> Gencow는 WebSocket 실시간 동기화를 내장하고 있어, Hook을 사용하면 데이터가 **자동으로 동기화**됩니다.\n`;
98
+ md += `> \`fetch()\`를 직접 호출하거나 \`apiPost()\` 같은 래퍼를 만들지 마세요.\n\n`;
99
+ md += `\`\`\`typescript\n`;
100
+ md += `import { api } from "@/gencow/api"; // 자동 생성됨\n`;
101
+ md += `import { useQuery, useMutation } from "@gencow/react";\n\n`;
102
+ for (const [ns, fns] of Object.entries(apiObj)) {
103
+ if (fns.queries.length > 0) {
104
+ md += `// ${ns} 데이터 조회 (실시간 자동 갱신)\n`;
105
+ md += `const ${ns} = useQuery(api.${ns}.${fns.queries[0]}); // ${capitalize(ns)}[] | undefined\n`;
106
+ }
107
+ if (fns.mutations.length > 0) {
108
+ md += `// ${ns} 데이터 변경 (로딩/에러 자동 관리)\n`;
109
+ md += `const [${fns.mutations[0]}${capitalize(ns)}, isPending] = useMutation(api.${ns}.${fns.mutations[0]});\n`;
110
+ }
111
+ md += `\n`;
112
+ }
113
+ md += `\`\`\`\n\n`;
114
+
115
+ // Anti-pattern
116
+ md += `### ❌ 절대 하지 마세요\n\n`;
117
+ md += `\`\`\`typescript\n`;
118
+ md += `// ❌ fetch()로 직접 API 호출하지 마세요\n`;
119
+ md += `fetch("/api/query", { body: JSON.stringify({ name: "tasks.create", args: {...} }) })\n`;
120
+ md += `apiPost("tasks/create", { ... }) // ❌ 래퍼도 만들지 마세요\n\n`;
121
+ md += `// ✅ useMutation을 사용하세요 — 실시간 동기화 + 로딩 상태 자동 관리\n`;
122
+ md += `const [create] = useMutation(api.tasks.create);\n`;
123
+ md += `await create({ title: "새 태스크" });\n`;
124
+ md += `// → 서버가 WebSocket으로 useQuery를 자동 갱신 — fetchTasks() 같은 수동 리페치 불필요!\n`;
125
+ md += `\`\`\`\n\n`;
126
+
127
+ // Conditional query
128
+ md += `### 조건부 쿼리 (skip/enabled)\n\n`;
129
+ md += `선택된 항목이 없을 때 등 조건부로 쿼리를 건너뛰어야 할 때:\n\n`;
130
+ md += `\`\`\`typescript\n`;
131
+ md += `// 방법 A: "skip" 토큰 (Convex 스타일) — 추천\n`;
132
+ md += `const messages = useQuery(api.chat.getMessages,\n`;
133
+ md += ` conversationId ? { conversationId } : "skip"\n`;
134
+ md += `);\n\n`;
135
+ md += `// 방법 B: enabled 옵션 (TanStack Query 스타일)\n`;
136
+ md += `const messages = useQuery(api.chat.getMessages,\n`;
137
+ md += ` { conversationId },\n`;
138
+ md += ` { enabled: !!conversationId }\n`;
139
+ md += `);\n`;
140
+ md += `// → skip 상태에서는 API 호출 없이 undefined 반환\n`;
141
+ md += `\`\`\`\n\n`;
142
+
143
+ return md;
144
+ }
145
+
146
+ export function buildFrontendSetup() {
147
+ let md = `---\n\n## 🏗️ 프론트엔드 초기 설정 (3단계)\n\n`;
148
+ md += `> 프론트엔드에서 Gencow API를 사용하려면 아래 3단계를 설정하세요.\n`;
149
+ md += `> ⚠️ 이 설정 없이 \`useQuery\`/\`useMutation\`을 사용하면 에러가 발생합니다.\n\n`;
150
+ md += `### 1단계: Auth 클라이언트 생성\n\n`;
151
+ md += `\`\`\`typescript\n`;
152
+ md += `// src/lib/auth.ts\n`;
153
+ md += `import { gencowAuth } from "@gencow/react";\n\n`;
154
+ md += `// VITE_API_URL은 .env에서 자동 읽음 (gencow init 시 자동 설정)\n`;
155
+ md += `export const { signIn, signUp, signOut, useAuth } = gencowAuth();\n`;
156
+ md += `\`\`\`\n\n`;
157
+ md += `### 2단계: GencowProvider 설정\n\n`;
158
+ md += `\`\`\`tsx\n`;
159
+ md += `// src/main.tsx\n`;
160
+ md += `import { GencowProvider } from "@gencow/react";\n`;
161
+ md += `import { useAuth } from "./lib/auth";\n\n`;
162
+ md += `function App() {\n`;
163
+ md += ` const { token } = useAuth();\n`;
164
+ md += ` const baseUrl = import.meta.env.VITE_API_URL;\n`;
165
+ md += ` return (\n`;
166
+ md += ` <GencowProvider baseUrl={baseUrl} token={token}>\n`;
167
+ md += ` <YourApp />\n`;
168
+ md += ` </GencowProvider>\n`;
169
+ md += ` );\n`;
170
+ md += `}\n`;
171
+ md += `\`\`\`\n\n`;
172
+ md += `### 3단계: api.ts import 후 Hook 사용\n\n`;
173
+ md += `\`\`\`typescript\n`;
174
+ md += `import { api } from "@/gencow/api"; // gencow dev가 자동 생성\n`;
175
+ md += `import { useQuery, useMutation } from "@gencow/react";\n\n`;
176
+ md += `const tasks = useQuery(api.tasks.list); // 실시간 구독\n`;
177
+ md += `const [create] = useMutation(api.tasks.create); // 데이터 변경\n`;
178
+ md += `\`\`\`\n\n`;
179
+ md += `### ❌ 흔한 실수\n\n`;
180
+ md += `\`\`\`typescript\n`;
181
+ md += `// ❌ 문자열을 전달하지 마세요 — 타입 에러 발생\n`;
182
+ md += `useQuery("tasks.list"); // TS2345: string은 QueryDef에 할당 불가\n\n`;
183
+ md += `// ✅ api 객체의 정의를 전달하세요\n`;
184
+ md += `useQuery(api.tasks.list);\n\n`;
185
+ md += `// ❌ GencowProvider props\n`;
186
+ md += `<GencowProvider url="http://..."> // 'url' prop은 없습니다\n\n`;
187
+ md += `// ✅ baseUrl + token을 사용하세요\n`;
188
+ md += `<GencowProvider baseUrl={apiUrl} token={token}>\n`;
189
+ md += `\`\`\`\n\n`;
190
+ return md;
191
+ }
192
+
193
+ export function buildAuthSection() {
194
+ let md = `---\n\n## 🔐 인증 (better-auth, 세션 기반)\n\n`;
195
+ md += `| 엔드포인트 | 메서드 | 설명 |\n`;
196
+ md += `| :--- | :--- | :--- |\n`;
197
+ md += `| \`/api/auth/sign-up/email\` | POST | 회원가입 (\`{ email, password, name }\`) |\n`;
198
+ md += `| \`/api/auth/sign-in/email\` | POST | 로그인 (\`{ email, password }\`) |\n`;
199
+ md += `| \`/api/auth/sign-out\` | POST | 로그아웃 |\n\n`;
200
+ md += `> ⚠️ 모든 API 요청에 \`credentials: "include"\`를 포함해야 합니다 (세션 쿠키 전송)\n`;
201
+ md += `> ⚠️ JWT 토큰이나 \`/auth/register\` 같은 커스텀 경로를 만들지 마세요.\n\n`;
202
+ md += `### 프론트엔드 연동 (프록시 설정)\n\n`;
203
+ md += `로컬 개발 시 프론트엔드와 백엔드가 다른 포트에서 실행되므로,\n`;
204
+ md += `프록시를 설정해야 인증이 작동합니다:\n\n`;
205
+ md += `\`\`\`typescript\n`;
206
+ md += `// vite.config.ts (Vite) 또는 next.config.ts (Next.js)\n`;
207
+ md += `// 로컬 개발 전용 — 배포 시에는 VITE_API_URL 환경변수 사용\n`;
208
+ md += `const BACKEND_URL = process.env.VITE_API_URL || "http://localhost:5456";\n`;
209
+ md += `// proxy: { "/api": { target: BACKEND_URL }, "/ws": { target: BACKEND_URL, ws: true } }\n`;
210
+ md += `\`\`\`\n\n`;
211
+ md += `\`gencowAuth()\` 사용 시 반드시 \`VITE_API_URL\` 환경변수를 전달하세요:\n\n`;
212
+ md += `\`\`\`typescript\n`;
213
+ md += `const { signIn, useAuth } = gencowAuth(import.meta.env.VITE_API_URL);\n`;
214
+ md += `\`\`\`\n\n`;
215
+ md += `> ⚠️ \`VITE_API_URL\`은 \`.env\`에 설정하세요. \`gencow init\` / \`gencow deploy\` 시 자동으로 설정됩니다.\n\n`;
216
+ return md;
217
+ }
218
+
219
+ /**
220
+ * @param {ApiObj} apiObj
221
+ * @param {string[]} namespaces
222
+ */
223
+ export function buildRpcSection(apiObj, namespaces) {
224
+ let md = `---\n\n`;
225
+ md += `<details>\n`;
226
+ md += `<summary>📡 RPC 직접 호출 (Node.js / cURL / 비-React 환경)</summary>\n\n`;
227
+ md += `> ⚠️ React에서는 이 방법을 사용하지 마세요. 위의 \`useQuery\`/\`useMutation\`을 사용하세요.\n\n`;
228
+ md += `\`\`\`typescript\n`;
229
+ md += `// Query 호출 (서버 내부 self-fetch 시 GENCOW_INTERNAL_URL 자동 주입됨)\n`;
230
+ md += `const baseUrl = process.env.GENCOW_INTERNAL_URL; // 서버 부팅 시 자동 설정\n`;
231
+ md += `const res = await fetch(\`\${baseUrl}/api/query\`, {\n`;
232
+ md += ` method: "POST",\n`;
233
+ md += ` headers: { "Content-Type": "application/json" },\n`;
234
+ md += ` credentials: "include",\n`;
235
+ if (namespaces.length > 0 && Object.values(apiObj)[0]?.queries?.[0]) {
236
+ md += ` body: JSON.stringify({ name: "${namespaces[0]}.${Object.values(apiObj)[0].queries[0]}", args: {} }),\n`;
237
+ } else {
238
+ md += ` body: JSON.stringify({ name: "namespace.functionName", args: {} }),\n`;
239
+ }
240
+ md += `});\n\n`;
241
+ if (Object.values(apiObj)[0]?.mutations?.length > 0) {
242
+ const firstMut = Object.values(apiObj)[0].mutations[0];
243
+ md += `// Mutation 호출\n`;
244
+ md += `const res = await fetch(\`\${baseUrl}/api/mutation\`, {\n`;
245
+ md += ` method: "POST",\n`;
246
+ md += ` headers: { "Content-Type": "application/json" },\n`;
247
+ md += ` credentials: "include",\n`;
248
+ md += ` body: JSON.stringify({ name: "${namespaces[0]}.${firstMut}", args: { /* ... */ } }),\n`;
249
+ md += `});\n`;
250
+ }
251
+ md += `\`\`\`\n\n`;
252
+ md += `</details>\n\n`;
253
+ return md;
254
+ }
255
+
256
+ /**
257
+ * @param {ApiObj} apiObj
258
+ * @param {string[]} namespaces
259
+ */
260
+ export function buildAiPrompt(apiObj, namespaces) {
261
+ let md = `---\n\n## 🤖 AI Vibe-Coding Prompt (복사해서 AI에게 전달)\n\n`;
262
+ md += `> 아래 프롬프트를 Cursor / Claude / ChatGPT에 붙여넣으세요.\n\n`;
263
+ md += `\`\`\`\n`;
264
+ md += `다음은 현재 Gencow 백엔드에 등록된 API 구조야:\n\n`;
265
+ for (const [ns, fns] of Object.entries(apiObj)) {
266
+ md += `[${ns}]\n`;
267
+ for (const q of fns.queries) md += ` - api.${ns}.${q} (query)\n`;
268
+ for (const m of fns.mutations) md += ` - api.${ns}.${m} (mutation)\n`;
269
+ }
270
+ md += `\n`;
271
+ md += `React Hook 사용법:\n`;
272
+ md += ` useQuery(api.namespace.fnName) // 데이터 구독 (실시간 자동 갱신)\n`;
273
+ md += ` useMutation(api.namespace.fnName) // 데이터 변경 (로딩/에러 자동 관리)\n`;
274
+ md += `\n`;
275
+ md += `⚠️ 중요 규칙:\n`;
276
+ md += `- 반드시 useQuery와 useMutation을 사용해서 데이터와 연결해줘.\n`;
277
+ md += `- fetch()를 직접 호출하거나 apiPost() 같은 래퍼를 만들지 마.\n`;
278
+ md += `- gencow/api.ts는 자동 생성된 파일이야. 수동으로 만들지 마.\n`;
279
+ md += `- gencow/index.ts의 re-export는 export * as moduleName from "./moduleName" 패턴을 써.\n`;
280
+ md += ` Module, Mod 같은 접미사를 붙이지 마.\n`;
281
+ md += `\n`;
282
+ md += `⚠️ mutation 제한:\n`;
283
+ md += `- mutation은 10초 이내에 완료되어야 해. 외부 API나 LLM 호출이 길면 단계별로 분리해.\n`;
284
+ md += `- 긴 작업은 ctx.scheduler.runAfter(0, "module.nextStep", { sessionId }) 로 다음 단계를 예약.\n`;
285
+ md += `- 예: 크롤링(Step1) → 필터링(Step2) → 요약(Step3) 각각 별도 mutation으로 분리.\n`;
286
+ md += `- 같은 서버의 다른 모듈 함수를 호출할 때는 fetch()가 아닌 직접 import해서 호출해.\n`;
287
+ md += ` 예: import { fetchNews } from "./naverApi"; → const result = await fetchNews.handler(ctx, { keyword });\n`;
288
+ md += ` HTTP self-fetch (fetch("/api/mutation")) 패턴은 불필요한 네트워크 우회이므로 사용하지 마.\n`;
289
+ md += `\n`;
290
+ md += `크론 잡 (예약 작업):\n`;
291
+ md += ` gencow/crons.ts에서 cronJobs()로 선언\n`;
292
+ md += ` crons.interval("name", { minutes: N }, "module.mutation")\n`;
293
+ md += ` crons.daily("name", { hour: H }, "module.mutation")\n`;
294
+ md += ` crons.weekly("name", { dayOfWeek: D, hour: H }, "module.mutation")\n`;
295
+ md += ` crons.cron("name", "cron-expression", "module.mutation")\n`;
296
+ md += ` 반드시 export default crons 필요\n`;
297
+ md += `\n`;
298
+ md += `위 API를 기반으로 Next.js + Tailwind CSS UI 컴포넌트를 만들어줘.\n`;
299
+ md += `- TypeScript 타입을 최대한 활용하고, 로딩/에러 상태도 처리해줘.\n`;
300
+ md += `- ctx.ai.chat()을 사용해서 AI를 호출하고, OpenAI SDK를 직접 설치하지 마.\n`;
301
+ md += `배포 규칙:\n`;
302
+ md += `- 백엔드: \`npx gencow deploy\` (gencow/ 폴더만 배포됨. 프론트엔드는 포함 안 됨)\n`;
303
+ md += `- 풀스택: VITE_API_URL=https://{앱ID}.gencow.app npm run build 후 \`npx gencow deploy --static dist/\`\n`;
304
+ md += ` → 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
305
+ md += `- 프론트엔드만 배포: \`npx gencow deploy --static --no-backend dist/\`\n`;
306
+ md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 클라우드에 설정해.\n`;
307
+ md += `- .env 파일은 로컬 개발 전용이야. 클라우드에는 gencow env push로 올려.\n`;
308
+ md += `- 로컬 dev 서버에 설정하려면 \`gencow env set --local KEY=VALUE\`를 써.\n`;
309
+ md += `\n`;
310
+ md += `⚠️ 패키지 제한 (중요):\n`;
311
+ md += `- gencow 클라우드에서 사용 가능한 npm 패키지는 아래만 해당돼:\n`;
312
+ md += ` @gencow/core, drizzle-orm, better-auth, postgres, hono, ai, @ai-sdk/openai, zod, esbuild\n`;
313
+ md += `- npm install로 추가한 서드파티 패키지(langfuse, openai, axios, cheerio 등)는 배포 시 사용 불가.\n`;
314
+ md += ` 배포하면 "new version unhealthy" 에러가 발생해.\n`;
315
+ md += `\n`;
316
+ md += `⚠️ 해싱/암호화:\n`;
317
+ md += `- node:crypto 모듈은 사용 불가. 해싱이 필요하면 Web Crypto API를 사용해:\n`;
318
+ md += ` const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));\n`;
319
+ md += ` const hex = [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, "0")).join("");\n`;
320
+ md += `- 직접 구현한 simpleHash 같은 32비트 해시는 충돌 위험이 있으니 SHA-256을 사용해.\n`;
321
+ md += `- AI 호출이 필요하면 ctx.ai.chat()을 사용해. OpenAI SDK를 직접 설치하지 마.\n`;
322
+ md += `\`\`\`\n\n`;
323
+ return md;
324
+ }
325
+
326
+ export function buildAiUsageSection() {
327
+ let md = `---\n\n## 🤖 AI 사용법 (ctx.ai)\n\n`;
328
+ md += `> Gencow는 OpenAI GPT-4o/4o-mini를 내장 지원합니다.\n`;
329
+ md += `> API 키 관리, 과금, 보안을 플랫폼이 자동 처리합니다.\n\n`;
330
+ md += `### 백엔드에서 AI 호출\n\n`;
331
+ md += `\`\`\`typescript\n`;
332
+ md += `// gencow/ai.ts\n`;
333
+ md += `import { mutation, v } from "@gencow/core";\n\n`;
334
+ md += `export const chat = mutation("ai.chat", {\n`;
335
+ md += ` args: { message: v.string() },\n`;
336
+ md += ` handler: async (ctx, args) => {\n`;
337
+ md += ` const result = await ctx.ai.chat({\n`;
338
+ md += ` model: "gpt-4o-mini", // 또는 "gpt-4o"\n`;
339
+ md += ` messages: [{ role: "user", content: args.message }],\n`;
340
+ md += ` });\n`;
341
+ md += ` return result.text;\n`;
342
+ md += ` },\n`;
343
+ md += `});\n`;
344
+ md += `\`\`\`\n\n`;
345
+ md += `### 로컬 개발 설정\n\n`;
346
+ md += `로컬에서 AI 기능을 테스트하려면 \`.env\`에 키를 추가하세요:\n\n`;
347
+ md += `\`\`\`\n# .env\nOPENAI_API_KEY=sk-your-key-here\n\`\`\`\n\n`;
348
+ md += `> ⚠️ 배포 후에는 플랫폼 키가 자동 사용됩니다. .env의 키는 로컬 전용입니다.\n\n`;
349
+ md += `### ❌ 하지 마세요\n\n`;
350
+ md += `\`\`\`typescript\n`;
351
+ md += `// ❌ OpenAI SDK를 직접 설치하지 마세요\n`;
352
+ md += `import OpenAI from "openai";\n`;
353
+ md += `const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n`;
354
+ md += `// ✅ ctx.ai.chat()을 사용하세요 — 키 관리 + 과금 자동\n`;
355
+ md += `const result = await ctx.ai.chat({ model: "gpt-4o-mini", messages: [...] });\n`;
356
+ md += `\`\`\`\n\n`;
357
+ return md;
358
+ }
359
+
360
+ export function buildDeploySection() {
361
+ let md = `---\n\n## 🚀 배포\n\n`;
362
+ md += `### 클라우드 배포 (3단계)\n\n`;
363
+ md += `\`\`\`bash\n`;
364
+ md += `# 1. 로그인 (최초 1회)\n`;
365
+ md += `npx gencow login\n\n`;
366
+ md += `# 2. 배포\n`;
367
+ md += `npx gencow deploy\n\n`;
368
+ md += `# 3. 환경변수 설정 (필요 시)\n`;
369
+ md += `npx gencow env set DATABASE_URL=postgres://... # 클라우드에 설정\n`;
370
+ md += `npx gencow env list # 클라우드 환경변수 목록\n`;
371
+ md += `\`\`\`\n\n`;
372
+ md += `> 배포 후 앱은 \`https://{앱이름}.gencow.app\`에서 접근 가능\n\n`;
373
+ md += `### 환경변수 관리\n\n`;
374
+ md += `| 명령어 | 설명 |\n`;
375
+ md += `| :--- | :--- |\n`;
376
+ md += `| \`gencow env list\` | 클라우드 환경변수 목록 |\n`;
377
+ md += `| \`gencow env set KEY=VALUE\` | 클라우드 환경변수 추가/수정 |\n`;
378
+ md += `| \`gencow env unset KEY\` | 클라우드 환경변수 삭제 |\n`;
379
+ md += `| \`gencow env push\` | .env를 클라우드에 일괄 업로드 |\n`;
380
+ md += `| \`--local\` 옵션 | 로컬 dev 서버 대상 (예: \`env set --local K=V\`) |\n\n`;
381
+
382
+ // 상세 배포 가이드
383
+ md += `---\n\n## 🚀 배포\n\n`;
384
+ md += `### 백엔드 API 배포\n`;
385
+ md += `\`\`\`bash\n`;
386
+ md += `gencow deploy # gencow/ 폴더 → 클라우드 서버 배포\n`;
387
+ md += `\`\`\`\n\n`;
388
+ md += `### 프론트엔드 배포 (frontend/ 있는 경우)\n`;
389
+ md += `\`\`\`bash\n`;
390
+ md += `# 1. 백엔드 URL을 환경변수로 빌드\n`;
391
+ md += `cd frontend\n`;
392
+ md += `VITE_API_URL=https://{앱ID}.gencow.app npm run build\n\n`;
393
+ md += `# 2. --static 배포 — 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
394
+ md += `gencow deploy --static dist/\n`;
395
+ md += `\`\`\`\n\n`;
396
+ md += `> 💡 동일 프로젝트에 \`gencow/\` 폴더가 있으면 백엔드를 자동 감지하여 백엔드 → 프론트엔드 순서로 배포합니다.\n`;
397
+ md += `> 프론트엔드만 배포하려면: \`gencow deploy --static --no-backend dist/\`\n\n`;
398
+ md += `### 정적 사이트 전용 (API 없는 경우)\n`;
399
+ md += `\`\`\`bash\n`;
400
+ md += `gencow deploy --static dist/ # 순수 HTML/CSS/JS만 배포\n`;
401
+ md += `\`\`\`\n\n`;
402
+ md += `> ⚠️ 프론트엔드에서 API를 호출하려면 빌드 시 \`VITE_API_URL\`을 반드시 설정하세요.\n\n`;
403
+
404
+ // 패키지 제한 경고
405
+ md += `### ⚠️ 서드파티 npm 패키지 제한\n\n`;
406
+ md += `Gencow 클라우드 런타임에서 사용 가능한 패키지는 다음으로 제한됩니다:\n\n`;
407
+ md += `| 패키지 | 설명 |\n`;
408
+ md += `| :--- | :--- |\n`;
409
+ md += `| \`@gencow/core\` | Gencow 핵심 프레임워크 |\n`;
410
+ md += `| \`drizzle-orm\` | ORM (스키마, 쿼리 빌더) |\n`;
411
+ md += `| \`better-auth\` | 인증 시스템 |\n`;
412
+ md += `| \`postgres\` | PostgreSQL 드라이버 |\n`;
413
+ md += `| \`hono\` | HTTP 프레임워크 |\n`;
414
+ md += `| \`ai\`, \`@ai-sdk/*\` | AI SDK |\n`;
415
+ md += `| \`zod\` | 밸리데이션 |\n\n`;
416
+ md += `> ⚠️ \`npm install\`로 추가한 서드파티 패키지(langfuse, openai, axios 등)는 배포 시 사용할 수 없습니다.\n`;
417
+ md += `> 배포하면 "new version unhealthy" 에러가 발생합니다.\n`;
418
+ md += `> AI 호출이 필요하면 \`ctx.ai.chat()\`을 사용하세요.\n\n`;
419
+ md += `### CORS 설정\n\n`;
420
+ md += `- \`*.gencow.app\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
421
+ md += `- 커스텀 도메인에서 API를 호출하려면 환경변수를 설정하세요:\n\n`;
422
+ md += `\`\`\`bash\n`;
423
+ md += `gencow env set CORS_ORIGINS=https://myapp.com,https://www.myapp.com # 클라우드에 설정\n`;
424
+ md += `\`\`\`\n\n`;
425
+
426
+ // 해싱/암호화 대안
427
+ md += `### 🔐 해싱/암호화 (node:crypto 대안)\n\n`;
428
+ md += `Gencow 클라우드에서 \`node:crypto\` 모듈은 사용할 수 없습니다.\n`;
429
+ md += `해싱이 필요하면 **Web Crypto API** (\`crypto.subtle\`)를 사용하세요:\n\n`;
430
+ md += `\`\`\`typescript\n`;
431
+ md += `// SHA-256 해싱 (node:crypto 대신 Web Crypto API)\n`;
432
+ md += `async function sha256(text: string): Promise<string> {\n`;
433
+ md += ` const data = new TextEncoder().encode(text);\n`;
434
+ md += ` const hash = await crypto.subtle.digest("SHA-256", data);\n`;
435
+ md += ` return [...new Uint8Array(hash)]\n`;
436
+ md += ` .map(b => b.toString(16).padStart(2, "0"))\n`;
437
+ md += ` .join("");\n`;
438
+ md += `}\n\n`;
439
+ md += `// 사용 예: 중복 탐지, 캐시 키, 콘텐츠 해싱\n`;
440
+ md += `const articleHash = await sha256(article.url + article.title);\n`;
441
+ md += `\`\`\`\n\n`;
442
+ md += `> ⚠️ 직접 구현한 \`simpleHash()\` 같은 32비트 해시는 대량 데이터에서 충돌 위험이 있습니다.\n`;
443
+ md += `> 반드시 SHA-256을 사용하세요 (2^256 가능 값, 사실상 충돌 0).\n\n`;
444
+
445
+ return md;
446
+ }
447
+
448
+ export function buildCronSection() {
449
+ let md = `---\n\n## ⏰ Cron Jobs (예약 작업)\n\n`;
450
+ md += `\`gencow/crons.ts\` 파일에 선언하면 서버 시작 시 자동 등록됩니다.\n\n`;
451
+ md += `\`\`\`typescript\n`;
452
+ md += `import { cronJobs } from "@gencow/core";\n\n`;
453
+ md += `const crons = cronJobs();\n\n`;
454
+ md += `// 매 30분마다 실행\n`;
455
+ md += `crons.interval("sync", { minutes: 30 }, "data.sync");\n\n`;
456
+ md += `// 매일 오전 9시 실행 (서버 시간)\n`;
457
+ md += `crons.daily("report", { hour: 9 }, "reports.generate");\n\n`;
458
+ md += `// 매주 월요일 오전 10시\n`;
459
+ md += `crons.weekly("weekly", { dayOfWeek: 1, hour: 10 }, "reports.weekly");\n\n`;
460
+ md += `// 커스텀 cron 표현식\n`;
461
+ md += `crons.cron("custom", "0 */2 * * *", "custom.handler");\n\n`;
462
+ md += `export default crons; // ← 필수!\n`;
463
+ md += `\`\`\`\n\n`;
464
+ md += `> ⚠️ \`export default crons\`가 없으면 서버가 cron을 인식하지 않습니다.\n`;
465
+ md += `> ⚠️ action 문자열은 기존 mutation 이름과 정확히 매칭되어야 합니다.\n\n`;
466
+ md += `> 💡 cron 핸들러에서 mutation을 호출하려면 self-fetch를 사용하세요:\n`;
467
+ md += `> \`\`\`typescript\n`;
468
+ md += `> const baseUrl = process.env.GENCOW_INTERNAL_URL; // 서버 부팅 시 자동 설정\n`;
469
+ md += `> await fetch(\`\${baseUrl}/api/mutation\`, { method: "POST", ... });\n`;
470
+ md += `> \`\`\`\n\n`;
471
+ return md;
472
+ }
473
+
474
+ export function buildDevTips() {
475
+ let md = `---\n\n## 💡 개발 팁\n\n`;
476
+ md += `- \`gencow/\` 폴더 내 파일을 수정하면 \`api.ts\`와 이 README가 **자동으로 재생성**됩니다.\n`;
477
+ md += `- 스키마 변경 후 \`gencow db:push\`를 실행하면 DB가 즉시 동기화됩니다.\n`;
478
+ md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
479
+ md += `- 로컬 개발: \`gencow dev\` — 로컬 서버 시작\n`;
480
+ md += `- 배포된 앱: \`https://{앱ID}.gencow.app\` — .env의 VITE_API_URL에 자동 설정됨\n`;
481
+ md += `- Self-fetch: \`process.env.GENCOW_INTERNAL_URL\` — cron/mutation에서 다른 함수 호출 시 사용 (자동 설정)\n`;
482
+ md += `- .env 파일은 로컬 전용. 클라우드에는 \`gencow env push\`로 올리세요. (로컬 서버는 \`--local\`)\n`;
483
+ return md;
484
+ }
485
+
486
+ // ─── Utilities ────────────────────────────────────────────
487
+
488
+ /** @param {string} s */
489
+ function capitalize(s) {
490
+ return s.charAt(0).toUpperCase() + s.slice(1);
491
+ }
492
+
493
+ /** Marker constants for component block preservation */
494
+ export const COMP_START = "<!-- gencow-components-start -->";
495
+ export const COMP_END = "<!-- gencow-components-end -->";
496
+
497
+ /**
498
+ * Extract the components block from existing README content.
499
+ * Returns the block string if found, otherwise undefined.
500
+ *
501
+ * @param {string} existingContent
502
+ * @returns {string | undefined}
503
+ */
504
+ export function extractComponentsBlock(existingContent) {
505
+ if (!existingContent?.includes(COMP_START)) return undefined;
506
+ const startIdx = existingContent.indexOf(COMP_START);
507
+ const endIdx = existingContent.indexOf(COMP_END);
508
+ if (endIdx === -1) return undefined;
509
+ return existingContent.slice(startIdx, endIdx + COMP_END.length);
510
+ }