gencow 0.1.186 → 0.1.188
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 +32 -7
- package/core/index.js +24 -0
- package/lib/app-command.mjs +86 -27
- package/lib/app-delete-operation.mjs +129 -0
- package/lib/app-response-contract.mjs +49 -0
- package/lib/app-response-error.mjs +24 -0
- package/lib/backup-command.mjs +8 -6
- package/lib/canonical-bundle-config.mjs +82 -0
- package/lib/cli-dev-runtime.mjs +2 -2
- package/lib/cli-project-runtime.mjs +114 -71
- package/lib/cloud-targets.mjs +12 -1
- package/lib/codegen-command.mjs +79 -15
- package/lib/coding-guardrails.mjs +13 -1
- package/lib/cron-manifest.mjs +25 -17
- package/lib/cron-source-structure.mjs +102 -0
- package/lib/db-command.mjs +12 -8
- package/lib/db-push-command.mjs +12 -4
- package/lib/deploy-auditor.mjs +13 -30
- package/lib/deploy-bundle-staging.mjs +132 -0
- package/lib/deploy-command.mjs +20 -2
- package/lib/deploy-dependency-compat.mjs +12 -94
- package/lib/deploy-package-claim.mjs +31 -18
- package/lib/deploy-package-runtime.mjs +138 -156
- package/lib/deploy-runtime.mjs +113 -22
- package/lib/deploy-static-fullstack-runtime.mjs +6 -1
- package/lib/deployment-operation-poll.mjs +78 -0
- package/lib/dev-cloud-bundle.mjs +63 -14
- package/lib/dev-cloud-command.mjs +56 -27
- package/lib/dev-cloud-migrations.mjs +8 -7
- package/lib/dev-local-command.mjs +47 -29
- package/lib/doctor-command.mjs +188 -13
- package/lib/domain-command.mjs +32 -7
- package/lib/files-command.mjs +14 -1
- package/lib/init-command.mjs +1 -1
- package/lib/jobs-command.mjs +25 -4
- package/lib/migration-bundle-manifest.mjs +38 -12
- package/lib/platform-client.mjs +2 -0
- package/lib/project-context.mjs +298 -0
- package/lib/project-metadata-runtime.mjs +168 -0
- package/lib/project-schema-deployment-preview.mjs +25 -7
- package/lib/readme-codegen.mjs +33 -21
- package/lib/release-client.mjs +1 -1
- package/lib/runtime-bom-api-contract.mjs +71 -0
- package/lib/runtime-bom-resolve.mjs +257 -0
- package/lib/runtime-bom.mjs +343 -0
- package/lib/runtime-command.mjs +273 -0
- package/lib/runtime-package-contract.mjs +53 -0
- package/lib/static-command.mjs +90 -15
- package/lib/static-deploy-command.mjs +22 -3
- package/package.json +3 -3
- package/runtime/config.mjs +63 -5
- package/runtime/server.mjs +14658 -5617
- package/runtime/server.mjs.map +4 -4
- package/runtime/tooling.mjs +1004 -411
- 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
|
@@ -55,6 +55,8 @@ import { createInitCommand } from "../lib/init-command.mjs";
|
|
|
55
55
|
import { createJobsCommand } from "../lib/jobs-command.mjs";
|
|
56
56
|
import { createLogsCommand } from "../lib/logs-command.mjs";
|
|
57
57
|
import { createOriginsCommand } from "../lib/origins-command.mjs";
|
|
58
|
+
import { parseProjectSelectionArgs, setCliInvocationSelection } from "../lib/project-context.mjs";
|
|
59
|
+
import { createRuntimeCommand } from "../lib/runtime-command.mjs";
|
|
58
60
|
import { createStaticDeployRuntime } from "../lib/static-deploy-command.mjs";
|
|
59
61
|
import { createStaticCommand } from "../lib/static-command.mjs";
|
|
60
62
|
import { createTemplateMarketplaceCommand } from "../lib/template-marketplace-command.mjs";
|
|
@@ -103,7 +105,10 @@ const runAddCommand = createAddCommand({
|
|
|
103
105
|
RESET,
|
|
104
106
|
YELLOW,
|
|
105
107
|
});
|
|
106
|
-
const runAppCommand = createAppCommand({
|
|
108
|
+
const runAppCommand = createAppCommand({
|
|
109
|
+
loadConfig,
|
|
110
|
+
errorImpl: (message) => console.error(`${RED} ✗${RESET} ${message}`),
|
|
111
|
+
});
|
|
107
112
|
const runBackupCommand = createBackupCommand();
|
|
108
113
|
const { login: runLoginCommand, logout: runLogoutCommand, whoami: runWhoamiCommand } = createAuthCommands();
|
|
109
114
|
const runCodegenCommand = createCodegenCommand({
|
|
@@ -130,14 +135,17 @@ const runDoctorCommand = createDoctorCommand({
|
|
|
130
135
|
loadConfig,
|
|
131
136
|
logImpl: log,
|
|
132
137
|
infoImpl: info,
|
|
138
|
+
warnImpl: warn,
|
|
133
139
|
errorImpl: error,
|
|
134
140
|
successImpl: success,
|
|
135
141
|
exitImpl: (code) => process.exit(code),
|
|
136
142
|
cwdImpl: () => process.cwd(),
|
|
137
143
|
BOLD,
|
|
138
144
|
CYAN,
|
|
145
|
+
DIM,
|
|
139
146
|
RESET,
|
|
140
147
|
});
|
|
148
|
+
const runRuntimeCommand = createRuntimeCommand();
|
|
141
149
|
const {
|
|
142
150
|
dbReset: runDbResetCommand,
|
|
143
151
|
dbSeed: runDbSeedCommand,
|
|
@@ -227,6 +235,7 @@ const runStaticDeployRuntime = createStaticDeployRuntime({
|
|
|
227
235
|
cliVersion: CLI_VERSION,
|
|
228
236
|
cwdImpl: () => process.cwd(),
|
|
229
237
|
drizzleKitCmdImpl: _drizzleKitCmd,
|
|
238
|
+
loadConfigImpl: loadConfig,
|
|
230
239
|
verifyAppReadyImpl: verifyAppReady,
|
|
231
240
|
updateEnvLocalUrlImpl: updateEnvLocalUrl,
|
|
232
241
|
prepareReleaseAttemptImpl: prepareReleaseAttempt,
|
|
@@ -298,6 +307,7 @@ const runLogsCommand = createLogsCommand({ loadConfig });
|
|
|
298
307
|
const runOriginsCommand = createOriginsCommand();
|
|
299
308
|
const runStaticCommand = createStaticCommand({
|
|
300
309
|
cwdImpl: () => process.cwd(),
|
|
310
|
+
loadConfigImpl: loadConfig,
|
|
301
311
|
requireCredsImpl: requireCreds,
|
|
302
312
|
runStaticDeployRuntimeImpl: runStaticDeployRuntime,
|
|
303
313
|
});
|
|
@@ -356,18 +366,25 @@ ${BOLD}Usage:${RESET}
|
|
|
356
366
|
|
|
357
367
|
${BOLD}Quick Start:${RESET}
|
|
358
368
|
${GREEN}init <name>${RESET} Create a new Gencow project
|
|
359
|
-
${DIM}--template, -t Select template (default, task-app, fullstack, ai-chat)${RESET}
|
|
369
|
+
${DIM}--template, -t Select template (default, task-app, admin-tool, fullstack, ai-chat)${RESET}
|
|
360
370
|
${DIM}--force, -f Initialize in non-empty directory (preserves existing files)${RESET}
|
|
361
371
|
${GREEN}add <comp...>${RESET} Add components ${DIM}(AI Agent RAG Tools Memory ...)${RESET}
|
|
362
|
-
${GREEN}codegen${RESET} Generate
|
|
372
|
+
${GREEN}codegen${RESET} Generate client codegen artifacts from schema
|
|
363
373
|
${DIM}--outdir, -o Output directory (default: src/gencow/)${RESET}
|
|
374
|
+
${DIM}--target NAME Select declared codegen client${RESET}
|
|
364
375
|
${GREEN}doctor${RESET} Run pre-run source diagnostics
|
|
376
|
+
${DIM}--plan/--diff/--fix backend autofix; --no-sync skips BOM rewrite${RESET}
|
|
377
|
+
${GREEN}runtime list${RESET} List runtime releases ${DIM}(marks current tip)${RESET}
|
|
378
|
+
${GREEN}runtime show${RESET} Show managed BOM package versions
|
|
379
|
+
${GREEN}runtime check${RESET} Compare package.json against runtime BOM
|
|
380
|
+
${GREEN}runtime sync${RESET} Align managed dependency versions to BOM
|
|
365
381
|
|
|
366
382
|
${BOLD}Commands (login required):${RESET}
|
|
367
383
|
${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
|
|
368
384
|
${GREEN}logout${RESET} Clear credentials
|
|
369
385
|
${GREEN}whoami${RESET} Show current user info
|
|
370
386
|
${GREEN}dev${RESET} Watch + auto-deploy to cloud + live logs
|
|
387
|
+
${DIM}--local Run local Bun + Hono + PGlite development${RESET}
|
|
371
388
|
${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
|
|
372
389
|
${GREEN}db:push${RESET} Apply versioned migrations to cloud DB
|
|
373
390
|
${DIM}--prod Push to production DB (confirmation required)${RESET}
|
|
@@ -378,6 +395,8 @@ ${BOLD}Commands (login required):${RESET}
|
|
|
378
395
|
${DIM}--prod Seed production app (confirmation required)${RESET}
|
|
379
396
|
${GREEN}static [dir]${RESET} Deploy static files only ${DIM}(dist/, out/, build/)${RESET}
|
|
380
397
|
${DIM}--prod Deploy to production app${RESET}
|
|
398
|
+
${DIM}--client NAME Select declared client workspace${RESET}
|
|
399
|
+
${DIM}--frontend NAME Compatibility alias for --client${RESET}
|
|
381
400
|
${DIM}--force, -f Skip optional dependency scan${RESET}
|
|
382
401
|
${GREEN}templates publish${RESET} Publish current project as a marketplace template
|
|
383
402
|
${DIM}--title, --slug, --price, --private, --unlisted${RESET}
|
|
@@ -385,7 +404,7 @@ ${BOLD}Commands (login required):${RESET}
|
|
|
385
404
|
${GREEN}templates clone <slug>${RESET} Clone a template into a local directory
|
|
386
405
|
${GREEN}deploy${RESET} Deploy backend to cloud ${DIM}(dev by default)${RESET}
|
|
387
406
|
${DIM}--static [dir] Deploy backend, then static files${RESET}
|
|
388
|
-
${DIM}--prod Deploy to production (
|
|
407
|
+
${DIM}--prod Deploy to production (Startup/Enterprise)${RESET}
|
|
389
408
|
${DIM}--rollback Rollback to previous deployment${RESET}
|
|
390
409
|
${DIM}--force, -f Skip optional dependency scan${RESET}
|
|
391
410
|
${DIM}logs [-n N] Show cloud app server logs${RESET}
|
|
@@ -393,7 +412,7 @@ ${BOLD}Commands (login required):${RESET}
|
|
|
393
412
|
${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
|
|
394
413
|
${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
|
|
395
414
|
${GREEN}env unset KEY${RESET} Remove cloud env var
|
|
396
|
-
${GREEN}env push${RESET} Push
|
|
415
|
+
${GREEN}env push${RESET} Push backend env file ${DIM}(gencow/.env by default)${RESET}
|
|
397
416
|
${GREEN}cors list${RESET} Show legacy CORS overrides
|
|
398
417
|
${GREEN}cors add URL${RESET} Add legacy CORS override ${DIM}(compat only)${RESET}
|
|
399
418
|
${GREEN}cors remove URL${RESET} Remove legacy CORS override
|
|
@@ -446,7 +465,7 @@ ${BOLD}Examples:${RESET}
|
|
|
446
465
|
${DIM}# Deploy built frontend files only:${RESET}
|
|
447
466
|
gencow static dist/
|
|
448
467
|
|
|
449
|
-
${DIM}# Deploy to production (
|
|
468
|
+
${DIM}# Deploy to production (Startup/Enterprise):${RESET}
|
|
450
469
|
gencow deploy --prod
|
|
451
470
|
|
|
452
471
|
${DIM}# Inspect stale workflows:${RESET}
|
|
@@ -497,6 +516,9 @@ ${BOLD}Examples:${RESET}
|
|
|
497
516
|
// ── doctor — 실행 전 소스 진단 ────────────────────
|
|
498
517
|
doctor: runDoctorCommand,
|
|
499
518
|
|
|
519
|
+
// ── runtime — BOM list/show/check/sync ─────────────
|
|
520
|
+
runtime: runRuntimeCommand,
|
|
521
|
+
|
|
500
522
|
// ── app (subcommands: create, list, delete) ────────
|
|
501
523
|
app: runAppCommand,
|
|
502
524
|
|
|
@@ -539,7 +561,10 @@ ${BOLD}Examples:${RESET}
|
|
|
539
561
|
|
|
540
562
|
// ─── updateReadme: gencow/ 폴더 스캔 → README에 컴포넌트 문서 자동 추가 ────
|
|
541
563
|
|
|
542
|
-
const [, , rawCmd = "help", ...
|
|
564
|
+
const [, , rawCmd = "help", ...rawArgs] = process.argv;
|
|
565
|
+
const globalProjectSelection = parseProjectSelectionArgs(rawArgs);
|
|
566
|
+
setCliInvocationSelection(globalProjectSelection);
|
|
567
|
+
const args = [...globalProjectSelection.remainingArgs];
|
|
543
568
|
const cmd =
|
|
544
569
|
rawCmd === "--help" || rawCmd === "-h"
|
|
545
570
|
? "help"
|
package/core/index.js
CHANGED
|
@@ -2979,6 +2979,7 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
2979
2979
|
"GENCOW_PLATFORM_URL",
|
|
2980
2980
|
"GENCOW_PLATFORM_INTERNAL_URL",
|
|
2981
2981
|
"GENCOW_PLATFORM_INTERNAL_URL_ALT",
|
|
2982
|
+
"GENCOW_REALTIME_GATEWAY_INTERNAL_URL",
|
|
2982
2983
|
"GENCOW_PLATFORM_DB",
|
|
2983
2984
|
"GENCOW_MANAGED_PUBLIC_ORIGINS",
|
|
2984
2985
|
"PLATFORM_OPENAI_KEY",
|
|
@@ -2986,6 +2987,18 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
2986
2987
|
"PLATFORM_GOOGLE_CLIENT_ID",
|
|
2987
2988
|
"PLATFORM_GOOGLE_CLIENT_SECRET",
|
|
2988
2989
|
"PLATFORM_INTERNAL_SECRET",
|
|
2990
|
+
"STRIPE_SECRET_KEY",
|
|
2991
|
+
"STRIPE_WEBHOOK_SECRET",
|
|
2992
|
+
"TOSS_BILLING_CLIENT_KEY",
|
|
2993
|
+
"TOSS_BILLING_SECRET_KEY",
|
|
2994
|
+
"TOSS_BILLING_WEBHOOK_SECRET",
|
|
2995
|
+
"GENCOW_BILLING_STRIPE_MODE",
|
|
2996
|
+
"GENCOW_BILLING_STRIPE_ENVIRONMENT",
|
|
2997
|
+
"GENCOW_BILLING_STRIPE_EXPECTED_ACCOUNT_ID",
|
|
2998
|
+
"GENCOW_BILLING_STRIPE_API_VERSION",
|
|
2999
|
+
"GENCOW_BILLING_NEW_CHECKOUT_PROVIDER",
|
|
3000
|
+
"GENCOW_BILLING_STRIPE_ANNUAL_ENABLED",
|
|
3001
|
+
"GENCOW_BILLING_STRIPE_CANARY_USER_IDS",
|
|
2989
3002
|
"INVITE_ONLY",
|
|
2990
3003
|
"RUNNER_TYPE",
|
|
2991
3004
|
"COWBOX_PROFILE",
|
|
@@ -2993,6 +3006,8 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
2993
3006
|
"PGBOUNCER_PORT",
|
|
2994
3007
|
"DIRECT_PG_PORT",
|
|
2995
3008
|
"GENCOW_DIRECT_DATABASE_URL",
|
|
3009
|
+
"GENCOW_CATALOG_READ_DATABASE_URL",
|
|
3010
|
+
"GENCOW_DIRECT_DDL_DATABASE_URL",
|
|
2996
3011
|
"DIRECT_DATABASE_URL",
|
|
2997
3012
|
"GENCOW_APP_USER_PASS",
|
|
2998
3013
|
"GENCOW_DDL_USER_PASS",
|
|
@@ -3016,10 +3031,15 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
3016
3031
|
"GENCOW_BACKUP_RETENTION_V2_EFFECTIVE_AT",
|
|
3017
3032
|
"GENCOW_MIGRATIONS",
|
|
3018
3033
|
"GENCOW_APP_NAME",
|
|
3034
|
+
"GENCOW_MANAGED_RUNTIME",
|
|
3019
3035
|
"GENCOW_APP_DATA_DIR",
|
|
3020
3036
|
"GENCOW_RUNTIME_COMPATIBILITY",
|
|
3021
3037
|
"GENCOW_RUNTIME_RELEASE_ID",
|
|
3022
3038
|
"GENCOW_RUNTIME_RELEASE_DIR",
|
|
3039
|
+
"GENCOW_RUNTIME_GENERATION_DIR",
|
|
3040
|
+
"GENCOW_APP_RUNTIME_DIR",
|
|
3041
|
+
"GENCOW_APP_OWNED_DEPS_DIR",
|
|
3042
|
+
"GENCOW_RUNTIME_RUNTIME_DIR",
|
|
3023
3043
|
"GENCOW_RUNTIME_NODE_MODULES",
|
|
3024
3044
|
"GENCOW_INTERNAL_TOKEN",
|
|
3025
3045
|
"GENCOW_CRON_TOKEN",
|
|
@@ -3035,8 +3055,10 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
3035
3055
|
"GENCOW_SHUTDOWN_MARKER_PATH",
|
|
3036
3056
|
"GENCOW_SKIP_MIGRATION",
|
|
3037
3057
|
"GENCOW_DB_MAX_CONNECTIONS",
|
|
3058
|
+
"GENCOW_DB_POOL_POLICY_VERSION",
|
|
3038
3059
|
"GENCOW_DB_IDLE_TIMEOUT_SECONDS",
|
|
3039
3060
|
"GENCOW_DB_CONNECTION_TIMEOUT_SECONDS",
|
|
3061
|
+
"GENCOW_DB_OPERATION_TIMEOUT_MS",
|
|
3040
3062
|
"GENCOW_MEMORY_MB",
|
|
3041
3063
|
"BUN_JSC_forceRAMSize",
|
|
3042
3064
|
"MIMALLOC_PURGE_DELAY",
|
|
@@ -3046,6 +3068,7 @@ var RESERVED_TENANT_RUNTIME_ENV_KEY_SET = new Set(RESERVED_TENANT_RUNTIME_ENV_KE
|
|
|
3046
3068
|
var RESERVED_TENANT_RUNTIME_ENV_PREFIXES = [
|
|
3047
3069
|
"__GENCOW_",
|
|
3048
3070
|
"GENCOW_CONTROL_PLANE_",
|
|
3071
|
+
"GENCOW_BUILDER_",
|
|
3049
3072
|
"GENCOW_PLATFORM_STORAGE_",
|
|
3050
3073
|
"GENCOW_RELEASE_",
|
|
3051
3074
|
"GENCOW_DOCUMENT_",
|
|
@@ -3053,6 +3076,7 @@ var RESERVED_TENANT_RUNTIME_ENV_PREFIXES = [
|
|
|
3053
3076
|
"GENCOW_WARM_",
|
|
3054
3077
|
"GENCOW_DIRECT_RUNNER_",
|
|
3055
3078
|
"GENCOW_RUNTIME_PINNING_",
|
|
3079
|
+
"GENCOW_FUNCTION_TIMEOUT_RESTART_",
|
|
3056
3080
|
"GENCOW_COWBOX_"
|
|
3057
3081
|
];
|
|
3058
3082
|
function isReservedTenantRuntimeEnvKey(key) {
|
package/lib/app-command.mjs
CHANGED
|
@@ -7,8 +7,30 @@ 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 { pollAppDeleteOperation } from "./app-delete-operation.mjs";
|
|
16
|
+
import { appResponseError } from "./app-response-error.mjs";
|
|
17
|
+
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
10
18
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
|
|
11
19
|
import { loadCreds, rpcMutation, rpcQuery, saveCreds, requireCreds } from "./platform-client.mjs";
|
|
20
|
+
import { resolveProjectMetadataPath, resolveProjectSelection } from "./project-context.mjs";
|
|
21
|
+
import { updateEnvLocalUrl } from "./cli-project-runtime.mjs";
|
|
22
|
+
|
|
23
|
+
// Covers the 60s runtime-observer restart grace plus multiple 5s reconciliation scans.
|
|
24
|
+
const DEFAULT_DELETE_POLL_ATTEMPTS = 75;
|
|
25
|
+
const DELETE_POLL_INTERVAL_MS = 1_000;
|
|
26
|
+
|
|
27
|
+
function appResponseStatus(response) {
|
|
28
|
+
return Number.isSafeInteger(response?.status) ? `HTTP ${response.status}` : "unknown HTTP status";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function invalidAppResponse(operation, response, kind) {
|
|
32
|
+
return `CLI_APP_RESPONSE_INVALID: ${operation} returned an invalid ${kind} response (${appResponseStatus(response)})`;
|
|
33
|
+
}
|
|
12
34
|
|
|
13
35
|
export function formatAppDeployedAgo(dateStr, now = Date.now()) {
|
|
14
36
|
if (!dateStr) return `${DIM}never${RESET}`;
|
|
@@ -89,7 +111,10 @@ export function createAppCommand({
|
|
|
89
111
|
rpcMutationImpl = rpcMutation,
|
|
90
112
|
rpcQueryImpl = rpcQuery,
|
|
91
113
|
saveCredsImpl = saveCreds,
|
|
114
|
+
sleepImpl = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
|
|
115
|
+
deletePollAttempts = DEFAULT_DELETE_POLL_ATTEMPTS,
|
|
92
116
|
successImpl = success,
|
|
117
|
+
updateEnvLocalUrlImpl = updateEnvLocalUrl,
|
|
93
118
|
warnImpl = warn,
|
|
94
119
|
}) {
|
|
95
120
|
return async function app(subcmd, ...rest) {
|
|
@@ -109,11 +134,18 @@ export function createAppCommand({
|
|
|
109
134
|
if (!subcmd || subcmd === "list") {
|
|
110
135
|
logImpl(`\n${BOLD}${CYAN}Your Apps${RESET}\n`);
|
|
111
136
|
const res = await rpcQueryImpl(creds, "apps.list");
|
|
137
|
+
const body = await res.json().catch(() => null);
|
|
112
138
|
if (!res.ok) {
|
|
113
|
-
errorImpl((
|
|
139
|
+
errorImpl(appResponseError(body, invalidAppResponse("app list", res, "error")));
|
|
140
|
+
processRef.exit(1);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const apps = parseAppListResponse(body);
|
|
144
|
+
if (!apps) {
|
|
145
|
+
errorImpl(invalidAppResponse("app list", res, "success"));
|
|
146
|
+
processRef.exit(1);
|
|
114
147
|
return;
|
|
115
148
|
}
|
|
116
|
-
const apps = await res.json();
|
|
117
149
|
if (!apps.length) {
|
|
118
150
|
infoImpl("No apps yet. Run: gencow app create <name>");
|
|
119
151
|
return;
|
|
@@ -143,10 +175,16 @@ export function createAppCommand({
|
|
|
143
175
|
logImpl(`\n${BOLD}${CYAN}Gencow App Create${RESET}\n`);
|
|
144
176
|
infoImpl(`Creating app "${name}"...`);
|
|
145
177
|
const res = await rpcMutationImpl(creds, "apps.create", { name });
|
|
146
|
-
const data = await res
|
|
178
|
+
const data = await readJsonObjectResponse(res);
|
|
147
179
|
if (!res.ok) {
|
|
148
|
-
errorImpl(data
|
|
180
|
+
errorImpl(appResponseError(data, invalidAppResponse("app create", res, "error")));
|
|
181
|
+
processRef.exit(1);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!data) {
|
|
185
|
+
errorImpl(invalidAppResponse("app create", res, "success"));
|
|
149
186
|
processRef.exit(1);
|
|
187
|
+
return;
|
|
150
188
|
}
|
|
151
189
|
|
|
152
190
|
let createdApp;
|
|
@@ -162,9 +200,14 @@ export function createAppCommand({
|
|
|
162
200
|
saveCredsImpl({ ...creds, currentApp: appId });
|
|
163
201
|
|
|
164
202
|
const cwd = cwdImpl();
|
|
165
|
-
const
|
|
203
|
+
const projectSelection = resolveProjectSelection({
|
|
204
|
+
allowMissingConfig: true,
|
|
205
|
+
cwd,
|
|
206
|
+
});
|
|
207
|
+
const projectDir = projectSelection.projectDir;
|
|
208
|
+
const config = await loadConfig({ cwd: projectDir });
|
|
166
209
|
writeFileSync(
|
|
167
|
-
|
|
210
|
+
resolveProjectMetadataPath(projectDir, resolve),
|
|
168
211
|
`${JSON.stringify(
|
|
169
212
|
buildCreatedAppProjectMetadata({ appId, displayName: name, platformUrl: creds.platformUrl }),
|
|
170
213
|
null,
|
|
@@ -172,20 +215,15 @@ export function createAppCommand({
|
|
|
172
215
|
)}\n`,
|
|
173
216
|
);
|
|
174
217
|
successImpl(`gencow.json — saved appId = "${appId}"`);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
writeFileSync(envFilePath, `# Gencow app backend URL\n${envLine}\n`);
|
|
185
|
-
successImpl(`.env — created`);
|
|
186
|
-
}
|
|
218
|
+
await Promise.resolve(
|
|
219
|
+
updateEnvLocalUrlImpl(appUrl, {
|
|
220
|
+
config,
|
|
221
|
+
cwd,
|
|
222
|
+
projectDir,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
187
225
|
|
|
188
|
-
const functionsDir = resolve(
|
|
226
|
+
const functionsDir = resolve(projectDir, config.rootDir);
|
|
189
227
|
const indexPath = resolve(functionsDir, "index.ts");
|
|
190
228
|
if (!existsSync(indexPath)) {
|
|
191
229
|
mkdirSync(functionsDir, { recursive: true });
|
|
@@ -196,7 +234,7 @@ export function createAppCommand({
|
|
|
196
234
|
successImpl(`${config.rootDir}/index.ts — created (starter)`);
|
|
197
235
|
}
|
|
198
236
|
|
|
199
|
-
const configPath = resolve(
|
|
237
|
+
const configPath = projectSelection.configPath ?? resolve(projectDir, "gencow.config.ts");
|
|
200
238
|
if (existsSync(configPath)) {
|
|
201
239
|
const src = readFileSync(configPath, "utf8");
|
|
202
240
|
const nextSrc = addDeployAppToConfigSource(src, appId);
|
|
@@ -241,10 +279,28 @@ ${dashboardLine}
|
|
|
241
279
|
|
|
242
280
|
infoImpl(`Deleting app "${name}"...`);
|
|
243
281
|
const delRes = await rpcMutationImpl(creds, "apps.delete", { name });
|
|
244
|
-
|
|
245
|
-
if (!
|
|
246
|
-
|
|
282
|
+
let delData = await readJsonObjectResponse(delRes);
|
|
283
|
+
if (!isExactAppDeleteResponse(delData, name)) {
|
|
284
|
+
const operationResult = await pollAppDeleteOperation({
|
|
285
|
+
creds,
|
|
286
|
+
initialBody: delData,
|
|
287
|
+
name,
|
|
288
|
+
pollAttempts: deletePollAttempts,
|
|
289
|
+
pollIntervalMs: DELETE_POLL_INTERVAL_MS,
|
|
290
|
+
rpcQueryImpl,
|
|
291
|
+
sleepImpl,
|
|
292
|
+
});
|
|
293
|
+
if (operationResult) delData = operationResult;
|
|
294
|
+
}
|
|
295
|
+
if (!isExactAppDeleteResponse(delData, name)) {
|
|
296
|
+
errorImpl(
|
|
297
|
+
appResponseError(
|
|
298
|
+
delData,
|
|
299
|
+
invalidAppResponse("app delete", delRes, delRes.ok ? "success" : "error"),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
247
302
|
processRef.exit(1);
|
|
303
|
+
return;
|
|
248
304
|
}
|
|
249
305
|
|
|
250
306
|
const currentCreds = loadCredsImpl();
|
|
@@ -261,17 +317,20 @@ ${dashboardLine}
|
|
|
261
317
|
if (!name) {
|
|
262
318
|
errorImpl("Usage: gencow app status <name>");
|
|
263
319
|
processRef.exit(1);
|
|
320
|
+
return;
|
|
264
321
|
}
|
|
265
322
|
const res = await rpcQueryImpl(creds, "apps.get", { name });
|
|
266
|
-
const body = await res
|
|
267
|
-
const data = body?.result || body;
|
|
323
|
+
const body = await readJsonObjectResponse(res);
|
|
268
324
|
if (!res.ok) {
|
|
269
|
-
errorImpl(
|
|
325
|
+
errorImpl(appResponseError(body, invalidAppResponse("app status", res, "error")));
|
|
270
326
|
processRef.exit(1);
|
|
327
|
+
return;
|
|
271
328
|
}
|
|
329
|
+
const data = parseAppStatusResponse(body, name);
|
|
272
330
|
if (!data) {
|
|
273
|
-
errorImpl("
|
|
331
|
+
errorImpl(invalidAppResponse("app status", res, "success"));
|
|
274
332
|
processRef.exit(1);
|
|
333
|
+
return;
|
|
275
334
|
}
|
|
276
335
|
logImpl(`\n ${BOLD}${data.name}${RESET}`);
|
|
277
336
|
const status = formatAppStatusForCli(data);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isExactAppDeleteResponse } from "./app-response-contract.mjs";
|
|
2
|
+
import { APP_DELETE_OPERATION_PATTERN } from "./app-response-error.mjs";
|
|
3
|
+
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
4
|
+
|
|
5
|
+
const APP_DELETE_ACTIVE_STATES = new Set([
|
|
6
|
+
"accepted",
|
|
7
|
+
"stopping_children",
|
|
8
|
+
"draining_runtime",
|
|
9
|
+
"stop_dispatched",
|
|
10
|
+
"runtime_absent",
|
|
11
|
+
"tenant_db_drop_pending",
|
|
12
|
+
"catalog_delete_pending",
|
|
13
|
+
"blocked_ownership_ambiguous",
|
|
14
|
+
"blocked_active_work",
|
|
15
|
+
"blocked_runtime_outcome_unknown",
|
|
16
|
+
"failed_tenant_db_cleanup",
|
|
17
|
+
"failed_catalog_cleanup",
|
|
18
|
+
]);
|
|
19
|
+
const CORRELATION_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
20
|
+
const DIAGNOSTIC_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{2,127}$/u;
|
|
21
|
+
const APP_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+){2}$/u;
|
|
22
|
+
|
|
23
|
+
function isAllowedStatusPath(value, operationId, expectedAppId) {
|
|
24
|
+
if (value === undefined) return true;
|
|
25
|
+
if (typeof value !== "string" || value.length > 256 || value.includes("?") || value.includes("#")) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const match = /^\/api\/apps\/([^/]+)\/delete\/([^/]+)$/u.exec(value);
|
|
29
|
+
if (!match) return false;
|
|
30
|
+
try {
|
|
31
|
+
const appId = decodeURIComponent(match[1]);
|
|
32
|
+
return (
|
|
33
|
+
APP_ID_PATTERN.test(appId) &&
|
|
34
|
+
(!expectedAppId || appId === expectedAppId) &&
|
|
35
|
+
decodeURIComponent(match[2]) === operationId
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseAppDeleteOperationEnvelope(value, expectedAppId) {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
44
|
+
if (value.success !== undefined && value.success !== false) return null;
|
|
45
|
+
if (!APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "")) return null;
|
|
46
|
+
if (!APP_DELETE_ACTIVE_STATES.has(value.state)) return null;
|
|
47
|
+
if (
|
|
48
|
+
!CORRELATION_ID_PATTERN.test(value.correlationId ?? "") ||
|
|
49
|
+
(value.deleted !== undefined && value.deleted !== expectedAppId) ||
|
|
50
|
+
(value.code !== undefined && !DIAGNOSTIC_CODE_PATTERN.test(value.code)) ||
|
|
51
|
+
!isAllowedStatusPath(value.statusPath, value.operationId, expectedAppId)
|
|
52
|
+
) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function invalidPollResponse(accepted) {
|
|
59
|
+
return {
|
|
60
|
+
error: "CLI_APP_RESPONSE_INVALID: app delete status returned an invalid response",
|
|
61
|
+
code: "CLI_APP_RESPONSE_INVALID",
|
|
62
|
+
operationId: accepted.operationId,
|
|
63
|
+
correlationId: accepted.correlationId,
|
|
64
|
+
state: accepted.state,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isMatchingTerminalResponse(value, accepted, expectedAppId) {
|
|
69
|
+
return (
|
|
70
|
+
isExactAppDeleteResponse(value, expectedAppId) &&
|
|
71
|
+
value.operationId === accepted.operationId &&
|
|
72
|
+
value.correlationId === accepted.correlationId &&
|
|
73
|
+
value.state === "completed"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function pollAppDeleteOperation({
|
|
78
|
+
creds,
|
|
79
|
+
initialBody,
|
|
80
|
+
name,
|
|
81
|
+
pollAttempts,
|
|
82
|
+
pollIntervalMs,
|
|
83
|
+
rpcQueryImpl,
|
|
84
|
+
sleepImpl,
|
|
85
|
+
}) {
|
|
86
|
+
const accepted = parseAppDeleteOperationEnvelope(initialBody, name);
|
|
87
|
+
if (!accepted) return null;
|
|
88
|
+
|
|
89
|
+
let latest = accepted;
|
|
90
|
+
let successfulPollResponses = 0;
|
|
91
|
+
let transportFailures = 0;
|
|
92
|
+
for (let attempt = 0; attempt < pollAttempts; attempt += 1) {
|
|
93
|
+
try {
|
|
94
|
+
const statusRes = await rpcQueryImpl(creds, "apps.deleteStatus", {
|
|
95
|
+
name,
|
|
96
|
+
operationId: accepted.operationId,
|
|
97
|
+
});
|
|
98
|
+
const statusData = await readJsonObjectResponse(statusRes);
|
|
99
|
+
if (!statusRes?.ok || !statusData) return invalidPollResponse(accepted);
|
|
100
|
+
successfulPollResponses += 1;
|
|
101
|
+
if (isMatchingTerminalResponse(statusData, accepted, name)) return statusData;
|
|
102
|
+
const operation = parseAppDeleteOperationEnvelope(statusData, name);
|
|
103
|
+
if (
|
|
104
|
+
!operation ||
|
|
105
|
+
operation.operationId !== accepted.operationId ||
|
|
106
|
+
operation.correlationId !== accepted.correlationId
|
|
107
|
+
) {
|
|
108
|
+
return invalidPollResponse(accepted);
|
|
109
|
+
}
|
|
110
|
+
latest = operation;
|
|
111
|
+
} catch {
|
|
112
|
+
transportFailures += 1;
|
|
113
|
+
// The durable receipt remains authoritative across transient polling failures.
|
|
114
|
+
}
|
|
115
|
+
if (attempt + 1 < pollAttempts) await sleepImpl(pollIntervalMs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const pollingUnavailable = successfulPollResponses === 0 && transportFailures > 0;
|
|
119
|
+
return {
|
|
120
|
+
error: pollingUnavailable
|
|
121
|
+
? "App deletion status is temporarily unavailable. Retry the command with the same app."
|
|
122
|
+
: "App deletion is still pending. Retry the command to resume this operation.",
|
|
123
|
+
code: pollingUnavailable ? "APP_DELETE_STATUS_POLL_UNAVAILABLE" : "APP_DELETE_OPERATION_PENDING",
|
|
124
|
+
operationCode: latest.code ?? accepted.code,
|
|
125
|
+
operationId: accepted.operationId,
|
|
126
|
+
correlationId: latest.correlationId ?? accepted.correlationId,
|
|
127
|
+
state: latest.state,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const APP_DELETE_OPERATION_PATTERN =
|
|
2
|
+
/^app_delete_(?:[a-f0-9]{32}|[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$/u;
|
|
3
|
+
|
|
4
|
+
export function appResponseError(body, fallback) {
|
|
5
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
|
|
6
|
+
const message = typeof body.error === "string" && body.error.trim() ? body.error.trim() : fallback;
|
|
7
|
+
const lines = [message];
|
|
8
|
+
if (/^[A-Za-z][A-Za-z0-9_]{2,127}$/u.test(body.code ?? "")) {
|
|
9
|
+
lines.push(`Code: ${body.code}`);
|
|
10
|
+
}
|
|
11
|
+
if (/^[A-Za-z][A-Za-z0-9_]{2,127}$/u.test(body.operationCode ?? "")) {
|
|
12
|
+
lines.push(`Operation code: ${body.operationCode}`);
|
|
13
|
+
}
|
|
14
|
+
if (/^[A-Za-z0-9_-]{8,128}$/u.test(body.correlationId ?? "")) {
|
|
15
|
+
lines.push(`Correlation ID: ${body.correlationId}`);
|
|
16
|
+
}
|
|
17
|
+
if (APP_DELETE_OPERATION_PATTERN.test(body.operationId ?? "")) {
|
|
18
|
+
lines.push(`Operation ID: ${body.operationId}`);
|
|
19
|
+
}
|
|
20
|
+
if (/^[a-z][a-z0-9_]{2,63}$/u.test(body.state ?? "")) {
|
|
21
|
+
lines.push(`State: ${body.state}`);
|
|
22
|
+
}
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
package/lib/backup-command.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "fs";
|
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW, error, info, log, success } from "./output.mjs";
|
|
4
4
|
import { platformFetch, requireCreds, rpcMutation, rpcQuery } from "./platform-client.mjs";
|
|
5
|
+
import { readProjectMetadata } from "./project-context.mjs";
|
|
5
6
|
|
|
6
7
|
export function resolveBackupAppId(restArgs, gencowJson = null) {
|
|
7
8
|
let appId = null;
|
|
@@ -94,7 +95,7 @@ function renderBackupHelp(logImpl = log, errorImpl = error, subCmd = null) {
|
|
|
94
95
|
logImpl(` gencow backup restore <id> Restore from backup`);
|
|
95
96
|
logImpl(` gencow backup restore-file <path> Restore from a downloaded dump file`);
|
|
96
97
|
logImpl(` gencow backup delete <id> Delete a backup`);
|
|
97
|
-
logImpl(` gencow backup download <id> Download backup file (
|
|
98
|
+
logImpl(` gencow backup download <id> Download backup file (plan-dependent)\n`);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export function buildRestoreConfirmPrompt(sourceLabel, { includeStorageRisk = false } = {}) {
|
|
@@ -139,11 +140,12 @@ export function createBackupCommand({
|
|
|
139
140
|
const subCmd = backupArgs[0] || "list";
|
|
140
141
|
const restArgs = backupArgs.slice(1);
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
const { gencowJson } = readProjectMetadata({
|
|
144
|
+
cwd: cwdImpl(),
|
|
145
|
+
existsSyncImpl,
|
|
146
|
+
readFileSyncImpl,
|
|
147
|
+
resolvePathImpl,
|
|
148
|
+
});
|
|
147
149
|
|
|
148
150
|
const appId = resolveBackupAppId(restArgs, gencowJson);
|
|
149
151
|
if (!appId) {
|