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.
Files changed (60) hide show
  1. package/bin/gencow.mjs +32 -7
  2. package/core/index.js +24 -0
  3. package/lib/app-command.mjs +86 -27
  4. package/lib/app-delete-operation.mjs +129 -0
  5. package/lib/app-response-contract.mjs +49 -0
  6. package/lib/app-response-error.mjs +24 -0
  7. package/lib/backup-command.mjs +8 -6
  8. package/lib/canonical-bundle-config.mjs +82 -0
  9. package/lib/cli-dev-runtime.mjs +2 -2
  10. package/lib/cli-project-runtime.mjs +114 -71
  11. package/lib/cloud-targets.mjs +12 -1
  12. package/lib/codegen-command.mjs +79 -15
  13. package/lib/coding-guardrails.mjs +13 -1
  14. package/lib/cron-manifest.mjs +25 -17
  15. package/lib/cron-source-structure.mjs +102 -0
  16. package/lib/db-command.mjs +12 -8
  17. package/lib/db-push-command.mjs +12 -4
  18. package/lib/deploy-auditor.mjs +13 -30
  19. package/lib/deploy-bundle-staging.mjs +132 -0
  20. package/lib/deploy-command.mjs +20 -2
  21. package/lib/deploy-dependency-compat.mjs +12 -94
  22. package/lib/deploy-package-claim.mjs +31 -18
  23. package/lib/deploy-package-runtime.mjs +138 -156
  24. package/lib/deploy-runtime.mjs +113 -22
  25. package/lib/deploy-static-fullstack-runtime.mjs +6 -1
  26. package/lib/deployment-operation-poll.mjs +78 -0
  27. package/lib/dev-cloud-bundle.mjs +63 -14
  28. package/lib/dev-cloud-command.mjs +56 -27
  29. package/lib/dev-cloud-migrations.mjs +8 -7
  30. package/lib/dev-local-command.mjs +47 -29
  31. package/lib/doctor-command.mjs +188 -13
  32. package/lib/domain-command.mjs +32 -7
  33. package/lib/files-command.mjs +14 -1
  34. package/lib/init-command.mjs +1 -1
  35. package/lib/jobs-command.mjs +25 -4
  36. package/lib/migration-bundle-manifest.mjs +38 -12
  37. package/lib/platform-client.mjs +2 -0
  38. package/lib/project-context.mjs +298 -0
  39. package/lib/project-metadata-runtime.mjs +168 -0
  40. package/lib/project-schema-deployment-preview.mjs +25 -7
  41. package/lib/readme-codegen.mjs +33 -21
  42. package/lib/release-client.mjs +1 -1
  43. package/lib/runtime-bom-api-contract.mjs +71 -0
  44. package/lib/runtime-bom-resolve.mjs +257 -0
  45. package/lib/runtime-bom.mjs +343 -0
  46. package/lib/runtime-command.mjs +273 -0
  47. package/lib/runtime-package-contract.mjs +53 -0
  48. package/lib/static-command.mjs +90 -15
  49. package/lib/static-deploy-command.mjs +22 -3
  50. package/package.json +3 -3
  51. package/runtime/config.mjs +63 -5
  52. package/runtime/server.mjs +14658 -5617
  53. package/runtime/server.mjs.map +4 -4
  54. package/runtime/tooling.mjs +1004 -411
  55. package/templates/SECURITY.md +3 -3
  56. package/templates/ai-chat/README.md +2 -2
  57. package/templates/ai-chat/prompt.md +5 -5
  58. package/templates/fullstack/README.md +1 -1
  59. package/templates/fullstack/prompt.md +3 -3
  60. 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({ loadConfig });
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 frontend api.ts from schema
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 (Pro+ only)${RESET}
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 .env to cloud ${DIM}(--prod reads .env.production)${RESET}
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 (Pro+ only):${RESET}
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", ...args] = process.argv;
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) {
@@ -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((await res.json().catch(() => ({}))).error || "Failed to list apps");
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.json();
178
+ const data = await readJsonObjectResponse(res);
147
179
  if (!res.ok) {
148
- errorImpl(data.error || "Failed to create app");
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 config = await loadConfig();
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
- resolve(cwd, "gencow.json"),
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
- const envFilePath = resolve(cwd, ".env");
176
- const envLine = `VITE_API_URL=${appUrl}`;
177
- if (existsSync(envFilePath)) {
178
- const envContent = readFileSync(envFilePath, "utf8");
179
- if (!envContent.includes("VITE_API_URL")) {
180
- writeFileSync(envFilePath, envContent.trimEnd() + "\n" + envLine + "\n");
181
- successImpl(`.env — added VITE_API_URL`);
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(cwd, config.rootDir);
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(cwd, "gencow.config.ts");
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
- const delData = await delRes.json();
245
- if (!delRes.ok) {
246
- errorImpl(delData.error || "Failed to delete app");
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.json();
267
- const data = body?.result || body;
323
+ const body = await readJsonObjectResponse(res);
268
324
  if (!res.ok) {
269
- errorImpl(data?.error || "App not found");
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("App not found");
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
+ }
@@ -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 (Pro+)\n`);
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
- let gencowJson = null;
143
- const gencowJsonPath = resolvePathImpl(cwdImpl(), "gencow.json");
144
- if (existsSyncImpl(gencowJsonPath)) {
145
- gencowJson = JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8"));
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) {