gencow 0.1.243 → 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.
package/core/index.js CHANGED
@@ -3636,6 +3636,7 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
3636
3636
  "GENCOW_RUNTIME_NODE_MODULES",
3637
3637
  "GENCOW_INTERNAL_TOKEN",
3638
3638
  "GENCOW_CRON_TOKEN",
3639
+ "GENCOW_SCHEDULED_JOB_PUBLIC_KEY",
3639
3640
  "GENCOW_DEPLOY_ID",
3640
3641
  "GENCOW_AI_PROXY_URL",
3641
3642
  "GENCOW_AI_PROXY_URL_ALT",
@@ -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
  }
@@ -3,6 +3,99 @@ export const APP_DELETE_OPERATION_PATTERN =
3
3
 
4
4
  const APP_ID_PATTERN = /^(?=.{3,63}$)[a-z][a-z0-9]*(?:-[a-z0-9]+){2,7}$/u;
5
5
  const CORRELATION_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/u;
6
+ const SAFE_DELETE_STATES = new Set([
7
+ "accepted",
8
+ "stopping_children",
9
+ "draining_runtime",
10
+ "stop_dispatched",
11
+ "runtime_absent",
12
+ "tenant_db_drop_pending",
13
+ "catalog_delete_pending",
14
+ "blocked_ownership_ambiguous",
15
+ "blocked_active_work",
16
+ "blocked_runtime_outcome_unknown",
17
+ "blocked_catalog_outcome_unknown",
18
+ "failed_tenant_db_cleanup",
19
+ "failed_catalog_cleanup",
20
+ "failed_delete_recovery_required",
21
+ "restoring",
22
+ "restored",
23
+ ]);
24
+ const RESTORE_ROUTE_ACTION_PRECONDITIONS = new Map([
25
+ [
26
+ "APP_DELETE_RESTORE_ROUTE_RECOVERY_DISABLED",
27
+ "Platform must enable serving-route publication for this app and environment before owner wake can proceed.",
28
+ ],
29
+ [
30
+ "APP_DELETE_RESTORE_APP_NOT_IN_ROUTE_ALLOWLIST",
31
+ "Platform must include this app in the serving-route rollout allowlist before owner wake can proceed.",
32
+ ],
33
+ [
34
+ "APP_DELETE_RESTORE_ROUTE_CONTROL_UNAVAILABLE",
35
+ "Platform must restore a valid serving-route rollout configuration before owner wake can proceed.",
36
+ ],
37
+ ]);
38
+ const RESTORE_ADMISSION_ACTIONS = new Map([
39
+ [
40
+ "APP_DELETE_RESTORE_TENANT_DATABASE_ABSENT",
41
+ {
42
+ retryable: false,
43
+ retryClass: "owner-action",
44
+ userAction: "RETRY_DELETE",
45
+ nextAction: "RETRY_DELETE",
46
+ retryPrecondition:
47
+ "The tenant database is absent. Wake cannot restore the app; retry the app deletion so catalog cleanup can finish.",
48
+ },
49
+ ],
50
+ [
51
+ "APP_DELETE_RESTORE_ACTIVE_DEPLOYMENT_ATTEMPT",
52
+ {
53
+ retryable: true,
54
+ retryClass: "platform-action",
55
+ userAction: "WAIT_THEN_RETRY_WAKE",
56
+ nextAction: "WAIT_FOR_PLATFORM_RECONCILIATION",
57
+ retryPrecondition:
58
+ "Wait until the active app deployment attempt finishes and its lease clears; then retry the same owner wake.",
59
+ },
60
+ ],
61
+ ...[
62
+ "APP_DELETE_RESTORE_ACTIVE_LEASE",
63
+ "APP_DELETE_RESTORE_ACTIVE_WORK",
64
+ "APP_DELETE_RESTORE_RUNTIME_NOT_ABSENT",
65
+ "APP_DELETE_RESTORE_TOMBSTONE_NOT_ACKNOWLEDGED",
66
+ "APP_DELETE_RESTORE_TENANT_DATABASE_AUTHORITY_UNAVAILABLE",
67
+ ].map((code) => [
68
+ code,
69
+ {
70
+ retryable: true,
71
+ retryClass: "platform-action",
72
+ userAction: "WAIT_THEN_RETRY_WAKE",
73
+ nextAction: "WAIT_FOR_PLATFORM_RECONCILIATION",
74
+ retryPrecondition:
75
+ "Wait until the stated runtime or serving-route condition is clear, then retry the same owner wake/resume operation.",
76
+ },
77
+ ]),
78
+ ...[
79
+ "APP_DELETE_RESTORE_ADMISSION_FAILED",
80
+ "APP_DELETE_RESTORE_CATALOG_MUTATED",
81
+ "APP_DELETE_RESTORE_CHILD_ACTIVE",
82
+ "APP_DELETE_RESTORE_HANDOFF_MISMATCH",
83
+ "APP_DELETE_RESTORE_IDENTITY_MISMATCH",
84
+ "APP_DELETE_RESTORE_RUNTIME_WAKE_MISSING",
85
+ "APP_DELETE_RESTORE_STATE_CONFLICT",
86
+ "APP_DELETE_RESTORE_TOMBSTONE_MISMATCH",
87
+ ].map((code) => [
88
+ code,
89
+ {
90
+ retryable: false,
91
+ retryClass: "platform-action",
92
+ userAction: "VIEW_DELETE_STATUS",
93
+ nextAction: "VIEW_DELETE_STATUS",
94
+ retryPrecondition:
95
+ "The failed delete receipt and serving-route identity require platform reconciliation before wake can proceed.",
96
+ },
97
+ ]),
98
+ ]);
6
99
 
