gencow 0.1.87 → 0.1.89

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
@@ -851,11 +851,22 @@ ${hasPrompt ? `
851
851
 
852
852
  // ── db:reset ─────────────────────────────────────────
853
853
  // TRUNCATE all user tables + seed.ts 실행.
854
- // 서버 실행 중: POST /_admin/reset API 호출
855
- // 서버 꺼진 상태 (오프라인): 기존 PGlite 파일 삭제 방식
854
+ // 로컬 전용: Cloud DB reset 위험하므로 CLI에서 미지원 (Dashboard에서만 가능)
855
+ // 사용: gencow db:reset --local
856
856
  async "db:reset"(...args) {
857
857
  const config = loadConfig();
858
858
  const cwd = process.cwd();
859
+ const isLocal = args.includes("--local");
860
+
861
+ if (!isLocal) {
862
+ // Cloud reset은 미지원 — 안내 메시지
863
+ error("db:reset은 --local 플래그가 필요합니다.");
864
+ info(`${DIM}Cloud DB 초기화는 Dashboard에서만 가능합니다 (데이터 보호).${RESET}`);
865
+ info(`${DIM}로컬 DB를 초기화하려면: gencow db:reset --local${RESET}`);
866
+ log("");
867
+ return;
868
+ }
869
+
859
870
  const port = process.env.PORT || config.port || 5456;
860
871
 
861
872
  log(`\n${BOLD}${CYAN}Gencow DB Reset${RESET}\n`);
@@ -925,47 +936,115 @@ ${hasPrompt ? `
925
936
 
926
937
  // ── db:seed ──────────────────────────────────────────
927
938
  // seed.ts 실행 (TRUNCATE 없이). 서버가 실행 중이어야 함.
928
- // 사용: gencow db:seed
929
- async "db:seed"() {
939
+ // Cloud-first: 기본=Cloud, --local=로컬
940
+ async "db:seed"(...args) {
930
941
  const config = loadConfig();
931
- const port = process.env.PORT || config.port || 5456;
942
+ const isLocal = args.includes("--local");
932
943
 
933
- log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET}\n`);
944
+ if (isLocal) {
945
+ // ── 로컬 모드: localhost 서버에 seed ──
946
+ const port = process.env.PORT || config.port || 5456;
934
947
 
935
- // 서버가 실행 중인지 확인
936
- try {
937
- const statusRes = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
938
- if (!statusRes.ok) throw new Error("Server not ready");
939
- } catch {
940
- error(`Server not running on :${port}`);
941
- info(`${DIM}gencow dev 로 서버를 먼저 시작하세요.${RESET}`);
948
+ log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(local)${RESET}\n`);
949
+
950
+ // 서버가 실행 중인지 확인
951
+ try {
952
+ const statusRes = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
953
+ if (!statusRes.ok) throw new Error("Server not ready");
954
+ } catch {
955
+ error(`Server not running on :${port}`);
956
+ info(`${DIM}gencow dev --local 로 서버를 먼저 시작하세요.${RESET}`);
957
+ log("");
958
+ return;
959
+ }
960
+
961
+ try {
962
+ const res = await fetch(`http://localhost:${port}/_admin/seed`, {
963
+ method: "POST",
964
+ headers: { "Content-Type": "application/json" },
965
+ });
966
+ const data = await res.json();
967
+
968
+ if (res.status === 404) {
969
+ warn("gencow/seed.ts not found");
970
+ info(`\n ${DIM}Create gencow/seed.ts:${RESET}\n`);
971
+ log(` ${DIM}import { tasks } from "./schema";${RESET}`);
972
+ log(` ${DIM}export default async function seed(ctx) {${RESET}`);
973
+ log(` ${DIM} await ctx.db.insert(tasks).values([${RESET}`);
974
+ log(` ${DIM} { title: "Hello World" },${RESET}`);
975
+ log(` ${DIM} ]);${RESET}`);
976
+ log(` ${DIM}};${RESET}`);
977
+ } else if (!res.ok) {
978
+ error(`Seed failed: ${data.error || "Unknown error"}`);
979
+ } else {
980
+ success("Seed 실행 완료 ✓");
981
+ }
982
+ } catch (e) {
983
+ error(`Server connection failed: ${e.message}`);
984
+ }
942
985
  log("");
943
986
  return;
944
987
  }
