1#!/usr/bin/env bun
2
3// @ts-nocheck
4
5import * as Credentials from "@distilled.cloud/aws/Credentials";
6import * as ec2 from "@distilled.cloud/aws/ec2";
7import { Region } from "@distilled.cloud/aws/Region";
8import { NodeServices } from "@effect/platform-node";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as FetchHttpClient from "effect/http/FetchHttpClient";
12import { parseArgs } from "node:util";
13
14const { values } = parseArgs({
15 args: Bun.argv.slice(2),
16 options: {
17 region: { type: "string", short: "r" },
18 profile: { type: "string", short: "p" },
19 execute: { type: "boolean" },
20 "include-default-vpcs": { type: "boolean" },
21 passes: { type: "string" },
22 help: { type: "boolean", short: "h" },
23 },
24 strict: true,
25});
26
27const usage = `
28Best-effort EC2 regional cleanup using @distilled.cloud/aws.
29
30Usage:
31 bun alchemy/scripts/clean-aws.ts --region us-east-1 [--profile dev]
32 bun alchemy/scripts/clean-aws.ts --region us-east-1 --execute
33
34Options:
35 --region, -r AWS region to clean
36 --profile, -p Optional AWS profile
37 --execute Perform deletions (default is dry-run)
38 --include-default-vpcs Also attempt to delete default VPC resources
39 --passes Cleanup passes to run (default: 1 dry-run, 4 execute)
40 --help, -h Show this help
41`;
42
43if (values.help) {
44 console.log(usage.trim());
45 process.exit(0);
46}
47
48const region =
49 values.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
50if (!region) {
51 console.error("Missing --region. Set it explicitly or via AWS_REGION.");
52 console.error(usage.trim());
53 process.exit(1);
54}
55
56if (values.profile) {
57 process.env.AWS_PROFILE = values.profile;
58}
59
60const execute = values.execute ?? false;
61const includeDefaultVpcs = values["include-default-vpcs"] ?? false;
62const maxPasses = Number(values.passes ?? (execute ? "4" : "1"));
63
64if (!Number.isFinite(maxPasses) || maxPasses < 1) {
65 console.error("--passes must be a positive integer.");
66 process.exit(1);
67}
68
69const credentialsLayer = values.profile
70 ? Layer.provideMerge(
71 Credentials.fromSSO(values.profile),
72 Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer),
73 )
74 : Credentials.fromChain();
75
76const runtime = Layer.mergeAll(
77 NodeServices.layer,
78 FetchHttpClient.layer,
79 credentialsLayer,
80 Layer.succeed(Region, region),
81);
82
83const formatError = (error: unknown) => {
84 if (typeof error === "object" && error && "_tag" in error) {
85 const tagged = error as { _tag: string; message?: string };
86 return tagged.message ? `${tagged._tag}: ${tagged.message}` : tagged._tag;
87 }
88 if (error instanceof Error) {
89 return error.message;
90 }
91 try {
92 return JSON.stringify(error);
93 } catch {
94 return String(error);
95 }
96};
97
98const log = (message: string) => Effect.sync(() => console.log(message));
99const warn = (message: string) => Effect.sync(() => console.warn(message));
100
101const isNotFoundError = (error: unknown) =>
102 typeof error === "object" &&
103 error !== null &&
104 "_tag" in error &&
105 String((error as { _tag: string })._tag).includes("NotFound");
106
107const uniq = <T>(items: ReadonlyArray<T | null | undefined>) => [
108 ...new Set(
109 items.filter((item): item is T => item !== null && item !== undefined),
110 ),
111];
112
113const chunk = <T>(items: ReadonlyArray<T>, size: number) => {
114 const groups: T[][] = [];
115 for (let index = 0; index < items.length; index += size) {
116 groups.push(items.slice(index, index + size));
117 }
118 return groups;
119};
120
121const preview = (label: string, ids: ReadonlyArray<string>) =>
122 ids.length === 0
123 ? Effect.void
124 : log(`Found ${ids.length} ${label}: ${ids.join(", ")}`);
125
126const ignoreFailure = <A, E, R>(
127 label: string,
128 effect: Effect.Effect<A, E, R>,
129): Effect.Effect<void, E, R> =>
130 effect.pipe(
131 Effect.asVoid,
132 Effect.catch((error) =>
133 isNotFoundError(error)
134 ? warn(`Already gone ${label}: ${formatError(error)}`)
135 : Effect.fail(error as E),
136 ),
137 );
138
139const deleteEach = <R>(
140 label: string,
141 ids: ReadonlyArray<string>,
142 remove: (id: string) => Effect.Effect<any, any, R>,
143) =>
144 Effect.gen(function* () {
145 if (ids.length === 0) {
146 return;
147 }
148
149 yield* preview(label, ids);
150 if (!execute) {
151 return;
152 }
153
154 yield* Effect.forEach(
155 ids,
156 (id) => ignoreFailure(`${label} ${id}`, remove(id)),
157 {
158 concurrency: 1,
159 discard: true,
160 },
161 );
162 });
163
164const repeatUntil = <A, E, R>(
165 label: string,
166 effect: Effect.Effect<A, E, R>,
167 done: (value: A) => boolean,
168 attempts: number,
169 delayMs: number,
170): Effect.Effect<void, E, R> =>
171 Effect.gen(function* () {
172 const value = yield* effect;
173 if (done(value)) {
174 return;
175 }
176 if (attempts <= 1) {
177 yield* warn(`Timed out waiting for ${label}.`);
178 return;
179 }
180 yield* log(`Waiting for ${label}...`);
181 yield* Effect.sleep(delayMs);
182 yield* repeatUntil(label, effect, done, attempts - 1, delayMs);
183 });
184
185const terminateInstances = Effect.gen(function* () {
186 const response = yield* ec2.describeInstances({} as any);
187 const ids = uniq(
188 (response.Reservations ?? []).flatMap((reservation: any) =>
189 (reservation.Instances ?? [])
190 .filter((instance: any) => {
191 const state = instance.State?.Name;
192 return state !== "shutting-down" && state !== "terminated";
193 })
194 .map((instance: any) => instance.InstanceId),
195 ),
196 );
197
198 if (ids.length === 0) {
199 return;
200 }
201
202 yield* preview("instance(s)", ids);
203 if (!execute) {
204 return;
205 }
206
207 for (const group of chunk(ids, 100)) {
208 yield* ignoreFailure(
209 `instances ${group.join(", ")}`,
210 ec2.terminateInstances({ InstanceIds: group } as any),
211 );
212 }
213
214 yield* repeatUntil(
215 "instances to terminate",
216 ec2
217 .describeInstances({} as any)
218 .pipe(
219 Effect.map((result) =>
220 uniq(
221 (result.Reservations ?? []).flatMap((reservation: any) =>
222 (reservation.Instances ?? [])
223 .filter((instance: any) => ids.includes(instance.InstanceId))
224 .map((instance: any) => instance.State?.Name),
225 ),
226 ),
227 ),
228 ),
229 (states) => states.every((state) => state === "terminated"),
230 40,
231 5_000,
232 );
233});
234
235const deleteVpcEndpoints = Effect.gen(function* () {
236 const ids = uniq(
237 (yield* ec2.describeVpcEndpoints({} as any)).VpcEndpoints?.map(
238 (endpoint: any) => endpoint.VpcEndpointId,
239 ),
240 );
241
242 if (ids.length === 0) {
243 return;
244 }
245
246 yield* preview("VPC endpoint(s)", ids);
247 if (!execute) {
248 return;
249 }
250
251 for (const group of chunk(ids, 25)) {
252 yield* ignoreFailure(
253 `VPC endpoints ${group.join(", ")}`,
254 ec2.deleteVpcEndpoints({ VpcEndpointIds: group } as any),
255 );
256 }
257
258 yield* repeatUntil(
259 "VPC endpoints to delete",
260 ec2.describeVpcEndpoints({ VpcEndpointIds: ids } as any).pipe(
261 Effect.map((result) =>
262 uniq(
263 (result.VpcEndpoints ?? []).map(
264 (endpoint: any) => endpoint.VpcEndpointId,
265 ),
266 ),
267 ),
268 Effect.catch((error) =>
269 isNotFoundError(error)
270 ? Effect.succeed([] as string[])
271 : Effect.fail(error),
272 ),
273 ),
274 (remainingIds) => remainingIds.length === 0,
275 30,
276 5_000,
277 );
278});
279
280const deleteNatGateways = Effect.gen(function* () {
281 const ids = uniq(
282 (yield* ec2.describeNatGateways({} as any)).NatGateways?.filter(
283 (natGateway: any) => natGateway.State !== "deleted",
284 ).map((natGateway: any) => natGateway.NatGatewayId),
285 );
286
287 if (ids.length === 0) {
288 return;
289 }
290
291 yield* preview("NAT gateway(s)", ids);
292 if (!execute) {
293 return;
294 }
295
296 yield* Effect.forEach(
297 ids,
298 (id) =>
299 ignoreFailure(
300 `NAT gateway ${id}`,
301 ec2.deleteNatGateway({ NatGatewayId: id } as any),
302 ),
303 { concurrency: 1, discard: true },
304 );
305
306 yield* repeatUntil(
307 "NAT gateways to delete",
308 ec2
309 .describeNatGateways({} as any)
310 .pipe(
311 Effect.map((result) =>
312 uniq(
313 (result.NatGateways ?? [])
314 .filter((natGateway: any) =>
315 ids.includes(natGateway.NatGatewayId),
316 )
317 .map((natGateway: any) => natGateway.State),
318 ),
319 ),
320 ),
321 (states) => states.every((state) => state === "deleted"),
322 30,
323 10_000,
324 );
325});
326
327const deleteNetworkInterfaces = Effect.gen(function* () {
328 const interfaces =
329 (yield* ec2.describeNetworkInterfaces({} as any)).NetworkInterfaces ?? [];
330
331 const blocked = interfaces.filter(
332 (networkInterface: any) =>
333 networkInterface.Attachment ||
334 networkInterface.RequesterManaged ||
335 networkInterface.Status !== "available",
336 );
337
338 if (blocked.length > 0) {
339 return yield* Effect.fail(
340 new Error(
341 `Network interfaces still in use: ${blocked
342 .map(
343 (networkInterface: any) =>
344 `${networkInterface.NetworkInterfaceId}(status=${networkInterface.Status ?? "unknown"}, requesterManaged=${networkInterface.RequesterManaged ?? false}, attachment=${networkInterface.Attachment?.AttachmentId ?? "none"})`,
345 )
346 .join(", ")}`,
347 ),
348 );
349 }
350
351 const ids = uniq(
352 interfaces.map(
353 (networkInterface: any) => networkInterface.NetworkInterfaceId,
354 ),
355 );
356
357 yield* deleteEach("network interface(s)", ids, (id) =>
358 ec2.deleteNetworkInterface({ NetworkInterfaceId: id } as any),
359 );
360});
361
362const releaseElasticIps = Effect.gen(function* () {
363 const addresses = (yield* ec2.describeAddresses({} as any)).Addresses ?? [];
364 const ids = uniq(
365 addresses.map((address: any) => address.AllocationId ?? address.PublicIp),
366 );
367
368 if (ids.length === 0) {
369 return;
370 }
371
372 yield* preview("elastic IP(s)", ids);
373 if (!execute) {
374 return;
375 }
376
377 yield* Effect.forEach(
378 addresses,
379 (address: any) =>
380 Effect.gen(function* () {
381 if (address.AssociationId) {
382 yield* ignoreFailure(
383 `elastic IP association ${address.AssociationId}`,
384 ec2.disassociateAddress({
385 AssociationId: address.AssociationId,
386 } as any),
387 );
388 }
389 yield* ignoreFailure(
390 `elastic IP ${address.AllocationId ?? address.PublicIp}`,
391 ec2.releaseAddress(
392 address.AllocationId
393 ? ({ AllocationId: address.AllocationId } as any)
394 : ({ PublicIp: address.PublicIp } as any),
395 ),
396 );
397 }),
398 { concurrency: 1, discard: true },
399 );
400});
401
402const deleteVpcPeeringConnections = Effect.gen(function* () {
403 const ids = uniq(
404 (yield* ec2.describeVpcPeeringConnections(
405 {} as any,
406 )).VpcPeeringConnections?.filter(
407 (connection: any) => connection.Status?.Code !== "deleted",
408 ).map((connection: any) => connection.VpcPeeringConnectionId),
409 );
410
411 yield* deleteEach("VPC peering connection(s)", ids, (id) =>
412 ec2.deleteVpcPeeringConnection({ VpcPeeringConnectionId: id } as any),
413 );
414});
415
416const deleteVpnConnections = Effect.gen(function* () {
417 const ids = uniq(
418 (yield* ec2.describeVpnConnections({} as any)).VpnConnections?.filter(
419 (connection: any) => connection.State !== "deleted",
420 ).map((connection: any) => connection.VpnConnectionId),
421 );
422
423 yield* deleteEach("VPN connection(s)", ids, (id) =>
424 ec2.deleteVpnConnection({ VpnConnectionId: id } as any),
425 );
426});
427
428const deleteVpnGateways = Effect.gen(function* () {
429 const gateways =
430 (yield* ec2.describeVpnGateways({} as any)).VpnGateways ?? [];
431 const ids = uniq(
432 gateways
433 .filter((gateway: any) => gateway.State !== "deleted")
434 .map((gateway: any) => gateway.VpnGatewayId),
435 );
436
437 if (ids.length === 0) {
438 return;
439 }
440
441 yield* preview("VPN gateway(s)", ids);
442 if (!execute) {
443 return;
444 }
445
446 yield* Effect.forEach(
447 gateways,
448 (gateway: any) =>
449 Effect.gen(function* () {
450 for (const attachment of gateway.VpcAttachments ?? []) {
451 if (attachment.VpcId && attachment.State !== "detached") {
452 yield* ignoreFailure(
453 `VPN gateway attachment ${gateway.VpnGatewayId}`,
454 ec2.detachVpnGateway({
455 VpcId: attachment.VpcId,
456 VpnGatewayId: gateway.VpnGatewayId,
457 } as any),
458 );
459 }
460 }
461
462 yield* ignoreFailure(
463 `VPN gateway ${gateway.VpnGatewayId}`,
464 ec2.deleteVpnGateway({ VpnGatewayId: gateway.VpnGatewayId } as any),
465 );
466 }),
467 { concurrency: 1, discard: true },
468 );
469});
470
471const deleteCustomerGateways = Effect.gen(function* () {
472 const ids = uniq(
473 (yield* ec2.describeCustomerGateways({} as any)).CustomerGateways?.map(
474 (gateway: any) => gateway.CustomerGatewayId,
475 ),
476 );
477
478 yield* deleteEach("customer gateway(s)", ids, (id) =>
479 ec2.deleteCustomerGateway({ CustomerGatewayId: id } as any),
480 );
481});
482
483const deleteInternetGateways = Effect.gen(function* () {
484 const gateways =
485 (yield* ec2.describeInternetGateways({} as any)).InternetGateways ?? [];
486 const ids = uniq(gateways.map((gateway: any) => gateway.InternetGatewayId));
487
488 if (ids.length === 0) {
489 return;
490 }
491
492 yield* preview("internet gateway(s)", ids);
493 if (!execute) {
494 return;
495 }
496
497 yield* Effect.forEach(
498 gateways,
499 (gateway: any) =>
500 Effect.gen(function* () {
501 for (const attachment of gateway.Attachments ?? []) {
502 if (attachment.VpcId) {
503 yield* ignoreFailure(
504 `internet gateway attachment ${gateway.InternetGatewayId}`,
505 ec2.detachInternetGateway({
506 InternetGatewayId: gateway.InternetGatewayId,
507 VpcId: attachment.VpcId,
508 } as any),
509 );
510 }
511 }
512
513 yield* ignoreFailure(
514 `internet gateway ${gateway.InternetGatewayId}`,
515 ec2.deleteInternetGateway({
516 InternetGatewayId: gateway.InternetGatewayId,
517 } as any),
518 );
519 }),
520 { concurrency: 1, discard: true },
521 );
522});
523
524const deleteEgressOnlyInternetGateways = Effect.gen(function* () {
525 const gateways =
526 (yield* ec2.describeEgressOnlyInternetGateways({} as any))
527 .EgressOnlyInternetGateways ?? [];
528 const ids = uniq(
529 gateways.map((gateway: any) => gateway.EgressOnlyInternetGatewayId),
530 );
531
532 yield* deleteEach("egress-only internet gateway(s)", ids, (id) =>
533 ec2.deleteEgressOnlyInternetGateway({
534 EgressOnlyInternetGatewayId: id,
535 } as any),
536 );
537});
538
539const deleteRouteTables = Effect.gen(function* () {
540 const routeTables =
541 (yield* ec2.describeRouteTables({} as any)).RouteTables?.filter(
542 (routeTable: any) =>
543 !(routeTable.Associations ?? []).some(
544 (association: any) => association.Main,
545 ),
546 ) ?? [];
547
548 const ids = uniq(
549 routeTables.map((routeTable: any) => routeTable.RouteTableId),
550 );
551
552 if (ids.length === 0) {
553 return;
554 }
555
556 yield* preview("non-main route table(s)", ids);
557 if (!execute) {
558 return;
559 }
560
561 yield* Effect.forEach(
562 routeTables,
563 (routeTable: any) =>
564 Effect.gen(function* () {
565 for (const association of routeTable.Associations ?? []) {
566 if (!association.Main && association.RouteTableAssociationId) {
567 yield* ignoreFailure(
568 `route table association ${association.RouteTableAssociationId}`,
569 ec2.disassociateRouteTable({
570 AssociationId: association.RouteTableAssociationId,
571 } as any),
572 );
573 }
574 }
575
576 yield* ignoreFailure(
577 `route table ${routeTable.RouteTableId}`,
578 ec2.deleteRouteTable({
579 RouteTableId: routeTable.RouteTableId,
580 } as any),
581 );
582 }),
583 { concurrency: 1, discard: true },
584 );
585});
586
587const deleteSubnets = Effect.gen(function* () {
588 const ids = uniq(
589 (yield* ec2.describeSubnets({} as any)).Subnets?.filter((subnet: any) =>
590 includeDefaultVpcs ? true : !subnet.DefaultForAz,
591 ).map((subnet: any) => subnet.SubnetId),
592 );
593
594 yield* deleteEach("subnet(s)", ids, (id) =>
595 ec2.deleteSubnet({ SubnetId: id } as any),
596 );
597});
598
599const deleteSecurityGroups = Effect.gen(function* () {
600 const ids = uniq(
601 (yield* ec2.describeSecurityGroups({} as any)).SecurityGroups?.filter(
602 (group: any) => group.GroupName !== "default",
603 ).map((group: any) => group.GroupId),
604 );
605
606 yield* deleteEach("security group(s)", ids, (id) =>
607 ec2.deleteSecurityGroup({ GroupId: id } as any),
608 );
609});
610
611const deleteNetworkAcls = Effect.gen(function* () {
612 const ids = uniq(
613 (yield* ec2.describeNetworkAcls({} as any)).NetworkAcls?.filter(
614 (acl: any) => !acl.IsDefault,
615 ).map((acl: any) => acl.NetworkAclId),
616 );
617
618 yield* deleteEach("network ACL(s)", ids, (id) =>
619 ec2.deleteNetworkAcl({ NetworkAclId: id } as any),
620 );
621});
622
623const deleteAvailableVolumes = Effect.gen(function* () {
624 const ids = uniq(
625 (yield* ec2.describeVolumes({} as any)).Volumes?.filter(
626 (volume: any) => volume.State === "available",
627 ).map((volume: any) => volume.VolumeId),
628 );
629
630 yield* deleteEach("available volume(s)", ids, (id) =>
631 ec2.deleteVolume({ VolumeId: id } as any),
632 );
633});
634
635const deregisterImages = Effect.gen(function* () {
636 const ids = uniq(
637 (yield* ec2.describeImages({ Owners: ["self"] } as any)).Images?.map(
638 (image: any) => image.ImageId,
639 ),
640 );
641
642 yield* deleteEach("AMI(s)", ids, (id) =>
643 ec2.deregisterImage({ ImageId: id } as any),
644 );
645});
646
647const deleteSnapshots = Effect.gen(function* () {
648 const ids = uniq(
649 (yield* ec2.describeSnapshots({
650 OwnerIds: ["self"],
651 } as any)).Snapshots?.map((snapshot: any) => snapshot.SnapshotId),
652 );
653
654 yield* deleteEach("snapshot(s)", ids, (id) =>
655 ec2.deleteSnapshot({ SnapshotId: id } as any),
656 );
657});
658
659const deleteLaunchTemplates = Effect.gen(function* () {
660 const ids = uniq(
661 (yield* ec2.describeLaunchTemplates({} as any)).LaunchTemplates?.map(
662 (template: any) => template.LaunchTemplateId,
663 ),
664 );
665
666 yield* deleteEach("launch template(s)", ids, (id) =>
667 ec2.deleteLaunchTemplate({ LaunchTemplateId: id } as any),
668 );
669});
670
671const deletePlacementGroups = Effect.gen(function* () {
672 const ids = uniq(
673 (yield* ec2.describePlacementGroups({} as any)).PlacementGroups?.map(
674 (group: any) => group.GroupName,
675 ),
676 );
677
678 yield* deleteEach("placement group(s)", ids, (id) =>
679 ec2.deletePlacementGroup({ GroupName: id } as any),
680 );
681});
682
683const deleteKeyPairs = Effect.gen(function* () {
684 const ids = uniq(
685 (yield* ec2.describeKeyPairs({} as any)).KeyPairs?.map(
686 (keyPair: any) => keyPair.KeyPairId,
687 ),
688 );
689
690 yield* deleteEach("key pair(s)", ids, (id) =>
691 ec2.deleteKeyPair({ KeyPairId: id } as any),
692 );
693});
694
695const deleteVpcs = Effect.gen(function* () {
696 const ids = uniq(
697 (yield* ec2.describeVpcs({} as any)).Vpcs?.filter((vpc: any) =>
698 includeDefaultVpcs ? true : !vpc.IsDefault,
699 ).map((vpc: any) => vpc.VpcId),
700 );
701
702 yield* deleteEach("VPC(s)", ids, (id) => ec2.deleteVpc({ VpcId: id } as any));
703});
704
705const deleteDhcpOptions = Effect.gen(function* () {
706 const associatedDhcpOptionsIds = new Set(
707 uniq(
708 (yield* ec2.describeVpcs({} as any)).Vpcs?.map(
709 (vpc: any) => vpc.DhcpOptionsId,
710 ),
711 ),
712 );
713
714 const ids = uniq(
715 (yield* ec2.describeDhcpOptions({} as any)).DhcpOptions?.map(
716 (dhcpOptions: any) => dhcpOptions.DhcpOptionsId,
717 ).filter(
718 (id: string) => id !== "default" && !associatedDhcpOptionsIds.has(id),
719 ),
720 );
721
722 yield* deleteEach("DHCP options set(s)", ids, (id) =>
723 ec2.deleteDhcpOptions({ DhcpOptionsId: id } as any),
724 );
725});
726
727const main = Effect.gen(function* () {
728 yield* log(
729 [
730 `Mode=${execute ? "execute" : "dry-run"}`,
731 `Region=${region}`,
732 values.profile ? `Profile=${values.profile}` : undefined,
733 includeDefaultVpcs ? "Including default VPCs" : "Skipping default VPCs",
734 ]
735 .filter((value): value is string => !!value)
736 .join(" | "),
737 );
738
739 for (let pass = 1; pass <= maxPasses; pass++) {
740 yield* log(`\n=== Cleanup pass ${pass}/${maxPasses} ===`);
741
742 yield* terminateInstances;
743 yield* deleteVpcEndpoints;
744 yield* deleteNatGateways;
745 yield* deleteNetworkInterfaces;
746 yield* releaseElasticIps;
747 yield* deleteVpnConnections;
748 yield* deleteVpnGateways;
749 yield* deleteCustomerGateways;
750 yield* deleteVpcPeeringConnections;
751 yield* deleteInternetGateways;
752 yield* deleteEgressOnlyInternetGateways;
753 yield* deleteRouteTables;
754 yield* deleteSubnets;
755 yield* deleteSecurityGroups;
756 yield* deleteNetworkAcls;
757 yield* deleteAvailableVolumes;
758 yield* deregisterImages;
759 yield* deleteSnapshots;
760 yield* deleteLaunchTemplates;
761 yield* deletePlacementGroups;
762 yield* deleteKeyPairs;
763 yield* deleteVpcs;
764 yield* deleteDhcpOptions;
765
766 if (!execute) {
767 yield* log(
768 "\nDry-run complete. Re-run with --execute to perform deletions.",
769 );
770 return;
771 }
772 }
773
774 yield* log("\nCleanup passes complete.");
775});
776
777Effect.runPromise(main.pipe(Effect.provide(runtime))).catch((error) => {
778 console.error(formatError(error));
779 process.exit(1);
780});