7
100
  export function isAppDeleteDiagnosticAppId(value) {
8
101
  return APP_ID_PATTERN.test(value ?? "");
@@ -33,6 +126,158 @@ function isCanonicalTimestamp(value) {
33
126
  return Number.isFinite(date.getTime()) && date.toISOString() === value;
34
127
  }
35
128
 
129
+ export function readRestoreRouteActionAppDeleteDiagnostic(value, expectedAppId) {
130
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
131
+ const retryPrecondition = RESTORE_ROUTE_ACTION_PRECONDITIONS.get(value.code);
132
+ if (
133
+ !retryPrecondition ||
134
+ !isAppDeleteDiagnosticAppId(expectedAppId) ||
135
+ !APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "") ||
136
+ !CORRELATION_ID_PATTERN.test(value.correlationId ?? "") ||
137
+ !SAFE_DELETE_STATES.has(value.state) ||
138
+ value.stage !== "restore-admission" ||
139
+ !["runtime-resume", "runtime-reconcile"].includes(value.capability) ||
140
+ ![
141
+ "The failed delete receipt remains unchanged; the app runtime was not started.",
142
+ "The same restore receipt remains active; no replacement delete or restore operation was created.",
143
+ ].includes(value.stateImpact) ||
144
+ value.retryable !== false ||
145
+ value.retryPrecondition !== retryPrecondition ||
146
+ value.retryClass !== "platform-action" ||
147
+ value.userAction !== "WAIT_FOR_PLATFORM_RECONCILIATION" ||
148
+ value.nextAction !== "WAIT_FOR_PLATFORM_RECONCILIATION"
149
+ ) {
150
+ return null;
151
+ }
152
+ return {
153
+ code: value.code,
154
+ operationId: value.operationId,
155
+ correlationId: value.correlationId,
156
+ state: value.state,
157
+ stateImpact: value.stateImpact,
158
+ retryPrecondition,
159
+ statusCommand: `gencow app status ${expectedAppId}`,
160
+ };
161
+ }
162
+
163
+ export function isRestoreRouteActionAppDeleteDiagnosticCandidate(value) {
164
+ return Boolean(
165
+ value &&
166
+ typeof value === "object" &&
167
+ !Array.isArray(value) &&
168
+ RESTORE_ROUTE_ACTION_PRECONDITIONS.has(value.code),
169
+ );
170
+ }
171
+
172
+ export function readRestoreAdmissionAppDeleteDiagnostic(value, expectedAppId) {
173
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
174
+ const action = RESTORE_ADMISSION_ACTIONS.get(value.code);
175
+ const stateImpactByState = {
176
+ failed_delete_recovery_required:
177
+ "The failed delete receipt remains unchanged; the app runtime was not started.",
178
+ restoring:
179
+ "The same restore receipt remains active; no replacement delete or restore operation was created.",
180
+ };
181
+ const stateImpact = stateImpactByState[value.state];
182
+ if (
183
+ !action ||
184
+ !isAppDeleteDiagnosticAppId(expectedAppId) ||
185
+ value.status !== 409 ||
186
+ !APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "") ||
187
+ !CORRELATION_ID_PATTERN.test(value.correlationId ?? "") ||
188
+ !stateImpact ||
189
+ value.stage !== "restore-admission" ||
190
+ !["runtime-resume", "runtime-reconcile"].includes(value.capability) ||
191
+ value.stateImpact !== stateImpact ||
192
+ value.retryable !== action.retryable ||
193
+ value.retryClass !== action.retryClass ||
194
+ value.userAction !== action.userAction ||
195
+ value.nextAction !== action.nextAction ||
196
+ value.retryPrecondition !== action.retryPrecondition
197
+ ) {
198
+ return null;
199
+ }
200
+ return {
201
+ code: value.code,
202
+ operationId: value.operationId,
203
+ correlationId: value.correlationId,
204
+ state: value.state,
205
+ stateImpact,
206
+ retryable: action.retryable,
207
+ retryPrecondition: action.retryPrecondition,
208
+ nextAction: action.nextAction,
209
+ statusCommand: `gencow app status ${expectedAppId}`,
210
+ nextCommand: action.userAction === "RETRY_DELETE" ? `gencow app delete ${expectedAppId}` : null,
211
+ };
212
+ }
213
+
214
+ export function isRestoreAdmissionAppDeleteDiagnosticCandidate(value) {
215
+ return Boolean(
216
+ value && typeof value === "object" && !Array.isArray(value) && RESTORE_ADMISSION_ACTIONS.has(value.code),
217
+ );
218
+ }
219
+
220
+ export function formatRestoreAdmissionAppDeleteDiagnosticLines(diagnostic) {
221
+ const lines = [
222
+ "App wake is blocked while Gencow checks the failed delete recovery.",
223
+ `Code: ${diagnostic.code}`,
224
+ `Operation ID: ${diagnostic.operationId}`,
225
+ `Correlation ID: ${diagnostic.correlationId}`,
226
+ `State: ${diagnostic.state}`,
227
+ `State impact: ${diagnostic.stateImpact}`,
228
+ `Retryable: ${diagnostic.retryable ? "yes" : "no"}`,
229
+ `Retry precondition: ${diagnostic.retryPrecondition}`,
230
+ `Next action: ${diagnostic.nextAction}`,
231
+ ];
232
+ lines.push(`Next command: ${diagnostic.nextCommand ?? diagnostic.statusCommand}`);
233
+ return lines;
234
+ }
235
+
236
+ export function formatInvalidRestoreAdmissionAppDeleteDiagnosticLines(value, expectedAppId) {
237
+ const lines = ["App wake is blocked; the failed delete recovery diagnosis could not be verified safely."];
238
+ if (RESTORE_ADMISSION_ACTIONS.has(value.code)) lines.push(`Code: ${value.code}`);
239
+ if (APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "")) {
240
+ lines.push(`Operation ID: ${value.operationId}`);
241
+ }
242
+ if (CORRELATION_ID_PATTERN.test(value.correlationId ?? "")) {
243
+ lines.push(`Correlation ID: ${value.correlationId}`);
244
+ }
245
+ if (isAppDeleteDiagnosticAppId(expectedAppId)) {
246
+ lines.push(`Next command: gencow app status ${expectedAppId}`);
247
+ }
248
+ return lines;
249
+ }
250
+
251
+ export function formatRestoreRouteActionAppDeleteDiagnosticLines(diagnostic) {
252
+ return [
253
+ "App wake is blocked by Platform serving-route configuration.",
254
+ `Code: ${diagnostic.code}`,
255
+ `Operation ID: ${diagnostic.operationId}`,
256
+ `Correlation ID: ${diagnostic.correlationId}`,
257
+ `State: ${diagnostic.state}`,
258
+ `State impact: ${diagnostic.stateImpact}`,
259
+ "Retryable: no",
260
+ `Retry precondition: ${diagnostic.retryPrecondition}`,
261
+ "Next action: WAIT_FOR_PLATFORM_RECONCILIATION",
262
+ `Next command: ${diagnostic.statusCommand}`,
263
+ ];
264
+ }
265
+
266
+ export function formatInvalidRestoreRouteActionAppDeleteDiagnosticLines(value, expectedAppId) {
267
+ const lines = [
268
+ "App wake is blocked; the Platform serving-route diagnosis could not be verified safely.",
269
+ `Next command: gencow app status ${expectedAppId}`,
270
+ ];
271
+ if (RESTORE_ROUTE_ACTION_PRECONDITIONS.has(value.code)) lines.splice(1, 0, `Code: ${value.code}`);
272
+ if (APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "")) {
273
+ lines.splice(lines.length - 1, 0, `Operation ID: ${value.operationId}`);
274
+ }
275
+ if (CORRELATION_ID_PATTERN.test(value.correlationId ?? "")) {
276
+ lines.splice(lines.length - 1, 0, `Correlation ID: ${value.correlationId}`);
277
+ }
278
+ return lines;
279
+ }
280
+
36
281
  // The Platform's blocked receipt is an untrusted transport value until all of
