gencow 0.1.185 → 0.1.187

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
@@ -349,7 +349,7 @@ ${BOLD}Usage:${RESET}
349
349
 
350
350
  ${BOLD}Quick Start:${RESET}
351
351
  ${GREEN}init <name>${RESET} Create a new Gencow project
352
- ${DIM}--template, -t Select template (default, task-app, fullstack, ai-chat)${RESET}
352
+ ${DIM}--template, -t Select template (default, task-app, admin-tool, fullstack, ai-chat)${RESET}
353
353
  ${DIM}--force, -f Initialize in non-empty directory (preserves existing files)${RESET}
354
354
  ${GREEN}add <comp...>${RESET} Add components ${DIM}(AI Agent RAG Tools Memory ...)${RESET}
355
355
  ${GREEN}codegen${RESET} Generate frontend api.ts from schema
@@ -361,6 +361,7 @@ ${BOLD}Commands (login required):${RESET}
361
361
  ${GREEN}logout${RESET} Clear credentials
362
362
  ${GREEN}whoami${RESET} Show current user info
363
363
  ${GREEN}dev${RESET} Watch + auto-deploy to cloud + live logs
364
+ ${DIM}--local Run local Bun + Hono + PGlite development${RESET}
364
365
  ${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
365
366
  ${GREEN}db:push${RESET} Apply versioned migrations to cloud DB
366
367
  ${DIM}--prod Push to production DB (confirmation required)${RESET}
@@ -378,7 +379,7 @@ ${BOLD}Commands (login required):${RESET}
378
379
  ${GREEN}templates clone <slug>${RESET} Clone a template into a local directory
379
380
  ${GREEN}deploy${RESET} Deploy backend to cloud ${DIM}(dev by default)${RESET}
380
381
  ${DIM}--static [dir] Deploy backend, then static files${RESET}
381
- ${DIM}--prod Deploy to production (Pro+ only)${RESET}
382
+ ${DIM}--prod Deploy to production (Startup/Enterprise)${RESET}
382
383
  ${DIM}--rollback Rollback to previous deployment${RESET}
383
384
  ${DIM}--force, -f Skip optional dependency scan${RESET}
384
385
  ${DIM}logs [-n N] Show cloud app server logs${RESET}
@@ -386,7 +387,7 @@ ${BOLD}Commands (login required):${RESET}
386
387
  ${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
387
388
  ${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
388
389
  ${GREEN}env unset KEY${RESET} Remove cloud env var
389
- ${GREEN}env push${RESET} Push .env to cloud ${DIM}(--prod reads .env.production)${RESET}
390
+ ${GREEN}env push${RESET} Push backend env file ${DIM}(gencow/.env by default)${RESET}
390
391
  ${GREEN}cors list${RESET} Show legacy CORS overrides
391
392
  ${GREEN}cors add URL${RESET} Add legacy CORS override ${DIM}(compat only)${RESET}
392
393
  ${GREEN}cors remove URL${RESET} Remove legacy CORS override
@@ -439,7 +440,7 @@ ${BOLD}Examples:${RESET}
439
440
  ${DIM}# Deploy built frontend files only:${RESET}
440
441
  gencow static dist/
441
442
 
442
- ${DIM}# Deploy to production (Pro+ only):${RESET}
443
+ ${DIM}# Deploy to production (Startup/Enterprise):${RESET}
443
444
  gencow deploy --prod
444
445
 
445
446
  ${DIM}# Inspect stale workflows:${RESET}
@@ -7,9 +7,27 @@ import {
7
7
  selectAppDiagnosticLogLines,
8
8
  } from "./app-diagnostics.mjs";
9
9
  import { resolveCreatedAppId, resolveCreatedAppResponse } from "./app-create-response.mjs";
10
+ import {
11
+ isExactAppDeleteResponse,
12
+ parseAppListResponse,
13
+ parseAppStatusResponse,
14
+ } from "./app-response-contract.mjs";
15
+ import { readJsonObjectResponse } from "./http-response-json.mjs";
10
16
  import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
11
17
  import { loadCreds, rpcMutation, rpcQuery, saveCreds, requireCreds } from "./platform-client.mjs";
12
18
 
19
+ function appResponseStatus(response) {
20
+ return Number.isSafeInteger(response?.status) ? `HTTP ${response.status}` : "unknown HTTP status";
21
+ }
22
+
23
+ function invalidAppResponse(operation, response, kind) {
24
+ return `CLI_APP_RESPONSE_INVALID: ${operation} returned an invalid ${kind} response (${appResponseStatus(response)})`;
25
+ }
26
+
27
+ function appResponseError(body, fallback) {
28
+ return typeof body?.error === "string" && body.error.trim() ? body.error : fallback;
29
+ }
30
+
13
31
  export function formatAppDeployedAgo(dateStr, now = Date.now()) {
14
32
  if (!dateStr) return `${DIM}never${RESET}`;
15
33
  const diff = now - new Date(dateStr).getTime();
@@ -109,11 +127,18 @@ export function createAppCommand({
109
127
  if (!subcmd || subcmd === "list") {
110
128
  logImpl(`\n${BOLD}${CYAN}Your Apps${RESET}\n`);
111
129
  const res = await rpcQueryImpl(creds, "apps.list");
130
+ const body = await res.json().catch(() => null);
112
131
  if (!res.ok) {
113
- errorImpl((await res.json().catch(() => ({}))).error || "Failed to list apps");
132
+ errorImpl(appResponseError(body, invalidAppResponse("app list", res, "error")));
133
+ processRef.exit(1);
134
+ return;
135
+ }
136
+ const apps = parseAppListResponse(body);
137
+ if (!apps) {
138
+ errorImpl(invalidAppResponse("app list", res, "success"));
139
+ processRef.exit(1);
114
140
  return;
115
141
  }
116
- const apps = await res.json();
117
142
  if (!apps.length) {
118
143
  infoImpl("No apps yet. Run: gencow app create <name>");
119
144
  return;
@@ -143,10 +168,16 @@ export function createAppCommand({
143
168
  logImpl(`\n${BOLD}${CYAN}Gencow App Create${RESET}\n`);
144
169
  infoImpl(`Creating app "${name}"...`);
145
170
  const res = await rpcMutationImpl(creds, "apps.create", { name });
146
- const data = await res.json();
171
+ const data = await readJsonObjectResponse(res);
147
172
  if (!res.ok) {
148
- errorImpl(data.error || "Failed to create app");
173
+ errorImpl(appResponseError(data, invalidAppResponse("app create", res, "error")));
149
174
  processRef.exit(1);
175
+ return;
176
+ }
177
+ if (!data) {
178
+ errorImpl(invalidAppResponse("app create", res, "success"));
179
+ processRef.exit(1);
180
+ return;
150
181
  }
151
182
 
152
183
  let createdApp;
@@ -241,10 +272,16 @@ ${dashboardLine}
241
272
 
242
273
  infoImpl(`Deleting app "${name}"...`);
243
274
  const delRes = await rpcMutationImpl(creds, "apps.delete", { name });
244
- const delData = await delRes.json();
275
+ const delData = await readJsonObjectResponse(delRes);
245
276
  if (!delRes.ok) {
246
- errorImpl(delData.error || "Failed to delete app");
277
+ errorImpl(appResponseError(delData, invalidAppResponse("app delete", delRes, "error")));
247
278
  processRef.exit(1);
279
+ return;
280
+ }
281
+ if (!isExactAppDeleteResponse(delData, name)) {
282
+ errorImpl(invalidAppResponse("app delete", delRes, "success"));
283
+ processRef.exit(1);
284
+ return;
248
285
  }
249
286
 
250
287
  const currentCreds = loadCredsImpl();
@@ -261,17 +298,20 @@ ${dashboardLine}
261
298
  if (!name) {
262
299
  errorImpl("Usage: gencow app status <name>");
263
300
  processRef.exit(1);
301
+ return;
264
302
  }
265
303
  const res = await rpcQueryImpl(creds, "apps.get", { name });
266
- const body = await res.json();
267
- const data = body?.result || body;
304
+ const body = await readJsonObjectResponse(res);
268
305
  if (!res.ok) {
269
- errorImpl(data?.error || "App not found");
306
+ errorImpl(appResponseError(body, invalidAppResponse("app status", res, "error")));
270
307
  processRef.exit(1);
308
+ return;
271
309
  }
310
+ const data = parseAppStatusResponse(body, name);
272
311
  if (!data) {
273
- errorImpl("App not found");
312
+ errorImpl(invalidAppResponse("app status", res, "success"));
274
313
  processRef.exit(1);
314
+ return;
275
315
  }
276
316
  logImpl(`\n ${BOLD}${data.name}${RESET}`);
277
317
  const status = formatAppStatusForCli(data);
@@ -0,0 +1,49 @@
1
+ function record(value) {
2
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3
+ }
4
+
5
+ function nonEmptyString(value) {
6
+ return typeof value === "string" && value.trim().length > 0;
7
+ }
8
+
9
+ function optionalString(value) {
10
+ return value == null || typeof value === "string";
11
+ }
12
+
13
+ export function parseAppListResponse(value) {
14
+ if (!Array.isArray(value)) return null;
15
+ return value.every((row) => {
16
+ const app = record(row);
17
+ return (
18
+ nonEmptyString(app?.name) &&
19
+ nonEmptyString(app?.status) &&
20
+ optionalString(app?.url) &&
21
+ optionalString(app?.lastDeployedAt)
22
+ );
23
+ })
24
+ ? value
25
+ : null;
26
+ }
27
+
28
+ export function parseAppStatusResponse(value, expectedAppId) {
29
+ const body = record(value);
30
+ if (!body) return null;
31
+ const hasEnvelope = Object.prototype.hasOwnProperty.call(body, "result");
32
+ const app = record(hasEnvelope ? body.result : body);
33
+ if (
34
+ !app ||
35
+ app.name !== expectedAppId ||
36
+ !nonEmptyString(app.status) ||
37
+ !Number.isSafeInteger(app.port) ||
38
+ app.port <= 0 ||
39
+ app.port > 65535
40
+ ) {
41
+ return null;
42
+ }
43
+ return app;
44
+ }
45
+
46
+ export function isExactAppDeleteResponse(value, expectedAppId) {
47
+ const body = record(value);
48
+ return body?.success === true && body.deleted === expectedAppId;
49
+ }
@@ -94,7 +94,7 @@ function renderBackupHelp(logImpl = log, errorImpl = error, subCmd = null) {
94
94
  logImpl(` gencow backup restore <id> Restore from backup`);
95
95
  logImpl(` gencow backup restore-file <path> Restore from a downloaded dump file`);
96
96
  logImpl(` gencow backup delete <id> Delete a backup`);
97
- logImpl(` gencow backup download <id> Download backup file (Pro+)\n`);
97
+ logImpl(` gencow backup download <id> Download backup file (plan-dependent)\n`);
98
98
  }
99
99
 
100
100
  export function buildRestoreConfirmPrompt(sourceLabel, { includeStorageRisk = false } = {}) {
@@ -299,9 +299,9 @@ export function createDbCommands({
299
299
  cleanupOldBackups(backupDir, 3);
300
300
 
301
301
  rmSyncImpl(dbPath, { recursive: true, force: true });
302
- successImpl("DB deleted — restart with gencow dev:local to create a new one");
302
+ successImpl("DB deleted — restart with gencow dev --local to create a new one");
303
303
  } else {
304
- infoImpl("DB does not exist yet. Run gencow dev:local to get started.");
304
+ infoImpl("DB does not exist yet. Run gencow dev --local to get started.");
305
305
  }
306
306
  }
307
307
  logImpl("");
@@ -327,7 +327,7 @@ export function createDbCommands({
327
327
  if (!statusRes.ok) throw new Error("Server not ready");
328
328
  } catch {
329
329
  errorImpl(`Server not running on :${port}`);
330
- infoImpl(`${DIM}Start the local server first: ${GREEN}gencow dev:local${RESET}`);
330
+ infoImpl(`${DIM}Start the local server first: ${GREEN}gencow dev --local${RESET}`);
331
331
  logImpl("");
332
332
  return;
333
333
  }
@@ -451,7 +451,7 @@ export function createDbCommands({
451
451
  }
452
452
  cpSyncImpl(latestPath, dbPath, { recursive: true });
453
453
  successImpl(`Restored from: .gencow/backups/${latest}`);
454
- infoImpl("Restart with gencow dev.");
454
+ infoImpl("Restart with gencow dev --local.");
455
455
  logImpl("");
456
456
  },
457
457
 
@@ -781,7 +781,7 @@ export function formatBundleSizeWarning(result) {
781
781
  " • 불필요한 devDependencies를 dependencies에서 제거하세요",
782
782
  " • 무거운 패키지를 가벼운 대안으로 교체해보세요",
783
783
  " (예: moment → dayjs, lodash → lodash-es)",
784
- " • 메모리 한도가 부족하면 Pro/Scale 플랜으로 업그레이드하세요",
784
+ " • 메모리 한도가 부족하면 Startup/Enterprise 플랜을 확인하세요",
785
785
  " (Hobby: 256MB, Pro: 512MB, Scale: 1024MB)",
786
786
  "",
787
787
  " 💡 이 경고는 배포를 차단하지 않습니다.",
@@ -12,7 +12,7 @@ export function renderDeployHelp() {
12
12
  log(` ${BOLD}Usage:${RESET} gencow deploy [options]\n`);
13
13
  log(` ${BOLD}Options:${RESET}`);
14
14
  log(` ${DIM}--static [dir]${RESET} Deploy backend first, then static files`);
15
- log(` ${DIM}--prod${RESET} Deploy to production (Pro+ only)`);
15
+ log(` ${DIM}--prod${RESET} Deploy to production (Startup/Enterprise)`);
16
16
  log(` ${DIM}--region <slug>${RESET} First production app placement region`);
17
17
  log(` ${DIM}--rollback${RESET} Rollback to previous deployment`);
18
18
  log(` ${DIM}--force, -f${RESET} Skip optional dependency scan`);
@@ -88,8 +88,8 @@ export function formatDependencyVersionAuditError(result) {
88
88
 
89
89
  lines.push("");
90
90
  lines.push(" Fix:");
91
- lines.push(` pnpm add ${installTargets.join(" ")}`);
92
- lines.push(" npx gencow@latest deploy");
91
+ lines.push(` bun add ${installTargets.join(" ")}`);
92
+ lines.push(" bunx gencow@latest deploy");
93
93
  lines.push("");
94
94
 
95
95
  return lines.join("\n");
@@ -0,0 +1,95 @@
1
+ import { MIGRATION_NEXT_ACTION_KINDS } from "@gencow/migration-contract";
2
+
3
+ import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
4
+
5
+ const DEPLOY_FAILURE_STAGES = new Set([
6
+ "admission",
7
+ "artifact",
8
+ "schema",
9
+ "candidate",
10
+ "activation",
11
+ "route",
12
+ "runtime",
13
+ "controller",
14
+ ]);
15
+ const RETRY_CLASSES = new Set(["terminal-client", "retryable-infra", "platform-action"]);
16
+ const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION"]);
17
+ const STRUCTURED_CONTRACT_FIELDS = ["code", "stage", "retryClass", "userAction", "correlationId"];
18
+ const PLATFORM_ACTION_REQUIRED_MESSAGE = "Platform action is required before retrying";
19
+
20
+ function safeText(value, maxLength) {
21
+ if (typeof value !== "string") return null;
22
+ const normalized = value.trim();
23
+ if (!normalized || normalized.length > maxLength || /[\u0000-\u001f\u007f]/u.test(normalized)) {
24
+ return null;
25
+ }
26
+ return normalized;
27
+ }
28
+
29
+ function readDeployFailureContract(value) {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
31
+ const code = safeText(value.code, 100);
32
+ const stage = safeText(value.stage, 20);
33
+ const retryClass = safeText(value.retryClass, 32);
34
+ const userAction = safeText(value.userAction, 32);
35
+ const correlationId = value.correlationId === undefined ? null : safeText(value.correlationId, 128);
36
+ if (
37
+ !code?.match(/^[A-Z][A-Z0-9_]{2,99}$/u) ||
38
+ !stage ||
39
+ !DEPLOY_FAILURE_STAGES.has(stage) ||
40
+ !retryClass ||
41
+ !RETRY_CLASSES.has(retryClass) ||
42
+ !userAction ||
43
+ !USER_ACTIONS.has(userAction) ||
44
+ (value.correlationId !== undefined && !correlationId) ||
45
+ (correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId))
46
+ ) {
47
+ return null;
48
+ }
49
+ return {
50
+ code,
51
+ stage,
52
+ userAction: userAction === "CONTACT_SUPPORT" ? "OPERATOR_ACTION" : userAction,
53
+ correlationId,
54
+ legacySupportAction: userAction === "CONTACT_SUPPORT",
55
+ };
56
+ }
57
+
58
+ function normalizeLegacySupportMessage(message) {
59
+ return typeof message === "string" && /contact support/iu.test(message)
60
+ ? PLATFORM_ACTION_REQUIRED_MESSAGE
61
+ : message;
62
+ }
63
+
64
+ export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
65
+ const contract = readDeployFailureContract(responseBody);
66
+ const fallbackMessage = safeText(statusText, 120) ?? "Request failed";
67
+ if (!contract) {
68
+ const structuredFields =
69
+ responseBody &&
70
+ typeof responseBody === "object" &&
71
+ !Array.isArray(responseBody) &&
72
+ STRUCTURED_CONTRACT_FIELDS.filter((field) => Object.hasOwn(responseBody, field));
73
+ const correlationLine = formatDeployCorrelationId(responseBody?.correlationId);
74
+ const isLegacyCorrelationEnvelope =
75
+ Array.isArray(structuredFields) &&
76
+ structuredFields.length === 1 &&
77
+ structuredFields[0] === "correlationId" &&
78
+ correlationLine;
79
+ const canTrustLegacyMessage =
80
+ (Array.isArray(structuredFields) && structuredFields.length === 0) || isLegacyCorrelationEnvelope;
81
+ const legacyMessage = canTrustLegacyMessage ? safeText(responseBody?.error, 240) : null;
82
+ const lines = [`Deploy failed: ${normalizeLegacySupportMessage(legacyMessage) || fallbackMessage}`];
83
+ if (isLegacyCorrelationEnvelope) lines.push(correlationLine);
84
+ return lines;
85
+ }
86
+ const publicMessage = contract.legacySupportAction
87
+ ? PLATFORM_ACTION_REQUIRED_MESSAGE
88
+ : (safeText(responseBody.error, 240) ?? fallbackMessage);
89
+ const lines = [`Deploy failed: ${publicMessage}`];
90
+ lines.push(`Code: ${contract.code}`, `Stage: ${contract.stage}`);
91
+ const correlationLine = formatDeployCorrelationId(contract.correlationId);
92
+ if (correlationLine) lines.push(correlationLine);
93
+ lines.push(`Next action: ${contract.userAction}`);
94
+ return lines;
95
+ }
@@ -13,6 +13,8 @@ import {
13
13
  runDrizzleGenerateWithFallback,
14
14
  } from "./drizzle-generate.mjs";
15
15
  import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
16
+ import { renderDeployFailureDiagnosticLines } from "./deploy-failure-diagnostic.mjs";
17
+ import { readJsonObjectResponse } from "./http-response-json.mjs";
16
18
  import { acquireDeployReceipt, releaseDeployReceipt } from "./deploy-receipt-store.mjs";
17
19
  import { CYAN, DIM, RED, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
18
20
  import { platformFetch, rpcMutation } from "./platform-client.mjs";
@@ -31,7 +33,6 @@ import {
31
33
  renderMigrationDiagnosticLines,
32
34
  } from "./migration-diagnostic.mjs";
33
35
  import { emitMigrationAdvisories, emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
34
- import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
35
36
  import { detectDeployLockfile, formatDeployLockfileSelectionWarning } from "./deploy-lockfile-selection.mjs";
36
37
  import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
37
38
 
@@ -575,13 +576,13 @@ export function createDeployPackageRuntime({
575
576
  infoImpl("Creating app (auto-generating ID)...");
576
577
  const createRes = await rpcMutationImpl(creds, "apps.create", { name: displayName });
577
578
  if (!createRes.ok) {
578
- const createErr = await createRes.json().catch(() => ({}));
579
- errorImpl(`App creation failed: ${createErr.error || createRes.statusText}`);
579
+ const createErr = await readJsonObjectResponse(createRes);
580
+ errorImpl(`App creation failed: ${createErr?.error || createRes.statusText}`);
580
581
  exitImpl(1);
581
582
  return null;
582
583
  }
583
584
 
584
- const createData = await createRes.json();
585
+ const createData = await readJsonObjectResponse(createRes);
585
586
  let nextAppId;
586
587
  try {
587
588
  nextAppId = resolveCreatedAppResponse(createData, { platformUrl: creds.platformUrl }).appId;
@@ -727,7 +728,7 @@ export function createDeployPackageRuntime({
727
728
  }
728
729
 
729
730
  if (!deployRes.ok) {
730
- const errData = await deployRes.json().catch(() => ({}));
731
+ const errData = await readJsonObjectResponse(deployRes);
731
732
  const retryableResponse =
732
733
  errData?.code === "DEPLOY_REQUEST_IN_PROGRESS" ||
733
734
  errData?.code === "CONTROL_PLANE_OBSERVATION_INCOMPLETE" ||
@@ -741,16 +742,23 @@ export function createDeployPackageRuntime({
741
742
  errorImpl(lines[0]);
742
743
  lines.slice(1).forEach((line) => logImpl(line));
743
744
  } else {
744
- errorImpl(`Deploy failed: ${errData.error || deployRes.statusText}`);
745
- const correlationLine = formatDeployCorrelationId(errData?.correlationId);
746
- if (correlationLine) errorImpl(correlationLine);
745
+ const lines = renderDeployFailureDiagnosticLines(errData, deployRes.statusText);
746
+ errorImpl(lines[0]);
747
+ lines
748
+ .slice(1)
749
+ .forEach((line) => (line.startsWith("Correlation ID: ") ? errorImpl(line) : logImpl(line)));
747
750
  }
748
751
  await showCrashLogs(errData, "Server startup failure cause:", targetAppId);
749
752
  exitImpl(1);
750
753
  return null;
751
754
  }
752
755
 
753
- const deployData = await deployRes.json();
756
+ const deployData = await readJsonObjectResponse(deployRes);
757
+ if (!deployData) {
758
+ errorImpl("Deploy failed: server returned an invalid JSON response; retry the same deploy request");
759
+ exitImpl(1);
760
+ return null;
761
+ }
754
762
  emitMigrationAdvisories(deployData?.warnings, warnImpl);
755
763
  if (receipt) releaseDeployReceipt(receipt);
756
764
  const deployElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -9,6 +9,7 @@ import {
9
9
  runStaticDeployFlow,
10
10
  } from "./deploy-static-fullstack-runtime.mjs";
11
11
  import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
12
+ import { readJsonObjectResponse } from "./http-response-json.mjs";
12
13
 
13
14
  export function parseDeployArgs(deployArgs) {
14
15
  const parsed = {
@@ -120,7 +121,7 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
120
121
  logImpl("");
121
122
  infoImpl("Usage: gencow deploy [options]");
122
123
  infoImpl(" gencow deploy Dev backend deploy");
123
- infoImpl(" gencow deploy --prod Production backend deploy (Pro+)");
124
+ infoImpl(" gencow deploy --prod Production backend deploy (Startup/Enterprise)");
124
125
  infoImpl(" gencow deploy --prod --region kr First production app placement");
125
126
  infoImpl(" gencow deploy --rollback Rollback to previous version");
126
127
  infoImpl(" gencow deploy logs View server logs");
@@ -196,7 +197,7 @@ async function ensureProductionDeployTarget({
196
197
  });
197
198
 
198
199
  if (!createProdRes.ok) {
199
- const errData = await createProdRes.json().catch(() => ({}));
200
+ const errData = await readJsonObjectResponse(createProdRes);
200
201
  if (createProdRes.status === 403) {
201
202
  logImpl("");
202
203
  logImpl(` ${RED}⛔ Production Deploy — Startup or Enterprise required${RESET}`);
@@ -212,13 +213,18 @@ async function ensureProductionDeployTarget({
212
213
  logImpl(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Startup${RESET}`);
213
214
  logImpl("");
214
215
  } else {
215
- errorImpl(`Prod app creation failed: ${errData.error || createProdRes.statusText}`);
216
+ errorImpl(`Prod app creation failed: ${errData?.error || createProdRes.statusText}`);
216
217
  }
217
218
  exitImpl(1);
218
219
  return null;
219
220
  }
220
221
 
221
- const createProdData = await createProdRes.json();
222
+ const createProdData = await readJsonObjectResponse(createProdRes);
223
+ if (!createProdData) {
224
+ errorImpl("Prod app creation failed: server returned an invalid JSON response");
225
+ exitImpl(1);
226
+ return null;
227
+ }
222
228
  const nextProdAppId = createProdData.prodApp;
223
229
  const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
224
230
  gencowJson.prodApp = nextProdAppId;
@@ -285,13 +291,18 @@ async function runRollbackDeploy({
285
291
  processRef.stdout.write("\r" + " ".repeat(50) + "\r");
286
292
 
287
293
  if (!rollbackRes.ok) {
288
- const errData = await rollbackRes.json().catch(() => ({}));
289
- errorImpl(`Rollback failed: ${errData.error || rollbackRes.statusText}`);
294
+ const errData = await readJsonObjectResponse(rollbackRes);
295
+ errorImpl(`Rollback failed: ${errData?.error || rollbackRes.statusText}`);
290
296
  exitImpl(1);
291
297
  return;
292
298
  }
293
299
 
294
- const rollbackData = await rollbackRes.json();
300
+ const rollbackData = await readJsonObjectResponse(rollbackRes);
301
+ if (!rollbackData) {
302
+ errorImpl("Rollback failed: server returned an invalid JSON response");
303
+ exitImpl(1);
304
+ return;
305
+ }
295
306
  const rollbackElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
296
307
  logImpl("");
297
308
  successImpl(`🔄 Rollback complete! (${rollbackElapsed}s)`);
@@ -0,0 +1,9 @@
1
+ export async function readJsonObjectResponse(response) {
2
+ let value;
3
+ try {
4
+ value = await response.json();
5
+ } catch {
6
+ return null;
7
+ }
8
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9
+ }
@@ -199,7 +199,7 @@ function renderInitHelp(logImpl) {
199
199
  logImpl(` ${BOLD}Arguments:${RESET}`);
200
200
  logImpl(` ${CYAN}name${RESET} Project name (creates directory) or "." for current dir\n`);
201
201
  logImpl(` ${BOLD}Options:${RESET}`);
202
- logImpl(` ${DIM}--template, -t${RESET} Select template (default, task-app, fullstack, ai-chat)`);
202
+ logImpl(` ${DIM}--template, -t${RESET} Select template (default, task-app, admin-tool, fullstack, ai-chat)`);
203
203
  logImpl(` ${DIM}--force, -f${RESET} Initialize in non-empty directory`);
204
204
  logImpl(` ${DIM}--no-install${RESET} Skip dependency install (CI/canary)\n`);
205
205
  logImpl(` ${BOLD}Examples:${RESET}`);
@@ -272,12 +272,16 @@ export function buildAiPrompt(apiObj, namespaces) {
272
272
  md += `- Set a batch limit and AI fanout budget for crawler, backfill, import, and AI-heavy cron actions.\n`;
273
273
  md += `- Avoid unbounded Promise.all, unlimited crawling, and unlimited LLM/embedding calls.\n\n`;
274
274
  md += `Deployment rules:\n`;
275
- md += `- Development: npx gencow dev\n`;
276
- md += `- Static-only dev deploy: npx gencow static dist/\n`;
277
- md += `- Production backend deploy: npx gencow deploy --prod (Pro+ only)\n`;
278
- md += `- Fullstack static deploy: VITE_API_URL=https://{appId}.{domain} npm run build, then npx gencow deploy --static dist/\n`;
275
+ md += `- Development: bunx gencow@latest dev\n`;
276
+ md += `- Local development: bunx gencow@latest dev --local\n`;
277
+ md += `- Static-only dev deploy: bunx gencow@latest static dist/\n`;
278
+ md += `- Production backend deploy: bunx gencow@latest deploy --prod (Startup/Enterprise)\n`;
279
+ md += `- Fullstack static deploy: VITE_API_URL=https://{appId}.{domain} bun run build, then bunx gencow@latest deploy --static dist/\n`;
280
+ md += `- Node.js users can run the same CLI commands with npx gencow@latest.\n`;
281
+ md += `- Both @latest runners resolve the registry latest tag and reuse a matching cached package when possible.\n`;
282
+ md += `- Plain gencow can run the project-installed pinned CLI; use it only when that version pin is intentional.\n`;
279
283
  md += `- If a backend is present, Gencow deploys the backend before frontend assets.\n`;
280
- md += `- Set cloud env vars with npx gencow env set KEY=VALUE.\n`;
284
+ md += `- Set cloud env vars with bunx gencow@latest env set KEY=VALUE.\n`;
281
285
  md += `- Read process.env inside handlers so hot-reloaded env values are current.\n\n`;
282
286
  md += `Data modeling rules:\n`;
283
287
  md += `- Use pgTable() and ownerRls() for user-owned data.\n`;
@@ -286,7 +290,7 @@ export function buildAiPrompt(apiObj, namespaces) {
286
290
  md += `- Use allowedFilters for filterable fields. Undefined filters are ignored by default.\n`;
287
291
  md += `- PostgreSQL RLS handles owner filtering, so do not duplicate userId filters unless a custom policy requires it.\n\n`;
288
292
  md += `Package and crypto rules:\n`;
289
- md += `- npm-installed third-party packages are installed during deploy.\n`;
293
+ md += `- Packages added with bun add (or npm install for Node.js users) are installed during deploy.\n`;
290
294
  md += `- child_process, vm, os, cluster, and worker_threads are blocked in deployed tenant apps.\n`;
291
295
  md += `- Prefer SHA-256 with node:crypto or Web Crypto API instead of custom 32-bit hashes.\n`;
292
296
  md += ` const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));\n\n`;
@@ -298,7 +302,7 @@ export function buildAiPrompt(apiObj, namespaces) {
298
302
  md += `- Use rerank({ model: createGencowAI().rerankingModel(...) }) or ai.rerank() only after retrieval authorization; do not pass private metadata as provider payload.\n`;
299
303
  md += `- createGencowAI().rerankingModel(...) is proxy-only today. Local direct mode with only OPENAI_API_KEY must fail fast for reranking.\n`;
300
304
  md += `- The generated reranker starter may use an explicit fallbackModel compatibility path, but strict providerPreference such as azure_cohere must still fail closed on auth/config errors.\n`;
301
- md += `- Recommended chat models: gpt-5.4-mini for most apps, gpt-5.4 for Pro/Scale highest quality, gpt-5.4-nano for high-volume extraction.\n`;
305
+ md += `- Recommended chat models: gpt-5.4-mini for most apps, gpt-5.4 for Startup/Enterprise highest quality, gpt-5.4-nano for high-volume extraction.\n`;
302
306
  md += `- Use ai.generateObject() with a Zod schema for structured output. Do not use ai.chat() + JSON.parse().\n`;
303
307
  md += `- Install AI with gencow add AI, RAG with gencow add RAG, Memory with gencow add Memory, and durable agents with gencow add Agent.\n`;
304
308
  md += `\`\`\`\n\n`;
@@ -307,7 +311,7 @@ export function buildAiPrompt(apiObj, namespaces) {
307
311
 
308
312
  export function buildAiUsageSection() {
309
313
  let md = `---\n\n## AI Usage\n\n`;
310
- md += `Gencow supports an Azure-first active chat catalog: gpt-5.4 on Pro/Scale, gpt-5.4-mini by default, gpt-5.4-nano for high volume, plus compatibility models gpt-5-mini and gpt-5-nano. Deprecated requests such as \`gpt-5.5\` and \`gpt-4o-mini\` are automatically substituted to the closest supported model before provider execution. Image and embedding models are also controlled by the platform \`model_pricing\` catalog.\n`;
314
+ md += `Gencow supports an Azure-first active chat catalog: gpt-5.4 on Startup/Enterprise, gpt-5.4-mini by default, gpt-5.4-nano for high volume, plus compatibility models gpt-5-mini and gpt-5-nano. Deprecated requests such as \`gpt-5.5\` and \`gpt-4o-mini\` are automatically substituted to the closest supported model before provider execution. Image and embedding models are also controlled by the platform \`model_pricing\` catalog.\n`;
311
315
  md += `\`OPENAI_API_KEY\` is only for local direct/fallback usage. Cloud deployments use the Gencow AI proxy with platform-managed internal credentials and service credits.\n\n`;
312
316
  md += `\`\`\`typescript\n`;
313
317
  md += `import { generateImage, generateText, rerank } from "ai";\n`;
@@ -347,7 +351,7 @@ export function buildAiUsageSection() {
347
351
  md += `\`\`\`\n\n`;
348
352
  md += `| Purpose | Recommended model |\n`;
349
353
  md += `|---|---|\n`;
350
- md += `| Highest quality / complex reasoning | \`gpt-5.4\` (Pro/Scale only) |\n`;
354
+ md += `| Highest quality / complex reasoning | \`gpt-5.4\` (Startup/Enterprise) |\n`;
351
355
  md += `| Most production chat, coding, and agent tasks | \`gpt-5.4-mini\` |\n`;
352
356
  md += `| High-volume classification and extraction | \`gpt-5.4-nano\` |\n`;
353
357
  md += `| Compatibility replacement for deprecated mini-class models | \`gpt-5-mini\` |\n`;
@@ -361,7 +365,7 @@ export function buildAiUsageSection() {
361
365
  md += `const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n`;
362
366
  md += `\`\`\`\n\n`;
363
367
  md += `\`\`\`bash\n`;
364
- md += `# Local-only .env\n`;
368
+ md += `# Local-only backend secrets: gencow/.env\n`;
365
369
  md += `OPENAI_API_KEY=sk-your-key-here\n`;
366
370
  md += `\`\`\`\n\n`;
367
371
  return md;
@@ -371,10 +375,10 @@ export function buildDeploySection() {
371
375
  let md = `---\n\n## Deploy\n\n`;
372
376
  md += `### Cloud deploy\n\n`;
373
377
  md += `\`\`\`bash\n`;
374
- md += `npx gencow login\n`;
375
- md += `npx gencow dev\n`;
376
- md += `npx gencow env set DATABASE_URL=postgres://...\n`;
377
- md += `npx gencow env list\n`;
378
+ md += `bunx gencow@latest login\n`;
379
+ md += `bunx gencow@latest dev\n`;
380
+ md += `bunx gencow@latest env set MY_API_KEY=value\n`;
381
+ md += `bunx gencow@latest env list\n`;
378
382
  md += `\`\`\`\n\n`;
379
383
  md += `After deploy, the app is available at \`https://{appName}.{domain}\`.\n\n`;
380
384
  md += `### Environment variables\n\n`;
@@ -395,18 +399,18 @@ export function buildDeploySection() {
395
399
  md += `\`\`\`bash\n`;
396
400
  md += `gencow dev\n`;
397
401
  md += `cd frontend\n`;
398
- md += `VITE_API_URL=https://{appId}.{domain} npm run build\n`;
399
- md += `gencow deploy --static dist/\n`;
400
- md += `gencow static dist/\n`;
401
- md += `gencow deploy --prod\n`;
402
- md += `gencow deploy --static --prod dist/\n`;
402
+ md += `VITE_API_URL=https://{appId}.{domain} bun run build\n`;
403
+ md += `bunx gencow@latest deploy --static dist/\n`;
404
+ md += `bunx gencow@latest static dist/\n`;
405
+ md += `bunx gencow@latest deploy --prod\n`;
406
+ md += `bunx gencow@latest deploy --static --prod dist/\n`;
403
407
  md += `\`\`\`\n\n`;
404
408
  md += `When a \`gencow/\` backend exists, \`gencow deploy --static dist/\` deploys the backend before frontend assets. Use \`gencow static dist/\` for frontend-only static deploys.\n\n`;
405
409
  md += `### Hosted analytics\n\n`;
406
410
  md += `Gencow-hosted frontend HTML receives same-origin analytics automatically at deploy time. Do not add analytics database credentials or collector internals to tenant app code.\n`;
407
411
  md += `External frontends cannot be instrumented automatically. Use the hosted snippet only after confirming the site is served by Gencow or an explicit external snippet is issued by the Dashboard.\n\n`;
408
- md += `### Third-party npm packages\n\n`;
409
- md += `Packages installed with \`npm install\` are installed during deploy. Platform-owned packages such as \`@gencow/core\`, \`drizzle-orm\`, \`better-auth\`, \`postgres\`, \`hono\`, \`ai\`, and \`zod\` are provided by default.\n`;
412
+ md += `### Third-party packages\n\n`;
413
+ md += `Packages added with \`bun add\` (or \`npm install\` for Node.js users) are installed during deploy. Platform-owned packages such as \`@gencow/core\`, \`drizzle-orm\`, \`better-auth\`, \`postgres\`, \`hono\`, \`ai\`, and \`zod\` are provided by default.\n`;
410
414
  md += `Blocked modules include \`child_process\`, \`vm\`, \`os\`, \`cluster\`, and \`worker_threads\`.\n\n`;
411
415
  md += `### Public origins\n\n`;
412
416
  md += `same-origin custom domains do not need public-origin configuration. Declare external frontend origins in \`gencow.config.js\` with \`frontendOrigins\` when hosting outside Gencow. Use \`gencow domain set <domain> --prod\` to connect a public domain to the production app when \`gencow.json\` has \`prodApp\`.\n\n`;