945
988
 
989
+ // ── Cloud 모드 (기본) ──
990
+ const gencowJsonPath = resolve(process.cwd(), "gencow.json");
991
+ let appId = null;
992
+ if (existsSync(gencowJsonPath)) {
993
+ try {
994
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
995
+ appId = gencowJson.appId || gencowJson.appName;
996
+ } catch { /* ignore parse error */ }
997
+ }
998
+
999
+ if (!appId) {
1000
+ error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
1001
+ info(`${DIM}먼저 gencow deploy 를 실행하세요.${RESET}`);
1002
+ info(`${DIM}로컬 서버에 seed하려면: gencow db:seed --local${RESET}`);
1003
+ log("");
1004
+ return;
1005
+ }
1006
+
1007
+ log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1008
+ info("Running seed.ts on cloud app...");
1009
+
1010
+ const cloudUrl = `https://${appId}.gencow.app/_admin/seed`;
1011
+
946
1012
  try {
947
- const res = await fetch(`http://localhost:${port}/_admin/seed`, {
1013
+ // 앱이 실행 중인지 확인 (status 체크)
1014
+ const statusRes = await fetch(`https://${appId}.gencow.app/_admin/status`, {
1015
+ signal: AbortSignal.timeout(5000),
1016
+ }).catch(() => null);
1017
+
1018
+ if (!statusRes || !statusRes.ok) {
1019
+ error(`Cloud app "${appId}" is not running`);
1020
+ info(`${DIM}Dashboard → Apps → ${appId} → Resume 으로 앱을 시작하세요.${RESET}`);
1021
+ info(`${DIM}또는: gencow deploy 로 배포 후 시도하세요.${RESET}`);
1022
+ log("");
1023
+ return;
1024
+ }
1025
+
1026
+ const res = await fetch(cloudUrl, {
948
1027
  method: "POST",
949
1028
  headers: { "Content-Type": "application/json" },
1029
+ signal: AbortSignal.timeout(30000), // seed는 시간이 걸릴 수 있음
950
1030
  });
951
1031
  const data = await res.json();
952
1032
 
953
1033
  if (res.status === 404) {
954
- warn("gencow/seed.ts not found");
955
- info(`\n ${DIM}Create gencow/seed.ts:${RESET}\n`);
956
- log(` ${DIM}import { tasks } from "./schema";${RESET}`);
1034
+ warn("gencow/seed.ts not found on cloud app");
1035
+ info(`\n ${DIM}Create gencow/seed.ts and redeploy:${RESET}\n`);
957
1036
  log(` ${DIM}export default async function seed(ctx) {${RESET}`);
958
- log(` ${DIM} await ctx.db.insert(tasks).values([${RESET}`);
959
- log(` ${DIM} { title: "Hello World" },${RESET}`);
960
- log(` ${DIM} ]);${RESET}`);
961
- log(` ${DIM}};${RESET}`);
1037
+ log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
1038
+ log(` ${DIM}};${RESET}\n`);
1039
+ info(`${DIM}After creating seed.ts: gencow deploy && gencow db:seed${RESET}`);
962
1040
  } else if (!res.ok) {
963
- error(`Seed failed: ${data.error || "Unknown error"}`);
1041
+ error(`Cloud seed failed: ${data.error || "Unknown error"}`);
964
1042
  } else {
965
- success("Seed 실행 완료 ✓");
1043
+ success("Cloud seed 실행 완료 ✓");
966
1044
  }
967
1045
  } catch (e) {
968
- error(`Server connection failed: ${e.message}`);
1046
+ error(`Cloud app connection failed: ${e.message}`);
1047
+ info(`${DIM}앱이 실행 중인지 확인하세요: https://${appId}.gencow.app${RESET}`);
969
1048
  }
970
1049
  log("");
971
1050
  },
@@ -1242,10 +1321,22 @@ ${hasPrompt ? `
1242
1321
  },
1243
1322
 
1244
1323
  // ── db:push ──────────────────────────────────────────
1245
- async "db:push"() {
1324
+ // Cloud-first: 기본=Cloud, --local=로컬
1325
+ async "db:push"(...args) {
1246
1326
  const config = loadConfig();
1327
+ const isLocal = args.includes("--local");
1247
1328
 
1248
- // Cloud 모드 감지: gencow.json에 appId가 있으면 Cloud DB에 push
1329
+ if (isLocal) {
1330
+ // ── 로컬 모드: 기존 동작 유지 ──
1331
+ log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(local)${RESET}\n`);
1332
+ info("Pushing schema.ts → database (no migration files)...");
1333
+ runInServer("pnpm db:push --force", buildEnv(config));
1334
+ success("Schema pushed!");
1335
+ log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
1336
+ return;
1337
+ }
1338
+
1339
+ // ── Cloud 모드 (기본) ──
1249
1340
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1250
1341
  let appId = null;