37
282
  // its identity, app scope, and safe-action fields agree. Keep that boundary in
38
283
  // one module so polling and terminal rendering cannot drift apart.
@@ -64,14 +309,123 @@ export function readBlockedAppDeleteDiagnostic(value, expectedAppId) {
64
309
  };
65
310
  }
66
311
 
312
+ export function readRestoreInProgressAppDeleteDiagnostic(value, expectedAppId) {
313
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
314
+ const immediateRpc =
315
+ value.success === undefined &&
316
+ value.error === "APP_DELETE_RESTORE_IN_PROGRESS" &&
317
+ value.stateImpact ===
318
+ "The same restore receipt remains active; no replacement delete or restore operation was created." &&
319
+ value.userAction === "WAIT_FOR_RESTORE" &&
320
+ value.nextAction === "WAIT_FOR_RESTORE";
321
+ const statusReceipt =
322
+ value.success === false &&
323
+ value.deleted === expectedAppId &&
324
+ typeof value.statusPath === "string" &&
325
+ isAllowedAppDeleteStatusPath(value.statusPath, value.operationId, expectedAppId);
326
+ if (
327
+ !isAppDeleteDiagnosticAppId(expectedAppId) ||
328
+ !APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "") ||
329
+ !CORRELATION_ID_PATTERN.test(value.correlationId ?? "") ||
330
+ value.state !== "restoring" ||
331
+ value.code !== "APP_DELETE_RESTORE_IN_PROGRESS" ||
332
+ value.retryable !== false ||
333
+ value.retryPrecondition !==
334
+ "Wait until this restore receipt leaves restoring; then check the app status." ||
335
+ (!immediateRpc && !statusReceipt)
336
+ ) {
337
+ return null;
338
+ }
339
+ return {
340
+ code: value.code,
341
+ operationId: value.operationId,
342
+ correlationId: value.correlationId,
343
+ state: value.state,
344
+ stateImpact:
345
+ "The same restore receipt remains active; no replacement delete or restore operation was created.",
346
+ retryPrecondition: "Wait until this restore receipt leaves restoring; then check the app status.",
347
+ statusCommand: `gencow app status ${expectedAppId}`,
348
+ };
349
+ }
350
+
351
+ export function formatRestoreInProgressAppDeleteDiagnosticLines(diagnostic) {
352
+ return [
353
+ "App restoration is still in progress; this delete did not complete.",
354
+ `Code: ${diagnostic.code}`,
355
+ `Operation ID: ${diagnostic.operationId}`,
356
+ `Correlation ID: ${diagnostic.correlationId}`,
357
+ `State: ${diagnostic.state}`,
358
+ `State impact: ${diagnostic.stateImpact}`,
359
+ "Retryable: no",
360
+ `Retry precondition: ${diagnostic.retryPrecondition}`,
361
+ "Next action: WAIT_FOR_RESTORE",
362
+ `Next command: ${diagnostic.statusCommand}`,
363
+ ];
364
+ }
365
+
366
+ export function readRestoredAppDeleteDiagnostic(value, expectedAppId) {
367
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
368
+ const immediateRpc =
369
+ value.success === undefined &&
370
+ value.error === "APP_DELETE_RESTORE_COMPLETED_NEW_DELETE_REQUIRED" &&
371
+ value.stateImpact ===
372
+ "The previous restore is complete; this request did not create a delete operation." &&
373
+ value.retryPrecondition ===
374
+ "The old delete request is terminal. Start a new delete command to create a fresh operation." &&
375
+ value.userAction === "RETRY_DELETE_WITH_NEW_REQUEST" &&
376
+ value.nextAction === "RETRY_DELETE_WITH_NEW_REQUEST";
377
+ const statusReceipt =
378
+ value.success === false &&
379
+ value.deleted === expectedAppId &&
380
+ value.failureClass === "OWNER_ACTION_REQUIRED" &&
381
+ value.action === "RETRY_DELETE_WITH_NEW_REQUEST" &&
382
+ isAllowedAppDeleteStatusPath(value.statusPath, value.operationId, expectedAppId);
383
+ if (
384
+ !isAppDeleteDiagnosticAppId(expectedAppId) ||
385
+ !APP_DELETE_OPERATION_PATTERN.test(value.operationId ?? "") ||
386
+ !CORRELATION_ID_PATTERN.test(value.correlationId ?? "") ||
387
+ value.state !== "restored" ||
388
+ value.code !== "APP_DELETE_RESTORE_COMPLETED_NEW_DELETE_REQUIRED" ||
389
+ value.retryable !== false ||
390
+ (!immediateRpc && !statusReceipt)
391
+ ) {
392
+ return null;
393
+ }
394
+ return {
395
+ code: value.code,
396
+ operationId: value.operationId,
397
+ correlationId: value.correlationId,
398
+ state: value.state,
399
+ stateImpact: "The previous restore is complete; this request did not create a delete operation.",
400
+ retryPrecondition:
401
+ "The old delete request is terminal. Start a new delete command to create a fresh operation.",
402
+ deleteCommand: `gencow app delete ${expectedAppId}`,
403
+ };
404
+ }
405
+
406
+ export function formatRestoredAppDeleteDiagnosticLines(diagnostic) {
407
+ return [
408
+ "The app was restored; the previous delete request did not delete it.",
409
+ `Code: ${diagnostic.code}`,
410
+ `Operation ID: ${diagnostic.operationId}`,
411
+ `Correlation ID: ${diagnostic.correlationId}`,
412
+ `State: ${diagnostic.state}`,
413
+ `State impact: ${diagnostic.stateImpact}`,
414
+ "Retryable: no",
415
+ `Retry precondition: ${diagnostic.retryPrecondition}`,
416
+ "Next action: RETRY_DELETE_WITH_NEW_REQUEST",
417
+ `Next command: ${diagnostic.deleteCommand}`,
418
+ ];
419
+ }
420
+
67
421
  export function isBlockedAppDeleteDiagnosticCandidate(value) {
68
422
  return Boolean(
69
423
  value &&
70
- typeof value === "object" &&
71
- !Array.isArray(value) &&
72
- (value.code === "PLATFORM_ACTION_REQUIRED" ||
73
- value.failureClass === "PLATFORM_ACTION_REQUIRED" ||
74
- value.action === "VIEW_DELETE_STATUS"),
424
+ typeof value === "object" &&
425
+ !Array.isArray(value) &&
426
+ (value.code === "PLATFORM_ACTION_REQUIRED" ||
427
+ value.failureClass === "PLATFORM_ACTION_REQUIRED" ||
428
+ value.action === "VIEW_DELETE_STATUS"),
75
429
  );
76
430
  }
