gencow 0.1.244 → 0.1.245

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.
@@ -164,7 +164,7 @@ async function confirmDelete(name) {
164
164
  const rl = createInterface({ input: process.stdin, output: process.stdout });
165
165
  const answer = await new Promise((resolveAnswer) =>
166
166
  rl.question(
167
- `\n ${RED}!${RESET} Remove "${BOLD}${name}${RESET}" from your account? An administrator will stop it and permanently delete its data. The name remains unavailable until then.\n Type the app name to confirm: `,
167
+ `\n ${RED}!${RESET} Remove "${BOLD}${name}${RESET}" from your account? An administrator will stop it and permanently delete its data. Redeploying this project can create a new app ID with the same display name.\n Type the app name to confirm: `,
168
168
  resolveAnswer,
169
169
  ),
170
170
  );
@@ -408,7 +408,7 @@ ${dashboardLine}
408
408
  saveCredsImpl({ ...currentCreds, currentApp: undefined });
409
409
  }
410
410
 
411
- successImpl(`App "${name}" removed from your account. An administrator will permanently delete it. The name remains unavailable until then.`);
411
+ successImpl(`App "${name}" removed from your account. An administrator will stop it and permanently delete it. Redeploying this project can create a new app ID with the same display name.`);
412
412
  if (delData.stopErrorCode) {
413
413
  warnImpl(`App stop needs administrator review. Code: ${delData.stopErrorCode}. Handoff ID: ${delData.handoffId}`);
414
414
  }
