gencow 0.1.216 → 0.1.218

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.
@@ -76,6 +76,10 @@ function invalidPollResponse(accepted, response = null) {
76
76
  };
77
77
  }
78
78
 
79
+ function isRetryableStatusPollFailure(response) {
80
+ return response?.status === 503 || response?.status >= 500;
81
+ }
82
+
79
83
  function isMatchingTerminalResponse(value, accepted, expectedAppId) {
80
84
  return (
81
85
  isExactAppDeleteResponse(value, expectedAppId) &&
@@ -125,7 +129,17 @@ export async function pollAppDeleteOperation({
125
129
  operationId: accepted.operationId,
126
130
  });
127
131
  const statusData = await readJsonObjectResponse(statusRes);
128
- if (!statusRes?.ok || !statusData) return invalidPollResponse(accepted, statusRes);
132
+ // A lifecycle operation is durable. A transient 5xx while the status
133
+ // query is being served must not turn that durable receipt into a client
134
+ // contract failure or abandon the operation's correlation identity.
135
+ if (!statusRes?.ok || !statusData) {
136
+ if (isRetryableStatusPollFailure(statusRes)) {
137
+ transportFailures += 1;
138
+ if (attempt + 1 < pollAttempts) await sleepImpl(pollIntervalMs);
139
+ continue;
140
+ }
141
+ return invalidPollResponse(accepted, statusRes);
142
+ }
129
143
  successfulPollResponses += 1;
130
144
  if (isMatchingTerminalResponse(statusData, accepted, name)) return statusData;
131
145
  if (isMatchingTerminalFailureResponse(statusData, accepted, name)) {
@@ -41,6 +41,37 @@ import {
41
41
  } from "./deploy-lockfile-selection.mjs";
42
42
  import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
43
43
  import { writeCanonicalDeployBundle } from "./deploy-bundle-staging.mjs";
44
+
45
+ const INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS = 3;
46
+
47
+ function isRetryableInitialDeploymentAdmission(response) {
48
+ return Number.isInteger(response?.status) && response.status >= 500 && response.status <= 599;
49
+ }
50
+
51
+ /**
52
+ * A deploy claim is an idempotency key. Before the Platform returns the
53
+ * operation receipt, a transport/5xx outcome is intentionally ambiguous;
54
+ * resubmit only that same claim a bounded number of times rather than making
55
+ * the customer invent a second operation.
56
+ */
57
+ export async function submitDeploymentWithIdempotentRetry({
58
+ submit,
59
+ sleepImpl = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms)),
60
+ }) {
61
+ let response;
62
+ for (let attempt = 1; attempt <= INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS; attempt += 1) {
63
+ response = await submit();
64
+ if (
65
+ !isRetryableInitialDeploymentAdmission(response) ||
66
+ attempt === INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS
67
+ ) {
68
+ return response;
69
+ }
70
+ await sleepImpl(250 * attempt);
71
+ }
72
+ return response;
73
+ }
74
+
44
75
  async function loadProjectConfigDefault(options) {
45
76
  const { loadConfig } = await import("./cli-project-runtime.mjs");
46
77
  return loadConfig(options);
@@ -637,6 +668,15 @@ export function createDeployPackageRuntime({
637
668
  platformFetch: platformFetchImpl,
638
669
  setTimeoutImpl,
639
670
  });
671
+ const submitDeployAdmission = () => {
672
+ // A legacy request without a deploy claim has no stable pre-receipt
673
+ // identity. Never retry that request automatically.
674
+ if (!activeDeployClaim) return deployOnce(targetAppId);
675
+ return submitDeploymentWithIdempotentRetry({
676
+ submit: () => deployOnce(targetAppId),
677
+ sleepImpl: (ms) => new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, ms)),
678
+ });
679
+ };
640
680
 
641
681
  const showCrashLogs = async (errData, title, targetAppId) => {
642
682
  let diagnosticLines = normalizeDiagnosticLogLines(errData?.crashLogs);
@@ -661,7 +701,7 @@ export function createDeployPackageRuntime({
661
701
  const spinner = startSpinner("Deploying...");
662
702
  let deployRes;
663
703
  try {
664
- deployRes = await deployOnce(targetAppId);
704
+ deployRes = await submitDeployAdmission();
665
705
  if (deployRes.status === 202) {
666
706
  deployRes = await pollAcceptedDeployment(deployRes);
667
707
  }
@@ -698,7 +738,7 @@ export function createDeployPackageRuntime({
698
738
  infoImpl("Retrying deploy...");
699
739
  const retrySpinner = startSpinner("Deploying...");
700
740
  try {
701
- deployRes = await deployOnce(targetAppId);
741
+ deployRes = await submitDeployAdmission();
702
742
  if (deployRes.status === 202) {
703
743
  deployRes = await pollAcceptedDeployment(deployRes);
704
744
  }
@@ -737,7 +777,9 @@ export function createDeployPackageRuntime({
737
777
 
738
778
  const deployData = await readJsonObjectResponse(deployRes);
739
779
  if (!deployData) {
740
- errorImpl(`Deploy failed: no terminal deployment receipt was returned after packaging.\nCode: CLI_DEPLOY_TERMINAL_RECEIPT_MISSING\nStage: controller\nHTTP status: ${deployRes?.status ?? "unknown"}\nNext action: RETRY`);
780
+ errorImpl(
781
+ `Deploy failed: no terminal deployment receipt was returned after packaging.\nCode: CLI_DEPLOY_TERMINAL_RECEIPT_MISSING\nStage: controller\nHTTP status: ${deployRes?.status ?? "unknown"}\nNext action: RETRY`,
782
+ );
741
783
  exitImpl(1);
742
784
  return null;
743
785
  }
@@ -748,7 +790,11 @@ export function createDeployPackageRuntime({
748
790
  successImpl(`Server build complete! (${deployElapsed}s)`);
749
791
 
750
792
  if (deployData.url) {
751
- const ready = await verifyAppReadyImpl(releaseAttemptId ? { appUrl: deployData.url, appId: targetAppId, announceReady: false } : { appUrl: deployData.url, appId: targetAppId });
793
+ const ready = await verifyAppReadyImpl(
794
+ releaseAttemptId
795
+ ? { appUrl: deployData.url, appId: targetAppId, announceReady: false }
796
+ : { appUrl: deployData.url, appId: targetAppId },
797
+ );
752
798
  if (!ready) {
753
799
  errorImpl("Deploy failed: app did not become ready after deployment.");
754
800
  await showCrashLogs(null, "Readiness failure diagnostics:", targetAppId);
@@ -144,6 +144,13 @@ export function readDeployProjectContext({
144
144
  return { appId, displayName, gencowJsonPath, prodAppId, projectDir };
145
145
  }
146
146
 
147
+ // gencow.json deliberately keeps the first production deployment separate
148
+ // from the optional development app. Every production operation must resolve
149
+ // that persisted production identity before it can create or deploy anything.
150
+ export function resolveEnvironmentDeployTarget({ appId = null, envTarget = "dev", prodAppId = null } = {}) {
151
+ return envTarget === "prod" ? (prodAppId ?? appId) : appId;
152
+ }
153
+
147
154
  function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, logImpl = log } = {}) {
148
155
  errorImpl(`Unknown deploy argument: "${arg}"`);
149
156
  logImpl("");
@@ -516,6 +523,7 @@ export function createDeployCommand({
516
523
  resolvePathImpl,
517
524
  });
518
525
  let { appId, displayName, gencowJsonPath, prodAppId, projectDir } = context;
526
+ appId = resolveEnvironmentDeployTarget({ appId, envTarget: parsed.envTarget, prodAppId });
519
527
  const backendProjectRoot = projectDir ?? projectRoot;
520
528
  const projectConfig = loadConfigImpl
521
529
  ? await loadConfigImpl({
@@ -5,6 +5,11 @@ import { createDeployPackageRuntime, writeProjectMetadata } from "./deploy-packa
5
5
  import { BOLD, CYAN, DIM, RESET, error, info, log, success } from "./output.mjs";
6
6
  import { preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
7
7
 
8
+ function shouldPreserveFinalizeAttempt(error) {
9
+ return error?.retryable === true ||
10
+ error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
11
+ }
12
+
8
13
  export function createScopedDeployPackageRuntime({
9
14
  buildEnvImpl,
10
15
  buildDeployClaimFromArchiveImpl,
@@ -181,7 +186,9 @@ export async function deployBackendPackage({
181
186
  try {
182
187
  await finalizeReleaseAttempt({ appId: deployed.appId, attemptId: releaseAttempt.attemptId });
183
188
  } catch (caught) {
184
- await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
189
+ if (!shouldPreserveFinalizeAttempt(caught)) {
190
+ await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
191
+ }
185
192
  throw caught;
186
193
  }
187
194
  successImpl("App Ready! (release finalized)");
@@ -63,6 +63,15 @@ function isNonterminalResponse(response, body, identity) {
63
63
  );
64
64
  }
65
65
 
66
+ function isTransientPinnedOperationRead(response, expected) {
67
+ return (
68
+ expected.operationId !== null &&
69
+ Number.isInteger(response?.status) &&
70
+ response.status >= 500 &&
71
+ response.status <= 599
72
+ );
73
+ }
74
+
66
75
  export async function pollAcceptedDeploymentOperation({
67
76
  initialResponse,
68
77
  creds,
@@ -153,6 +162,22 @@ export async function pollAcceptedDeploymentOperation({
153
162
  response = polledResponse;
154
163
  continue;
155
164
  }
165
+ // Once the server has sealed the operation identity, a gateway/database
166
+ // read failure cannot safely be presented as that operation's terminal
167
+ // result. Keep polling the same receipt; never submit a second deploy.
168
+ if (isTransientPinnedOperationRead(response, expected)) {
169
+ if (nowImpl() >= deadlineMs) return pendingResponse();
170
+ const retryAfter = Number(response.headers?.get?.("Retry-After"));
171
+ const retryDelayMs =
172
+ Number.isFinite(retryAfter) && retryAfter >= 1 && retryAfter <= 10 ? retryAfter * 1000 : 1000;
173
+ const remainingMs = Math.max(0, deadlineMs - nowImpl());
174
+ await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, Math.min(retryDelayMs, remainingMs)));
175
+ if (nowImpl() >= deadlineMs) return pendingResponse();
176
+ const polledResponse = await fetchWithinDeadline();
177
+ if (!polledResponse) return pendingResponse();
178
+ response = polledResponse;
179
+ continue;
180
+ }
156
181
  if (
157
182
  expected.operationId === null ||
158
183
  !TERMINAL_STATES.has(identity.state) ||
@@ -587,7 +587,10 @@ export function createInitCommand({
587
587
  successImpl("Dependencies installed");
588
588
  } catch (caught) {
589
589
  installStatus = "failed";
590
- warnImpl(`${caught?.code ?? "CLI_INIT_DEPENDENCY_INSTALL_FAILED"}: Dependency setup failed.`);
590
+ const reason = typeof caught?.reason === "string" ? ` (${caught.reason})` : "";
591
+ warnImpl(
592
+ `${caught?.code ?? "CLI_INIT_DEPENDENCY_INSTALL_FAILED"}${reason}: Dependency setup failed.`,
593
+ );
591
594
  exitImpl(1);
592
595
  return;
593
596
  }
@@ -15,6 +15,33 @@ function corepackCommand(platform) {
15
15
  return platform === "win32" ? "corepack.cmd" : "corepack";
16
16
  }
17
17
 
18
+ function isCommandUnavailable(error) {
19
+ return error?.code === "ENOENT";
20
+ }
21
+
22
+ function installProjectDependencies(projectDir, deps) {
23
+ const pnpmArgs = [approvedPnpmSelector(), "install", "--ignore-workspace"];
24
+ try {
25
+ deps.execFileSyncImpl(corepackCommand(deps.platform), pnpmArgs, {
26
+ cwd: projectDir,
27
+ stdio: ["ignore", "pipe", "pipe"],
28
+ });
29
+ return { executor: "corepack" };
30
+ } catch (error) {
31
+ // Corepack is deliberately preferred because it is Node's package-manager
32
+ // authority. Some supported CLI hosts intentionally provide only Bun;
33
+ // boot the *same pinned pnpm* through Bun rather than switching lockfile
34
+ // authorities or requiring a machine-global install.
35
+ if (!isCommandUnavailable(error)) throw error;
36
+ }
37
+
38
+ deps.execFileSyncImpl(deps.bunRuntimePath, ["x", "--bun", ...pnpmArgs], {
39
+ cwd: projectDir,
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ });
42
+ return { executor: "bun-pnpm" };
43
+ }
44
+
18
45
  function readProjectDependencyFiles(projectDir, deps) {
19
46
  const packagePath = resolve(projectDir, PACKAGE_FILE);
20
47
  const lockPath = resolve(projectDir, LOCK_FILE);
@@ -65,6 +92,7 @@ function restoreFile(path, previous, deps) {
65
92
 
66
93
  function dependencyIoDeps(overrides = {}) {
67
94
  return {
95
+ bunRuntimePath: overrides.bunRuntimePath ?? process.execPath,
68
96
  execFileSyncImpl: overrides.execFileSyncImpl ?? execFileSync,
69
97
  existsSyncImpl: overrides.existsSyncImpl ?? existsSync,
70
98
  readFileSyncImpl: overrides.readFileSyncImpl ?? readFileSync,
@@ -77,17 +105,13 @@ function dependencyIoDeps(overrides = {}) {
77
105
  export function installAndVerifyProjectDependencies(projectDir, overrides = {}) {
78
106
  const deps = dependencyIoDeps(overrides);
79
107
  try {
80
- deps.execFileSyncImpl(
81
- corepackCommand(deps.platform),
82
- [approvedPnpmSelector(), "install", "--ignore-workspace"],
83
- {
84
- cwd: projectDir,
85
- stdio: ["ignore", "pipe", "pipe"],
86
- },
87
- );
88
- } catch {
108
+ installProjectDependencies(projectDir, deps);
109
+ } catch (cause) {
89
110
  const error = new Error("CLI_INIT_DEPENDENCY_INSTALL_FAILED");
90
111
  error.code = "CLI_INIT_DEPENDENCY_INSTALL_FAILED";
112
+ error.reason = isCommandUnavailable(cause)
113
+ ? "APPROVED_PNPM_EXECUTOR_UNAVAILABLE"
114
+ : "APPROVED_PNPM_INSTALL_FAILED";
91
115
  throw error;
92
116
  }
93
117
  return assertDeployValidLock(projectDir, deps, "CLI_INIT_LOCK_POSTCONDITION_FAILED");
@@ -24,6 +24,7 @@ const PUBLIC_RELEASE_ERROR_CODES = new Set([
24
24
  "APP_RELEASE_ENVIRONMENT_INVALID",
25
25
  "APP_RELEASE_ENVIRONMENT_MISMATCH",
26
26
  "APP_RELEASE_ENV_KEYS_MISSING",
27
+ "APP_RELEASE_FINALIZATION_PENDING",
27
28
  "APP_RELEASE_IDEMPOTENCY_CONFLICT",
28
29
  "APP_RELEASE_METADATA_INVALID",
29
30
  "APP_RELEASE_POLICY_READ_UNAVAILABLE",
@@ -113,7 +114,8 @@ export async function prepareReleaseAttempt({
113
114
  capabilityTimeoutSetTimeoutImpl = setTimeout,
114
115
  capabilityTimeoutClearTimeoutImpl = clearTimeout,
115
116
  }) {
116
- const capabilityRequest = (signal) => releaseCapability({ creds, appId, environment, platformFetchImpl, signal });
117
+ const capabilityRequest = (signal) =>
118
+ releaseCapability({ creds, appId, environment, platformFetchImpl, signal });
117
119
  let capability;
118
120
  if (Number.isFinite(capabilityTimeoutMs) && capabilityTimeoutMs > 0) {
119
121
  let timeout;
@@ -122,13 +124,10 @@ export async function prepareReleaseAttempt({
122
124
  capability = await Promise.race([
123
125
  capabilityRequest(controller.signal),
124
126
  new Promise((_, reject) => {
125
- timeout = capabilityTimeoutSetTimeoutImpl(
126
- () => {
127
- controller.abort();
128
- reject(new Error("APP_RELEASE_CAPABILITY_TIMEOUT"));
129
- },
130
- capabilityTimeoutMs,
131
- );
127
+ timeout = capabilityTimeoutSetTimeoutImpl(() => {
128
+ controller.abort();
129
+ reject(new Error("APP_RELEASE_CAPABILITY_TIMEOUT"));
130
+ }, capabilityTimeoutMs);
132
131
  timeout?.unref?.();
133
132
  }),
134
133
  ]);
@@ -20,6 +20,11 @@ import { getCliInvocationSelection } from "./project-context.mjs";
20
20
  import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
21
21
  import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
22
22
 
23
+ function shouldPreserveFinalizeAttempt(error) {
24
+ return error?.retryable === true ||
25
+ error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
26
+ }
27
+
23
28
  export function resolveStaticDeployDir({
24
29
  staticDirArg,
25
30
  cwd = process.cwd(),
@@ -447,7 +452,9 @@ export function createStaticDeployRuntime({
447
452
  platformFetchImpl,
448
453
  });
449
454
  } catch (caught) {
450
- await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
455
+ if (!shouldPreserveFinalizeAttempt(caught)) {
456
+ await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
457
+ }
451
458
  throw caught;
452
459
  }
453
460
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.216",
3
+ "version": "0.1.218",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,8 +33,8 @@
33
33
  "devDependencies": {
34
34
  "@types/node": "^25.9.5",
35
35
  "better-auth": "^1.6.23",
36
- "@gencow/core": "0.1.43",
37
36
  "@gencow/client": "0.2.7",
37
+ "@gencow/core": "0.1.43",
38
38
  "@gencow/react": "0.2.7"
39
39
  },
40
40
  "scripts": {