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.
- package/lib/app-command.mjs +30 -34
- package/lib/app-response-contract.mjs +9 -0
- package/lib/app-response-error.mjs +8 -0
- package/package.json +1 -1
- package/runtime/server.mjs +442 -265
package/lib/app-command.mjs
CHANGED
|
@@ -10,11 +10,10 @@ import {
|
|
|
10
10
|
import { resolveCreatedAppId, resolveCreatedAppResponse } from "./app-create-response.mjs";
|
|
11
11
|
import {
|
|
12
12
|
appNotFoundDiagnostic,
|
|
13
|
-
|
|
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}
|
|
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
|
-
|
|
373
|
-
|
|
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(`
|
|
386
|
-
const requestId = createDeleteRequestIdImpl();
|
|
387
|
-
const
|
|
388
|
-
let
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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}"
|
|
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
|
}
|