@@ -0,0 +1,71 @@
1
+ import { resolveCreatedAppResponse } from "./app-create-response.mjs";
2
+ import { resolveDeployAppId } from "./deploy-app-target.mjs";
3
+ import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
4
+ import { readJsonObjectResponse } from "./http-response-json.mjs";
5
+ import { DIM, RESET } from "./output.mjs";
6
+
7
+ export function createCloudAppIfNeeded({
8
+ platformFetchImpl,
9
+ rpcMutationImpl,
10
+ errorImpl,
11
+ infoImpl,
12
+ exitImpl,
13
+ successImpl,
14
+ setTimeoutImpl,
15
+ writeFileSyncImpl,
16
+ }) {
17
+ return async function createAppIfNeeded({
18
+ creds,
19
+ appId,
20
+ displayName,
21
+ envTarget = "dev",
22
+ gencowJsonPath,
23
+ allowMissingAppReplacement = true,
24
+ }) {
25
+ const resolvedAppId = await resolveDeployAppId({
26
+ creds,
27
+ appId,
28
+ displayName,
29
+ environment: envTarget,
30
+ platformFetchImpl,
31
+ allowMissingAppReplacement,
32
+ infoImpl,
33
+ errorImpl,
34
+ exitImpl,
35
+ });
36
+ if (resolvedAppId === false) return null;
37
+ if (resolvedAppId) return resolvedAppId;
38
+
39
+ infoImpl("Creating app (auto-generating ID)...");
40
+ const createRes = await rpcMutationImpl(creds, "apps.create", { name: displayName });
41
+ if (!createRes.ok) {
42
+ const createErr = await readJsonObjectResponse(createRes);
43
+ errorImpl(`App creation failed: ${createErr?.error || createRes.statusText}`);
44
+ exitImpl(1);
45
+ return null;
46
+ }
47
+
48
+ const createData = await readJsonObjectResponse(createRes);
49
+ let nextAppId;
50
+ try {
51
+ nextAppId = resolveCreatedAppResponse(createData, { platformUrl: creds.platformUrl }).appId;
52
+ } catch (caught) {
53
+ errorImpl(`App creation failed: ${caught.message}`);
54
+ exitImpl(1);
55
+ return null;
56
+ }
57
+
58
+ successImpl(`App "${nextAppId}" created. Waiting for provisioning...`);
59
+ writeProjectMetadata({
60
+ appId: nextAppId,
61
+ displayName,
62
+ envTarget,
63
+ platformUrl: creds.platformUrl,
64
+ gencowJsonPath,
65
+ writeFileSyncImpl,
66
+ });
67
+ infoImpl(`${DIM}gencow.json created (appId: ${nextAppId})${RESET}`);
68
+ await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
69
+ return nextAppId;
70
+ };
71
+ }
@@ -0,0 +1,182 @@
1
+ import { readJsonObjectResponse } from "./http-response-json.mjs";
2
+
3
+ const PUBLIC_LOOKUP_ERROR_CODES = new Set([
4
+ "ACCOUNT_DELETED",
5
+ "ACCOUNT_DELETION_PENDING",
6
+ "ACCOUNT_SUSPENDED",
7
+ "APP_RELEASE_AUTH_REQUIRED",
8
+ "APP_RELEASE_CAPABILITY_UNAVAILABLE",
9
+ "APP_RELEASE_ENVIRONMENT_INVALID",
10
+ "APP_RELEASE_POLICY_READ_UNAVAILABLE",
11
+ ]);
12
+ const SAFE_CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
13
+
14
+ function publicLookupFailure(response, body) {
15
+ const code = PUBLIC_LOOKUP_ERROR_CODES.has(body?.code)
16
+ ? body.code
17
+ : response.status === 401
18
+ ? "APP_RELEASE_AUTH_REQUIRED"
19
+ : "APP_DEPLOY_TARGET_LOOKUP_FAILED";
20
+ return {
21
+ kind: "error",
22
+ code,
23
+ status: response.status,
24
+ retryable: typeof body?.retryable === "boolean" ? body.retryable : response.status >= 500,
25
+ ...(typeof body?.correlationId === "string" && SAFE_CORRELATION_ID.test(body.correlationId)
26
+ ? { correlationId: body.correlationId }
27
+ : {}),
28
+ };
29
+ }
30
+
31
+ function formatLookupFailure(failure, target) {
32
+ const code = failure?.kind === "error" ? failure.code : "APP_DEPLOY_TARGET_LOOKUP_FAILED";
33
+ const lines = [
34
+ `${code}: Could not verify the configured ${target} app. No replacement app was created and gencow.json was unchanged.`,
35
+ ];
36
+ if (failure?.status) lines.push(`HTTP status: ${failure.status}`);
37
+ if (typeof failure?.retryable === "boolean") lines.push(`Retryable: ${failure.retryable ? "yes" : "no"}`);
38
+ if (failure?.correlationId) lines.push(`Correlation ID: ${failure.correlationId}`);
39
+ if (code === "APP_RELEASE_AUTH_REQUIRED") {
40
+ lines.push("Action: run gencow login, or replace/unset the environment credential, then retry.");
41
+ } else if (code === "ACCOUNT_SUSPENDED" || code === "ACCOUNT_DELETION_PENDING" || code === "ACCOUNT_DELETED") {
42
+ lines.push("Action: resolve the account status in Platform before retrying.");
43
+ } else if (failure?.retryable) {
44
+ lines.push("Action: wait for Platform release capability to recover, then retry.");
45
+ } else {
46
+ lines.push("Action: check that the credential can access this app, then retry.");
47
+ }
48
+ return lines.join("\n");
49
+ }
50
+
51
+ export async function lookupOwnedApp({ creds, appId, environment, platformFetchImpl }) {
52
+ try {
53
+ const response = await platformFetchImpl(
54
+ creds,
55
+ `/platform/apps/${appId}/release-capability?environment=${environment}`,
56
+ { method: "GET" },
57
+ );
58
+ const body = await readJsonObjectResponse(response);
59
+ if (response.status === 404) {
60
+ if (body?.code === "APP_RELEASE_APP_NOT_FOUND") return "missing";
61
+ return body?.code ? publicLookupFailure(response, body) : "unsupported";
62
+ }
63
+ if (response.status === 409 && body?.code === "APP_RELEASE_VERSIONS_DISABLED") return "found";
64
+ if (!response.ok) return publicLookupFailure(response, body);
65
+ return body?.protocol === "app-release-v1" && typeof body.enabled === "boolean" ? "found" : "error";
66
+ } catch {
67
+ return {
68
+ kind: "error",
69
+ code: "APP_RELEASE_CAPABILITY_UNAVAILABLE",
70
+ retryable: true,
71
+ };
72
+ }
73
+ }
74
+
75
+ export async function resolveDeployAppId({
76
+ creds,
77
+ appId,
78
+ displayName,
79
+ environment = "dev",
80
+ platformFetchImpl,
81
+ allowMissingAppReplacement,
82
+ infoImpl,
83
+ errorImpl,
84
+ exitImpl,
85
+ }) {
86
+ if (!appId || !platformFetchImpl) return appId;
87
+ const lookup = await lookupOwnedApp({ creds, appId, environment, platformFetchImpl });
88
+ if (lookup === "found" || lookup === "unsupported") return appId;
89
+ if (lookup?.kind === "error" || lookup === "error") {
90
+ errorImpl(formatLookupFailure(lookup, `"${appId}"`));
91
+ exitImpl(1);
92
+ return false;
93
+ }
94
+ if (!allowMissingAppReplacement) {
95
+ errorImpl(
96
+ `APP_NOT_FOUND: The existing app ID "${appId}" is unavailable. This deployment requires the original app; restore access before retrying. No replacement app was created.`,
97
+ );
98
+ exitImpl(1);
99
+ return false;
100
+ }
101
+ infoImpl(`Configured app ID is no longer available. Creating a new app named "${displayName}"...`);
102
+ return null;
103
+ }
104
+
105
+ export async function reconcileProductionAppTarget({
106
+ appId,
107
+ prodAppId,
108
+ creds,
109
+ displayName,
110
+ gencowJsonPath,
111
+ platformFetchImpl,
112
+ createCloudAppIfNeeded,
113
+ infoImpl,
114
+ errorImpl,
115
+ exitImpl,
116
+ }) {
117
+ let productionParentAppId = appId;
118
+ let existingProdAppId = prodAppId;
119
+ if (!platformFetchImpl || (!productionParentAppId && !existingProdAppId)) {
120
+ return { appId: productionParentAppId, prodAppId: existingProdAppId };
121
+ }
122
+
123
+ const failLookup = (failure, target) => {
124
+ errorImpl(formatLookupFailure(failure, target));
125
+ exitImpl(1);
126
+ return null;
127
+ };
128
+
129
+ if (existingProdAppId) {
130
+ const prodState = await lookupOwnedApp({
131
+ creds,
132
+ appId: existingProdAppId,
133
+ environment: "prod",
134
+ platformFetchImpl,
135
+ });
136
+ if (prodState?.kind === "error" || prodState === "error") return failLookup(prodState, "production");
137
+ if (prodState === "found" || prodState === "unsupported") {
138
+ return { appId: productionParentAppId, prodAppId: existingProdAppId };
139
+ }
140
+ infoImpl(
141
+ `Configured production app ID is no longer available. Creating a production app for "${displayName}"...`,
142
+ );
143
+ existingProdAppId = null;
144
+ }
145
+
146
+ const parentState = productionParentAppId
147
+ ? await lookupOwnedApp({
148
+ creds,
149
+ appId: productionParentAppId,
150
+ environment: "dev",
151
+ platformFetchImpl,
152
+ })
153
+ : "missing";
154
+ if (productionParentAppId && (parentState?.kind === "error" || parentState === "error")) {
155
+ return failLookup(parentState, "root");
156
+ }
157
+
158
+ if (productionParentAppId && parentState === "missing") {
159
+ infoImpl(`Configured app ID is no longer available. Creating a new app named "${displayName}"...`);
160
+ productionParentAppId = await createCloudAppIfNeeded({
161
+ creds,
162
+ appId: null,
163
+ displayName,
164
+ envTarget: "dev",
165
+ gencowJsonPath,
166
+ });
167
+ if (!productionParentAppId) return null;
168
+ }
169
+
170
+ if (!productionParentAppId) {
171
+ productionParentAppId = await createCloudAppIfNeeded({
172
+ creds,
173
+ appId: null,
174
+ displayName,
175
+ envTarget: "dev",
176
+ gencowJsonPath,
177
+ });
178
+ if (!productionParentAppId) return null;
179
+ }
180
+
181
+ return { appId: productionParentAppId, prodAppId: existingProdAppId };
182
+ }
@@ -3,9 +3,8 @@ import { randomUUID } from "node:crypto";
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
4
4
  import { dirname, resolve } from "path";