1251
1342
  if (existsSync(gencowJsonPath)) {
@@ -1255,74 +1346,73 @@ ${hasPrompt ? `
1255
1346
  } catch { /* ignore parse error */ }
1256
1347
  }
1257
1348
 
1258
- if (appId) {
1259
- // ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
1260
- const creds = requireCreds();
1261
- log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1262
- info("Pushing schema.ts → cloud database...");
1263
-
1264
- // gencow/ 폴더를 tar.gz로 패키징
1265
- const functionsDir = config.functionsDir || "./gencow";
1266
- const absoluteFunctions = resolve(process.cwd(), functionsDir);
1267
- if (!existsSync(absoluteFunctions)) {
1268
- error(`Functions directory not found: ${functionsDir}`);
1269
- process.exit(1);
1270
- }
1349
+ if (!appId) {
1350
+ error("Cloud app not found gencow.json이 없거나 appId가 설정되지 않았습니다.");
1351
+ info(`${DIM}먼저 gencow deploy 를 실행하세요.${RESET}`);
1352
+ info(`${DIM}로컬 DB push하려면: gencow db:push --local${RESET}`);
1353
+ log("");
1354
+ return;
1355
+ }
1271
1356
 
1272
- // schema.ts 존재 확인
1273
- const schemaPath = resolve(absoluteFunctions, "schema.ts");
1274
- if (!existsSync(schemaPath)) {
1275
- error("gencow/schema.ts not found — nothing to push");
1276
- process.exit(1);
1277
- }
1357
+ // ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
1358
+ const creds = requireCreds();
1359
+ log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1360
+ info("Pushing schema.ts cloud database...");
1278
1361
 
1279
- const tmpBundle = resolve(process.cwd(), ".gencow", "schema-bundle.tar.gz");
1280
- mkdirSync(dirname(tmpBundle), { recursive: true });
1362
+ // gencow/ 폴더를 tar.gz로 패키징
1363
+ const functionsDir = config.functionsDir || "./gencow";
1364
+ const absoluteFunctions = resolve(process.cwd(), functionsDir);
1365
+ if (!existsSync(absoluteFunctions)) {
1366
+ error(`Functions directory not found: ${functionsDir}`);
1367
+ process.exit(1);
1368
+ }
1281
1369
 
1282
- // tar.gz 생성: gencow/ 폴더 전체 (schema.ts + import된 파일들)
1283
- execSync(
1284
- `tar -czf "${tmpBundle}" -C "${process.cwd()}" "${functionsDir.replace(/^\.\//,'')}/"`,
1285
- { stdio: ["ignore", "pipe", "pipe"] }
1286
- );
1370
+ // schema.ts 존재 확인
1371
+ const schemaPath = resolve(absoluteFunctions, "schema.ts");
1372
+ if (!existsSync(schemaPath)) {
1373
+ error("gencow/schema.ts not found nothing to push");
1374
+ process.exit(1);
1375
+ }
1287
1376
 
1288
- const bundleData = readFileSync(tmpBundle);
1289
- info(`Bundle: ${(bundleData.length / 1024).toFixed(1)} KB`);
1377
+ const tmpBundle = resolve(process.cwd(), ".gencow", "schema-bundle.tar.gz");
1378
+ mkdirSync(dirname(tmpBundle), { recursive: true });
1290
1379
 
1291
- try {
1292
- const res = await platformFetch(
1293
- creds,
1294
- `/platform/apps/${appId}/schema-push`,
1295
- {
1296
- method: "POST",
1297
- headers: { "Content-Type": "application/octet-stream" },
1298
- body: bundleData,
1299
- }
1300
- );
1380
+ // tar.gz 생성: gencow/ 폴더 전체 (schema.ts + import된 파일들)
1381
+ execSync(
1382
+ `tar -czf "${tmpBundle}" -C "${process.cwd()}" "${functionsDir.replace(/^\.\//,'')}/"`,
1383
+ { stdio: ["ignore", "pipe", "pipe"] }
1384
+ );
1301
1385
 
1302
- const data = await res.json().catch(() => ({}));
1386
+ const bundleData = readFileSync(tmpBundle);
1387
+ info(`Bundle: ${(bundleData.length / 1024).toFixed(1)} KB`);
1303
1388
 
1304
- if (!res.ok) {
1305
- error(`Schema push failed: ${data.error || res.statusText}`);
1306
- process.exit(1);
1389
+ try {
1390
+ const res = await platformFetch(
1391
+ creds,
1392
+ `/platform/apps/${appId}/schema-push`,
1393
+ {
1394
+ method: "POST",
1395
+ headers: { "Content-Type": "application/octet-stream" },
1396
+ body: bundleData,
1307
1397
  }
1398
+ );
1308
1399
 
1309
- success("Schema pushed to cloud DB!");
1310
- log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
1311
- } catch (e) {
1312
- error(`Platform 연결 실패: ${e.message}`);
1313
- info("서버가 실행 중인지 확인하세요.");
1400
+ const data = await res.json().catch(() => ({}));
1401
+
1402
+ if (!res.ok) {
1403
+ error(`Schema push failed: ${data.error || res.statusText}`);
1314
1404
  process.exit(1);
1315
- } finally {
1316
- // 임시 번들 정리
1317
- try { unlinkSync(tmpBundle); } catch { /* ignore */ }
1318
1405
  }
1319
- } else {
1320
- // ── 로컬 모드: 기존 동작 유지 ──
1321
- log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(local)${RESET}\n`);
1322
- info("Pushing schema.ts → database (no migration files)...");
1323
- runInServer("pnpm db:push --force", buildEnv(config));
1324
- success("Schema pushed!");
1406
+
1407
+ success("Schema pushed to cloud DB!");
1325
1408
  log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
1409
+ } catch (e) {
1410
+ error(`Platform 연결 실패: ${e.message}`);
1411
+ info("서버가 실행 중인지 확인하세요.");
1412
+ process.exit(1);
1413
+ } finally {
1414
+ // 임시 번들 정리
1415
+ try { unlinkSync(tmpBundle); } catch { /* ignore */ }
1326
1416
  }
1327
1417
  },
1328
1418
 
@@ -1458,10 +1548,13 @@ ${BOLD}Dev commands:${RESET}
1458
1548
  ${DIM}--local Use local PGlite instead of cloud${RESET}
1459
1549
  ${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
1460
1550
  ${GREEN}dashboard${RESET} Open the Gencow admin dashboard in browser
1461
- ${GREEN}db:push${RESET} Sync schema.ts → DB instantly ${DIM}(auto-detects cloud/local)${RESET}
1551
+ ${GREEN}db:push${RESET} Sync schema.ts → cloud DB ${DIM}(default: cloud)${RESET}
1552
+ ${DIM}--local Push to local DB instead of cloud${RESET}
1462
1553
  ${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
1463
- ${GREEN}db:seed${RESET} Run gencow/seed.ts ${DIM}(insert seed data)${RESET}
1464
- ${GREEN}db:reset${RESET} TRUNCATE all data + run seed.ts ${DIM}(or delete DB if offline)${RESET}
1554
+ ${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app ${DIM}(default: cloud)${RESET}
1555
+ ${DIM}--local Seed local DB instead of cloud${RESET}
1556
+ ${GREEN}db:reset${RESET} TRUNCATE all data + run seed.ts ${DIM}(local only)${RESET}
1557
+ ${DIM}--local Required — local DB reset only (cloud: use Dashboard)${RESET}
1465
1558
  ${GREEN}db:studio${RESET} Open Drizzle Studio ${DIM}(visual DB browser)${RESET}
1466
1559
  ${GREEN}add <comp...>${RESET} Add components ${DIM}(AI RAG Tools Memory Analytics)${RESET}
1467
1560
  ${GREEN}mcp${RESET} Start MCP server for AI agent integration ${DIM}(stdio)${RESET}
@@ -3222,10 +3315,10 @@ process.exit(0);
3222
3315
  info(`Use ${GREEN}gencow env set KEY=VALUE${RESET} to add one.`);
3223
3316
  } else {
3224
3317
  const maxLen = Math.max(...vars.map(v => v.key.length), 4);
3225
- log(` ${DIM}${"NAME".padEnd(maxLen)} ENV${RESET}`);
3226
- log(` ${DIM}${"─".repeat(maxLen)} ${"─".repeat(6)}${RESET}`);
3318
+ log(` ${DIM}${"NAME".padEnd(maxLen)}${RESET}`);
3319
+ log(` ${DIM}${"─".repeat(maxLen)}${RESET}`);
3227
3320
  for (const v of vars) {
3228
- log(` ${BOLD}${v.key.padEnd(maxLen)}${RESET} ${DIM}${v.env}${RESET}`);
3321
+ log(` ${BOLD}${v.key.padEnd(maxLen)}${RESET}`);
3229
3322
  }
3230
3323
  }
3231
3324
  log("");
@@ -3245,7 +3338,7 @@ process.exit(0);
3245
3338
  for (const pair of pairs) {
3246
3339
  const eqIdx = pair.indexOf("=");
3247
3340
  if (eqIdx < 1) { error(`Invalid format: ${pair}. Use KEY=VALUE`); continue; }
3248
- const name = pair.slice(0, eqIdx).trim();
3341
+ const name = pair.slice(0, eqIdx).trim().toUpperCase().replace(/[^A-Z0-9_]/g, "");
3249
3342
  const value = pair.slice(eqIdx + 1).trim();
3250
3343
  if (!name) { error("Key name cannot be empty"); continue; }
3251
3344
  if (!value) { error(`Value for ${name} cannot be empty`); continue; }
@@ -3274,7 +3367,8 @@ process.exit(0);
3274
3367
  if (subcmd === "unset" || subcmd === "remove" || subcmd === "delete") {
3275
3368
  const keys = filteredArgs.slice(1);
3276
3369
  if (keys.length === 0) { error("Usage: gencow env unset KEY [KEY2 ...]"); return; }
3277
- for (const key of keys) {
3370
+ for (const rawKey of keys) {
3371
+ const key = rawKey.toUpperCase().replace(/[^A-Z0-9_]/g, "");
3278
3372
  try {
3279
3373
  const res = await platformFetch(creds, `/platform/apps/${appId}/env/${encodeURIComponent(key)}`, {
3280
3374
  method: "DELETE",
@@ -3304,7 +3398,7 @@ process.exit(0);
3304
3398
  if (!trimmed || trimmed.startsWith("#")) continue;
3305
3399
  const eqIdx = trimmed.indexOf("=");
3306
3400
  if (eqIdx < 1) continue;
3307
- const key = trimmed.slice(0, eqIdx).trim();
3401
+ const key = trimmed.slice(0, eqIdx).trim().toUpperCase().replace(/[^A-Z0-9_]/g, "");
3308
3402
  let val = trimmed.slice(eqIdx + 1).trim();
3309
3403
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
3310
3404
  val = val.slice(1, -1);