gencow 0.1.242 → 0.1.243

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.
@@ -10,11 +10,10 @@ import {
10
10
  import { resolveCreatedAppId, resolveCreatedAppResponse } from "./app-create-response.mjs";
11
11
  import {
12
12
  appNotFoundDiagnostic,
13
- isExactAppDeleteResponse,
13
+ isExactAppDeleteHandoffResponse,
14
14
  parseAppListResponse,
15
15
  parseAppLookupResponse,
16
16
  } from "./app-response-contract.mjs";
17
- import { pollAppDeleteOperation } from "./app-delete-operation.mjs";
18
17
  import { appResponseError } from "./app-response-error.mjs";
19
18
  import { readJsonObjectResponse, readJsonValueResponse } from "./http-response-json.mjs";
20
19
  import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
@@ -22,10 +21,6 @@ import { loadCreds, rpcMutation, rpcQuery, saveCreds, requireCreds } from "./pla
22
21
  import { resolveProjectMetadataPath, resolveProjectSelection } from "./project-context.mjs";
23
22
  import { updateEnvLocalUrl } from "./cli-project-runtime.mjs";
24
23
 
25
- // Covers the 60s runtime-observer restart grace, reconciliation scans, and
26
- // one delayed worker acquisition without reporting a completed deletion as pending.
27
- const DEFAULT_DELETE_POLL_ATTEMPTS = 120;
28
- const DELETE_POLL_INTERVAL_MS = 1_000;
29
24
  const DEFAULT_APP_CREATE_TIMEOUT_MS = 60_000;
30
25
 
31
26
  function resolveAppCreateTimeoutMs(value) {
@@ -169,7 +164,7 @@ async function confirmDelete(name) {
169
164
  const rl = createInterface({ input: process.stdin, output: process.stdout });
170
165
  const answer = await new Promise((resolveAnswer) =>
171
166
  rl.question(
172
- `\n ${RED}!${RESET} Delete "${BOLD}${name}${RESET}"? Stops process + removes data.\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. The name remains unavailable until then.\n Type the app name to confirm: `,
173
168
  resolveAnswer,
174
169
  ),
175
170
  );
@@ -194,8 +189,6 @@ export function createAppCommand({
194
189
  rpcQueryImpl = rpcQuery,
195
190
  saveCredsImpl = saveCreds,
196
191
  setTimeoutImpl = setTimeout,
197
- sleepImpl = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
198
- deletePollAttempts = DEFAULT_DELETE_POLL_ATTEMPTS,
199
192
  successImpl = success,
200
193
  updateEnvLocalUrlImpl = updateEnvLocalUrl,
201
194
  warnImpl = warn,
@@ -365,12 +358,16 @@ ${dashboardLine}
365
358
 
366
359
  if (subcmd === "delete") {
367
360
  const unsupportedOption = rest.find(
368
- (value) => value.startsWith("-") && value !== "--force" && value !== "-f",
361
+ (value) => value.startsWith("-") && value !== "--force" && value !== "-f" &&
362
+ !value.startsWith("--request-id="),
369
363
  );
370
364
  const force = rest.includes("--force") || rest.includes("-f");
371
365
  const name = rest.find((value) => !value.startsWith("-")) || loadCredsImpl()?.currentApp;
372
- if (!name || unsupportedOption) {
373
- errorImpl("Usage: gencow app delete <name> [--force]");
366
+ const requestIdOptions = rest.filter((value) => value.startsWith("--request-id="));
367
+ const suppliedRequestId = requestIdOptions[0]?.slice("--request-id=".length);
368
+ if (!name || unsupportedOption || requestIdOptions.length > 1 ||
369
+ (suppliedRequestId !== undefined && !/^[A-Za-z0-9_-]{8,128}$/u.test(suppliedRequestId))) {
370
+ errorImpl("Usage: gencow app delete <name> [--force] [--request-id=<id>]");
374
371
  return 1;
375
372
  }
376
373
 
@@ -382,31 +379,27 @@ ${dashboardLine}
382
379
  }
383
380
  }
384
381
 
385
- infoImpl(`Deleting app "${name}"...`);
386
- const requestId = createDeleteRequestIdImpl();
387
- const delRes = await rpcMutationImpl(creds, "apps.delete", { name, requestId });
388
- let delData = await readJsonObjectResponse(delRes);
389
- if (!isExactAppDeleteResponse(delData, name)) {
390
- const operationResult = await pollAppDeleteOperation({
391
- creds,
392
- initialBody: delData,
393
- name,
394
- pollAttempts: deletePollAttempts,
395
- pollIntervalMs: DELETE_POLL_INTERVAL_MS,
396
- rpcQueryImpl,
397
- sleepImpl,
398
- });
399
- if (operationResult) delData = operationResult;
382
+ infoImpl(`Removing app "${name}" from your account...`);
383
+ const requestId = suppliedRequestId ?? createDeleteRequestIdImpl();
384
+ const retryCommand = `gencow app delete '${name.replaceAll("'", "'\\''")}' --force --request-id=${requestId}`;
385
+ let delRes;
386
+ try {
387
+ delRes = await rpcMutationImpl(creds, "apps.deleteAsync", { name, requestId });
388
+ } catch {
389
+ errorImpl(`APP_DELETE_ASYNC_UNCONFIRMED: The request may have completed. Retry the same request: ${retryCommand}`);
390
+ return 1;
400
391
  }
401
- if (!isExactAppDeleteResponse(delData, name)) {
402
- errorImpl(
403
- appResponseError(
392
+ const delData = await readJsonObjectResponse(delRes);
393
+ if (!isExactAppDeleteHandoffResponse(delData, name, requestId)) {
394
+ const diagnosis = appResponseError(
404
395
  delData,
405
- invalidAppResponse("app delete", delRes, delRes.ok ? "success" : "error"),
396
+ invalidAppResponse("app delete handoff", delRes, delRes.ok ? "success" : "error"),
406
397
  delRes,
407
398
  { expectedAppId: name },
408
- ),
409
- );
399
+ );
400
+ errorImpl(delRes.ok || delRes.status >= 500
401
+ ? `${diagnosis}\nRequest ID: ${requestId}. Retry the same request: ${retryCommand}`
402
+ : diagnosis);
410
403
  return 1;
411
404
  }
412
405
 
@@ -415,7 +408,10 @@ ${dashboardLine}
415
408
  saveCredsImpl({ ...currentCreds, currentApp: undefined });
416
409
  }
417
410
 
418
- successImpl(`App "${name}" deleted`);
411
+ successImpl(`App "${name}" removed from your account. An administrator will permanently delete it. The name remains unavailable until then.`);
412
+ if (delData.stopErrorCode) {
413
+ warnImpl(`App stop needs administrator review. Code: ${delData.stopErrorCode}. Handoff ID: ${delData.handoffId}`);
414
+ }
419
415
  return 0;
420
416
  }
421
417
 
@@ -56,3 +56,12 @@ export function isExactAppDeleteResponse(value, expectedAppId) {
56
56
  const body = record(value);
57
57
  return body?.success === true && body.deleted === expectedAppId;
58
58
  }
59
+
60
+ export function isExactAppDeleteHandoffResponse(value, expectedAppId, expectedRequestId) {
61
+ const body = record(value);
62
+ return body?.success === true && body.name === expectedAppId &&
63
+ body.requestId === expectedRequestId &&
64
+ body.ownerTransferredTo === "todelete@gencow.dev" &&
65
+ nonEmptyString(body.handoffId) &&
66
+ Number.isSafeInteger(body.transferredApps) && body.transferredApps >= 1;
67
+ }
@@ -62,6 +62,14 @@ export function appResponseError(body, fallback, response = null, options = {})
62
62
  if (/^[A-Za-z0-9_-]{8,128}$/u.test(body.correlationId ?? "")) {
63
63
  lines.push(`Correlation ID: ${body.correlationId}`);
64
64
  }
65
+ if (body.stage === "admission") lines.push("Stage: admission");
66
+ if (body.stateImpact === "This deletion request made no ownership or resource change.") {
67
+ lines.push(`State impact: ${body.stateImpact}`);
68
+ }
69
+ if (typeof body.retryPrecondition === "string" &&
70
+ /^[A-Za-z0-9 ,.;:-]{1,200}$/u.test(body.retryPrecondition)) {
71
+ lines.push(`Retry condition: ${body.retryPrecondition}`);
72
+ }
65
73
  if (APP_DELETE_OPERATION_PATTERN.test(body.operationId ?? "")) {
66
74
  lines.push(`Operation ID: ${body.operationId}`);
67
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.242",
3
+ "version": "0.1.243",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {