gencow 0.1.90 → 0.1.91
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 +9 -29
- package/lib/readme-codegen.mjs +4 -3
- package/package.json +1 -1
- package/server/index.js +124 -2
- package/server/index.js.map +3 -3
- package/templates/SECURITY.md +13 -9
- package/templates/admin-tool/README.md +23 -0
- package/templates/admin-tool/index.ts +6 -0
- package/templates/admin-tool/items.ts +107 -0
- package/templates/admin-tool/schema.ts +24 -0
package/bin/gencow.mjs
CHANGED
|
@@ -131,34 +131,13 @@ function loadConfig() {
|
|
|
131
131
|
const configPath = resolve(cwd, "gencow.config.ts");
|
|
132
132
|
const jsConfigPath = resolve(cwd, "gencow.config.js");
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (parent === dir) break;
|
|
142
|
-
dir = parent;
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
145
|
-
})();
|
|
146
|
-
|
|
147
|
-
const defaults = isMonorepo
|
|
148
|
-
? {
|
|
149
|
-
functionsDir: "./apps/sample/gencow",
|
|
150
|
-
schema: "./apps/sample/gencow/schema.ts",
|
|
151
|
-
storage: "./apps/sample/uploads",
|
|
152
|
-
db: { url: "./.gencow/data" },
|
|
153
|
-
port: 5456,
|
|
154
|
-
}
|
|
155
|
-
: {
|
|
156
|
-
functionsDir: "./gencow",
|
|
157
|
-
schema: "./gencow/schema.ts",
|
|
158
|
-
storage: "./.gencow/uploads",
|
|
159
|
-
db: { url: "./.gencow/data" },
|
|
160
|
-
port: 5456,
|
|
161
|
-
};
|
|
134
|
+
const defaults = {
|
|
135
|
+
functionsDir: "./gencow",
|
|
136
|
+
schema: "./gencow/schema.ts",
|
|
137
|
+
storage: "./.gencow/uploads",
|
|
138
|
+
db: { url: "./.gencow/data" },
|
|
139
|
+
port: 5456,
|
|
140
|
+
};
|
|
162
141
|
|
|
163
142
|
// Try reading .ts config — extract values via simple regex (no full TS execution needed)
|
|
164
143
|
const path = existsSync(configPath) ? configPath : existsSync(jsConfigPath) ? jsConfigPath : null;
|
|
@@ -234,7 +213,7 @@ function findServerRoot() {
|
|
|
234
213
|
function buildEnv(config) {
|
|
235
214
|
const cwd = process.cwd();
|
|
236
215
|
|
|
237
|
-
// Load .env from the app directory (e.g.
|
|
216
|
+
// Load .env from the app directory (e.g. gencow/.env)
|
|
238
217
|
// so that OPENAI_API_KEY and other user-defined env vars are available to the server
|
|
239
218
|
const appEnv = {};
|
|
240
219
|
const appDir = resolve(cwd, config.functionsDir, "..");
|
|
@@ -490,6 +469,7 @@ const commands = {
|
|
|
490
469
|
_templates: [
|
|
491
470
|
{ id: "default", label: "빈 프로젝트", deps: {} },
|
|
492
471
|
{ id: "task-app", label: "Task + Files CRUD 백엔드", deps: {} },
|
|
472
|
+
{ id: "admin-tool", label: "관리자/내부 도구 (인증 없음)", deps: {} },
|
|
493
473
|
{ id: "fullstack", label: "Tasks + Files + AI Chat + Agent 백엔드", deps: { "ai": "^4.0.0", "@ai-sdk/openai": "^1.0.0" } },
|
|
494
474
|
{ id: "ai-chat", label: "AI 챗봇 + Memory 백엔드", deps: { "ai": "^4.0.0", "@ai-sdk/openai": "^1.0.0" } },
|
|
495
475
|
],
|
package/lib/readme-codegen.mjs
CHANGED
|
@@ -514,10 +514,11 @@ export function buildCronSection() {
|
|
|
514
514
|
md += `\`\`\`\n\n`;
|
|
515
515
|
md += `> ⚠️ \`export default crons\`가 없으면 서버가 cron을 인식하지 않습니다.\n`;
|
|
516
516
|
md += `> ⚠️ action 문자열은 기존 mutation 이름과 정확히 매칭되어야 합니다.\n\n`;
|
|
517
|
-
md += `> 💡 cron 핸들러에서
|
|
517
|
+
md += `> 💡 cron 핸들러에서 다른 모듈의 함수를 호출하려면 직접 import하세요:\n`;
|
|
518
518
|
md += `> \`\`\`typescript\n`;
|
|
519
|
-
md += `>
|
|
520
|
-
md += `> await
|
|
519
|
+
md += `> import { fetchNews } from "./crawler";\n`;
|
|
520
|
+
md += `> const result = await fetchNews.handler(ctx, { keyword });\n`;
|
|
521
|
+
md += `> // ❌ fetch("/api/mutation") self-fetch는 사용하지 마세요\n`;
|
|
521
522
|
md += `> \`\`\`\n\n`;
|
|
522
523
|
return md;
|
|
523
524
|
}
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -72155,9 +72155,20 @@ function printAuditReport(result, functionsPath) {
|
|
|
72155
72155
|
function auditSchemaPatterns(schemaPath) {
|
|
72156
72156
|
try {
|
|
72157
72157
|
const fs2 = __require("fs");
|
|
72158
|
+
const path2 = __require("path");
|
|
72158
72159
|
const content = fs2.readFileSync(schemaPath, "utf-8");
|
|
72159
|
-
const
|
|
72160
|
+
const fileName = path2.basename(schemaPath);
|
|
72161
|
+
if (fileName === "auth-schema.ts") return;
|
|
72160
72162
|
const warnings = [];
|
|
72163
|
+
const hasPgTableImport = /import\s*\{[^}]*\bpgTable\b[^}]*\}\s*from\s*["']drizzle-orm\/pg-core["']/.test(content);
|
|
72164
|
+
const hasGencowTableImport = /import\s*\{[^}]*\bgencowTable\b[^}]*\}\s*from\s*["']@gencow\/core["']/.test(content);
|
|
72165
|
+
if (hasPgTableImport && !hasGencowTableImport) {
|
|
72166
|
+
warnings.push(` \u26D4 pgTable\uC744 \uC9C1\uC811 \uC0AC\uC6A9\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4. gencowTable\uB85C \uC804\uD658\uD558\uC138\uC694.`);
|
|
72167
|
+
warnings.push(` \u2192 import { gencowTable } from "@gencow/core";`);
|
|
72168
|
+
warnings.push(` \u2192 \uAD00\uB9AC\uC790 \uC571: gencowTable("name", columns, { filter: () => true })`);
|
|
72169
|
+
warnings.push(` \u2192 \uBA40\uD2F0\uC720\uC800 \uC571: gencowTable("name", columns, ownerFilter("userId"))`);
|
|
72170
|
+
}
|
|
72171
|
+
const tableRegex = /export\s+const\s+(\w+)\s*=\s*pgTable\s*\(\s*"([^"]+)"\s*,\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
72161
72172
|
const SYSTEM_TABLES = /* @__PURE__ */ new Set([
|
|
72162
72173
|
"user",
|
|
72163
72174
|
"session",
|
|
@@ -72195,7 +72206,7 @@ function auditSchemaPatterns(schemaPath) {
|
|
|
72195
72206
|
}
|
|
72196
72207
|
if (warnings.length > 0) {
|
|
72197
72208
|
console.log(`
|
|
72198
|
-
[auditor] \u{1F50D} \uC2A4\uD0A4\uB9C8 \uBCF4\uC548 \uD328\uD134 \uC810\uAC80 (${
|
|
72209
|
+
[auditor] \u{1F50D} \uC2A4\uD0A4\uB9C8 \uBCF4\uC548 \uD328\uD134 \uC810\uAC80 (${fileName}):`);
|
|
72199
72210
|
for (const w of warnings) {
|
|
72200
72211
|
console.log(`[auditor] ${w}`);
|
|
72201
72212
|
}
|
|
@@ -72205,6 +72216,112 @@ function auditSchemaPatterns(schemaPath) {
|
|
|
72205
72216
|
} catch {
|
|
72206
72217
|
}
|
|
72207
72218
|
}
|
|
72219
|
+
function auditSelfFetch(functionsDir) {
|
|
72220
|
+
const usages = [];
|
|
72221
|
+
try {
|
|
72222
|
+
const fs2 = __require("fs");
|
|
72223
|
+
const path2 = __require("path");
|
|
72224
|
+
const files = findTsFiles(functionsDir);
|
|
72225
|
+
const selfFetchPattern = /\/api\/(query|mutation)/;
|
|
72226
|
+
for (const file3 of files) {
|
|
72227
|
+
const content = fs2.readFileSync(file3, "utf-8");
|
|
72228
|
+
const lines = content.split("\n");
|
|
72229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72230
|
+
const line = lines[i];
|
|
72231
|
+
const trimmed = line.trim();
|
|
72232
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
72233
|
+
if (line.includes("fetch") && selfFetchPattern.test(line)) {
|
|
72234
|
+
usages.push({
|
|
72235
|
+
file: path2.relative(functionsDir, file3),
|
|
72236
|
+
line: i + 1,
|
|
72237
|
+
snippet: trimmed
|
|
72238
|
+
});
|
|
72239
|
+
}
|
|
72240
|
+
}
|
|
72241
|
+
}
|
|
72242
|
+
} catch {
|
|
72243
|
+
}
|
|
72244
|
+
return { usages };
|
|
72245
|
+
}
|
|
72246
|
+
function printSelfFetchReport(result) {
|
|
72247
|
+
if (result.usages.length === 0) return;
|
|
72248
|
+
console.log(`
|
|
72249
|
+
[auditor] \u26A0\uFE0F Self-fetch detected in ${result.usages.length} location(s):`);
|
|
72250
|
+
for (const u of result.usages) {
|
|
72251
|
+
console.log(`[auditor] ${u.file}:${u.line} \u2192 ${u.snippet}`);
|
|
72252
|
+
}
|
|
72253
|
+
console.log(`[auditor] \u2192 \uAC19\uC740 \uC11C\uBC84\uC758 \uD568\uC218\uB294 \uC9C1\uC811 import\uD558\uC138\uC694:`);
|
|
72254
|
+
console.log(`[auditor] import { fetchNews } from "./crawler";`);
|
|
72255
|
+
console.log(`[auditor] const result = await fetchNews.handler(ctx, { keyword });`);
|
|
72256
|
+
console.log(`[auditor] \u2192 fetch("/api/mutation") \uD328\uD134\uC740 \uBD88\uD544\uC694\uD55C \uB124\uD2B8\uC6CC\uD06C \uC6B0\uD68C\uC785\uB2C8\uB2E4.
|
|
72257
|
+
`);
|
|
72258
|
+
}
|
|
72259
|
+
function auditFrontendAntiPatterns(projectDir) {
|
|
72260
|
+
const issues = [];
|
|
72261
|
+
try {
|
|
72262
|
+
const fs2 = __require("fs");
|
|
72263
|
+
const path2 = __require("path");
|
|
72264
|
+
const srcDir = path2.join(projectDir, "src");
|
|
72265
|
+
if (!fs2.existsSync(srcDir)) return { issues };
|
|
72266
|
+
const SUSPECT_NAMES = /* @__PURE__ */ new Set([
|
|
72267
|
+
"api.ts",
|
|
72268
|
+
"api.tsx",
|
|
72269
|
+
"api-client.ts",
|
|
72270
|
+
"api-client.tsx",
|
|
72271
|
+
"rpc.ts",
|
|
72272
|
+
"rpc.tsx",
|
|
72273
|
+
"client.ts",
|
|
72274
|
+
"client.tsx"
|
|
72275
|
+
]);
|
|
72276
|
+
const selfFetchPattern = /\/api\/(query|mutation)/;
|
|
72277
|
+
const allFiles = findTsFiles(srcDir);
|
|
72278
|
+
for (const file3 of allFiles) {
|
|
72279
|
+
const basename = path2.basename(file3);
|
|
72280
|
+
const relPath = path2.relative(projectDir, file3);
|
|
72281
|
+
if (relPath.startsWith("gencow/") || relPath.startsWith("gencow\\")) continue;
|
|
72282
|
+
if (SUSPECT_NAMES.has(basename)) {
|
|
72283
|
+
const content = fs2.readFileSync(file3, "utf-8");
|
|
72284
|
+
if (selfFetchPattern.test(content)) {
|
|
72285
|
+
issues.push({
|
|
72286
|
+
file: relPath,
|
|
72287
|
+
reason: `fetch("/api/query" \uB610\uB294 "/api/mutation") \uC9C1\uC811 \uD638\uCD9C \uAC10\uC9C0 \u2014 \uC774 \uD30C\uC77C\uC744 \uC0AD\uC81C\uD558\uC138\uC694`
|
|
72288
|
+
});
|
|
72289
|
+
}
|
|
72290
|
+
}
|
|
72291
|
+
}
|
|
72292
|
+
} catch {
|
|
72293
|
+
}
|
|
72294
|
+
return { issues };
|
|
72295
|
+
}
|
|
72296
|
+
function printFrontendAntiPatternReport(result) {
|
|
72297
|
+
if (result.issues.length === 0) return;
|
|
72298
|
+
console.log(`
|
|
72299
|
+
[auditor] \u26A0\uFE0F Custom API client detected:`);
|
|
72300
|
+
for (const issue3 of result.issues) {
|
|
72301
|
+
console.log(`[auditor] ${issue3.file} \u2014 ${issue3.reason}`);
|
|
72302
|
+
}
|
|
72303
|
+
console.log(`[auditor] \u2192 gencow/api.ts (\uC790\uB3D9 \uC0DD\uC131)\uC640 useQuery/useMutation\uC744 \uC0AC\uC6A9\uD558\uC138\uC694`);
|
|
72304
|
+
console.log(`[auditor] \u2192 \u{1F4C4} README.md "\u274C \uC808\uB300 \uD558\uC9C0 \uB9C8\uC138\uC694" \uC139\uC158 \uCC38\uC870
|
|
72305
|
+
`);
|
|
72306
|
+
}
|
|
72307
|
+
function findTsFiles(dir) {
|
|
72308
|
+
const fs2 = __require("fs");
|
|
72309
|
+
const path2 = __require("path");
|
|
72310
|
+
const results = [];
|
|
72311
|
+
try {
|
|
72312
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
72313
|
+
for (const entry of entries) {
|
|
72314
|
+
const fullPath = path2.join(dir, entry.name);
|
|
72315
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
72316
|
+
results.push(...findTsFiles(fullPath));
|
|
72317
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
|
|
72318
|
+
results.push(fullPath);
|
|
72319
|
+
}
|
|
72320
|
+
}
|
|
72321
|
+
} catch {
|
|
72322
|
+
}
|
|
72323
|
+
return results;
|
|
72324
|
+
}
|
|
72208
72325
|
|
|
72209
72326
|
// ../server/src/admin.ts
|
|
72210
72327
|
init_src();
|
|
@@ -73083,6 +73200,11 @@ async function main() {
|
|
|
73083
73200
|
if (existsSync4(schemaFile)) {
|
|
73084
73201
|
auditSchemaPatterns(schemaFile);
|
|
73085
73202
|
}
|
|
73203
|
+
const selfFetchResult = auditSelfFetch(functionsPath);
|
|
73204
|
+
printSelfFetchReport(selfFetchResult);
|
|
73205
|
+
const projectDir = resolve5(functionsPath, "..");
|
|
73206
|
+
const frontendResult = auditFrontendAntiPatterns(projectDir);
|
|
73207
|
+
printFrontendAntiPatternReport(frontendResult);
|
|
73086
73208
|
appModule = await import(functionsPath);
|
|
73087
73209
|
const regQ = getRegisteredQueries().length;
|
|
73088
73210
|
const regM = getRegisteredMutations().length;
|