@mtndev/cli 1.1.0 → 2.0.1

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.
Files changed (260) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/cjs/__chunks/VH7oDXx4.cjs +28 -0
  3. package/cjs/commands/aws/ec2/allow-ip/handler.cjs +261 -0
  4. package/cjs/commands/aws/ec2/allow-ip/handler.d.ts +6 -0
  5. package/cjs/commands/aws/ec2/allow-ip/index.cjs +25 -0
  6. package/cjs/commands/aws/ec2/allow-ip/index.d.ts +3 -0
  7. package/cjs/commands/aws/ec2/allow-ip/schema.cjs +33 -0
  8. package/cjs/commands/aws/ec2/allow-ip/schema.d.ts +33 -0
  9. package/cjs/commands/aws/ec2/allow-ip/utils.cjs +379 -0
  10. package/cjs/commands/aws/ec2/allow-ip/utils.d.ts +189 -0
  11. package/cjs/commands/aws/iam/admin-group.cjs +111 -0
  12. package/cjs/commands/aws/iam/admin-group.d.ts +55 -0
  13. package/cjs/commands/aws/iam/break-glass/handler.cjs +127 -0
  14. package/cjs/commands/aws/iam/break-glass/handler.d.ts +7 -0
  15. package/cjs/commands/aws/iam/break-glass/index.cjs +7 -0
  16. package/cjs/commands/aws/iam/break-glass/index.d.ts +2 -0
  17. package/cjs/commands/aws/iam/break-glass/schema.cjs +21 -0
  18. package/cjs/commands/aws/iam/break-glass/schema.d.ts +25 -0
  19. package/cjs/commands/aws/iam/check-admin/handler.cjs +89 -0
  20. package/cjs/commands/aws/iam/check-admin/handler.d.ts +5 -0
  21. package/cjs/commands/aws/iam/check-admin/index.cjs +7 -0
  22. package/cjs/commands/aws/iam/check-admin/index.d.ts +2 -0
  23. package/cjs/commands/aws/iam/check-admin/schema.cjs +19 -0
  24. package/cjs/commands/aws/iam/check-admin/schema.d.ts +19 -0
  25. package/cjs/commands/aws/iam/policies/s3-restrictions-deny-non-s3-services.json +20 -0
  26. package/cjs/commands/aws/iam/policies/sts-federation.json +11 -0
  27. package/cjs/commands/aws/iam/policies/user-admins-deny-list.json +175 -0
  28. package/cjs/commands/aws/iam/policies/user-admins-deny-regions-list.json +41 -0
  29. package/cjs/commands/aws/iam/policies/user-admins-self-service-iam.json +56 -0
  30. package/cjs/commands/aws/iam/policies/user-admins-write-list.json +117 -0
  31. package/cjs/commands/aws/iam/remove-admin/handler.cjs +120 -0
  32. package/cjs/commands/aws/iam/remove-admin/handler.d.ts +6 -0
  33. package/cjs/commands/aws/iam/remove-admin/index.cjs +7 -0
  34. package/cjs/commands/aws/iam/remove-admin/index.d.ts +2 -0
  35. package/cjs/commands/aws/iam/remove-admin/schema.cjs +22 -0
  36. package/cjs/commands/aws/iam/remove-admin/schema.d.ts +28 -0
  37. package/cjs/commands/aws/iam/setup-acl/handler.cjs +227 -0
  38. package/cjs/commands/aws/iam/setup-acl/handler.d.ts +6 -0
  39. package/cjs/commands/aws/iam/setup-acl/index.cjs +13 -0
  40. package/cjs/commands/aws/iam/setup-acl/index.d.ts +3 -0
  41. package/cjs/commands/aws/iam/setup-acl/schema.cjs +19 -0
  42. package/cjs/commands/aws/iam/setup-acl/schema.d.ts +24 -0
  43. package/cjs/commands/aws/iam/setup-acl/utils.cjs +305 -0
  44. package/cjs/commands/aws/iam/setup-acl/utils.d.ts +61 -0
  45. package/cjs/commands/aws/iam/users/handler.cjs +59 -0
  46. package/cjs/commands/aws/iam/users/handler.d.ts +5 -0
  47. package/cjs/commands/aws/iam/users/index.cjs +18 -0
  48. package/cjs/commands/aws/iam/users/index.d.ts +3 -0
  49. package/cjs/commands/aws/iam/users/schema.cjs +17 -0
  50. package/cjs/commands/aws/iam/users/schema.d.ts +18 -0
  51. package/cjs/commands/aws/iam/users/utils.cjs +454 -0
  52. package/cjs/commands/aws/iam/users/utils.d.ts +119 -0
  53. package/cjs/commands/aws/index.cjs +223 -0
  54. package/cjs/commands/aws/index.d.ts +14 -0
  55. package/cjs/commands/aws/route53/add-record/handler.cjs +264 -0
  56. package/cjs/commands/aws/route53/add-record/handler.d.ts +36 -0
  57. package/cjs/commands/aws/route53/add-record/index.cjs +26 -0
  58. package/cjs/commands/aws/route53/add-record/index.d.ts +3 -0
  59. package/cjs/commands/aws/route53/add-record/schema.cjs +43 -0
  60. package/cjs/commands/aws/route53/add-record/schema.d.ts +45 -0
  61. package/cjs/commands/aws/route53/add-record/utils.cjs +404 -0
  62. package/cjs/commands/aws/route53/add-record/utils.d.ts +216 -0
  63. package/cjs/commands/aws/route53/remove-record/handler.cjs +114 -0
  64. package/cjs/commands/aws/route53/remove-record/handler.d.ts +6 -0
  65. package/cjs/commands/aws/route53/remove-record/index.cjs +5 -0
  66. package/cjs/commands/aws/route53/remove-record/index.d.ts +2 -0
  67. package/cjs/commands/aws/route53/remove-record/schema.cjs +22 -0
  68. package/cjs/commands/aws/route53/remove-record/schema.d.ts +30 -0
  69. package/cjs/commands/aws/schema.cjs +19 -0
  70. package/cjs/commands/aws/schema.d.ts +13 -0
  71. package/cjs/commands/aws/ses/send-email/handler.cjs +73 -0
  72. package/cjs/commands/aws/ses/send-email/handler.d.ts +5 -0
  73. package/cjs/commands/aws/ses/send-email/index.cjs +9 -0
  74. package/cjs/commands/aws/ses/send-email/index.d.ts +3 -0
  75. package/cjs/commands/aws/ses/send-email/schema.cjs +24 -0
  76. package/cjs/commands/aws/ses/send-email/schema.d.ts +39 -0
  77. package/cjs/commands/aws/ses/send-email/utils.cjs +87 -0
  78. package/cjs/commands/aws/ses/send-email/utils.d.ts +47 -0
  79. package/cjs/commands/aws/ses/smtp-password.cjs +72 -0
  80. package/cjs/commands/aws/ses/smtp-password.d.ts +21 -0
  81. package/cjs/commands/aws/utils/exec-aws.cjs +145 -0
  82. package/cjs/commands/aws/utils/exec-aws.d.ts +79 -0
  83. package/cjs/commands/aws/utils/resolve-policy-path.cjs +15 -0
  84. package/cjs/commands/aws/utils/resolve-policy-path.d.ts +8 -0
  85. package/cjs/commands/aws/utils/resolve-profile.cjs +93 -0
  86. package/cjs/commands/aws/utils/resolve-profile.d.ts +28 -0
  87. package/cjs/commands/aws/utils/tty.cjs +54 -0
  88. package/cjs/commands/aws/utils/tty.d.ts +24 -0
  89. package/cjs/commands/db/create-readonly-user/handler.cjs +254 -0
  90. package/cjs/commands/db/create-readonly-user/handler.d.ts +5 -0
  91. package/cjs/commands/db/create-readonly-user/index.cjs +18 -0
  92. package/cjs/commands/db/create-readonly-user/index.d.ts +3 -0
  93. package/cjs/commands/db/create-readonly-user/mysql.cjs +102 -0
  94. package/cjs/commands/db/create-readonly-user/mysql.d.ts +33 -0
  95. package/cjs/commands/db/create-readonly-user/postgres.cjs +209 -0
  96. package/cjs/commands/db/create-readonly-user/postgres.d.ts +31 -0
  97. package/cjs/commands/db/create-readonly-user/schema.cjs +39 -0
  98. package/cjs/commands/db/create-readonly-user/schema.d.ts +55 -0
  99. package/cjs/commands/db/create-readonly-user/utils.cjs +160 -0
  100. package/cjs/commands/db/create-readonly-user/utils.d.ts +112 -0
  101. package/cjs/commands/db/index.cjs +46 -0
  102. package/cjs/commands/db/index.d.ts +6 -0
  103. package/cjs/commands/doppler/handler.cjs +95 -9
  104. package/cjs/commands/doppler/index.cjs +33 -41
  105. package/cjs/commands/doppler/index.d.ts +1 -0
  106. package/cjs/commands/doppler/schema.cjs +130 -6
  107. package/cjs/commands/doppler/schema.d.ts +233 -17
  108. package/cjs/commands/doppler/set-secret.cjs +378 -0
  109. package/cjs/commands/doppler/set-secret.d.ts +113 -0
  110. package/cjs/commands/doppler/utils/exec.cjs +106 -12
  111. package/cjs/commands/doppler/utils.cjs +412 -30
  112. package/cjs/commands/doppler/utils.d.ts +32 -8
  113. package/cjs/commands/icons/handler.cjs +82 -8
  114. package/cjs/commands/icons/index.cjs +15 -31
  115. package/cjs/commands/icons/schema.cjs +101 -13
  116. package/cjs/commands/icons/schema.d.ts +1 -1
  117. package/cjs/commands/icons/utils.cjs +131 -13
  118. package/cjs/commands/index.cjs +54 -82
  119. package/cjs/commands/schema.cjs +25 -8
  120. package/cjs/index.cjs +3 -4
  121. package/esm/commands/aws/ec2/allow-ip/handler.d.ts +6 -0
  122. package/esm/commands/aws/ec2/allow-ip/handler.js +258 -0
  123. package/esm/commands/aws/ec2/allow-ip/index.d.ts +3 -0
  124. package/esm/commands/aws/ec2/allow-ip/index.js +4 -0
  125. package/esm/commands/aws/ec2/allow-ip/schema.d.ts +33 -0
  126. package/esm/commands/aws/ec2/allow-ip/schema.js +29 -0
  127. package/esm/commands/aws/ec2/allow-ip/utils.d.ts +189 -0
  128. package/esm/commands/aws/ec2/allow-ip/utils.js +363 -0
  129. package/esm/commands/aws/iam/admin-group.d.ts +55 -0
  130. package/esm/commands/aws/iam/admin-group.js +104 -0
  131. package/esm/commands/aws/iam/break-glass/handler.d.ts +7 -0
  132. package/esm/commands/aws/iam/break-glass/handler.js +124 -0
  133. package/esm/commands/aws/iam/break-glass/index.d.ts +2 -0
  134. package/esm/commands/aws/iam/break-glass/index.js +4 -0
  135. package/esm/commands/aws/iam/break-glass/schema.d.ts +25 -0
  136. package/esm/commands/aws/iam/break-glass/schema.js +19 -0
  137. package/esm/commands/aws/iam/check-admin/handler.d.ts +5 -0
  138. package/esm/commands/aws/iam/check-admin/handler.js +86 -0
  139. package/esm/commands/aws/iam/check-admin/index.d.ts +2 -0
  140. package/esm/commands/aws/iam/check-admin/index.js +4 -0
  141. package/esm/commands/aws/iam/check-admin/schema.d.ts +19 -0
  142. package/esm/commands/aws/iam/check-admin/schema.js +17 -0
  143. package/esm/commands/aws/iam/policies/s3-restrictions-deny-non-s3-services.json +20 -0
  144. package/esm/commands/aws/iam/policies/sts-federation.json +11 -0
  145. package/esm/commands/aws/iam/policies/user-admins-deny-list.json +175 -0
  146. package/esm/commands/aws/iam/policies/user-admins-deny-regions-list.json +41 -0
  147. package/esm/commands/aws/iam/policies/user-admins-self-service-iam.json +56 -0
  148. package/esm/commands/aws/iam/policies/user-admins-write-list.json +117 -0
  149. package/esm/commands/aws/iam/remove-admin/handler.d.ts +6 -0
  150. package/esm/commands/aws/iam/remove-admin/handler.js +117 -0
  151. package/esm/commands/aws/iam/remove-admin/index.d.ts +2 -0
  152. package/esm/commands/aws/iam/remove-admin/index.js +4 -0
  153. package/esm/commands/aws/iam/remove-admin/schema.d.ts +28 -0
  154. package/esm/commands/aws/iam/remove-admin/schema.js +20 -0
  155. package/esm/commands/aws/iam/setup-acl/handler.d.ts +6 -0
  156. package/esm/commands/aws/iam/setup-acl/handler.js +224 -0
  157. package/esm/commands/aws/iam/setup-acl/index.d.ts +3 -0
  158. package/esm/commands/aws/iam/setup-acl/index.js +4 -0
  159. package/esm/commands/aws/iam/setup-acl/schema.d.ts +24 -0
  160. package/esm/commands/aws/iam/setup-acl/schema.js +18 -0
  161. package/esm/commands/aws/iam/setup-acl/utils.d.ts +61 -0
  162. package/esm/commands/aws/iam/setup-acl/utils.js +298 -0
  163. package/esm/commands/aws/iam/users/handler.d.ts +5 -0
  164. package/esm/commands/aws/iam/users/handler.js +56 -0
  165. package/esm/commands/aws/iam/users/index.d.ts +3 -0
  166. package/esm/commands/aws/iam/users/index.js +4 -0
  167. package/esm/commands/aws/iam/users/schema.d.ts +18 -0
  168. package/esm/commands/aws/iam/users/schema.js +16 -0
  169. package/esm/commands/aws/iam/users/utils.d.ts +119 -0
  170. package/esm/commands/aws/iam/users/utils.js +440 -0
  171. package/esm/commands/aws/index.d.ts +14 -0
  172. package/esm/commands/aws/index.js +142 -0
  173. package/esm/commands/aws/route53/add-record/handler.d.ts +36 -0
  174. package/esm/commands/aws/route53/add-record/handler.js +258 -0
  175. package/esm/commands/aws/route53/add-record/index.d.ts +3 -0
  176. package/esm/commands/aws/route53/add-record/index.js +4 -0
  177. package/esm/commands/aws/route53/add-record/schema.d.ts +45 -0
  178. package/esm/commands/aws/route53/add-record/schema.js +40 -0
  179. package/esm/commands/aws/route53/add-record/utils.d.ts +216 -0
  180. package/esm/commands/aws/route53/add-record/utils.js +389 -0
  181. package/esm/commands/aws/route53/remove-record/handler.d.ts +6 -0
  182. package/esm/commands/aws/route53/remove-record/handler.js +111 -0
  183. package/esm/commands/aws/route53/remove-record/index.d.ts +2 -0
  184. package/esm/commands/aws/route53/remove-record/index.js +3 -0
  185. package/esm/commands/aws/route53/remove-record/schema.d.ts +30 -0
  186. package/esm/commands/aws/route53/remove-record/schema.js +21 -0
  187. package/esm/commands/aws/schema.d.ts +13 -0
  188. package/esm/commands/aws/schema.js +16 -0
  189. package/esm/commands/aws/ses/send-email/handler.d.ts +5 -0
  190. package/esm/commands/aws/ses/send-email/handler.js +70 -0
  191. package/esm/commands/aws/ses/send-email/index.d.ts +3 -0
  192. package/esm/commands/aws/ses/send-email/index.js +4 -0
  193. package/esm/commands/aws/ses/send-email/schema.d.ts +39 -0
  194. package/esm/commands/aws/ses/send-email/schema.js +23 -0
  195. package/esm/commands/aws/ses/send-email/utils.d.ts +47 -0
  196. package/esm/commands/aws/ses/send-email/utils.js +84 -0
  197. package/esm/commands/aws/ses/smtp-password.d.ts +21 -0
  198. package/esm/commands/aws/ses/smtp-password.js +69 -0
  199. package/esm/commands/aws/utils/exec-aws.d.ts +79 -0
  200. package/esm/commands/aws/utils/exec-aws.js +137 -0
  201. package/esm/commands/aws/utils/resolve-policy-path.d.ts +8 -0
  202. package/esm/commands/aws/utils/resolve-policy-path.js +14 -0
  203. package/esm/commands/aws/utils/resolve-profile.d.ts +28 -0
  204. package/esm/commands/aws/utils/resolve-profile.js +88 -0
  205. package/esm/commands/aws/utils/tty.d.ts +24 -0
  206. package/esm/commands/aws/utils/tty.js +51 -0
  207. package/esm/commands/db/create-readonly-user/handler.d.ts +5 -0
  208. package/esm/commands/db/create-readonly-user/handler.js +251 -0
  209. package/esm/commands/db/create-readonly-user/index.d.ts +3 -0
  210. package/esm/commands/db/create-readonly-user/index.js +4 -0
  211. package/esm/commands/db/create-readonly-user/mysql.d.ts +33 -0
  212. package/esm/commands/db/create-readonly-user/mysql.js +98 -0
  213. package/esm/commands/db/create-readonly-user/postgres.d.ts +31 -0
  214. package/esm/commands/db/create-readonly-user/postgres.js +205 -0
  215. package/esm/commands/db/create-readonly-user/schema.d.ts +55 -0
  216. package/esm/commands/db/create-readonly-user/schema.js +35 -0
  217. package/esm/commands/db/create-readonly-user/utils.d.ts +112 -0
  218. package/esm/commands/db/create-readonly-user/utils.js +151 -0
  219. package/esm/commands/db/index.d.ts +6 -0
  220. package/esm/commands/db/index.js +31 -0
  221. package/esm/commands/doppler/handler.js +92 -9
  222. package/esm/commands/doppler/index.d.ts +1 -0
  223. package/esm/commands/doppler/index.js +6 -41
  224. package/esm/commands/doppler/schema.d.ts +233 -17
  225. package/esm/commands/doppler/schema.js +125 -7
  226. package/esm/commands/doppler/set-secret.d.ts +113 -0
  227. package/esm/commands/doppler/set-secret.js +370 -0
  228. package/esm/commands/doppler/utils/exec.js +100 -12
  229. package/esm/commands/doppler/utils.d.ts +32 -8
  230. package/esm/commands/doppler/utils.js +396 -30
  231. package/esm/commands/icons/handler.js +79 -8
  232. package/esm/commands/icons/index.js +4 -31
  233. package/esm/commands/icons/schema.d.ts +1 -1
  234. package/esm/commands/icons/schema.js +97 -15
  235. package/esm/commands/icons/utils.js +125 -13
  236. package/esm/commands/index.js +49 -78
  237. package/esm/commands/schema.js +22 -7
  238. package/esm/index.js +3 -4
  239. package/package.json +12 -2
  240. package/vitest.config.ts +11 -0
  241. package/cjs/__chunks/4GFJUCSP.cjs +0 -52
  242. package/cjs/__chunks/4SFWHCB3.cjs +0 -133
  243. package/cjs/__chunks/B2KFW7T7.cjs +0 -1
  244. package/cjs/__chunks/EMMC33QA.cjs +0 -401
  245. package/cjs/__chunks/INFOPAHN.cjs +0 -117
  246. package/cjs/__chunks/J36P6UNE.cjs +0 -75
  247. package/cjs/__chunks/NIMXXHJQ.cjs +0 -92
  248. package/cjs/__chunks/NSRFG4DF.cjs +0 -100
  249. package/cjs/__chunks/WNJ3ZYLI.cjs +0 -1
  250. package/cjs/__chunks/ZMDIOZPT.cjs +0 -22
  251. package/esm/__chunks/46HGXFME.js +0 -0
  252. package/esm/__chunks/6G4PXXKF.js +0 -75
  253. package/esm/__chunks/6L5RNR7G.js +0 -100
  254. package/esm/__chunks/FL7AILGH.js +0 -133
  255. package/esm/__chunks/HJSXC6HP.js +0 -117
  256. package/esm/__chunks/O262GK4B.js +0 -401
  257. package/esm/__chunks/O3CCA76Q.js +0 -22
  258. package/esm/__chunks/SJXQJ7RS.js +0 -92
  259. package/esm/__chunks/SNCKIAOR.js +0 -0
  260. package/esm/__chunks/X3R5PSWH.js +0 -52