77
431
 
@@ -3,6 +3,8 @@ import {
3
3
  APP_DELETE_OPERATION_PATTERN,
4
4
  isAllowedAppDeleteStatusPath,
5
5
  readBlockedAppDeleteDiagnostic,
6
+ readRestoredAppDeleteDiagnostic,
7
+ readRestoreInProgressAppDeleteDiagnostic,
6
8
  } from "./app-delete-diagnostic.mjs";
7
9
  import { readJsonObjectResponse } from "./http-response-json.mjs";
8
10
 
@@ -21,8 +23,9 @@ const APP_DELETE_ACTIVE_STATES = new Set([
21
23
  "failed_tenant_db_cleanup",
22
24
  "failed_catalog_cleanup",
23
25
  "failed_delete_recovery_required",
26
+ "restoring",
24
27
  ]);
25
- const APP_DELETE_TERMINAL_FAILURE_STATES = new Set();
28
+ const APP_DELETE_TERMINAL_FAILURE_STATES = new Set(["restored"]);
26
29
  const APP_DELETE_RESPONSE_STATES = new Set([
27
30
  ...APP_DELETE_ACTIVE_STATES,
28
31
  ...APP_DELETE_TERMINAL_FAILURE_STATES,
@@ -98,6 +101,12 @@ export async function pollAppDeleteOperation({
98
101
  rpcQueryImpl,
99
102
  sleepImpl,
100
103
  }) {
104
+ if (
105
+ readRestoreInProgressAppDeleteDiagnostic(initialBody, name) ||
106
+ readRestoredAppDeleteDiagnostic(initialBody, name)
107
+ ) {
108
+ return initialBody;
109
+ }
101
110
  const accepted = parseAppDeleteOperationEnvelope(initialBody, name);
102
111
  if (!accepted) return null;
103
112
  if (isMatchingTerminalFailureResponse(initialBody, accepted, name)) {
@@ -147,6 +156,20 @@ export async function pollAppDeleteOperation({
147
156
  }
148
157
 
149
158
  const pollingUnavailable = successfulPollResponses === 0 && transportFailures > 0;
159
+ if (latest.state === "restoring") {
160
+ return {
161
+ success: false,
162
+ deleted: name,
163
+ error: "App restoration is still in progress; this delete did not complete.",
164
+ code: "APP_DELETE_RESTORE_IN_PROGRESS",
165
+ operationId: accepted.operationId,
166
+ correlationId: latest.correlationId ?? accepted.correlationId,
167
+ statusPath: `/api/apps/${encodeURIComponent(name)}/delete/${encodeURIComponent(accepted.operationId)}`,
168
+ state: "restoring",
169
+ retryable: false,
170
+ retryPrecondition: "Wait until this restore receipt leaves restoring; then check the app status.",
171
+ };
172
+ }
150
173
  return {
151
174
  error: pollingUnavailable
152
175
  ? "App deletion status is temporarily unavailable. Retry the command with the same app."
@@ -4,8 +4,20 @@ import {
4
4
  APP_DELETE_OPERATION_PATTERN,
5
5
  formatBlockedAppDeleteDiagnosticLines,
6
6
  formatInvalidBlockedAppDeleteDiagnosticLines,
7
+ formatRestoreInProgressAppDeleteDiagnosticLines,
8
+ formatRestoreRouteActionAppDeleteDiagnosticLines,
9
+ formatInvalidRestoreRouteActionAppDeleteDiagnosticLines,
10
+ formatRestoreAdmissionAppDeleteDiagnosticLines,
11
+ formatInvalidRestoreAdmissionAppDeleteDiagnosticLines,
12
+ formatRestoredAppDeleteDiagnosticLines,
13
+ isRestoreAdmissionAppDeleteDiagnosticCandidate,
14
+ isRestoreRouteActionAppDeleteDiagnosticCandidate,
7
15
  isBlockedAppDeleteDiagnosticCandidate,
8
16
  readBlockedAppDeleteDiagnostic,
17
+ readRestoreInProgressAppDeleteDiagnostic,
18
+ readRestoreRouteActionAppDeleteDiagnostic,
19
+ readRestoreAdmissionAppDeleteDiagnostic,
20
+ readRestoredAppDeleteDiagnostic,
9
21
  } from "./app-delete-diagnostic.mjs";
10
22
 
11
23
  export { APP_DELETE_OPERATION_PATTERN } from "./app-delete-diagnostic.mjs";
@@ -35,6 +47,28 @@ export function appResponseError(body, fallback, response = null, options = {})
35
47
  if (blockedDeleteDiagnostic) {
36
48
  return formatBlockedAppDeleteDiagnosticLines(blockedDeleteDiagnostic).join("\n");
37
49
  }
50
+ const restoreInProgressDiagnostic = readRestoreInProgressAppDeleteDiagnostic(body, options.expectedAppId);
51
+ if (restoreInProgressDiagnostic) {
52
+ return formatRestoreInProgressAppDeleteDiagnosticLines(restoreInProgressDiagnostic).join("\n");
53
+ }
54
+ const restoreRouteActionDiagnostic = readRestoreRouteActionAppDeleteDiagnostic(body, options.expectedAppId);
55
+ if (restoreRouteActionDiagnostic) {
56
+ return formatRestoreRouteActionAppDeleteDiagnosticLines(restoreRouteActionDiagnostic).join("\n");
57
+ }
58
+ if (isRestoreRouteActionAppDeleteDiagnosticCandidate(body)) {
59
+ return formatInvalidRestoreRouteActionAppDeleteDiagnosticLines(body, options.expectedAppId).join("\n");
60
+ }
61
+ const restoredDeleteDiagnostic = readRestoredAppDeleteDiagnostic(body, options.expectedAppId);
62
+ if (restoredDeleteDiagnostic) {
63
+ return formatRestoredAppDeleteDiagnosticLines(restoredDeleteDiagnostic).join("\n");
64
+ }
65
+ const restoreAdmissionDiagnostic = readRestoreAdmissionAppDeleteDiagnostic(body, options.expectedAppId);
66
+ if (restoreAdmissionDiagnostic) {
67
+ return formatRestoreAdmissionAppDeleteDiagnosticLines(restoreAdmissionDiagnostic).join("\n");
68
+ }
69
+ if (isRestoreAdmissionAppDeleteDiagnosticCandidate(body)) {
70
+ return formatInvalidRestoreAdmissionAppDeleteDiagnosticLines(body, options.expectedAppId).join("\n");
71
+ }
38
72
  if (isBlockedAppDeleteDiagnosticCandidate(body)) {
39
73
  return formatInvalidBlockedAppDeleteDiagnosticLines(body, options.expectedAppId).join("\n");
40
74
  }
@@ -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
+ }