5
5
  import { fetchAppDiagnosticLogsViaRest, normalizeDiagnosticLogLines } from "./app-diagnostics.mjs";
6
- import { resolveCreatedAppResponse } from "./app-create-response.mjs";
6
+ import { createCloudAppIfNeeded as createCloudAppIfNeededImpl } from "./deploy-app-creation.mjs";
7
7
  import { resolveConfiguredAuditorImplementation } from "./auditor-mode.mjs";
8
- import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
9
8
  import {
10
9
  loadDeployMigrationGenerationContext,
11
10
  prepareDeployCodegenArtifacts,
@@ -557,41 +556,16 @@ export function createDeployPackageRuntime({
557
556
  return { bundleBuffer, bundleSize, deployClaim, tmpBundle };
558
557
  }
559
558
 
560
- async function createCloudAppIfNeeded({ creds, appId, displayName, envTarget = "dev", gencowJsonPath }) {
561
- if (appId) return appId;
562
-
563
- infoImpl("Creating app (auto-generating ID)...");
564
- const createRes = await rpcMutationImpl(creds, "apps.create", { name: displayName });
565
- if (!createRes.ok) {
566
- const createErr = await readJsonObjectResponse(createRes);
567
- errorImpl(`App creation failed: ${createErr?.error || createRes.statusText}`);
568
- exitImpl(1);
569
- return null;
570
- }
571
-
572
- const createData = await readJsonObjectResponse(createRes);
573
- let nextAppId;
574
- try {
575
- nextAppId = resolveCreatedAppResponse(createData, { platformUrl: creds.platformUrl }).appId;
576
- } catch (caught) {
577
- errorImpl(`App creation failed: ${caught.message}`);
578
- exitImpl(1);
579
- return null;
580
- }
581
-
582
- successImpl(`App "${nextAppId}" created. Waiting for provisioning...`);
583
- writeProjectMetadata({
584
- appId: nextAppId,
585
- displayName,
586
- envTarget,
587
- platformUrl: creds.platformUrl,
588
- gencowJsonPath,
589
- writeFileSyncImpl,
590
- });
591
- infoImpl(`${DIM}gencow.json created (appId: ${nextAppId})${RESET}`);
592
- await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
593
- return nextAppId;
594
- }
559
+ const createCloudAppIfNeeded = createCloudAppIfNeededImpl({
560
+ platformFetchImpl,
561
+ rpcMutationImpl,
562
+ errorImpl,
563
+ infoImpl,
564
+ exitImpl,
565
+ successImpl,
566
+ setTimeoutImpl,
567
+ writeFileSyncImpl,
568
+ });
595
569
 
596
570
  function startSpinner(label) {
597
571
  const spinnerFrames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
@@ -17,6 +17,7 @@ import {
17
17
  } from "./deploy-static-fullstack-runtime.mjs";
18
18
  import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
19
19
  import { readJsonObjectResponse } from "./http-response-json.mjs";
20
+ import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
20
21
 
21
22
  export function parseDeployArgs(deployArgs) {
22
23
  const parsed = {
@@ -192,6 +193,8 @@ async function confirmFirstProductionDeploy({ appId, createInterfaceImpl, infoIm
192
193
 
193
194
  async function ensureProductionDeployTarget({
194
195
  appId,
196
+ displayName,
197
+ createCloudAppIfNeeded,
195
198
  createInterfaceImpl,
196
199
  creds,
197
200
  deployArgs,
@@ -209,14 +212,28 @@ async function ensureProductionDeployTarget({
209
212
  prodAppId = null,
210
213
  region = null,
211
214
  }) {
212
- if (!appId || prodAppId) {
213
- return prodAppId ?? appId;
214
- }
215
+ const reconciledTarget = await reconcileProductionAppTarget({
216
+ appId,
217
+ prodAppId,
218
+ creds,
219
+ displayName,
220
+ gencowJsonPath,
221
+ platformFetchImpl,
222
+ createCloudAppIfNeeded,
223
+ infoImpl,
224
+ errorImpl,
225
+ exitImpl,
226
+ });
227
+ if (!reconciledTarget) return null;
228
+ const productionParentAppId = reconciledTarget.appId;
229
+ const existingProdAppId = reconciledTarget.prodAppId;
230
+ if (existingProdAppId) return existingProdAppId;
231
+ if (!productionParentAppId) return null;
215
232
 
216
233
  const ciMode = processEnv.CI === "true" || deployArgs.includes("--yes");
217
234
  if (!ciMode) {
218
235
  const confirmed = await confirmFirstProductionDeploy({
219
- appId,
236
+ appId: productionParentAppId,
220
237
  createInterfaceImpl,
221
238
  infoImpl,
222
239
  logImpl,
@@ -225,11 +242,15 @@ async function ensureProductionDeployTarget({
225
242
  }
226
243
 
227
244
  infoImpl("Creating prod app...");
228
- const createProdRes = await platformFetchImpl(creds, `/platform/apps/${appId}/create-prod`, {
229
- method: "POST",
230
- headers: { "Content-Type": "application/json" },
231
- body: JSON.stringify(region ? { region } : {}),
232
- });
245
+ const createProdRes = await platformFetchImpl(
246
+ creds,
247
+ `/platform/apps/${productionParentAppId}/create-prod`,
248
+ {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify(region ? { region } : {}),
252
+ },
253
+ );
233
254
 
234
255
  if (!createProdRes.ok) {
235
256
  const errData = await readJsonObjectResponse(createProdRes);
@@ -526,6 +547,7 @@ export function createDeployCommand({
526
547
  resolvePathImpl,
527
548
  });
528
549
  let { appId, displayName, gencowJsonPath, prodAppId, projectDir } = context;
550
+ const rootAppId = appId;
529
551
  appId = resolveEnvironmentDeployTarget({ appId, envTarget: parsed.envTarget, prodAppId });
530
552
  const backendProjectRoot = projectDir ?? projectRoot;
531
553
  const projectConfig = loadConfigImpl
@@ -588,7 +610,9 @@ export function createDeployCommand({
588
610
 
589
611
  if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode) {
590
612
  const prodTarget = await ensureProductionDeployTarget({
591
- appId,
613
+ appId: rootAppId,
614
+ displayName,
615
+ createCloudAppIfNeeded: backendDeployRuntime.createCloudAppIfNeeded,
592
616
  createInterfaceImpl,
593
617
  creds,
594
618
  deployArgs,
@@ -124,6 +124,7 @@ export async function deployBackendPackage({
124
124
  displayName,
125
125
  envTarget,
126
126
  gencowJsonPath,
127
+ allowMissingAppReplacement: !existingBundleMode,
127
128
  });
128
129
  if (!activeAppId) return null;
129
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.244",
3
+ "version": "0.1.245",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,9 +37,10 @@
37
37
  "ai": "^6.0.233",
38
38
  "@types/node": "^25.9.5",
39
39
  "better-auth": "^1.6.23",
40
- "@gencow/core": "0.1.45",
40
+ "@typescript/native": "npm:typescript@7.0.2",
41
41
  "@gencow/client": "0.2.7",
42
- "@gencow/react": "0.2.7"
42
+ "@gencow/react": "0.2.7",
43
+ "@gencow/core": "0.1.45"
43
44
  },
44
45
  "scripts": {
45
46
  "prebuild": "pnpm --filter @gencow/migration-contract run build && pnpm --filter @gencow/server run build",