@@ -0,0 +1,389 @@
1
+ import { runAws } from "../../utils/exec-aws.js";
2
+ import "./schema.js";
3
+ //#region src/commands/aws/route53/add-record/utils.js
4
+ /**
5
+ * @typedef {Object} HostedZone
6
+ * @property {string} id - Bare hosted zone ID (without /hostedzone/ prefix)
7
+ * @property {string} name - Zone name with trailing dot (e.g. mountaindev.com.)
8
+ * @property {boolean} privateZone
9
+ * @property {number} recordCount
10
+ */
11
+ /**
12
+ * @typedef {Object} ResourceRecordSet
13
+ * @property {string} name
14
+ * @property {string} type
15
+ * @property {number | undefined} ttl
16
+ * @property {string[]} values
17
+ * @property {boolean} isAlias
18
+ * @property {any} [aliasTarget] - Raw AliasTarget block (present when isAlias)
19
+ */
20
+ /**
21
+ * Strip the `/hostedzone/` prefix from a Route53 hosted zone Id.
22
+ * @param {string} rawId
23
+ * @returns {string}
24
+ */
25
+ function bareHostedZoneId(rawId) {
26
+ return rawId.replace(/^\/hostedzone\//, "");
27
+ }
28
+ /**
29
+ * Ensure a DNS name ends with a trailing dot (Route53 FQDN form).
30
+ * @param {string} name
31
+ * @returns {string}
32
+ */
33
+ function withTrailingDot(name) {
34
+ const trimmed = name.trim();
35
+ if (trimmed === "") return trimmed;
36
+ return trimmed.endsWith(".") ? trimmed : `${trimmed}.`;
37
+ }
38
+ /**
39
+ * Strip a trailing dot for display.
40
+ * @param {string} name
41
+ * @returns {string}
42
+ */
43
+ function withoutTrailingDot(name) {
44
+ return name.endsWith(".") ? name.slice(0, -1) : name;
45
+ }
46
+ /**
47
+ * List hosted zones for the profile.
48
+ * @param {{ profile?: string; verbose?: boolean }} params
49
+ * @returns {HostedZone[]}
50
+ */
51
+ function listHostedZones(params) {
52
+ const { profile, verbose } = params;
53
+ const data = runAws({
54
+ profile,
55
+ args: ["route53", "list-hosted-zones"],
56
+ verbose
57
+ });
58
+ /** @type {HostedZone[]} */
59
+ const zones = [];
60
+ for (const zone of data.HostedZones ?? []) {
61
+ const id = bareHostedZoneId(String(zone.Id ?? ""));
62
+ const name = withTrailingDot(String(zone.Name ?? ""));
63
+ if (!id || !name) continue;
64
+ zones.push({
65
+ id,
66
+ name,
67
+ privateZone: Boolean(zone.Config?.PrivateZone),
68
+ recordCount: Number(zone.ResourceRecordSetCount ?? 0)
69
+ });
70
+ }
71
+ return zones.sort((a, b) => a.name.localeCompare(b.name));
72
+ }
73
+ /**
74
+ * Resolve a hosted zone from a name (mountaindev.com) or bare/prefixed ID.
75
+ * @param {{ zones: HostedZone[]; zoneInput: string }} params
76
+ * @returns {HostedZone | null}
77
+ */
78
+ function findHostedZone(params) {
79
+ const { zones, zoneInput } = params;
80
+ const input = zoneInput.trim();
81
+ if (!input) return null;
82
+ const bareId = bareHostedZoneId(input);
83
+ const byId = zones.find((zone) => zone.id === bareId);
84
+ if (byId) return byId;
85
+ const normalized = withTrailingDot(input).toLowerCase();
86
+ const matches = zones.filter((zone) => zone.name.toLowerCase() === normalized);
87
+ if (matches.length === 1) return matches[0] ?? null;
88
+ if (matches.length > 1) return matches.find((zone) => !zone.privateZone) ?? matches[0] ?? null;
89
+ return null;
90
+ }
91
+ /**
92
+ * Build a display label for a hosted zone in the picker.
93
+ * @param {HostedZone} zone
94
+ * @returns {string}
95
+ */
96
+ function hostedZoneLabel(zone) {
97
+ const visibility = zone.privateZone ? "private" : "public";
98
+ return `${withoutTrailingDot(zone.name)} (${zone.id}, ${visibility}, ${zone.recordCount} records)`;
99
+ }
100
+ /**
101
+ * Turn a relative or absolute name into a Route53 FQDN under the zone.
102
+ * Apex / "@" / empty → the zone apex.
103
+ * @param {{ nameInput: string; zoneName: string }} params
104
+ * @returns {string} FQDN with trailing dot
105
+ */
106
+ function resolveRecordName(params) {
107
+ const { nameInput, zoneName } = params;
108
+ const zoneFqdn = withTrailingDot(zoneName);
109
+ const raw = nameInput.trim();
110
+ if (raw === "" || raw === "@") return zoneFqdn;
111
+ const asFqdn = withTrailingDot(raw);
112
+ const zoneLower = zoneFqdn.toLowerCase();
113
+ if (asFqdn.toLowerCase() === zoneLower) return zoneFqdn;
114
+ if (asFqdn.toLowerCase().endsWith(`.${zoneLower}`)) return asFqdn;
115
+ return withTrailingDot(`${raw.replace(/\.$/, "")}.${withoutTrailingDot(zoneFqdn)}`);
116
+ }
117
+ /**
118
+ * Fetch every resource record set at an exact name, across all types.
119
+ * Paginates when needed; Route53 list is ordered by name then type.
120
+ * @param {{
121
+ * profile?: string;
122
+ * hostedZoneId: string;
123
+ * name: string;
124
+ * verbose?: boolean;
125
+ * }} params
126
+ * @returns {ResourceRecordSet[]}
127
+ */
128
+ function getRecordSetsAtName(params) {
129
+ const { profile, hostedZoneId, name, verbose } = params;
130
+ const targetName = withTrailingDot(name).toLowerCase();
131
+ let startRecordName = withTrailingDot(name);
132
+ /** @type {string | undefined} */
133
+ let startRecordType;
134
+ /** @type {string | undefined} */
135
+ let nextRecordIdentifier;
136
+ /** @type {ResourceRecordSet[]} */
137
+ const collected = [];
138
+ for (let page = 0; page < 20; page += 1) {
139
+ /** @type {string[]} */
140
+ const args = [
141
+ "route53",
142
+ "list-resource-record-sets",
143
+ "--hosted-zone-id",
144
+ hostedZoneId,
145
+ "--max-items",
146
+ "100"
147
+ ];
148
+ if (startRecordName) args.push("--start-record-name", startRecordName);
149
+ if (startRecordType) args.push("--start-record-type", startRecordType);
150
+ if (nextRecordIdentifier) args.push("--start-record-identifier", nextRecordIdentifier);
151
+ const data = runAws({
152
+ profile,
153
+ args,
154
+ verbose
155
+ });
156
+ const sets = data.ResourceRecordSets ?? [];
157
+ for (const set of sets) {
158
+ const setName = withTrailingDot(String(set.Name ?? "")).toLowerCase();
159
+ if (setName === targetName) {
160
+ collected.push(normalizeRecordSet(set));
161
+ continue;
162
+ }
163
+ if (setName > targetName) return collected;
164
+ }
165
+ if (!(data.IsTruncated === true)) return collected;
166
+ startRecordName = data.NextRecordName ? String(data.NextRecordName) : startRecordName;
167
+ startRecordType = data.NextRecordType ? String(data.NextRecordType) : void 0;
168
+ nextRecordIdentifier = data.NextRecordIdentifier ? String(data.NextRecordIdentifier) : void 0;
169
+ }
170
+ return collected;
171
+ }
172
+ /**
173
+ * Fetch a single resource record set by exact name + type, if it exists.
174
+ * @param {{
175
+ * profile?: string;
176
+ * hostedZoneId: string;
177
+ * name: string;
178
+ * type: string;
179
+ * verbose?: boolean;
180
+ * }} params
181
+ * @returns {ResourceRecordSet | null}
182
+ */
183
+ function getResourceRecordSet(params) {
184
+ const { type, ...rest } = params;
185
+ const targetType = type.toUpperCase();
186
+ return getRecordSetsAtName(rest).find((set) => set.type === targetType) ?? null;
187
+ }
188
+ /**
189
+ * Route53 forbids a CNAME from coexisting with any other record type at the
190
+ * same name. Given the type about to be written and the record sets already at
191
+ * that name, return the sets that would conflict (empty when there is none).
192
+ * A same-type set is an overwrite, not a conflict.
193
+ * @param {{ existingSets: ResourceRecordSet[]; type: string }} params
194
+ * @returns {ResourceRecordSet[]}
195
+ */
196
+ function findCnameConflicts(params) {
197
+ const { existingSets, type } = params;
198
+ const upperType = type.toUpperCase();
199
+ return existingSets.filter((set) => {
200
+ if (set.type === upperType) return false;
201
+ return upperType === "CNAME" || set.type === "CNAME";
202
+ });
203
+ }
204
+ /**
205
+ * @param {any} set
206
+ * @returns {ResourceRecordSet}
207
+ */
208
+ function normalizeRecordSet(set) {
209
+ const values = (set.ResourceRecords ?? []).map((record) => String(record.Value ?? ""));
210
+ return {
211
+ name: withTrailingDot(String(set.Name ?? "")),
212
+ type: String(set.Type ?? "").toUpperCase(),
213
+ ttl: typeof set.TTL === "number" ? set.TTL : void 0,
214
+ values,
215
+ isAlias: Boolean(set.AliasTarget),
216
+ aliasTarget: set.AliasTarget ?? void 0
217
+ };
218
+ }
219
+ /**
220
+ * Format existing values for console display.
221
+ * @param {string[]} values
222
+ * @returns {string}
223
+ */
224
+ function formatValues(values) {
225
+ if (values.length === 0) return "(none)";
226
+ if (values.length === 1) return values[0] ?? "(none)";
227
+ return values.map((value) => `\n - ${value}`).join("");
228
+ }
229
+ /**
230
+ * Normalize a single record value for a given type (trailing dots, TXT quotes).
231
+ * @param {{ type: string; value: string }} params
232
+ * @returns {string}
233
+ */
234
+ function normalizeRecordValue(params) {
235
+ const { type, value } = params;
236
+ const trimmed = value.trim();
237
+ if (type === "CNAME" || type === "NS" || type === "MX" || type === "SRV") {
238
+ if (type === "MX") {
239
+ const parts = trimmed.split(/\s+/);
240
+ if (parts.length >= 2) return `${parts[0]} ${withTrailingDot(parts.slice(1).join(" "))}`;
241
+ }
242
+ if (type === "SRV") {
243
+ const parts = trimmed.split(/\s+/);
244
+ if (parts.length >= 4) return `${parts.slice(0, 3).join(" ")} ${withTrailingDot(parts.slice(3).join(" "))}`;
245
+ }
246
+ return withTrailingDot(trimmed);
247
+ }
248
+ if (type === "TXT") {
249
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed;
250
+ return `"${trimmed}"`;
251
+ }
252
+ return trimmed;
253
+ }
254
+ /**
255
+ * Decide how the new value relates to an existing RRSet.
256
+ * @param {{
257
+ * existing: ResourceRecordSet | null;
258
+ * newValue: string;
259
+ * mode?: "replace" | "append";
260
+ * }} params
261
+ * @returns {{
262
+ * action: "create" | "overwrite" | "append" | "noop";
263
+ * finalValues: string[];
264
+ * summary: string;
265
+ * }}
266
+ */
267
+ function planRecordChange(params) {
268
+ const { existing, newValue, mode = "replace" } = params;
269
+ if (!existing) return {
270
+ action: "create",
271
+ finalValues: [newValue],
272
+ summary: "Adding a new record (nothing exists at this name + type)."
273
+ };
274
+ if (existing.isAlias) return {
275
+ action: "overwrite",
276
+ finalValues: [newValue],
277
+ summary: "Overwriting an existing ALIAS record with a standard record set."
278
+ };
279
+ const alreadyPresent = existing.values.some((value) => value === newValue || value === `"${newValue}"`);
280
+ if (alreadyPresent && mode !== "replace") return {
281
+ action: "noop",
282
+ finalValues: existing.values,
283
+ summary: "Record already has this value. Nothing to do."
284
+ };
285
+ if (alreadyPresent && mode === "replace" && existing.values.length === 1 && existing.values[0] === newValue) return {
286
+ action: "noop",
287
+ finalValues: existing.values,
288
+ summary: "Record already has exactly this value. Nothing to do."
289
+ };
290
+ if (mode === "append") return {
291
+ action: "append",
292
+ finalValues: [...existing.values, newValue],
293
+ summary: `Adding to the existing ${existing.type} record (${existing.values.length} value(s) → ${existing.values.length + 1}).`
294
+ };
295
+ return {
296
+ action: "overwrite",
297
+ finalValues: [newValue],
298
+ summary: existing.values.length === 1 ? `Overwriting existing value:\n ${existing.values[0]}\n → ${newValue}` : `Replacing ${existing.values.length} existing values with a single new value.`
299
+ };
300
+ }
301
+ /**
302
+ * Submit a Route53 UPSERT for a simple (non-alias) resource record set.
303
+ * @param {{
304
+ * profile?: string;
305
+ * hostedZoneId: string;
306
+ * name: string;
307
+ * type: string;
308
+ * ttl: number;
309
+ * values: string[];
310
+ * verbose?: boolean;
311
+ * }} params
312
+ * @returns {any} ChangeInfo from Route53
313
+ */
314
+ function upsertResourceRecordSet(params) {
315
+ const { profile, hostedZoneId, name, type, ttl, values, verbose } = params;
316
+ const changeBatch = {
317
+ Comment: `mtn aws route53 add-record: ${type} ${name}`,
318
+ Changes: [{
319
+ Action: "UPSERT",
320
+ ResourceRecordSet: {
321
+ Name: withTrailingDot(name),
322
+ Type: type,
323
+ TTL: ttl ?? 300,
324
+ ResourceRecords: values.map((value) => ({ Value: value }))
325
+ }
326
+ }]
327
+ };
328
+ const data = runAws({
329
+ profile,
330
+ args: [
331
+ "route53",
332
+ "change-resource-record-sets",
333
+ "--hosted-zone-id",
334
+ hostedZoneId,
335
+ "--change-batch",
336
+ JSON.stringify(changeBatch)
337
+ ],
338
+ verbose
339
+ });
340
+ return data.ChangeInfo ?? data;
341
+ }
342
+ /**
343
+ * Submit a Route53 DELETE for an existing resource record set. Route53 requires
344
+ * the record's exact current definition, so this is driven by the record set
345
+ * returned from {@link getResourceRecordSet} — alias records reuse their
346
+ * AliasTarget; standard records reuse their TTL + values.
347
+ * @param {{
348
+ * profile?: string;
349
+ * hostedZoneId: string;
350
+ * record: ResourceRecordSet;
351
+ * verbose?: boolean;
352
+ * }} params
353
+ * @returns {any} ChangeInfo from Route53
354
+ */
355
+ function deleteResourceRecordSet(params) {
356
+ const { profile, hostedZoneId, record, verbose } = params;
357
+ /** @type {Record<string, any>} */
358
+ const resourceRecordSet = {
359
+ Name: withTrailingDot(record.name),
360
+ Type: record.type
361
+ };
362
+ if (record.isAlias && record.aliasTarget) resourceRecordSet.AliasTarget = record.aliasTarget;
363
+ else {
364
+ resourceRecordSet.TTL = record.ttl ?? 300;
365
+ resourceRecordSet.ResourceRecords = record.values.map((value) => ({ Value: value }));
366
+ }
367
+ const changeBatch = {
368
+ Comment: `mtn aws route53 remove-record: ${record.type} ${record.name}`,
369
+ Changes: [{
370
+ Action: "DELETE",
371
+ ResourceRecordSet: resourceRecordSet
372
+ }]
373
+ };
374
+ const data = runAws({
375
+ profile,
376
+ args: [
377
+ "route53",
378
+ "change-resource-record-sets",
379
+ "--hosted-zone-id",
380
+ hostedZoneId,
381
+ "--change-batch",
382
+ JSON.stringify(changeBatch)
383
+ ],
384
+ verbose
385
+ });
386
+ return data.ChangeInfo ?? data;
387
+ }
388
+ //#endregion
389
+ export { bareHostedZoneId, deleteResourceRecordSet, findCnameConflicts, findHostedZone, formatValues, getRecordSetsAtName, getResourceRecordSet, hostedZoneLabel, listHostedZones, normalizeRecordValue, planRecordChange, resolveRecordName, upsertResourceRecordSet, withTrailingDot, withoutTrailingDot };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `aws route53 remove-record` handler: interactively (or via flags) delete a
3
+ * Route53 resource record set, showing the record before it is removed.
4
+ * @param {unknown} options
5
+ */
6
+ export function removeRecordCommand(options: unknown): Promise<void>;
@@ -0,0 +1,111 @@
1
+ import { promptYesNo } from "../../utils/tty.js";
2
+ import { AwsCliError, getCallerIdentity, requireCommands } from "../../utils/exec-aws.js";
3
+ import { resolveProfile } from "../../utils/resolve-profile.js";
4
+ import { deleteResourceRecordSet, formatValues, getResourceRecordSet, hostedZoneLabel, withoutTrailingDot } from "../add-record/utils.js";
5
+ import { resolveHostedZone, resolveName, resolveRecordType } from "../add-record/handler.js";
6
+ import { removeRecordOptionsSchema } from "./schema.js";
7
+ import chalk from "chalk";
8
+ import { ZodError } from "zod";
9
+ //#region src/commands/aws/route53/remove-record/handler.js
10
+ /**
11
+ * `aws route53 remove-record` handler: interactively (or via flags) delete a
12
+ * Route53 resource record set, showing the record before it is removed.
13
+ * @param {unknown} options
14
+ */
15
+ async function removeRecordCommand(options) {
16
+ try {
17
+ const { profile: profileOption, zone: zoneOption, type: typeOption, name: nameOption, yes, dryRun, verbose } = removeRecordOptionsSchema.parse(options);
18
+ const profile = await resolveProfile({ profile: profileOption });
19
+ if (!profile) {
20
+ console.error(chalk.red("Error: pass --profile or set AWS_PROFILE."));
21
+ process.exit(1);
22
+ }
23
+ requireCommands(["aws"]);
24
+ const identity = getCallerIdentity({
25
+ profile,
26
+ verbose
27
+ });
28
+ console.log("Caller:");
29
+ console.log(` Account: ${identity.Account}`);
30
+ console.log(` Arn: ${identity.Arn}`);
31
+ console.log("");
32
+ const zone = await resolveHostedZone({
33
+ profile,
34
+ zoneOption,
35
+ verbose
36
+ });
37
+ console.log(`Hosted zone: ${hostedZoneLabel(zone)}`);
38
+ console.log("");
39
+ const type = await resolveRecordType(typeOption);
40
+ const name = await resolveName({
41
+ nameOption,
42
+ zoneName: zone.name
43
+ });
44
+ const existing = getResourceRecordSet({
45
+ profile,
46
+ hostedZoneId: zone.id,
47
+ name,
48
+ type,
49
+ verbose
50
+ });
51
+ if (!existing) {
52
+ console.log(chalk.yellow(`No ${type} record at ${withoutTrailingDot(name)}. Nothing to remove.`));
53
+ return;
54
+ }
55
+ console.log("Record to remove:");
56
+ console.log(` Name: ${withoutTrailingDot(name)}`);
57
+ console.log(` Type: ${type}`);
58
+ if (existing.isAlias) console.log(` Alias: ${withoutTrailingDot(String(existing.aliasTarget?.DNSName ?? ""))}`);
59
+ else {
60
+ if (existing.ttl !== void 0) console.log(` TTL: ${existing.ttl}`);
61
+ console.log(` Value:${formatValues(existing.values)}`);
62
+ }
63
+ console.log("");
64
+ if (dryRun) {
65
+ console.log(chalk.cyan("Dry run — no changes made."));
66
+ return;
67
+ }
68
+ if (!yes) {
69
+ if (!await promptYesNo({
70
+ message: `Delete this ${type} record?`,
71
+ defaultYes: false
72
+ })) {
73
+ console.log("Aborted.");
74
+ return;
75
+ }
76
+ }
77
+ const changeInfo = deleteResourceRecordSet({
78
+ profile,
79
+ hostedZoneId: zone.id,
80
+ record: existing,
81
+ verbose
82
+ });
83
+ console.log("");
84
+ console.log(chalk.green(`Done. Deleted ${type} ${withoutTrailingDot(name)}.`));
85
+ if (changeInfo?.Id) {
86
+ console.log(` Change ID: ${changeInfo.Id}`);
87
+ console.log(` Status: ${changeInfo.Status ?? "PENDING"}`);
88
+ }
89
+ } catch (error) {
90
+ if (error instanceof ZodError) {
91
+ console.error(chalk.red("\nāŒ Invalid command options:"));
92
+ error.errors.forEach((err) => {
93
+ console.error(chalk.red(` ${err.path.join(".")}: ${err.message}`));
94
+ });
95
+ process.exit(1);
96
+ }
97
+ const verbose = options?.verbose || false;
98
+ console.error(chalk.red("\nāŒ remove-record failed:"));
99
+ if (error instanceof AwsCliError || error instanceof Error) {
100
+ console.error(chalk.red(` ${error.message}`));
101
+ if (verbose && error.stack) {
102
+ console.error(chalk.gray("\nStack trace:"));
103
+ console.error(chalk.gray(error.stack));
104
+ }
105
+ } else console.error(chalk.red(` ${String(error)}`));
106
+ console.error(chalk.yellow("\nšŸ’” Use --verbose for detailed error information\n"));
107
+ process.exit(1);
108
+ }
109
+ }
110
+ //#endregion
111
+ export { removeRecordCommand };
@@ -0,0 +1,2 @@
1
+ export * from "./handler.js";
2
+ export * from "./schema.js";
@@ -0,0 +1,3 @@
1
+ import { removeRecordOptionsSchema } from "./schema.js";
2
+ import { removeRecordCommand } from "./handler.js";
3
+ export { removeRecordCommand, removeRecordOptionsSchema };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Zod schema for `aws route53 remove-record` options.
3
+ */
4
+ export const removeRecordOptionsSchema: z.ZodObject<{
5
+ profile: z.ZodOptional<z.ZodString>;
6
+ zone: z.ZodOptional<z.ZodString>;
7
+ type: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>, string | undefined, string | undefined>;
8
+ name: z.ZodOptional<z.ZodString>;
9
+ yes: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
10
+ dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
11
+ verbose: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ verbose: boolean;
14
+ yes: boolean;
15
+ dryRun: boolean;
16
+ profile?: string | undefined;
17
+ type?: string | undefined;
18
+ name?: string | undefined;
19
+ zone?: string | undefined;
20
+ }, {
21
+ profile?: string | undefined;
22
+ verbose?: boolean | undefined;
23
+ yes?: boolean | undefined;
24
+ type?: string | undefined;
25
+ dryRun?: boolean | undefined;
26
+ name?: string | undefined;
27
+ zone?: string | undefined;
28
+ }>;
29
+ export type RemoveRecordOptions = z.infer<typeof removeRecordOptionsSchema>;
30
+ import { z } from "zod";
@@ -0,0 +1,21 @@
1
+ import { profileField, verboseField } from "../../schema.js";
2
+ import { RECORD_TYPES } from "../add-record/schema.js";
3
+ import { z } from "zod";
4
+ //#region src/commands/aws/route53/remove-record/schema.js
5
+ /**
6
+ * Zod schema for `aws route53 remove-record` options.
7
+ */
8
+ const removeRecordOptionsSchema = z.object({
9
+ profile: profileField,
10
+ zone: z.string().optional().describe("Hosted zone name or ID (e.g. mountaindev.com or Z123…). Skips the zone picker when set."),
11
+ type: z.string().optional().transform((value) => value === void 0 ? void 0 : value.toUpperCase()).refine((value) => value === void 0 || typeof value === "string" && RECORD_TYPES.includes(value), { message: `Record type must be one of: ${RECORD_TYPES.join(", ")}` }).describe(`DNS record type (${RECORD_TYPES.join(", ")})`),
12
+ name: z.string().optional().describe("Record name relative to the zone (e.g. \"redline\") or a fully-qualified name"),
13
+ yes: z.boolean().optional().default(false).describe("Skip the confirmation prompt"),
14
+ dryRun: z.boolean().optional().default(false).describe("Preview the deletion without calling Route53"),
15
+ verbose: verboseField
16
+ });
17
+ /**
18
+ * @typedef {z.infer<typeof removeRecordOptionsSchema>} RemoveRecordOptions
19
+ */
20
+ //#endregion
21
+ export { removeRecordOptionsSchema };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared Zod field for the AWS profile option.
3
+ */
4
+ export const profileField: z.ZodOptional<z.ZodString>;
5
+ /**
6
+ * Shared Zod field for the AWS region option.
7
+ */
8
+ export const regionField: z.ZodOptional<z.ZodString>;
9
+ /**
10
+ * Shared Zod field for verbose logging.
11
+ */
12
+ export const verboseField: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
13
+ import { z } from "zod";
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ //#region src/commands/aws/schema.js
3
+ /**
4
+ * Shared Zod field for the AWS profile option.
5
+ */
6
+ const profileField = z.string().optional().describe("AWS profile (defaults to the AWS_PROFILE environment variable)");
7
+ /**
8
+ * Shared Zod field for the AWS region option.
9
+ */
10
+ const regionField = z.string().optional().describe("AWS region");
11
+ /**
12
+ * Shared Zod field for verbose logging.
13
+ */
14
+ const verboseField = z.boolean().optional().default(false).describe("Enable verbose logging for detailed output and error traces");
15
+ //#endregion
16
+ export { profileField, regionField, verboseField };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `aws ses send-email` handler: send a single test email via the SES API or SMTP.
3
+ * @param {unknown} options
4
+ */
5
+ export function sendEmailCommand(options: unknown): Promise<void>;
@@ -0,0 +1,70 @@
1
+ import { resolveProfile } from "../../utils/resolve-profile.js";
2
+ import { sendEmailOptionsSchema } from "./schema.js";
3
+ import { defaultBody, sendViaApi, sendViaSmtp } from "./utils.js";
4
+ import chalk from "chalk";
5
+ import { ZodError } from "zod";
6
+ //#region src/commands/aws/ses/send-email/handler.js
7
+ /**
8
+ * `aws ses send-email` handler: send a single test email via the SES API or SMTP.
9
+ * @param {unknown} options
10
+ */
11
+ async function sendEmailCommand(options) {
12
+ try {
13
+ const parsed = sendEmailOptionsSchema.parse(options);
14
+ const { to, from, subject, transport, region, profile: profileOption, accessKeyId, dryRun, verbose } = parsed;
15
+ const body = parsed.body ?? defaultBody();
16
+ const profile = transport === "api" ? await resolveProfile({ profile: profileOption }) : profileOption;
17
+ console.log(chalk.blue("\nšŸ“§ SES send-email"));
18
+ console.log(chalk.gray(` transport: ${transport}`));
19
+ console.log(chalk.gray(` region: ${region}`));
20
+ console.log(chalk.gray(` from: ${from}`));
21
+ console.log(chalk.gray(` to: ${to}`));
22
+ console.log(chalk.gray(` subject: ${subject}`));
23
+ if (verbose) console.log(chalk.gray(` body: ${body}`));
24
+ if (dryRun) {
25
+ console.log(chalk.yellow("\n[dry-run] No email sent."));
26
+ return;
27
+ }
28
+ const messageId = transport === "api" ? await sendViaApi({
29
+ to,
30
+ from,
31
+ subject,
32
+ body,
33
+ region,
34
+ profile
35
+ }) : await sendViaSmtp({
36
+ to,
37
+ from,
38
+ subject,
39
+ body,
40
+ region,
41
+ accessKeyId
42
+ });
43
+ console.log(chalk.green(`\nāœ… Email sent${messageId ? ` (messageId: ${messageId})` : ""}.`));
44
+ } catch (error) {
45
+ if (error instanceof ZodError) {
46
+ console.error(chalk.red("\nāŒ Invalid command options:"));
47
+ error.errors.forEach((err) => {
48
+ console.error(chalk.red(` ${err.path.join(".")}: ${err.message}`));
49
+ });
50
+ process.exit(1);
51
+ }
52
+ const verbose = options?.verbose || false;
53
+ console.error(chalk.red("\nāŒ send-email failed:"));
54
+ if (error instanceof Error) {
55
+ console.error(chalk.red(` ${error.message}`));
56
+ if (verbose && error.stack) {
57
+ console.error(chalk.gray("\nStack trace:"));
58
+ console.error(chalk.gray(error.stack));
59
+ }
60
+ } else console.error(chalk.red(` ${String(error)}`));
61
+ console.error(chalk.yellow("\nšŸ’” Troubleshooting tips:"));
62
+ console.error(chalk.yellow(" • The sender address must be verified in SES"));
63
+ console.error(chalk.yellow(" • New SES accounts are sandboxed — recipients must also be verified"));
64
+ console.error(chalk.yellow(" • For smtp transport, set SES_SMTP_USER and SES_SMTP_PASSWORD"));
65
+ console.error(chalk.yellow(" • Use --verbose for detailed error information\n"));
66
+ process.exit(1);
67
+ }
68
+ }
69
+ //#endregion
70
+ export { sendEmailCommand };
@@ -0,0 +1,3 @@
1
+ export * from "./handler.js";
2
+ export * from "./schema.js";
3
+ export * from "./utils.js";
@@ -0,0 +1,4 @@
1
+ import { sendEmailOptionsSchema } from "./schema.js";
2
+ import { defaultBody, sendViaApi, sendViaSmtp } from "./utils.js";
3
+ import { sendEmailCommand } from "./handler.js";
4
+ export { defaultBody, sendEmailCommand, sendEmailOptionsSchema, sendViaApi, sendViaSmtp };