gencow 0.1.119 → 0.1.121

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 CHANGED
@@ -303,7 +303,8 @@ function _ensureEsbuildConsistency() {
303
303
  }
304
304
 
305
305
  /**
306
- * 모노레포 루트의 .npmrc에 esbuild hoisting 차단 규칙을 추가.
306
+ * 모노레포 루트의 .pnpmrc에 esbuild hoisting 차단 규칙을 추가.
307
+ * (.npmrc가 아닌 .pnpmrc — hoist-pattern은 pnpm 전용이므로 npm 경고 방지)
307
308
  * 이미 존재하면 스킵.
308
309
  */
309
310
  function _patchNpmrcForEsbuild(cwd) {
@@ -312,8 +313,9 @@ function _patchNpmrcForEsbuild(cwd) {
312
313
  let dir = cwd;
313
314
  for (let i = 0; i < 10; i++) {
314
315
  if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) {
315
- const npmrcPath = resolve(dir, ".npmrc");
316
- const content = existsSync(npmrcPath) ? readFileSync(npmrcPath, "utf8") : "";
316
+ // .pnpmrc 대상 (pnpm 전용 설정 → npm "Unknown project config" 경고 방지)
317
+ const pnpmrcPath = resolve(dir, ".pnpmrc");
318
+ const content = existsSync(pnpmrcPath) ? readFileSync(pnpmrcPath, "utf8") : "";
317
319
  if (!content.includes("hoist-pattern[]=!@esbuild/*")) {
318
320
  const patch = [
319
321
  "",
@@ -323,8 +325,8 @@ function _patchNpmrcForEsbuild(cwd) {
323
325
  "hoist-pattern[]=!esbuild",
324
326
  "",
325
327
  ].join("\n");
326
- appendFileSync(npmrcPath, patch);
327
- info(".npmrc에 esbuild hoisting 차단 규칙을 추가했습니다.");
328
+ appendFileSync(pnpmrcPath, patch);
329
+ info(".pnpmrc에 esbuild hoisting 차단 규칙을 추가했습니다.");
328
330
  info("다음 pnpm install 시 영구 적용됩니다.");
329
331
  }
330
332
  return;
@@ -333,7 +335,7 @@ function _patchNpmrcForEsbuild(cwd) {
333
335
  if (parent === dir) break;
334
336
  dir = parent;
335
337
  }
336
- } catch { /* .npmrc 수정 실패 — 무시 (즉시 우회로 이번 실행은 문제 없음) */ }
338
+ } catch { /* .pnpmrc 수정 실패 — 무시 (즉시 우회로 이번 실행은 문제 없음) */ }
337
339
  }
338
340
 
339
341
  function findServerRoot() {
@@ -1197,7 +1199,7 @@ ${hasPrompt ? `
1197
1199
 
1198
1200
  if (!appId) {
1199
1201
  error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
1200
- info(`${DIM}먼저 gencow deploy 를 실행하세요.${RESET}`);
1202
+ info(`${DIM}먼저 gencow dev 를 실행하세요.${RESET}`);
1201
1203
  info(`${DIM}로컬 서버에 seed하려면: gencow db:seed --local${RESET}`);
1202
1204
  log("");
1203
1205
  return;
@@ -1218,7 +1220,7 @@ ${hasPrompt ? `
1218
1220
  if (!statusRes || !statusRes.ok) {
1219
1221
  error(`Cloud app "${appId}" is not running`);
1220
1222
  info(`${DIM}Dashboard → Apps → ${appId} → Resume 으로 앱을 시작하세요.${RESET}`);
1221
- info(`${DIM}또는: gencow deploy배포 후 시도하세요.${RESET}`);
1223
+ info(`${DIM}또는: gencow dev 생성 후 시도하세요.${RESET}`);
1222
1224
  log("");
1223
1225
  return;
1224
1226
  }
@@ -1236,7 +1238,7 @@ ${hasPrompt ? `
1236
1238
  log(` ${DIM}export default async function seed(ctx) {${RESET}`);
1237
1239
  log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
1238
1240
  log(` ${DIM}};${RESET}\n`);
1239
- info(`${DIM}After creating seed.ts: gencow deploy && gencow db:seed${RESET}`);
1241
+ info(`${DIM}After creating seed.ts: gencow dev 실행 gencow db:seed${RESET}`);
1240
1242
  } else if (!res.ok) {
1241
1243
  error(`Cloud seed failed: ${data.error || "Unknown error"}`);
1242
1244
  } else {
@@ -1555,7 +1557,7 @@ ${hasPrompt ? `
1555
1557
 
1556
1558
  if (!appId) {
1557
1559
  error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
1558
- info(`${DIM}먼저 gencow deploy 를 실행하세요.${RESET}`);
1560
+ info(`${DIM}먼저 gencow dev 를 실행하세요.${RESET}`);
1559
1561
  info(`${DIM}로컬 DB에 push하려면: gencow db:push --local${RESET}`);
1560
1562
  log("");
1561
1563
  return;
@@ -1564,7 +1566,7 @@ ${hasPrompt ? `
1564
1566
  // ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
1565
1567
  const creds = requireCreds();
1566
1568
  log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1567
- warn("명령어 실행 전 먼저 gencow deploy 를 통해 최신 코드가 배포되어 있어야 합니다.");
1569
+ warn("명령어 실행 전 먼저 gencow dev 를 통해 최신 코드가 배포되어 있어야 합니다.");
1568
1570
  log("");
1569
1571
  info("Pushing schema.ts → cloud database...");
1570
1572
 
@@ -1805,10 +1807,11 @@ ${BOLD}BaaS commands (login required):${RESET}
1805
1807
  ${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
1806
1808
  ${GREEN}logout${RESET} Clear credentials
1807
1809
  ${GREEN}whoami${RESET} Show current user info
1808
- ${GREEN}deploy${RESET} Bundle gencow/ and deploy to platform
1809
- ${DIM}--static [dir] Deploy static files (dist/, out/, build/)${RESET}
1810
- ${DIM}--no-backend Skip backend auto-deploy in --static mode${RESET}
1811
- ${DIM}--prod Deploy to production app (Pro+, auto-creates)${RESET}
1810
+ ${GREEN}static [dir]${RESET} Deploy static files to dev ${DIM}(dist/, out/, build/)${RESET}
1811
+ ${DIM}--no-backend Skip backend auto-deploy${RESET}
1812
+ ${DIM}--force, -f Skip dependency audit${RESET}
1813
+ ${GREEN}deploy${RESET} Deploy to production ${DIM}(Pro+ only)${RESET}
1814
+ ${DIM}--static [dir] Deploy static files to production${RESET}
1812
1815
  ${DIM}--rollback Rollback to previous deployment${RESET}
1813
1816
  ${DIM}--force, -f Skip dependency audit${RESET}
1814
1817
  ${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
@@ -1985,14 +1988,55 @@ ${BOLD}Examples:${RESET}
1985
1988
  }
1986
1989
  },
1987
1990
 
1988
- // ── deploy ────────────────────────────────────────────
1991
+ // ── static — dev 정적 파일 배포 (독립 명령어) ─────────
1992
+ async static(...staticArgs) {
1993
+ const creds = requireCreds();
1994
+
1995
+ let staticDir = null;
1996
+ let forceDeploy = false;
1997
+ let noBackend = false;
1998
+ let appId = null;
1999
+
2000
+ for (let i = 0; i < staticArgs.length; i++) {
2001
+ const a = staticArgs[i];
2002
+ if (a === "--force" || a === "-f") forceDeploy = true;
2003
+ else if (a === "--no-backend") noBackend = true;
2004
+ else if (a === "--app" || a === "-a") appId = staticArgs[++i];
2005
+ else if (!a.startsWith("-")) {
2006
+ staticDir = a;
2007
+ }
2008
+ }
2009
+
2010
+ // gencow.json에서 appId 로드
2011
+ const gencowJsonPath = resolve(process.cwd(), "gencow.json");
2012
+ if (!appId && existsSync(gencowJsonPath)) {
2013
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
2014
+ appId = gencowJson.appId || gencowJson.appName;
2015
+ }
2016
+
2017
+ // displayName
2018
+ let displayName = null;
2019
+ const configPath = resolve(process.cwd(), "gencow.config.ts");
2020
+ if (existsSync(configPath)) {
2021
+ const content = readFileSync(configPath, "utf8");
2022
+ const match = content.match(/name:\s*["']([^"']+)["']/);
2023
+ if (match) displayName = match[1];
2024
+ }
2025
+ if (!displayName) displayName = basename(process.cwd());
2026
+
2027
+ return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, {
2028
+ forceDeploy, noBackend, envTarget: "dev",
2029
+ });
2030
+ },
2031
+
2032
+ // ── deploy — 프로덕션 배포 (Pro+ only) ───────────────
1989
2033
  async deploy(...deployArgs) {
1990
2034
  const creds = requireCreds();
1991
2035
 
1992
2036
  // gencow.json에서 앱 ID 확인 (자동 생성된 유니크 ID)
1993
2037
  let appId = null;
1994
2038
  let displayName = null;
1995
- let envTarget = "dev";
2039
+ let envTarget = "prod"; // v0.1.120: deploy = prod 기본값
1996
2040
  let staticDir = null; // --static 옵션
1997
2041
  let isStatic = false;
1998
2042
  let forceDeploy = false; // --force: skip dependency audit
@@ -2061,7 +2105,12 @@ ${BOLD}Examples:${RESET}
2061
2105
 
2062
2106
  for (let i = 0; i < deployArgs.length; i++) {
2063
2107
  const a = deployArgs[i];
2064
- if (a === "--prod") envTarget = "prod";
2108
+ if (a === "--prod") {
2109
+ // deprecated: deploy는 이미 prod 기본값
2110
+ warn(`${YELLOW}--prod 플래그는 더 이상 필요하지 않습니다.${RESET}`);
2111
+ info(`gencow deploy는 기본적으로 프로덕션에 배포합니다.`);
2112
+ envTarget = "prod";
2113
+ }
2065
2114
  else if (a === "--force" || a === "-f") forceDeploy = true;
2066
2115
  else if (a === "--no-backend") noBackend = true;
2067
2116
  else if (a === "--rollback") isRollback = true;
@@ -2080,13 +2129,16 @@ ${BOLD}Examples:${RESET}
2080
2129
  error(`알 수 없는 deploy 인자: "${a}"`);
2081
2130
  log("");
2082
2131
  info(`사용법: gencow deploy [옵션]`);
2083
- info(` gencow deploy 백엔드 배포`);
2084
- info(` gencow deploy --static 정적 파일 배포`);
2132
+ info(` gencow deploy 프로덕션 배포 (Pro+)`);
2133
+ info(` gencow deploy --static 프로덕션 정적 배포 (Pro+)`);
2085
2134
  info(` gencow deploy --rollback 이전 버전으로 롤백`);
2086
- info(` gencow deploy --prod 프로덕션 배포`);
2087
2135
  info(` gencow deploy logs 서버 로그 조회`);
2088
2136
  info(` gencow deploy status 앱 상태 확인`);
2089
2137
  info(` gencow deploy --force 의존성 감사 스킵`);
2138
+ log("");
2139
+ info(`💡 개발 환경 배포:`);
2140
+ info(` gencow dev 백엔드 실시간 배포`);
2141
+ info(` gencow static [dir] 정적 파일 배포`);
2090
2142
  process.exit(1);
2091
2143
  }
2092
2144
  }
@@ -2199,8 +2251,18 @@ ${BOLD}Examples:${RESET}
2199
2251
  if (!createProdRes.ok) {
2200
2252
  const errData = await createProdRes.json().catch(() => ({}));
2201
2253
  if (createProdRes.status === 403) {
2202
- error(errData.error || "Production deploys require Pro plan or higher.");
2203
- info("Run: gencow upgrade");
2254
+ log("");
2255
+ log(` ${RED}⛔ Production Deploy — Pro 플랜 이상 필요${RESET}`);
2256
+ log("");
2257
+ log(` ${DIM}gencow deploy는 프로덕션 환경에 배포하는 명령어입니다.${RESET}`);
2258
+ log("");
2259
+ log(` ${BOLD}💡 개발 환경 배포:${RESET}`);
2260
+ log(` ${GREEN}gencow dev${RESET} ${DIM}← 백엔드 실시간 배포 + 라이브 로그${RESET}`);
2261
+ log(` ${GREEN}gencow static dist/${RESET} ${DIM}← 정적 파일(dist/) 배포${RESET}`);
2262
+ log("");
2263
+ log(` ${BOLD}🚀 프로덕션 배포 잠금 해제:${RESET}`);
2264
+ log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro 플랜으로 업그레이드${RESET}`);
2265
+ log("");
2204
2266
  } else {
2205
2267
  error(`Prod 앱 생성 실패: ${errData.error || createProdRes.statusText}`);
2206
2268
  }
@@ -2635,7 +2697,7 @@ ${BOLD}Examples:${RESET}
2635
2697
  if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
2636
2698
  error(`정적 파일 폴더를 찾을 수 없습니다.`);
2637
2699
  info(`자동 감지 순서: ${AUTO_DETECT.join(", ")}`);
2638
- info(`수동 지정: gencow deploy --static <dir>`);
2700
+ info(`수동 지정: gencow static <dir>`);
2639
2701
  process.exit(1);
2640
2702
  }
2641
2703
 
@@ -3069,7 +3131,7 @@ ${BOLD}Examples:${RESET}
3069
3131
 
3070
3132
  // --prod인데 prod 앱이 없는 경우
3071
3133
  if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
3072
- error("Prod 앱이 아직 없습니다. gencow deploy --prod를 먼저 실행하세요.");
3134
+ error("Prod 앱이 아직 없습니다. gencow deploy를 먼저 실행하세요.");
3073
3135
  return;
3074
3136
  }
3075
3137
 
package/core/index.js CHANGED
@@ -1928,7 +1928,7 @@ function ownerRls(userIdColumn, options) {
1928
1928
  "[ownerRls] userIdColumn must have a .name property. Ensure you pass a valid Drizzle column reference (e.g. t.userId)."
1929
1929
  );
1930
1930
  }
1931
- const isOwner = sql`${userIdColumn} = current_setting('app.current_user_id')`;
1931
+ const isOwner = sql`${userIdColumn} = current_setting('app.current_user_id', true)`;
1932
1932
  const meta = {
1933
1933
  columnName: colName,
1934
1934
  readPublic: options?.read === "public"
@@ -1966,6 +1966,10 @@ function createRlsDb(db, userId) {
1966
1966
  // ../core/src/crud.ts
1967
1967
  import { eq, ne, gt, gte, lt, lte, desc, asc, like, ilike, inArray, notInArray, or, and, count as drizzleCount, getTableName, getTableColumns } from "drizzle-orm";
1968
1968
  import { getTableConfig } from "drizzle-orm/pg-core";
1969
+ var _ownerRlsTables = [];
1970
+ function getOwnerRlsTables() {
1971
+ return _ownerRlsTables;
1972
+ }
1969
1973
  function detectIdType(column) {
1970
1974
  const colType = column.dataType;
1971
1975
  if (colType === "string") return v.string();
@@ -2001,6 +2005,10 @@ function detectOwnerMeta(table) {
2001
2005
  registerOwnerRls(table, { columnName: colName, readPublic: false });
2002
2006
  return { column: userIdCol, columnName: colName, propertyName: propName, readPublic: false };
2003
2007
  }
2008
+ const tblName = getTableName(table);
2009
+ console.warn(
2010
+ `[crud] \u26A0\uFE0F Table "${tblName}" has ${config.policies.length} pgPolicy but no userId/user_id column found. ownerRls auto-isolation will NOT be applied. If you used ownerRls(), ensure the column is named 'userId' (JS) / 'user_id' (DB).`
2011
+ );
2004
2012
  }
2005
2013
  } catch {
2006
2014
  }
@@ -2088,6 +2096,13 @@ function crud(table, options) {
2088
2096
  const defaultOrderCol = createdAtCol || pk;
2089
2097
  const userIdCol = anyTable["userId"];
2090
2098
  const ownerMeta = detectOwnerMeta(table);
2099
+ if (ownerMeta && !_ownerRlsTables.some((t) => t.tableName === tableName)) {
2100
+ _ownerRlsTables.push({
2101
+ tableName,
2102
+ columnName: ownerMeta.columnName,
2103
+ readPublic: ownerMeta.readPublic
2104
+ });
2105
+ }
2091
2106
  if (ownerMeta && isPublic && !ownerMeta.readPublic) {
2092
2107
  console.warn(
2093
2108
  `[crud] \u26A0\uFE0F Table "${tableName}": ownerRls detected but public=true. CUD operations will still enforce ownerRls (auth required). Consider removing { public: true } or using ownerRls(col, { read: "public" }).`
@@ -2116,15 +2131,23 @@ function crud(table, options) {
2116
2131
  }
2117
2132
  return conditions.length > 0 ? and(...conditions) : void 0;
2118
2133
  }
2134
+ async function inRlsOrPlainTx(db, fn) {
2135
+ if (typeof db?.transaction === "function") {
2136
+ return await db.transaction(fn);
2137
+ }
2138
+ return await fn(db);
2139
+ }
2119
2140
  async function fetchListWithTotal(db, whereClause, userId) {
2120
2141
  let effectiveWhere = whereClause;
2121
2142
  if (ownerMeta && userId && !ownerMeta.readPublic) {
2122
2143
  const ownerFilter = eq(ownerMeta.column, userId);
2123
2144
  effectiveWhere = effectiveWhere ? and(effectiveWhere, ownerFilter) : ownerFilter;
2124
2145
  }
2125
- const data = await db.select().from(anyTable).where(effectiveWhere).orderBy(desc(defaultOrderCol));
2126
- const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(effectiveWhere);
2127
- return { data, total: Number(countResult[0]?.count ?? 0) };
2146
+ return await inRlsOrPlainTx(db, async (tx) => {
2147
+ const data = await tx.select().from(anyTable).where(effectiveWhere).orderBy(desc(defaultOrderCol));
2148
+ const countResult = await tx.select({ count: drizzleCount() }).from(anyTable).where(effectiveWhere);
2149
+ return { data, total: Number(countResult[0]?.count ?? 0) };
2150
+ });
2128
2151
  }
2129
2152
  const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
2130
2153
  const listDef = !enabledMethods.has("list") ? void 0 : query(`${prefix}.list`, {
@@ -2155,12 +2178,14 @@ function crud(table, options) {
2155
2178
  } else {
2156
2179
  orderByClause = desc(defaultOrderCol);
2157
2180
  }
2158
- const results = await ctx.db.select().from(anyTable).where(whereClause).orderBy(orderByClause).limit(limit).offset(offset);
2159
- const countResult = await ctx.db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
2160
- return {
2161
- data: results,
2162
- total: Number(countResult[0]?.count ?? 0)
2163
- };
2181
+ return await inRlsOrPlainTx(ctx.db, async (tx) => {
2182
+ const results = await tx.select().from(anyTable).where(whereClause).orderBy(orderByClause).limit(limit).offset(offset);
2183
+ const countResult = await tx.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
2184
+ return {
2185
+ data: results,
2186
+ total: Number(countResult[0]?.count ?? 0)
2187
+ };
2188
+ });
2164
2189
  }
2165
2190
  });
2166
2191
  const getDef = !enabledMethods.has("get") ? void 0 : query(`${prefix}.get`, {
@@ -2177,8 +2202,10 @@ function crud(table, options) {
2177
2202
  const sdField = anyTable[options.softDelete.field];
2178
2203
  whereCond = and(whereCond, eq(sdField, null));
2179
2204
  }
2180
- const [result] = await ctx.db.select().from(anyTable).where(whereCond).limit(1);
2181
- return result ?? null;
2205
+ return await inRlsOrPlainTx(ctx.db, async (tx) => {
2206
+ const [result] = await tx.select().from(anyTable).where(whereCond).limit(1);
2207
+ return result ?? null;
2208
+ });
2182
2209
  }
2183
2210
  });
2184
2211
  const createDef = !enabledMethods.has("create") ? void 0 : mutation(`${prefix}.create`, {
@@ -2186,6 +2213,12 @@ function crud(table, options) {
2186
2213
  handler: async (ctx, args) => {
2187
2214
  const user = ownerMeta || !isPublic ? ctx.auth.requireAuth() : null;
2188
2215
  let insertData = { ...args };
2216
+ if (ownerMeta && user) {
2217
+ const requestedOwner = insertData[ownerMeta.propertyName] ?? insertData[ownerMeta.columnName] ?? insertData.userId;
2218
+ if (requestedOwner != null && requestedOwner !== user.id) {
2219
+ throw new Error("Forbidden: cannot create resource for another user");
2220
+ }
2221
+ }
2189
2222
  if (ownerMeta && user) {
2190
2223
  insertData[ownerMeta.propertyName] = user.id;
2191
2224
  } else if (userIdCol && user && !insertData.userId) {
@@ -2194,7 +2227,10 @@ function crud(table, options) {
2194
2227
  if (options?.hooks?.beforeCreate) {
2195
2228
  insertData = await options.hooks.beforeCreate(insertData);
2196
2229
  }
2197
- const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
2230
+ const [result] = await inRlsOrPlainTx(
2231
+ ctx.db,
2232
+ async (tx) => tx.insert(anyTable).values(insertData).returning()
2233
+ );
2198
2234
  if (useRealtime && enabledMethods.has("list")) {
2199
2235
  const currentUserId = user?.id;
2200
2236
  const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
@@ -2223,7 +2259,10 @@ function crud(table, options) {
2223
2259
  if (options?.hooks?.beforeUpdate) {
2224
2260
  updateData = await options.hooks.beforeUpdate(updateData);
2225
2261
  }
2226
- const [result] = await ctx.db.update(anyTable).set(updateData).where(updateWhere).returning();
2262
+ const [result] = await inRlsOrPlainTx(
2263
+ ctx.db,
2264
+ async (tx) => tx.update(anyTable).set(updateData).where(updateWhere).returning()
2265
+ );
2227
2266
  if (useRealtime) {
2228
2267
  const currentUserId = ownerMeta ? user?.id : void 0;
2229
2268
  if (enabledMethods.has("list")) {
@@ -2245,12 +2284,14 @@ function crud(table, options) {
2245
2284
  if (ownerMeta && user) {
2246
2285
  deleteWhere = and(eq(pk, args.id), eq(ownerMeta.column, user.id));
2247
2286
  }
2248
- if (options?.softDelete) {
2249
- const sdField = options.softDelete.field;
2250
- await ctx.db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(deleteWhere);
2251
- } else {
2252
- await ctx.db.delete(anyTable).where(deleteWhere);
2253
- }
2287
+ await inRlsOrPlainTx(ctx.db, async (tx) => {
2288
+ if (options?.softDelete) {
2289
+ const sdField = options.softDelete.field;
2290
+ await tx.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(deleteWhere);
2291
+ } else {
2292
+ await tx.delete(anyTable).where(deleteWhere);
2293
+ }
2294
+ });
2254
2295
  if (useRealtime && enabledMethods.has("list")) {
2255
2296
  const currentUserId = ownerMeta ? user?.id : void 0;
2256
2297
  const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
@@ -2279,6 +2320,7 @@ export {
2279
2320
  deregisterClient,
2280
2321
  crud as gencowCrud,
2281
2322
  getOwnerRlsMeta,
2323
+ getOwnerRlsTables,
2282
2324
  getQueryDef,
2283
2325
  getQueryHandler,
2284
2326
  getRegisteredHttpActions,
@@ -252,8 +252,9 @@ describe("buildAiPrompt — AI Vibe-Coding 프롬프트", () => {
252
252
 
253
253
  it("배포 규칙 포함", () => {
254
254
  const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
255
+ expect(md).toContain("npx gencow dev");
256
+ expect(md).toContain("npx gencow static");
255
257
  expect(md).toContain("npx gencow deploy");
256
- expect(md).toContain("--static");
257
258
  expect(md).toContain("--no-backend");
258
259
  expect(md).toContain("백엔드가 감지되면");
259
260
  });
@@ -232,7 +232,7 @@ export function buildAuthSection() {
232
232
  md += `\`\`\`typescript\n`;
233
233
  md += `const { signIn, useAuth } = createAuthClient(import.meta.env.VITE_API_URL);\n`;
234
234
  md += `\`\`\`\n\n`;
235
- md += `> ⚠️ \`VITE_API_URL\`은 \`.env\`에 설정하세요. \`gencow init\` / \`gencow deploy\` 시 자동으로 설정됩니다.\n\n`;
235
+ md += `> ⚠️ \`VITE_API_URL\`은 \`.env\`에 설정하세요. \`gencow init\` / \`gencow dev\` 시 자동으로 설정됩니다.\n\n`;
236
236
  return md;
237
237
  }
238
238
 
@@ -334,10 +334,12 @@ export function buildAiPrompt(apiObj, namespaces) {
334
334
  md += `- TypeScript 타입을 최대한 활용하고, 로딩/에러 상태도 처리해줘.\n`;
335
335
  md += `- import { ai } from "./ai"; + ai.chat()을 사용해서 AI를 호출하고, OpenAI SDK를 직접 설치하지 마.\n`;
336
336
  md += `배포 규칙:\n`;
337
- md += `- 백엔드: \`npx gencow deploy\` (gencow/ 폴더만 배포됨. 프론트엔드는 포함 안 됨)\n`;
338
- md += `- 풀스택: VITE_API_URL=https://{앱ID}.{도메인} npm run build 후 \`npx gencow deploy --static dist/\`\n`;
337
+ md += `- 개발: \`npx gencow dev\` (실시간 배포 — gencow/ 변경 감지 자동 배포)\n`;
338
+ md += `- 정적 파일 배포: \`npx gencow static dist/\` (dev 환경, 1회성)\n`;
339
+ md += `- 프로덕션: \`npx gencow deploy\` (Pro+ only, prod 환경)\n`;
340
+ md += `- 풀스택 정적: VITE_API_URL=https://{앱ID}.{도메인} npm run build 후 \`npx gencow static dist/\`\n`;
339
341
  md += ` → 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
340
- md += `- 프론트엔드만 배포: \`npx gencow deploy --static --no-backend dist/\`\n`;
342
+ md += `- 프론트엔드만 배포: \`npx gencow static --no-backend dist/\`\n`;
341
343
  md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 클라우드에 설정해. 즉시 반영 (재시작 불필요).\n`;
342
344
  md += `- process.env.KEY는 반드시 handler 내부에서 접근해 (모듈 상단 캐싱 시 hot-reload 안 됨).\n`;
343
345
  md += `- .env 파일은 로컬 개발 전용이야. 클라우드에는 gencow env push로 올려.\n`;
@@ -469,8 +471,11 @@ export function buildDeploySection() {
469
471
  md += `\`\`\`bash\n`;
470
472
  md += `# 1. 로그인 (최초 1회)\n`;
471
473
  md += `npx gencow login\n\n`;
472
- md += `# 2. 배포 (마이그레이션 자동 생성 후 번들링)\n`;
473
- md += `npx gencow deploy\n\n`;
474
+ md += `# 2. 개발 (실시간 배포)
475
+ `;
476
+ md += `npx gencow dev\n\n`;
477
+ md += `# 3. 배포 후 URL 확인\n`;
478
+ md += `# → https://{앱이름}.{도메인}\n\n`;
474
479
  md += `# 3. 환경변수 설정 (필요 시)\n`;
475
480
  md += `npx gencow env set DATABASE_URL=postgres://... # 클라우드에 설정\n`;
476
481
  md += `npx gencow env list # 클라우드 환경변수 목록\n`;
@@ -499,23 +504,28 @@ export function buildDeploySection() {
499
504
 
500
505
  // 상세 배포 가이드
501
506
  md += `---\n\n## 🚀 배포\n\n`;
502
- md += `### 백엔드 API 배포\n`;
507
+ md += `### 백엔드 API 개발 (실시간)\n`;
503
508
  md += `\`\`\`bash\n`;
504
- md += `gencow deploy # (1) drizzle-kit generate 자동 실행 (2) gencow/ 번들 (3) 서버 배포\n`;
509
+ md += `gencow dev # Watch + 자동 배포 (schema 변경 drizzle-kit generate 자동)\n`;
505
510
  md += `\`\`\`\n\n`;
506
511
  md += `### 프론트엔드 배포 (frontend/ 있는 경우)\n`;
507
512
  md += `\`\`\`bash\n`;
508
513
  md += `# 1. 백엔드 URL을 환경변수로 빌드\n`;
509
514
  md += `cd frontend\n`;
510
515
  md += `VITE_API_URL=https://{앱ID}.{도메인} npm run build\n\n`;
511
- md += `# 2. --static 배포 — 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
512
- md += `gencow deploy --static dist/\n`;
516
+ md += `# 2. 정적 배포 — 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
517
+ md += `gencow static dist/\n`;
513
518
  md += `\`\`\`\n\n`;
514
519
  md += `> 💡 동일 프로젝트에 \`gencow/\` 폴더가 있으면 백엔드를 자동 감지하여 백엔드 → 프론트엔드 순서로 배포합니다.\n`;
515
- md += `> 프론트엔드만 배포하려면: \`gencow deploy --static --no-backend dist/\`\n\n`;
520
+ md += `> 프론트엔드만 배포하려면: \`gencow static --no-backend dist/\`\n\n`;
516
521
  md += `### 정적 사이트 전용 (API 없는 경우)\n`;
517
522
  md += `\`\`\`bash\n`;
518
- md += `gencow deploy --static dist/ # 순수 HTML/CSS/JS만 배포\n`;
523
+ md += `gencow static dist/ # 순수 HTML/CSS/JS만 배포\n`;
524
+ md += `\`\`\`\n\n`;
525
+ md += `### 프로덕션 배포 (Pro+ only)\n`;
526
+ md += `\`\`\`bash\n`;
527
+ md += `gencow deploy # 백엔드를 프로덕션에 배포\n`;
528
+ md += `gencow deploy --static dist/ # 정적 파일을 프로덕션에 배포\n`;
519
529
  md += `\`\`\`\n\n`;
520
530
  md += `> ⚠️ 프론트엔드에서 API를 호출하려면 빌드 시 \`VITE_API_URL\`을 반드시 설정하세요.\n\n`;
521
531
 
@@ -532,7 +542,7 @@ export function buildDeploySection() {
532
542
  md += `| \`hono\` | HTTP 프레임워크 |\n`;
533
543
  md += `| \`ai\`, \`@ai-sdk/*\` | AI SDK |\n`;
534
544
  md += `| \`zod\` | 밸리데이션 |\n\n`;
535
- md += `> 📦 langfuse, axios, cheerio 등 추가 패키지도 \`npm install\` 후 \`gencow deploy\`하면 자동 설치됩니다.\n`;
545
+ md += `> 📦 langfuse, axios, cheerio 등 추가 패키지도 \`npm install\` 후 배포하면 자동 설치됩니다.\n`;
536
546
  md += `> ⛔ \`child_process\`, \`vm\`, \`os\`, \`cluster\`, \`worker_threads\` 모듈은 보안상 차단됩니다.\n\n`;
537
547
  md += `### CORS 설정\n\n`;
538
548
  md += `- \`*.{BASE_DOMAIN}\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
@@ -593,7 +603,7 @@ export function buildDevTips() {
593
603
  md += `- \`gencow/\` 폴더 내 파일을 수정하면 \`api.ts\`와 이 README가 **자동으로 재생성**됩니다.\n`;
594
604
  md += `- 스키마 변경 후:\n`;
595
605
  md += ` - 로컬 개발 + 배포: \`gencow dev\` 실행 시 자동으로 \`drizzle-kit generate\` 실행 → \`gencow/migrations/\` 생성\n`;
596
- md += ` - \`gencow deploy\`도 번들링 전 자동으로 \`drizzle-kit generate\` 실행 — schema.ts 변경 후 그냥 deploy 하면 됨\n`;
606
+ md += ` - \`gencow deploy\`(프로덕션)도 번들링 전 자동으로 \`drizzle-kit generate\` 실행\n`;
597
607
  md += ` - ⚠️ drizzle.config.ts 없으면 generate 실패 경고 출력 (deploy는 계속 진행, 하지만 migrations/ 없으면 플랫폼이 스키마 스킵)\n`;
598
608
  md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
599
609
  md += `- 로컬 개발: \`gencow dev\` — 로컬 서버 시작\n`;
package/package.json CHANGED
@@ -1,33 +1,32 @@
1
1
  {
2
- "name": "gencow",
3
- "version": "0.1.119",
4
- "description": "Gencow — AI Backend Engine",
5
- "type": "module",
6
- "bin": {
7
- "gencow": "./bin/gencow.mjs"
8
- },
9
- "files": [
10
- "bin/",
11
- "lib/",
12
- "templates/",
13
- "server/",
14
- "core/",
15
- "scripts/",
16
- "dashboard/"
17
- ],
18
- "scripts": {
19
- "build": "node scripts/bundle-server.mjs",
20
- "prepublishOnly": "npm run build && node scripts/pre-publish-check.mjs"
21
- },
22
- "dependencies": {
23
- "@modelcontextprotocol/sdk": "^1.27.1",
24
- "open": "^10.1.0",
25
- "tar": "^7",
26
- "ws": "^8.19.0",
27
- "zod": "^4.3.6"
28
- },
29
- "devDependencies": {
30
- "@types/node": "^25",
31
- "esbuild": "^0.27.3"
32
- }
33
- }
2
+ "name": "gencow",
3
+ "version": "0.1.121",
4
+ "description": "Gencow — AI Backend Engine",
5
+ "type": "module",
6
+ "bin": {
7
+ "gencow": "./bin/gencow.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "templates/",
13
+ "server/",
14
+ "core/",
15
+ "scripts/",
16
+ "dashboard/"
17
+ ],
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.27.1",
20
+ "open": "^10.1.0",
21
+ "tar": "^7",
22
+ "ws": "^8.19.0",
23
+ "zod": "^4.3.6"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25",
27
+ "esbuild": "^0.27.3"
28
+ },
29
+ "scripts": {
30
+ "build": "node scripts/bundle-server.mjs"
31
+ }
32
+ }