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 +5 -4
- package/lib/app-command.mjs +50 -10
- package/lib/app-response-contract.mjs +49 -0
- package/lib/backup-command.mjs +1 -1
- package/lib/db-command.mjs +4 -4
- package/lib/deploy-auditor.mjs +1 -1
- package/lib/deploy-command.mjs +1 -1
- package/lib/deploy-dependency-compat.mjs +2 -2
- package/lib/deploy-failure-diagnostic.mjs +95 -0
- package/lib/deploy-package-runtime.mjs +17 -9
- package/lib/deploy-runtime.mjs +18 -7
- package/lib/http-response-json.mjs +9 -0
- package/lib/init-command.mjs +1 -1
- package/lib/readme-codegen.mjs +25 -21
- package/lib/static-command.mjs +8 -3
- package/lib/static-deploy-command.mjs +13 -7
- package/package.json +1 -1
- package/runtime/server.mjs +61 -32
- package/runtime/server.mjs.map +2 -2
- package/templates/SECURITY.md +3 -3
- package/templates/ai-chat/README.md +2 -2
- package/templates/ai-chat/prompt.md +5 -5
- package/templates/fullstack/README.md +1 -1
- package/templates/fullstack/prompt.md +3 -3
- package/templates/task-app/prompt.md +1 -1
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 (
|
|
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
|
|
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 (
|
|
443
|
+
${DIM}# Deploy to production (Startup/Enterprise):${RESET}
|
|
443
444
|
gencow deploy --prod
|
|
444
445
|
|
|
445
446
|
${DIM}# Inspect stale workflows:${RESET}
|
package/lib/app-command.mjs
CHANGED
|
@@ -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((
|
|
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
|
|
171
|
+
const data = await readJsonObjectResponse(res);
|
|
147
172
|
if (!res.ok) {
|
|
148
|
-
errorImpl(data
|
|
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
|
|
275
|
+
const delData = await readJsonObjectResponse(delRes);
|
|
245
276
|
if (!delRes.ok) {
|
|
246
|
-
errorImpl(delData
|
|
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
|
|
267
|
-
const data = body?.result || body;
|
|
304
|
+
const body = await readJsonObjectResponse(res);
|
|
268
305
|
if (!res.ok) {
|
|
269
|
-
errorImpl(
|
|
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("
|
|
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
|
+
}
|
package/lib/backup-command.mjs
CHANGED
|
@@ -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 (
|
|
97
|
+
logImpl(` gencow backup download <id> Download backup file (plan-dependent)\n`);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export function buildRestoreConfirmPrompt(sourceLabel, { includeStorageRisk = false } = {}) {
|
package/lib/db-command.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/lib/deploy-auditor.mjs
CHANGED
|
@@ -781,7 +781,7 @@ export function formatBundleSizeWarning(result) {
|
|
|
781
781
|
" • 불필요한 devDependencies를 dependencies에서 제거하세요",
|
|
782
782
|
" • 무거운 패키지를 가벼운 대안으로 교체해보세요",
|
|
783
783
|
" (예: moment → dayjs, lodash → lodash-es)",
|
|
784
|
-
" • 메모리 한도가 부족하면
|
|
784
|
+
" • 메모리 한도가 부족하면 Startup/Enterprise 플랜을 확인하세요",
|
|
785
785
|
" (Hobby: 256MB, Pro: 512MB, Scale: 1024MB)",
|
|
786
786
|
"",
|
|
787
787
|
" 💡 이 경고는 배포를 차단하지 않습니다.",
|
package/lib/deploy-command.mjs
CHANGED
|
@@ -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 (
|
|
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(`
|
|
92
|
-
lines.push("
|
|
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
|
|
579
|
-
errorImpl(`App creation failed: ${createErr
|
|
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
|
|
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
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
|
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);
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
289
|
-
errorImpl(`Rollback failed: ${errData
|
|
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
|
|
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)`);
|
package/lib/init-command.mjs
CHANGED
|
@@ -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}`);
|
package/lib/readme-codegen.mjs
CHANGED
|
@@ -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:
|
|
276
|
-
md += `-
|
|
277
|
-
md += `-
|
|
278
|
-
md += `-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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\` (
|
|
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
|
|
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 += `
|
|
375
|
-
md += `
|
|
376
|
-
md += `
|
|
377
|
-
md += `
|
|
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}
|
|
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
|
|
409
|
-
md += `Packages
|
|
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`;
|