@wraps.dev/cli 2.18.6 → 2.18.8

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/dist/cli.js CHANGED
@@ -66,1533 +66,736 @@ var init_ci_detection = __esm({
66
66
  }
67
67
  });
68
68
 
69
- // src/utils/shared/s3-state.ts
70
- var s3_state_exports = {};
71
- __export(s3_state_exports, {
72
- clearS3StackLocks: () => clearS3StackLocks,
73
- deleteMetadata: () => deleteMetadata,
74
- downloadMetadata: () => downloadMetadata,
75
- ensureStateBucket: () => ensureStateBucket,
76
- getS3BackendUrl: () => getS3BackendUrl,
77
- getStateBucketName: () => getStateBucketName,
78
- migrateLocalPulumiState: () => migrateLocalPulumiState,
79
- needsMigration: () => needsMigration,
80
- stateBucketExists: () => stateBucketExists,
81
- uploadMetadata: () => uploadMetadata
82
- });
83
- import { existsSync, statSync } from "fs";
84
- import { readdir, writeFile } from "fs/promises";
69
+ // src/utils/shared/aws-detection.ts
70
+ import { execSync } from "child_process";
71
+ import { existsSync, readdirSync, readFileSync } from "fs";
72
+ import { homedir } from "os";
85
73
  import { join } from "path";
86
- function has404StatusCode(error) {
87
- if (!(error instanceof Error)) {
74
+ import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
75
+ async function isAWSCLIInstalled() {
76
+ try {
77
+ execSync("aws --version", { stdio: "pipe" });
78
+ return true;
79
+ } catch {
88
80
  return false;
89
81
  }
90
- const metadataError = error;
91
- return metadataError.$metadata?.httpStatusCode === 404;
92
- }
93
- function getStateBucketName(accountId, region) {
94
- return `wraps-state-${accountId}-${region}`;
95
- }
96
- function getS3BackendUrl(accountId, region) {
97
- return `s3://${getStateBucketName(accountId, region)}`;
98
82
  }
99
- async function stateBucketExists(accountId, region) {
100
- const { S3Client: S3Client2, HeadBucketCommand } = await import("@aws-sdk/client-s3");
101
- const client = new S3Client2({ region });
102
- const bucketName = getStateBucketName(accountId, region);
83
+ async function getAWSCLIVersion() {
103
84
  try {
104
- await client.send(new HeadBucketCommand({ Bucket: bucketName }));
105
- return true;
106
- } catch (error) {
107
- if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error))) {
108
- return false;
109
- }
110
- throw error;
85
+ const output3 = execSync("aws --version", { encoding: "utf-8" });
86
+ const match = output3.match(/aws-cli\/(\d+\.\d+\.\d+)/);
87
+ return match ? match[1] : null;
88
+ } catch {
89
+ return null;
111
90
  }
112
91
  }
113
- async function ensureStateBucket(accountId, region) {
114
- const {
115
- S3Client: S3Client2,
116
- HeadBucketCommand,
117
- CreateBucketCommand,
118
- PutBucketEncryptionCommand,
119
- PutBucketVersioningCommand,
120
- PutPublicAccessBlockCommand,
121
- PutBucketTaggingCommand
122
- } = await import("@aws-sdk/client-s3");
123
- const client = new S3Client2({ region });
124
- const bucketName = getStateBucketName(accountId, region);
125
- try {
126
- await client.send(new HeadBucketCommand({ Bucket: bucketName }));
127
- return bucketName;
128
- } catch (error) {
129
- const isNotFound = error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error));
130
- if (!isNotFound) {
131
- throw error;
92
+ function detectCredentialSource() {
93
+ if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
94
+ return "environment";
95
+ }
96
+ if (process.env.AWS_SSO_ACCOUNT_ID || process.env.AWS_SSO_SESSION) {
97
+ return "sso";
98
+ }
99
+ if (process.env.AWS_PROFILE) {
100
+ return "profile";
101
+ }
102
+ const credentialsPath = join(homedir(), ".aws", "credentials");
103
+ if (existsSync(credentialsPath)) {
104
+ const content = readFileSync(credentialsPath, "utf-8");
105
+ if (content.includes("[default]")) {
106
+ return "profile";
132
107
  }
133
108
  }
134
- const createParams = { Bucket: bucketName };
135
- if (region !== "us-east-1") {
136
- createParams.CreateBucketConfiguration = {
137
- LocationConstraint: region
138
- };
109
+ const ssoCachePath = join(homedir(), ".aws", "sso", "cache");
110
+ if (existsSync(ssoCachePath)) {
111
+ return "sso";
139
112
  }
140
- await client.send(new CreateBucketCommand(createParams));
141
- await client.send(
142
- new PutBucketEncryptionCommand({
143
- Bucket: bucketName,
144
- ServerSideEncryptionConfiguration: {
145
- Rules: [
146
- {
147
- ApplyServerSideEncryptionByDefault: {
148
- SSEAlgorithm: "AES256"
149
- }
150
- }
151
- ]
152
- }
153
- })
154
- );
155
- await client.send(
156
- new PutBucketVersioningCommand({
157
- Bucket: bucketName,
158
- VersioningConfiguration: {
159
- Status: "Enabled"
160
- }
161
- })
162
- );
163
- await client.send(
164
- new PutPublicAccessBlockCommand({
165
- Bucket: bucketName,
166
- PublicAccessBlockConfiguration: {
167
- BlockPublicAcls: true,
168
- BlockPublicPolicy: true,
169
- IgnorePublicAcls: true,
170
- RestrictPublicBuckets: true
171
- }
172
- })
173
- );
174
- await client.send(
175
- new PutBucketTaggingCommand({
176
- Bucket: bucketName,
177
- Tagging: {
178
- TagSet: [
179
- { Key: "ManagedBy", Value: "wraps-cli" },
180
- { Key: "Purpose", Value: "state" }
181
- ]
182
- }
183
- })
184
- );
185
- return bucketName;
186
- }
187
- async function uploadMetadata(bucketName, metadata) {
188
- const { S3Client: S3Client2, PutObjectCommand } = await import("@aws-sdk/client-s3");
189
- const client = new S3Client2({ region: metadata.region });
190
- const key = `metadata/${metadata.accountId}-${metadata.region}.json`;
191
- await client.send(
192
- new PutObjectCommand({
193
- Bucket: bucketName,
194
- Key: key,
195
- Body: JSON.stringify(metadata, null, 2),
196
- ContentType: "application/json"
197
- })
198
- );
199
- }
200
- async function deleteMetadata(bucketName, accountId, region) {
201
- const { S3Client: S3Client2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
202
- const client = new S3Client2({ region });
203
- const key = `metadata/${accountId}-${region}.json`;
204
- await client.send(
205
- new DeleteObjectCommand({
206
- Bucket: bucketName,
207
- Key: key
208
- })
209
- );
113
+ if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_EXECUTION_ENV) {
114
+ return "instance";
115
+ }
116
+ return null;
210
117
  }
211
- async function clearS3StackLocks(accountId, region) {
212
- const { S3Client: S3Client2, ListObjectsV2Command: ListObjectsV2Command2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
213
- const client = new S3Client2({ region });
214
- const bucketName = getStateBucketName(accountId, region);
215
- const prefix = ".pulumi/locks/";
216
- const response = await client.send(
217
- new ListObjectsV2Command2({ Bucket: bucketName, Prefix: prefix })
218
- );
219
- const lockObjects = response.Contents ?? [];
220
- if (lockObjects.length === 0) {
221
- return 0;
118
+ function detectHostingProvider() {
119
+ if (process.env.VERCEL || process.env.VERCEL_ENV) {
120
+ return "vercel";
222
121
  }
223
- for (const obj of lockObjects) {
224
- if (obj.Key) {
225
- await client.send(
226
- new DeleteObjectCommand({ Bucket: bucketName, Key: obj.Key })
227
- );
228
- }
122
+ if (process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID) {
123
+ return "railway";
229
124
  }
230
- return lockObjects.length;
125
+ if (process.env.NETLIFY || process.env.NETLIFY_DEV) {
126
+ return "netlify";
127
+ }
128
+ if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV || process.env.ECS_CONTAINER_METADATA_URI || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
129
+ return "aws";
130
+ }
131
+ return null;
231
132
  }
232
- async function downloadMetadata(bucketName, accountId, region) {
233
- const { S3Client: S3Client2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
234
- const client = new S3Client2({ region });
235
- const key = `metadata/${accountId}-${region}.json`;
133
+ async function validateCredentials() {
236
134
  try {
237
- const response = await client.send(
238
- new GetObjectCommand2({
239
- Bucket: bucketName,
240
- Key: key
241
- })
242
- );
243
- const body = await response.Body?.transformToString();
244
- if (!body) {
245
- return null;
246
- }
247
- return JSON.parse(body);
248
- } catch (error) {
249
- if (error instanceof Error && (error.name === "NoSuchKey" || has404StatusCode(error))) {
250
- return null;
251
- }
252
- throw error;
135
+ const sts = new STSClient({ region: "us-east-1" });
136
+ const identity = await sts.send(new GetCallerIdentityCommand({}));
137
+ return identity.Account || null;
138
+ } catch {
139
+ return null;
253
140
  }
254
141
  }
255
- async function needsMigration(localPulumiDir, accountId, region) {
256
- const markerPath = join(localPulumiDir, `.migrated-${accountId}-${region}`);
257
- if (existsSync(markerPath)) {
258
- return false;
142
+ function getCurrentProfile() {
143
+ return process.env.AWS_PROFILE || "default";
144
+ }
145
+ function getCurrentRegion() {
146
+ if (process.env.AWS_REGION) {
147
+ return process.env.AWS_REGION;
259
148
  }
260
- const stacksDir = join(localPulumiDir, ".pulumi", "stacks");
261
- if (!existsSync(stacksDir)) {
262
- return false;
149
+ if (process.env.AWS_DEFAULT_REGION) {
150
+ return process.env.AWS_DEFAULT_REGION;
263
151
  }
264
152
  try {
265
- const entries = await readdir(stacksDir);
266
- for (const entry of entries) {
267
- const entryPath = join(stacksDir, entry);
268
- if (statSync(entryPath).isDirectory()) {
269
- const files = await readdir(entryPath);
270
- const matching = files.filter(
271
- (f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
272
- );
273
- if (matching.length > 0) {
274
- return true;
275
- }
276
- }
277
- }
278
- return false;
153
+ const region = execSync("aws configure get region", {
154
+ encoding: "utf-8",
155
+ stdio: ["pipe", "pipe", "pipe"]
156
+ }).trim();
157
+ return region || null;
279
158
  } catch {
280
- return false;
159
+ return null;
281
160
  }
282
161
  }
283
- async function migrateLocalPulumiState(localPulumiDir, bucketName, accountId, region) {
284
- const pulumi31 = await import("@pulumi/pulumi/automation/index.js");
285
- const stacksDir = join(localPulumiDir, ".pulumi", "stacks");
286
- const entries = await readdir(stacksDir);
287
- for (const entry of entries) {
288
- const entryPath = join(stacksDir, entry);
289
- if (!statSync(entryPath).isDirectory()) {
290
- continue;
291
- }
292
- const projectName = entry;
293
- const files = await readdir(entryPath);
294
- const stackFiles = files.filter(
295
- (f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
296
- );
297
- for (const stackFile of stackFiles) {
298
- const stackName = stackFile.replace(".json", "");
299
- try {
300
- const localStack = await pulumi31.LocalWorkspace.selectStack({
301
- stackName,
302
- workDir: localPulumiDir
303
- });
304
- const state = await localStack.exportStack();
305
- const s3Stack = await pulumi31.LocalWorkspace.createOrSelectStack(
306
- {
307
- stackName,
308
- projectName,
309
- program: async () => ({})
310
- },
311
- {
312
- workDir: localPulumiDir,
313
- envVars: {
314
- PULUMI_BACKEND_URL: `s3://${bucketName}`,
315
- PULUMI_CONFIG_PASSPHRASE: ""
316
- }
317
- }
318
- );
319
- await s3Stack.importStack(state);
320
- } catch (error) {
321
- console.error(
322
- `Warning: Failed to migrate stack ${stackName}: ${error instanceof Error ? error.message : error}`
323
- );
162
+ function parseSSOProfiles() {
163
+ const configPath = join(homedir(), ".aws", "config");
164
+ if (!existsSync(configPath)) {
165
+ return [];
166
+ }
167
+ const content = readFileSync(configPath, "utf-8");
168
+ const profiles = [];
169
+ const sessionMap = /* @__PURE__ */ new Map();
170
+ const sessionSections = content.split(/^\[/m).filter(Boolean);
171
+ for (const section of sessionSections) {
172
+ const lines = section.split("\n");
173
+ const header = lines[0]?.replace("]", "").trim();
174
+ if (header?.startsWith("sso-session ")) {
175
+ const sessionName = header.replace("sso-session ", "");
176
+ const config2 = {};
177
+ for (const line of lines.slice(1)) {
178
+ const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
179
+ if (match) {
180
+ config2[match[1]] = match[2];
181
+ }
324
182
  }
183
+ sessionMap.set(sessionName, {
184
+ startUrl: config2.sso_start_url || "",
185
+ region: config2.sso_region || ""
186
+ });
325
187
  }
326
188
  }
327
- const markerPath = join(localPulumiDir, `.migrated-${accountId}-${region}`);
328
- await writeFile(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
329
- }
330
- var init_s3_state = __esm({
331
- "src/utils/shared/s3-state.ts"() {
332
- "use strict";
333
- init_esm_shims();
334
- }
335
- });
336
-
337
- // src/utils/shared/fs.ts
338
- var fs_exports = {};
339
- __export(fs_exports, {
340
- clearLocalStackLocks: () => clearLocalStackLocks,
341
- ensurePulumiWorkDir: () => ensurePulumiWorkDir,
342
- ensureWrapsDir: () => ensureWrapsDir,
343
- getPulumiWorkDir: () => getPulumiWorkDir,
344
- getWrapsDir: () => getWrapsDir
345
- });
346
- import { existsSync as existsSync2 } from "fs";
347
- import { mkdir, readdir as readdir2, rm } from "fs/promises";
348
- import { homedir } from "os";
349
- import { join as join2 } from "path";
350
- function getWrapsDir() {
351
- return join2(homedir(), ".wraps");
352
- }
353
- function getPulumiWorkDir() {
354
- return join2(getWrapsDir(), "pulumi");
355
- }
356
- async function ensureWrapsDir() {
357
- const wrapsDir = getWrapsDir();
358
- if (!existsSync2(wrapsDir)) {
359
- await mkdir(wrapsDir, { recursive: true });
189
+ const sections = content.split(/^\[/m).filter(Boolean);
190
+ for (const section of sections) {
191
+ const lines = section.split("\n");
192
+ const header = lines[0]?.replace("]", "").trim();
193
+ if (!header?.startsWith("profile ") && header !== "default") {
194
+ continue;
195
+ }
196
+ const profileName = header === "default" ? "default" : header.replace("profile ", "");
197
+ const config2 = {};
198
+ for (const line of lines.slice(1)) {
199
+ const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
200
+ if (match) {
201
+ config2[match[1]] = match[2];
202
+ }
203
+ }
204
+ if (config2.sso_start_url || config2.sso_session) {
205
+ let ssoStartUrl = config2.sso_start_url || "";
206
+ let ssoRegion = config2.sso_region || "";
207
+ if (config2.sso_session) {
208
+ const session = sessionMap.get(config2.sso_session);
209
+ if (session) {
210
+ ssoStartUrl = ssoStartUrl || session.startUrl;
211
+ ssoRegion = ssoRegion || session.region;
212
+ }
213
+ }
214
+ profiles.push({
215
+ name: profileName,
216
+ ssoStartUrl,
217
+ ssoRegion,
218
+ ssoAccountId: config2.sso_account_id || "",
219
+ ssoRoleName: config2.sso_role_name || "",
220
+ region: config2.region,
221
+ ssoSession: config2.sso_session
222
+ });
223
+ }
360
224
  }
225
+ return profiles;
361
226
  }
362
- async function clearLocalStackLocks() {
363
- const locksDir = join2(getPulumiWorkDir(), ".pulumi", "locks");
364
- if (!existsSync2(locksDir)) {
365
- return 0;
227
+ function parseSSOSessions() {
228
+ const configPath = join(homedir(), ".aws", "config");
229
+ if (!existsSync(configPath)) {
230
+ return [];
366
231
  }
367
- let count = 0;
368
- async function walkAndDelete(dir) {
369
- const entries = await readdir2(dir, { withFileTypes: true });
370
- for (const entry of entries) {
371
- const fullPath = join2(dir, entry.name);
372
- if (entry.isDirectory()) {
373
- await walkAndDelete(fullPath);
374
- } else if (entry.name.endsWith(".json")) {
375
- await rm(fullPath);
376
- count++;
232
+ const content = readFileSync(configPath, "utf-8");
233
+ const sessions = [];
234
+ const sections = content.split(/^\[/m).filter(Boolean);
235
+ for (const section of sections) {
236
+ const lines = section.split("\n");
237
+ const header = lines[0]?.replace("]", "").trim();
238
+ if (!header?.startsWith("sso-session ")) {
239
+ continue;
240
+ }
241
+ const sessionName = header.replace("sso-session ", "");
242
+ const config2 = {};
243
+ for (const line of lines.slice(1)) {
244
+ const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
245
+ if (match) {
246
+ config2[match[1]] = match[2];
377
247
  }
378
248
  }
249
+ sessions.push({
250
+ name: sessionName,
251
+ ssoStartUrl: config2.sso_start_url || "",
252
+ ssoRegion: config2.sso_region || "",
253
+ ssoRegistrationScopes: config2.sso_registration_scopes?.split(",").map((s) => s.trim())
254
+ });
379
255
  }
380
- await walkAndDelete(locksDir);
381
- return count;
256
+ return sessions;
382
257
  }
383
- async function ensurePulumiWorkDir(options) {
384
- await ensureWrapsDir();
385
- const pulumiDir = getPulumiWorkDir();
386
- if (!existsSync2(pulumiDir)) {
387
- await mkdir(pulumiDir, { recursive: true });
258
+ function checkSSOTokenStatus(startUrl) {
259
+ const ssoCachePath = join(homedir(), ".aws", "sso", "cache");
260
+ if (!existsSync(ssoCachePath)) {
261
+ return {
262
+ valid: false,
263
+ expiresAt: null,
264
+ expired: true,
265
+ minutesRemaining: null,
266
+ startUrl: null
267
+ };
388
268
  }
389
- process.env.PULUMI_CONFIG_PASSPHRASE = "";
390
- const useS3 = options?.accountId && options?.region && process.env.WRAPS_LOCAL_ONLY !== "1";
391
- if (useS3) {
392
- try {
393
- const {
394
- ensureStateBucket: ensureStateBucket2,
395
- getS3BackendUrl: getS3BackendUrl2,
396
- needsMigration: needsMigration2,
397
- migrateLocalPulumiState: migrateLocalPulumiState2
398
- } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
399
- const bucketName = await ensureStateBucket2(
400
- options.accountId,
401
- options.region
402
- );
403
- const shouldMigrate = await needsMigration2(
404
- pulumiDir,
405
- options.accountId,
406
- options.region
407
- );
408
- if (shouldMigrate) {
409
- process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
410
- await migrateLocalPulumiState2(
411
- pulumiDir,
412
- bucketName,
413
- options.accountId,
414
- options.region
415
- );
269
+ try {
270
+ const cacheFiles = readdirSync(ssoCachePath).filter(
271
+ (f) => f.endsWith(".json")
272
+ );
273
+ for (const file of cacheFiles) {
274
+ const content = readFileSync(join(ssoCachePath, file), "utf-8");
275
+ const token = JSON.parse(content);
276
+ if (!(token.accessToken && token.expiresAt)) {
277
+ continue;
416
278
  }
417
- process.env.PULUMI_BACKEND_URL = getS3BackendUrl2(
418
- options.accountId,
419
- options.region
420
- );
421
- return;
422
- } catch (error) {
423
- const clack54 = await import("@clack/prompts");
424
- clack54.log.warn(
425
- `S3 state backend unavailable (${error instanceof Error ? error.message : error}). Using local state.`
279
+ if (startUrl && token.startUrl !== startUrl) {
280
+ continue;
281
+ }
282
+ const expiresAt = new Date(token.expiresAt);
283
+ const now = /* @__PURE__ */ new Date();
284
+ const expired = expiresAt <= now;
285
+ const minutesRemaining = Math.floor(
286
+ (expiresAt.getTime() - now.getTime()) / 6e4
426
287
  );
288
+ return {
289
+ valid: !expired,
290
+ expiresAt,
291
+ expired,
292
+ minutesRemaining,
293
+ startUrl: token.startUrl || null
294
+ };
427
295
  }
296
+ } catch {
428
297
  }
429
- process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
298
+ return {
299
+ valid: false,
300
+ expiresAt: null,
301
+ expired: true,
302
+ minutesRemaining: null,
303
+ startUrl: null
304
+ };
430
305
  }
431
- var init_fs = __esm({
432
- "src/utils/shared/fs.ts"() {
433
- "use strict";
434
- init_esm_shims();
435
- }
436
- });
437
-
438
- // src/utils/shared/config.ts
439
- import { existsSync as existsSync3 } from "fs";
440
- import { chmod, readFile, writeFile as writeFile2 } from "fs/promises";
441
- import { join as join3 } from "path";
442
- function getApiBaseUrl() {
443
- return process.env.WRAPS_API_URL || "https://api.wraps.dev";
306
+ function getActiveSSOProfile(profiles) {
307
+ const currentProfile = process.env.AWS_PROFILE || "default";
308
+ return profiles.find((p) => p.name === currentProfile) || null;
444
309
  }
445
- function getAppBaseUrl() {
446
- return process.env.WRAPS_APP_URL || "https://app.wraps.dev";
310
+ function getSSOLoginCommand(profile) {
311
+ if (profile && profile !== "default") {
312
+ return `aws sso login --profile ${profile}`;
313
+ }
314
+ return "aws sso login";
447
315
  }
448
- function getConfigPath() {
449
- return join3(getWrapsDir(), CONFIG_FILE);
450
- }
451
- async function readAuthConfig() {
452
- const path3 = getConfigPath();
453
- if (!existsSync3(path3)) {
454
- return null;
455
- }
456
- try {
457
- const content = await readFile(path3, "utf-8");
458
- return JSON.parse(content);
459
- } catch {
460
- return null;
461
- }
316
+ function formatSSOProfile(profile) {
317
+ return `${profile.name} (${profile.ssoAccountId} / ${profile.ssoRoleName})`;
462
318
  }
463
- async function saveAuthConfig(config2) {
464
- await ensureWrapsDir();
465
- const path3 = getConfigPath();
466
- const existing = await readAuthConfig();
467
- const merged = existing ? { ...existing, ...config2 } : config2;
468
- await writeFile2(path3, JSON.stringify(merged, null, 2), "utf-8");
469
- await chmod(path3, 384);
319
+ async function detectAWSState() {
320
+ const [cliInstalled, cliVersion, accountId] = await Promise.all([
321
+ isAWSCLIInstalled(),
322
+ getAWSCLIVersion(),
323
+ validateCredentials()
324
+ ]);
325
+ const credentialSource = detectCredentialSource();
326
+ const detectedProvider = detectHostingProvider();
327
+ const region = getCurrentRegion();
328
+ const profileName = getCurrentProfile();
329
+ const ssoProfiles = parseSSOProfiles();
330
+ const ssoSessions = parseSSOSessions();
331
+ const activeProfile = getActiveSSOProfile(ssoProfiles);
332
+ const tokenStatus = ssoProfiles.length > 0 ? checkSSOTokenStatus(activeProfile?.ssoStartUrl) : null;
333
+ const isUsingSSO = credentialSource === "sso" || activeProfile !== null && accountId !== null;
334
+ return {
335
+ cliInstalled,
336
+ cliVersion,
337
+ credentialsConfigured: accountId !== null,
338
+ credentialSource: isUsingSSO ? "sso" : accountId !== null ? credentialSource : null,
339
+ profileName,
340
+ accountId,
341
+ detectedProvider,
342
+ region,
343
+ sso: {
344
+ configured: ssoProfiles.length > 0,
345
+ profiles: ssoProfiles,
346
+ sessions: ssoSessions,
347
+ tokenStatus,
348
+ activeProfile: isUsingSSO ? activeProfile : null
349
+ }
350
+ };
470
351
  }
471
- async function clearAuthConfig() {
472
- const existing = await readAuthConfig();
473
- if (existing) {
474
- existing.auth = void 0;
475
- await saveAuthConfig(existing);
476
- }
352
+ function hasCredentialsFile() {
353
+ const credentialsPath = join(homedir(), ".aws", "credentials");
354
+ return existsSync(credentialsPath);
477
355
  }
478
- function resolveToken(flags2) {
479
- return flags2?.token || process.env.WRAPS_API_KEY || null;
356
+ function hasConfigFile() {
357
+ const configPath = join(homedir(), ".aws", "config");
358
+ return existsSync(configPath);
480
359
  }
481
- async function resolveTokenAsync(flags2) {
482
- const sync = resolveToken(flags2);
483
- if (sync) {
484
- return sync;
485
- }
486
- const config2 = await readAuthConfig();
487
- if (!config2?.auth?.token) {
488
- return null;
360
+ function getConfiguredProfiles() {
361
+ const profiles = [];
362
+ const credentialsPath = join(homedir(), ".aws", "credentials");
363
+ if (existsSync(credentialsPath)) {
364
+ const content = readFileSync(credentialsPath, "utf-8");
365
+ const matches = content.matchAll(/\[([^\]]+)\]/g);
366
+ for (const match of matches) {
367
+ profiles.push(match[1]);
368
+ }
489
369
  }
490
- if (config2.auth.expiresAt && new Date(config2.auth.expiresAt) <= /* @__PURE__ */ new Date()) {
491
- return null;
370
+ const configPath = join(homedir(), ".aws", "config");
371
+ if (existsSync(configPath)) {
372
+ const content = readFileSync(configPath, "utf-8");
373
+ const matches = content.matchAll(/\[profile ([^\]]+)\]/g);
374
+ for (const match of matches) {
375
+ if (!profiles.includes(match[1])) {
376
+ profiles.push(match[1]);
377
+ }
378
+ }
492
379
  }
493
- return config2.auth.token;
380
+ return profiles;
494
381
  }
495
- var CONFIG_FILE;
496
- var init_config = __esm({
497
- "src/utils/shared/config.ts"() {
382
+ var init_aws_detection = __esm({
383
+ "src/utils/shared/aws-detection.ts"() {
498
384
  "use strict";
499
385
  init_esm_shims();
500
- init_fs();
501
- CONFIG_FILE = "config.json";
502
386
  }
503
387
  });
504
388
 
505
- // src/telemetry/config.ts
506
- import Conf from "conf";
507
- import { v4 as uuidv4 } from "uuid";
508
- var CONFIG_DEFAULTS, TelemetryConfigManager;
509
- var init_config2 = __esm({
510
- "src/telemetry/config.ts"() {
389
+ // src/utils/shared/json-output.ts
390
+ function isJsonMode() {
391
+ return _jsonMode;
392
+ }
393
+ function setJsonMode(enabled) {
394
+ _jsonMode = enabled;
395
+ }
396
+ function jsonSuccess(command, data) {
397
+ const output3 = { success: true, command, data };
398
+ console.log(JSON.stringify(output3));
399
+ }
400
+ function jsonError(command, error) {
401
+ const output3 = { success: false, command, error };
402
+ console.log(JSON.stringify(output3));
403
+ }
404
+ var _jsonMode;
405
+ var init_json_output = __esm({
406
+ "src/utils/shared/json-output.ts"() {
511
407
  "use strict";
512
408
  init_esm_shims();
513
- CONFIG_DEFAULTS = {
514
- enabled: true,
515
- anonymousId: uuidv4(),
516
- notificationShown: false
517
- };
518
- TelemetryConfigManager = class {
519
- config;
520
- constructor(options) {
521
- this.config = new Conf({
522
- projectName: "wraps",
523
- configName: "telemetry",
524
- defaults: CONFIG_DEFAULTS,
525
- cwd: options?.cwd
526
- });
527
- }
528
- /**
529
- * Check if telemetry is enabled
530
- */
531
- isEnabled() {
532
- return this.config.get("enabled");
533
- }
534
- /**
535
- * Enable or disable telemetry
536
- */
537
- setEnabled(enabled) {
538
- this.config.set("enabled", enabled);
539
- }
540
- /**
541
- * Get the anonymous user ID
542
- */
543
- getAnonymousId() {
544
- return this.config.get("anonymousId");
545
- }
546
- /**
547
- * Check if the first-run notification has been shown
548
- */
549
- hasShownNotification() {
550
- return this.config.get("notificationShown");
551
- }
552
- /**
553
- * Mark the first-run notification as shown
554
- */
555
- markNotificationShown() {
556
- this.config.set("notificationShown", true);
557
- }
558
- /**
559
- * Get the full path to the configuration file
560
- */
561
- getConfigPath() {
562
- return this.config.path;
563
- }
564
- /**
565
- * Reset configuration to defaults
566
- */
567
- reset() {
568
- this.config.clear();
569
- this.config.set({
570
- ...CONFIG_DEFAULTS,
571
- anonymousId: uuidv4()
572
- });
573
- }
574
- };
409
+ _jsonMode = false;
575
410
  }
576
411
  });
577
412
 
578
- // src/telemetry/client.ts
579
- import { readFileSync } from "fs";
580
- import { dirname, join as join4 } from "path";
581
- import { fileURLToPath as fileURLToPath2 } from "url";
413
+ // src/utils/shared/errors.ts
414
+ var errors_exports = {};
415
+ __export(errors_exports, {
416
+ WrapsError: () => WrapsError,
417
+ awsErrorToWrapsError: () => awsErrorToWrapsError,
418
+ classifyDNSError: () => classifyDNSError,
419
+ errors: () => errors,
420
+ handleCLIError: () => handleCLIError,
421
+ isAWSError: () => isAWSError,
422
+ isAWSNotFoundError: () => isAWSNotFoundError,
423
+ isPulumiError: () => isPulumiError,
424
+ parseAWSError: () => parseAWSError,
425
+ parsePulumiError: () => parsePulumiError,
426
+ redactSensitiveValues: () => redactSensitiveValues,
427
+ sanitizeErrorMessage: () => sanitizeErrorMessage
428
+ });
429
+ import * as clack from "@clack/prompts";
582
430
  import pc from "picocolors";
583
- function getTelemetryClient() {
584
- if (!telemetryInstance) {
585
- telemetryInstance = new TelemetryClient();
431
+ function isAWSError(error) {
432
+ if (!(error instanceof Error)) {
433
+ return false;
586
434
  }
587
- return telemetryInstance;
435
+ const awsErrorNames = [
436
+ "ExpiredTokenException",
437
+ "InvalidClientTokenId",
438
+ "AccessDenied",
439
+ "AccessDeniedException",
440
+ "UnauthorizedAccess",
441
+ "InvalidAccessKeyId",
442
+ "SignatureDoesNotMatch",
443
+ "UnrecognizedClientException",
444
+ "CredentialsError",
445
+ "TokenRefreshRequired",
446
+ "SSOTokenExpired"
447
+ ];
448
+ return awsErrorNames.includes(error.name) || "$metadata" in error;
588
449
  }
589
- var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, TelemetryClient, telemetryInstance;
590
- var init_client = __esm({
591
- "src/telemetry/client.ts"() {
592
- "use strict";
593
- init_esm_shims();
594
- init_ci_detection();
595
- init_config();
596
- init_config2();
597
- DEFAULT_ENDPOINT = process.env.WRAPS_TELEMETRY_URL || "https://wraps.dev/api/telemetry";
598
- DEFAULT_TIMEOUT = 2e3;
599
- TelemetryClient = class {
600
- config;
601
- endpoint;
602
- timeout;
603
- debug;
604
- enabled;
605
- eventQueue = [];
606
- flushTimer;
607
- hasShownFooter = false;
608
- userId;
609
- userIdResolved = false;
610
- constructor(options = {}) {
611
- this.config = new TelemetryConfigManager();
612
- this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
613
- this.timeout = options.timeout || DEFAULT_TIMEOUT;
614
- this.debug = options.debug || process.env.WRAPS_TELEMETRY_DEBUG === "1";
615
- this.enabled = this.shouldBeEnabled();
616
- this.resolveUserId();
617
- }
618
- /**
619
- * Resolve authenticated user identity from CLI auth config.
620
- * Uses the first organization ID as the user identifier,
621
- * linking CLI telemetry to the same org tracked on the web dashboard.
622
- */
623
- async resolveUserId() {
624
- try {
625
- const config2 = await readAuthConfig();
626
- if (config2?.auth?.token && config2.auth.organizations?.length) {
627
- this.userId = config2.auth.organizations[0].id;
628
- }
629
- } catch {
630
- } finally {
631
- this.userIdResolved = true;
632
- }
633
- }
634
- /**
635
- * Determine if telemetry should be enabled based on environment and config
636
- */
637
- shouldBeEnabled() {
638
- if (process.env.DO_NOT_TRACK === "1") {
639
- return false;
640
- }
641
- if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
642
- return false;
643
- }
644
- if (isCI()) {
645
- return false;
646
- }
647
- if (!this.config.isEnabled()) {
648
- return false;
649
- }
650
- return true;
651
- }
652
- /**
653
- * Track an event
654
- *
655
- * @param event - Event name in format "category:action" (e.g., "command:init")
656
- * @param properties - Additional event properties (no PII)
657
- */
658
- track(event, properties) {
659
- const telemetryEvent = {
660
- event,
661
- properties: {
662
- ...properties,
663
- cli_version: this.getCLIVersion(),
664
- os: process.platform,
665
- node_version: process.version,
666
- ci: isCI()
667
- },
668
- anonymousId: this.config.getAnonymousId(),
669
- ...this.userId ? { userId: this.userId } : {},
670
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
671
- };
672
- if (this.debug) {
673
- console.log(
674
- "[Telemetry Debug] Event:",
675
- JSON.stringify(telemetryEvent, null, 2)
676
- );
677
- return;
678
- }
679
- if (!this.enabled) {
680
- return;
681
- }
682
- this.eventQueue.push(telemetryEvent);
683
- if (this.flushTimer) {
684
- clearTimeout(this.flushTimer);
685
- }
686
- this.flushTimer = setTimeout(() => this.flush(), 100);
687
- }
688
- /**
689
- * Flush queued events to server
690
- */
691
- async flush() {
692
- if (this.eventQueue.length === 0) {
693
- return;
694
- }
695
- const eventsToSend = [...this.eventQueue];
696
- this.eventQueue = [];
697
- try {
698
- const controller = new AbortController();
699
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
700
- const requestBody = {
701
- events: eventsToSend,
702
- batch: true
703
- };
704
- await fetch(this.endpoint, {
705
- method: "POST",
706
- headers: {
707
- "Content-Type": "application/json"
708
- },
709
- body: JSON.stringify(requestBody),
710
- signal: controller.signal
711
- });
712
- clearTimeout(timeoutId);
713
- } catch (error) {
714
- if (this.debug) {
715
- console.error("[Telemetry Debug] Failed to send events:", error);
716
- }
717
- }
718
- }
719
- /**
720
- * Flush and wait for all events to be sent
721
- * Should be called before CLI exits
722
- */
723
- async shutdown() {
724
- if (this.flushTimer) {
725
- clearTimeout(this.flushTimer);
726
- }
727
- if (!this.userIdResolved) {
728
- await new Promise((resolve) => {
729
- const check2 = () => {
730
- if (this.userIdResolved) {
731
- return resolve();
732
- }
733
- setTimeout(check2, 10);
734
- };
735
- check2();
736
- setTimeout(resolve, 100);
737
- });
738
- }
739
- if (this.userId) {
740
- for (const evt of this.eventQueue) {
741
- if (!evt.userId) {
742
- evt.userId = this.userId;
743
- }
744
- }
745
- }
746
- await this.flush();
747
- }
748
- /**
749
- * Enable telemetry.
750
- * Returns null if enabled, or a string describing why an env override prevented it.
751
- */
752
- enable() {
753
- this.config.setEnabled(true);
754
- const override = this.getEnvOverride();
755
- if (override) {
756
- this.enabled = false;
757
- return override;
758
- }
759
- this.enabled = true;
760
- return null;
761
- }
762
- /**
763
- * Check if an environment variable is overriding the config.
764
- * Returns a human-readable reason, or null if no override.
765
- */
766
- getEnvOverride() {
767
- if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
768
- return "DO_NOT_TRACK environment variable is set";
769
- }
770
- if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
771
- return "WRAPS_TELEMETRY_DISABLED environment variable is set";
772
- }
773
- if (isCI()) {
774
- return "CI environment detected";
775
- }
776
- return null;
777
- }
778
- /**
779
- * Disable telemetry
780
- */
781
- disable() {
782
- this.config.setEnabled(false);
783
- this.enabled = false;
784
- this.eventQueue = [];
785
- }
786
- /**
787
- * Check if telemetry is enabled
788
- */
789
- isEnabled() {
790
- return this.enabled;
791
- }
792
- /**
793
- * Get config file path
794
- */
795
- getConfigPath() {
796
- return this.config.getConfigPath();
797
- }
798
- /**
799
- * Show first-run notification
800
- */
801
- shouldShowNotification() {
802
- return this.enabled && !this.config.hasShownNotification();
803
- }
804
- /**
805
- * Mark notification as shown
806
- */
807
- markNotificationShown() {
808
- this.config.markNotificationShown();
809
- }
810
- /**
811
- * Show promotional footer once per CLI session.
812
- * Call this after successful completion of status/list commands.
813
- * Returns true if footer was shown, false if already shown this session.
814
- */
815
- showFooterOnce() {
816
- if (this.hasShownFooter) {
817
- return false;
818
- }
819
- this.hasShownFooter = true;
820
- console.log();
821
- console.log(pc.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
822
- console.log("\u{1F4CA} Wraps Platform \u2014 analytics, templates, automations");
823
- console.log(` From $10/mo \u2192 ${pc.cyan("https://wraps.dev/platform")}`);
824
- console.log();
825
- console.log(`\u{1F4AC} ${pc.cyan("hey@wraps.sh")}`);
826
- console.log(pc.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
827
- return true;
828
- }
829
- /**
830
- * Get CLI version from package.json
831
- */
832
- getCLIVersion() {
833
- try {
834
- const __filename3 = fileURLToPath2(import.meta.url);
835
- const __dirname4 = dirname(__filename3);
836
- const pkg = JSON.parse(
837
- readFileSync(join4(__dirname4, "../package.json"), "utf-8")
838
- );
839
- return pkg.version;
840
- } catch {
841
- return "unknown";
842
- }
843
- }
844
- };
845
- telemetryInstance = null;
450
+ function classifyDNSError(error) {
451
+ if (!(error instanceof Error)) {
452
+ return "unknown";
846
453
  }
847
- });
848
-
849
- // src/telemetry/events.ts
850
- function trackCommand(command, metadata) {
851
- const client = getTelemetryClient();
852
- const sanitized = metadata ? { ...metadata } : {};
853
- sanitized.domain = void 0;
854
- sanitized.accountId = void 0;
855
- sanitized.email = void 0;
856
- client.track(`command:${command}`, sanitized);
857
- }
858
- function trackServiceInit(service, success, metadata) {
859
- const client = getTelemetryClient();
860
- client.track("service:init", {
861
- service,
862
- success,
863
- ...metadata
864
- });
454
+ const code = error.code;
455
+ if (code === "ENOTFOUND" || code === "ENODATA") {
456
+ return "missing";
457
+ }
458
+ if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
459
+ return "network";
460
+ }
461
+ return "unknown";
865
462
  }
866
- function trackServiceDeployed(service, metadata) {
867
- const client = getTelemetryClient();
868
- client.track("service:deployed", {
869
- service,
870
- ...metadata
871
- });
463
+ function isAWSNotFoundError(error) {
464
+ if (!(error instanceof Error)) return false;
465
+ const awsError = error;
466
+ return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || awsError.$metadata?.httpStatusCode === 404;
872
467
  }
873
- function trackError(errorCode, command, metadata) {
874
- const client = getTelemetryClient();
875
- client.track("error:occurred", {
876
- error_code: errorCode,
877
- command,
878
- ...metadata
879
- });
880
- }
881
- function trackFeature(feature, metadata) {
882
- const client = getTelemetryClient();
883
- client.track(`feature:${feature}`, metadata || {});
884
- }
885
- function trackServiceUpgrade(service, metadata) {
886
- const client = getTelemetryClient();
887
- client.track("service:upgraded", {
888
- service,
889
- ...metadata
890
- });
891
- }
892
- function trackServiceRemoved(service, metadata) {
893
- const client = getTelemetryClient();
894
- client.track("service:removed", {
895
- service,
896
- ...metadata
897
- });
898
- }
899
- var init_events = __esm({
900
- "src/telemetry/events.ts"() {
901
- "use strict";
902
- init_esm_shims();
903
- init_client();
904
- }
905
- });
906
-
907
- // src/utils/shared/json-output.ts
908
- function isJsonMode() {
909
- return _jsonMode;
910
- }
911
- function setJsonMode(enabled) {
912
- _jsonMode = enabled;
913
- }
914
- function jsonSuccess(command, data) {
915
- const output3 = { success: true, command, data };
916
- console.log(JSON.stringify(output3));
917
- }
918
- function jsonError(command, error) {
919
- const output3 = { success: false, command, error };
920
- console.log(JSON.stringify(output3));
921
- }
922
- var _jsonMode;
923
- var init_json_output = __esm({
924
- "src/utils/shared/json-output.ts"() {
925
- "use strict";
926
- init_esm_shims();
927
- _jsonMode = false;
928
- }
929
- });
930
-
931
- // src/utils/shared/aws-detection.ts
932
- import { execSync } from "child_process";
933
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2 } from "fs";
934
- import { homedir as homedir2 } from "os";
935
- import { join as join5 } from "path";
936
- import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
937
- async function isAWSCLIInstalled() {
938
- try {
939
- execSync("aws --version", { stdio: "pipe" });
940
- return true;
941
- } catch {
468
+ function isPulumiError(error) {
469
+ if (!(error instanceof Error)) {
942
470
  return false;
943
471
  }
472
+ return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
944
473
  }
945
- async function getAWSCLIVersion() {
946
- try {
947
- const output3 = execSync("aws --version", { encoding: "utf-8" });
948
- const match = output3.match(/aws-cli\/(\d+\.\d+\.\d+)/);
949
- return match ? match[1] : null;
950
- } catch {
951
- return null;
952
- }
474
+ function parseAWSError(error) {
475
+ const errorName = error.name || "UnknownError";
476
+ const actionMatch = error.message?.match(/when calling the (\w+) operation/i);
477
+ const action = actionMatch?.[1];
478
+ const resourceMatch = error.message?.match(/resource[:\s]+([^\s,]+)/i);
479
+ const resource = resourceMatch?.[1];
480
+ return { code: errorName, action, resource };
953
481
  }
954
- function detectCredentialSource() {
955
- if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
956
- return "environment";
957
- }
958
- if (process.env.AWS_SSO_ACCOUNT_ID || process.env.AWS_SSO_SESSION) {
959
- return "sso";
960
- }
961
- if (process.env.AWS_PROFILE) {
962
- return "profile";
963
- }
964
- const credentialsPath = join5(homedir2(), ".aws", "credentials");
965
- if (existsSync4(credentialsPath)) {
966
- const content = readFileSync2(credentialsPath, "utf-8");
967
- if (content.includes("[default]")) {
968
- return "profile";
482
+ function parsePulumiError(error) {
483
+ const message = error.message || "";
484
+ if (message.includes("AccessDenied") || message.includes("access denied")) {
485
+ const actionMatch = message.match(
486
+ /(?:action|operation)[:\s]+["']?(\w+:\w+)["']?/i
487
+ );
488
+ if (actionMatch) {
489
+ const [service] = actionMatch[1].split(":");
490
+ return {
491
+ code: "IAM_PERMISSION_DENIED",
492
+ iamAction: actionMatch[1],
493
+ service
494
+ };
969
495
  }
496
+ if (message.includes("ses:") || message.includes("SES")) {
497
+ return { code: "SES_PERMISSION_DENIED", service: "ses" };
498
+ }
499
+ if (message.includes("dynamodb:") || message.includes("DynamoDB")) {
500
+ return { code: "DYNAMODB_PERMISSION_DENIED", service: "dynamodb" };
501
+ }
502
+ if (message.includes("lambda:") || message.includes("Lambda")) {
503
+ return { code: "LAMBDA_PERMISSION_DENIED", service: "lambda" };
504
+ }
505
+ if (message.includes("events:") || message.includes("EventBridge")) {
506
+ return { code: "EVENTBRIDGE_PERMISSION_DENIED", service: "events" };
507
+ }
508
+ if (message.includes("sqs:") || message.includes("SQS")) {
509
+ return { code: "SQS_PERMISSION_DENIED", service: "sqs" };
510
+ }
511
+ if (message.includes("iam:") || message.includes("IAM")) {
512
+ return { code: "IAM_PERMISSION_DENIED", service: "iam" };
513
+ }
514
+ return { code: "IAM_PERMISSION_DENIED" };
970
515
  }
971
- const ssoCachePath = join5(homedir2(), ".aws", "sso", "cache");
972
- if (existsSync4(ssoCachePath)) {
973
- return "sso";
974
- }
975
- if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_EXECUTION_ENV) {
976
- return "instance";
977
- }
978
- return null;
979
- }
980
- function detectHostingProvider() {
981
- if (process.env.VERCEL || process.env.VERCEL_ENV) {
982
- return "vercel";
983
- }
984
- if (process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID) {
985
- return "railway";
986
- }
987
- if (process.env.NETLIFY || process.env.NETLIFY_DEV) {
988
- return "netlify";
989
- }
990
- if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV || process.env.ECS_CONTAINER_METADATA_URI || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
991
- return "aws";
516
+ if (message.includes("AlreadyExists") || message.includes("already exists") || message.includes("already exist") || message.includes("ResourceConflictException") || message.includes("ResourceInUse") || message.includes("EntityAlreadyExists")) {
517
+ const nameMatch = message.match(/error creating '([^']+)'/);
518
+ const typeMatch = message.match(/\((aws:[^)]+)\)/);
519
+ return {
520
+ code: "RESOURCE_CONFLICT",
521
+ resourceName: nameMatch?.[1],
522
+ resourceType: typeMatch?.[1]
523
+ };
992
524
  }
993
- return null;
994
- }
995
- async function validateCredentials() {
996
- try {
997
- const sts = new STSClient({ region: "us-east-1" });
998
- const identity = await sts.send(new GetCallerIdentityCommand({}));
999
- return identity.Account || null;
1000
- } catch {
1001
- return null;
525
+ if (message.includes("stack is currently locked")) {
526
+ return { code: "STACK_LOCKED" };
1002
527
  }
528
+ return { code: "PULUMI_ERROR" };
1003
529
  }
1004
- function getCurrentProfile() {
1005
- return process.env.AWS_PROFILE || "default";
530
+ function redactSensitiveValues(input) {
531
+ let message = input;
532
+ message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
533
+ message = message.replace(
534
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
535
+ "[EMAIL]"
536
+ );
537
+ message = message.replace(
538
+ /(?<!\.amazonaws\.com|\.aws\.amazon\.com)\b[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b/g,
539
+ (match) => {
540
+ if (match.includes("amazonaws") || match.includes("aws.amazon")) {
541
+ return match;
542
+ }
543
+ return "[DOMAIN]";
544
+ }
545
+ );
546
+ message = message.replace(
547
+ /arn:aws:[^:]+:[^:]*:\d{12}:/g,
548
+ "arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
549
+ );
550
+ return message;
1006
551
  }
1007
- function getCurrentRegion() {
1008
- if (process.env.AWS_REGION) {
1009
- return process.env.AWS_REGION;
1010
- }
1011
- if (process.env.AWS_DEFAULT_REGION) {
1012
- return process.env.AWS_DEFAULT_REGION;
552
+ function sanitizeErrorMessage(error) {
553
+ if (!error) {
554
+ return "Unknown error";
1013
555
  }
1014
- try {
1015
- const region = execSync("aws configure get region", {
1016
- encoding: "utf-8",
1017
- stdio: ["pipe", "pipe", "pipe"]
1018
- }).trim();
1019
- return region || null;
1020
- } catch {
1021
- return null;
556
+ const raw = error instanceof Error ? error.message : String(error);
557
+ const redacted = redactSensitiveValues(raw);
558
+ if (redacted.length > 500) {
559
+ return `${redacted.slice(0, 500)}...`;
1022
560
  }
561
+ return redacted;
1023
562
  }
1024
- function parseSSOProfiles() {
1025
- const configPath = join5(homedir2(), ".aws", "config");
1026
- if (!existsSync4(configPath)) {
1027
- return [];
1028
- }
1029
- const content = readFileSync2(configPath, "utf-8");
1030
- const profiles = [];
1031
- const sessionMap = /* @__PURE__ */ new Map();
1032
- const sessionSections = content.split(/^\[/m).filter(Boolean);
1033
- for (const section of sessionSections) {
1034
- const lines = section.split("\n");
1035
- const header = lines[0]?.replace("]", "").trim();
1036
- if (header?.startsWith("sso-session ")) {
1037
- const sessionName = header.replace("sso-session ", "");
1038
- const config2 = {};
1039
- for (const line of lines.slice(1)) {
1040
- const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
1041
- if (match) {
1042
- config2[match[1]] = match[2];
1043
- }
1044
- }
1045
- sessionMap.set(sessionName, {
1046
- startUrl: config2.sso_start_url || "",
1047
- region: config2.sso_region || ""
563
+ function handleCLIError(error, command) {
564
+ const cmdContext = command || "unknown";
565
+ if (isJsonMode()) {
566
+ let code = "UNKNOWN_ERROR";
567
+ let message = "An unexpected error occurred";
568
+ let suggestion;
569
+ let docsUrl;
570
+ if (error instanceof WrapsError) {
571
+ trackError(error.code, cmdContext);
572
+ code = error.code;
573
+ message = error.message;
574
+ suggestion = error.suggestion;
575
+ docsUrl = error.docsUrl;
576
+ } else if (isAWSError(error)) {
577
+ const parsed = parseAWSError(error);
578
+ code = `AWS_${parsed.code}`;
579
+ trackError(code, cmdContext, { action: parsed.action });
580
+ const wrapsErr = awsErrorToWrapsError(parsed.code, parsed.action, error);
581
+ message = wrapsErr.message;
582
+ suggestion = wrapsErr.suggestion;
583
+ docsUrl = wrapsErr.docsUrl;
584
+ } else if (isPulumiError(error)) {
585
+ const parsed = parsePulumiError(error);
586
+ code = `PULUMI_${parsed.code}`;
587
+ trackError(code, cmdContext, {
588
+ iamAction: parsed.iamAction,
589
+ service: parsed.service,
590
+ errorType: error?.constructor?.name
1048
591
  });
592
+ const wrapsErr = pulumiErrorToWrapsError(
593
+ parsed.code,
594
+ parsed.iamAction,
595
+ parsed.service,
596
+ parsed.resourceName,
597
+ parsed.resourceType,
598
+ error?.message
599
+ );
600
+ message = wrapsErr.message;
601
+ suggestion = wrapsErr.suggestion;
602
+ docsUrl = wrapsErr.docsUrl;
603
+ } else {
604
+ trackError("UNHANDLED_ERROR", cmdContext, {
605
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
606
+ message: sanitizeErrorMessage(error)
607
+ });
608
+ message = error instanceof Error ? error.message : String(error || message);
1049
609
  }
1050
- }
1051
- const sections = content.split(/^\[/m).filter(Boolean);
1052
- for (const section of sections) {
1053
- const lines = section.split("\n");
1054
- const header = lines[0]?.replace("]", "").trim();
1055
- if (!header?.startsWith("profile ") && header !== "default") {
1056
- continue;
1057
- }
1058
- const profileName = header === "default" ? "default" : header.replace("profile ", "");
1059
- const config2 = {};
1060
- for (const line of lines.slice(1)) {
1061
- const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
1062
- if (match) {
1063
- config2[match[1]] = match[2];
610
+ jsonError(cmdContext, { code, message, suggestion, docsUrl });
611
+ process.exit(1);
612
+ }
613
+ console.error("");
614
+ if (error instanceof WrapsError) {
615
+ trackError(error.code, cmdContext);
616
+ clack.log.error(error.message);
617
+ if (error.suggestion) {
618
+ console.log(`
619
+ ${pc.yellow("Suggestion:")}`);
620
+ const lines = error.suggestion.split("\n");
621
+ for (const line of lines) {
622
+ console.log(` ${pc.white(line)}`);
1064
623
  }
624
+ console.log();
1065
625
  }
1066
- if (config2.sso_start_url || config2.sso_session) {
1067
- let ssoStartUrl = config2.sso_start_url || "";
1068
- let ssoRegion = config2.sso_region || "";
1069
- if (config2.sso_session) {
1070
- const session = sessionMap.get(config2.sso_session);
1071
- if (session) {
1072
- ssoStartUrl = ssoStartUrl || session.startUrl;
1073
- ssoRegion = ssoRegion || session.region;
1074
- }
1075
- }
1076
- profiles.push({
1077
- name: profileName,
1078
- ssoStartUrl,
1079
- ssoRegion,
1080
- ssoAccountId: config2.sso_account_id || "",
1081
- ssoRoleName: config2.sso_role_name || "",
1082
- region: config2.region,
1083
- ssoSession: config2.sso_session
1084
- });
626
+ if (error.docsUrl) {
627
+ console.log(`${pc.dim("Documentation:")}`);
628
+ console.log(` ${pc.blue(error.docsUrl)}
629
+ `);
1085
630
  }
631
+ process.exit(1);
1086
632
  }
1087
- return profiles;
1088
- }
1089
- function parseSSOSessions() {
1090
- const configPath = join5(homedir2(), ".aws", "config");
1091
- if (!existsSync4(configPath)) {
1092
- return [];
1093
- }
1094
- const content = readFileSync2(configPath, "utf-8");
1095
- const sessions = [];
1096
- const sections = content.split(/^\[/m).filter(Boolean);
1097
- for (const section of sections) {
1098
- const lines = section.split("\n");
1099
- const header = lines[0]?.replace("]", "").trim();
1100
- if (!header?.startsWith("sso-session ")) {
1101
- continue;
1102
- }
1103
- const sessionName = header.replace("sso-session ", "");
1104
- const config2 = {};
1105
- for (const line of lines.slice(1)) {
1106
- const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
1107
- if (match) {
1108
- config2[match[1]] = match[2];
633
+ if (isAWSError(error)) {
634
+ const { code, action } = parseAWSError(error);
635
+ trackError(`AWS_${code}`, cmdContext, { action });
636
+ const wrapsError = awsErrorToWrapsError(code, action, error);
637
+ clack.log.error(wrapsError.message);
638
+ if (wrapsError.suggestion) {
639
+ console.log(`
640
+ ${pc.yellow("Suggestion:")}`);
641
+ const lines = wrapsError.suggestion.split("\n");
642
+ for (const line of lines) {
643
+ console.log(` ${pc.white(line)}`);
1109
644
  }
645
+ console.log();
1110
646
  }
1111
- sessions.push({
1112
- name: sessionName,
1113
- ssoStartUrl: config2.sso_start_url || "",
1114
- ssoRegion: config2.sso_region || "",
1115
- ssoRegistrationScopes: config2.sso_registration_scopes?.split(",").map((s) => s.trim())
1116
- });
1117
- }
1118
- return sessions;
1119
- }
1120
- function checkSSOTokenStatus(startUrl) {
1121
- const ssoCachePath = join5(homedir2(), ".aws", "sso", "cache");
1122
- if (!existsSync4(ssoCachePath)) {
1123
- return {
1124
- valid: false,
1125
- expiresAt: null,
1126
- expired: true,
1127
- minutesRemaining: null,
1128
- startUrl: null
1129
- };
647
+ if (wrapsError.docsUrl) {
648
+ console.log(`${pc.dim("Documentation:")}`);
649
+ console.log(` ${pc.blue(wrapsError.docsUrl)}
650
+ `);
651
+ }
652
+ process.exit(1);
1130
653
  }
1131
- try {
1132
- const cacheFiles = readdirSync(ssoCachePath).filter(
1133
- (f) => f.endsWith(".json")
654
+ if (isPulumiError(error)) {
655
+ const { code, iamAction, service, resourceName, resourceType } = parsePulumiError(error);
656
+ trackError(`PULUMI_${code}`, cmdContext, {
657
+ iamAction,
658
+ service,
659
+ errorType: error?.constructor?.name
660
+ });
661
+ const wrapsError = pulumiErrorToWrapsError(
662
+ code,
663
+ iamAction,
664
+ service,
665
+ resourceName,
666
+ resourceType,
667
+ error?.message
1134
668
  );
1135
- for (const file of cacheFiles) {
1136
- const content = readFileSync2(join5(ssoCachePath, file), "utf-8");
1137
- const token = JSON.parse(content);
1138
- if (!(token.accessToken && token.expiresAt)) {
1139
- continue;
1140
- }
1141
- if (startUrl && token.startUrl !== startUrl) {
1142
- continue;
669
+ clack.log.error(wrapsError.message);
670
+ if (wrapsError.suggestion) {
671
+ console.log(`
672
+ ${pc.yellow("Suggestion:")}`);
673
+ const lines = wrapsError.suggestion.split("\n");
674
+ for (const line of lines) {
675
+ console.log(` ${pc.white(line)}`);
1143
676
  }
1144
- const expiresAt = new Date(token.expiresAt);
1145
- const now = /* @__PURE__ */ new Date();
1146
- const expired = expiresAt <= now;
1147
- const minutesRemaining = Math.floor(
1148
- (expiresAt.getTime() - now.getTime()) / 6e4
1149
- );
1150
- return {
1151
- valid: !expired,
1152
- expiresAt,
1153
- expired,
1154
- minutesRemaining,
1155
- startUrl: token.startUrl || null
1156
- };
1157
- }
1158
- } catch {
1159
- }
1160
- return {
1161
- valid: false,
1162
- expiresAt: null,
1163
- expired: true,
1164
- minutesRemaining: null,
1165
- startUrl: null
1166
- };
1167
- }
1168
- function getActiveSSOProfile(profiles) {
1169
- const currentProfile = process.env.AWS_PROFILE || "default";
1170
- return profiles.find((p) => p.name === currentProfile) || null;
1171
- }
1172
- function getSSOLoginCommand(profile) {
1173
- if (profile && profile !== "default") {
1174
- return `aws sso login --profile ${profile}`;
1175
- }
1176
- return "aws sso login";
1177
- }
1178
- function formatSSOProfile(profile) {
1179
- return `${profile.name} (${profile.ssoAccountId} / ${profile.ssoRoleName})`;
1180
- }
1181
- async function detectAWSState() {
1182
- const [cliInstalled, cliVersion, accountId] = await Promise.all([
1183
- isAWSCLIInstalled(),
1184
- getAWSCLIVersion(),
1185
- validateCredentials()
1186
- ]);
1187
- const credentialSource = detectCredentialSource();
1188
- const detectedProvider = detectHostingProvider();
1189
- const region = getCurrentRegion();
1190
- const profileName = getCurrentProfile();
1191
- const ssoProfiles = parseSSOProfiles();
1192
- const ssoSessions = parseSSOSessions();
1193
- const activeProfile = getActiveSSOProfile(ssoProfiles);
1194
- const tokenStatus = ssoProfiles.length > 0 ? checkSSOTokenStatus(activeProfile?.ssoStartUrl) : null;
1195
- const isUsingSSO = credentialSource === "sso" || activeProfile !== null && accountId !== null;
1196
- return {
1197
- cliInstalled,
1198
- cliVersion,
1199
- credentialsConfigured: accountId !== null,
1200
- credentialSource: isUsingSSO ? "sso" : accountId !== null ? credentialSource : null,
1201
- profileName,
1202
- accountId,
1203
- detectedProvider,
1204
- region,
1205
- sso: {
1206
- configured: ssoProfiles.length > 0,
1207
- profiles: ssoProfiles,
1208
- sessions: ssoSessions,
1209
- tokenStatus,
1210
- activeProfile: isUsingSSO ? activeProfile : null
677
+ console.log();
1211
678
  }
1212
- };
1213
- }
1214
- function hasCredentialsFile() {
1215
- const credentialsPath = join5(homedir2(), ".aws", "credentials");
1216
- return existsSync4(credentialsPath);
1217
- }
1218
- function hasConfigFile() {
1219
- const configPath = join5(homedir2(), ".aws", "config");
1220
- return existsSync4(configPath);
1221
- }
1222
- function getConfiguredProfiles() {
1223
- const profiles = [];
1224
- const credentialsPath = join5(homedir2(), ".aws", "credentials");
1225
- if (existsSync4(credentialsPath)) {
1226
- const content = readFileSync2(credentialsPath, "utf-8");
1227
- const matches = content.matchAll(/\[([^\]]+)\]/g);
1228
- for (const match of matches) {
1229
- profiles.push(match[1]);
679
+ if (wrapsError.docsUrl) {
680
+ console.log(`${pc.dim("Documentation:")}`);
681
+ console.log(` ${pc.blue(wrapsError.docsUrl)}
682
+ `);
1230
683
  }
684
+ process.exit(1);
1231
685
  }
1232
- const configPath = join5(homedir2(), ".aws", "config");
1233
- if (existsSync4(configPath)) {
1234
- const content = readFileSync2(configPath, "utf-8");
1235
- const matches = content.matchAll(/\[profile ([^\]]+)\]/g);
1236
- for (const match of matches) {
1237
- if (!profiles.includes(match[1])) {
1238
- profiles.push(match[1]);
1239
- }
1240
- }
686
+ trackError("UNHANDLED_ERROR", cmdContext, {
687
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
688
+ message: sanitizeErrorMessage(error)
689
+ });
690
+ clack.log.error("An unexpected error occurred");
691
+ if (error instanceof Error) {
692
+ console.error(pc.dim(error.message));
693
+ } else if (typeof error === "string") {
694
+ console.error(error);
1241
695
  }
1242
- return profiles;
696
+ console.log(`
697
+ ${pc.dim("If this persists, please report at:")}`);
698
+ console.log(` ${pc.blue("https://github.com/wraps-team/wraps/issues")}
699
+ `);
700
+ process.exit(1);
1243
701
  }
1244
- var init_aws_detection = __esm({
1245
- "src/utils/shared/aws-detection.ts"() {
1246
- "use strict";
1247
- init_esm_shims();
702
+ function awsErrorToWrapsError(code, action, originalError) {
703
+ switch (code) {
704
+ // Credential / token errors — these mean the request never reached the API
705
+ case "ExpiredTokenException":
706
+ case "TokenRefreshRequired":
707
+ case "SSOTokenExpired":
708
+ return errors.sessionTokenExpired();
709
+ case "InvalidClientTokenId":
710
+ case "InvalidAccessKeyId":
711
+ case "SignatureDoesNotMatch":
712
+ case "UnrecognizedClientException":
713
+ return errors.accessKeyInvalid();
714
+ // IAM permission errors — request reached AWS but was denied
715
+ case "AccessDenied":
716
+ case "AccessDeniedException":
717
+ case "UnauthorizedAccess":
718
+ return errors.iamPermissionDenied(
719
+ action || "unknown",
720
+ "AWS resource",
721
+ "Ensure your IAM user/role has the required permissions."
722
+ );
723
+ // SES SendEmail errors — request reached SES but was rejected
724
+ case "MessageRejected":
725
+ return errors.sesMessageRejected(sanitizeErrorMessage(originalError));
726
+ case "MailFromDomainNotVerifiedException":
727
+ return errors.sesMailFromNotVerified(sanitizeErrorMessage(originalError));
728
+ case "AccountSendingPausedException":
729
+ return errors.sesAccountSendingPaused();
730
+ case "ConfigurationSetSendingPausedException":
731
+ return errors.sesConfigSetSendingPaused();
732
+ case "ConfigurationSetDoesNotExistException":
733
+ return errors.sesConfigSetMissing(sanitizeErrorMessage(originalError));
734
+ // Throughput / quota errors
735
+ case "Throttling":
736
+ case "ThrottlingException":
737
+ case "TooManyRequestsException":
738
+ return errors.awsThrottled(action);
739
+ case "LimitExceededException":
740
+ case "ServiceQuotaExceededException":
741
+ return errors.awsLimitExceeded(
742
+ action,
743
+ sanitizeErrorMessage(originalError)
744
+ );
745
+ // Anything else — surface the real error instead of lying about credentials
746
+ default:
747
+ return errors.awsUnknownError(
748
+ code,
749
+ action,
750
+ sanitizeErrorMessage(originalError)
751
+ );
1248
752
  }
1249
- });
1250
-
1251
- // src/utils/shared/errors.ts
1252
- var errors_exports = {};
1253
- __export(errors_exports, {
1254
- WrapsError: () => WrapsError,
1255
- classifyDNSError: () => classifyDNSError,
1256
- errors: () => errors,
1257
- handleCLIError: () => handleCLIError,
1258
- isAWSError: () => isAWSError,
1259
- isAWSNotFoundError: () => isAWSNotFoundError,
1260
- isPulumiError: () => isPulumiError,
1261
- parseAWSError: () => parseAWSError,
1262
- parsePulumiError: () => parsePulumiError,
1263
- sanitizeErrorMessage: () => sanitizeErrorMessage
1264
- });
1265
- import * as clack4 from "@clack/prompts";
1266
- import pc5 from "picocolors";
1267
- function isAWSError(error) {
1268
- if (!(error instanceof Error)) {
1269
- return false;
1270
- }
1271
- const awsErrorNames = [
1272
- "ExpiredTokenException",
1273
- "InvalidClientTokenId",
1274
- "AccessDenied",
1275
- "AccessDeniedException",
1276
- "UnauthorizedAccess",
1277
- "InvalidAccessKeyId",
1278
- "SignatureDoesNotMatch",
1279
- "UnrecognizedClientException",
1280
- "CredentialsError",
1281
- "TokenRefreshRequired",
1282
- "SSOTokenExpired"
1283
- ];
1284
- return awsErrorNames.includes(error.name) || "$metadata" in error;
1285
- }
1286
- function classifyDNSError(error) {
1287
- if (!(error instanceof Error)) {
1288
- return "unknown";
1289
- }
1290
- const code = error.code;
1291
- if (code === "ENOTFOUND" || code === "ENODATA") {
1292
- return "missing";
1293
- }
1294
- if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
1295
- return "network";
1296
- }
1297
- return "unknown";
1298
- }
1299
- function isAWSNotFoundError(error) {
1300
- if (!(error instanceof Error)) return false;
1301
- const awsError = error;
1302
- return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || awsError.$metadata?.httpStatusCode === 404;
1303
- }
1304
- function isPulumiError(error) {
1305
- if (!(error instanceof Error)) {
1306
- return false;
1307
- }
1308
- return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
1309
- }
1310
- function parseAWSError(error) {
1311
- const errorName = error.name || "UnknownError";
1312
- const actionMatch = error.message?.match(/when calling the (\w+) operation/i);
1313
- const action = actionMatch?.[1];
1314
- const resourceMatch = error.message?.match(/resource[:\s]+([^\s,]+)/i);
1315
- const resource = resourceMatch?.[1];
1316
- return { code: errorName, action, resource };
1317
- }
1318
- function parsePulumiError(error) {
1319
- const message = error.message || "";
1320
- if (message.includes("AccessDenied") || message.includes("access denied")) {
1321
- const actionMatch = message.match(
1322
- /(?:action|operation)[:\s]+["']?(\w+:\w+)["']?/i
1323
- );
1324
- if (actionMatch) {
1325
- const [service] = actionMatch[1].split(":");
1326
- return {
1327
- code: "IAM_PERMISSION_DENIED",
1328
- iamAction: actionMatch[1],
1329
- service
1330
- };
1331
- }
1332
- if (message.includes("ses:") || message.includes("SES")) {
1333
- return { code: "SES_PERMISSION_DENIED", service: "ses" };
1334
- }
1335
- if (message.includes("dynamodb:") || message.includes("DynamoDB")) {
1336
- return { code: "DYNAMODB_PERMISSION_DENIED", service: "dynamodb" };
1337
- }
1338
- if (message.includes("lambda:") || message.includes("Lambda")) {
1339
- return { code: "LAMBDA_PERMISSION_DENIED", service: "lambda" };
1340
- }
1341
- if (message.includes("events:") || message.includes("EventBridge")) {
1342
- return { code: "EVENTBRIDGE_PERMISSION_DENIED", service: "events" };
1343
- }
1344
- if (message.includes("sqs:") || message.includes("SQS")) {
1345
- return { code: "SQS_PERMISSION_DENIED", service: "sqs" };
1346
- }
1347
- if (message.includes("iam:") || message.includes("IAM")) {
1348
- return { code: "IAM_PERMISSION_DENIED", service: "iam" };
1349
- }
1350
- return { code: "IAM_PERMISSION_DENIED" };
1351
- }
1352
- if (message.includes("AlreadyExists") || message.includes("already exists") || message.includes("already exist") || message.includes("ResourceConflictException") || message.includes("ResourceInUse") || message.includes("EntityAlreadyExists")) {
1353
- const nameMatch = message.match(/error creating '([^']+)'/);
1354
- const typeMatch = message.match(/\((aws:[^)]+)\)/);
1355
- return {
1356
- code: "RESOURCE_CONFLICT",
1357
- resourceName: nameMatch?.[1],
1358
- resourceType: typeMatch?.[1]
1359
- };
1360
- }
1361
- if (message.includes("stack is currently locked")) {
1362
- return { code: "STACK_LOCKED" };
1363
- }
1364
- return { code: "PULUMI_ERROR" };
1365
- }
1366
- function sanitizeErrorMessage(error) {
1367
- if (!error) {
1368
- return "Unknown error";
1369
- }
1370
- let message = error instanceof Error ? error.message : String(error);
1371
- message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
1372
- message = message.replace(
1373
- /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
1374
- "[EMAIL]"
1375
- );
1376
- message = message.replace(
1377
- /(?<!\.amazonaws\.com|\.aws\.amazon\.com)\b[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b/g,
1378
- (match) => {
1379
- if (match.includes("amazonaws") || match.includes("aws.amazon")) {
1380
- return match;
1381
- }
1382
- return "[DOMAIN]";
1383
- }
1384
- );
1385
- message = message.replace(
1386
- /arn:aws:[^:]+:[^:]*:\d{12}:/g,
1387
- "arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
1388
- );
1389
- if (message.length > 500) {
1390
- message = `${message.slice(0, 500)}...`;
1391
- }
1392
- return message;
1393
753
  }
1394
- function handleCLIError(error, command) {
1395
- const cmdContext = command || "unknown";
1396
- if (isJsonMode()) {
1397
- let code = "UNKNOWN_ERROR";
1398
- let message = "An unexpected error occurred";
1399
- let suggestion;
1400
- let docsUrl;
1401
- if (error instanceof WrapsError) {
1402
- trackError(error.code, cmdContext);
1403
- code = error.code;
1404
- message = error.message;
1405
- suggestion = error.suggestion;
1406
- docsUrl = error.docsUrl;
1407
- } else if (isAWSError(error)) {
1408
- const parsed = parseAWSError(error);
1409
- code = `AWS_${parsed.code}`;
1410
- trackError(code, cmdContext, { action: parsed.action });
1411
- const wrapsErr = awsErrorToWrapsError(parsed.code, parsed.action);
1412
- message = wrapsErr.message;
1413
- suggestion = wrapsErr.suggestion;
1414
- docsUrl = wrapsErr.docsUrl;
1415
- } else if (isPulumiError(error)) {
1416
- const parsed = parsePulumiError(error);
1417
- code = `PULUMI_${parsed.code}`;
1418
- trackError(code, cmdContext, {
1419
- iamAction: parsed.iamAction,
1420
- service: parsed.service,
1421
- errorType: error?.constructor?.name
1422
- });
1423
- const wrapsErr = pulumiErrorToWrapsError(
1424
- parsed.code,
1425
- parsed.iamAction,
1426
- parsed.service,
1427
- parsed.resourceName,
1428
- parsed.resourceType
754
+ function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourceType, originalMessage) {
755
+ switch (code) {
756
+ case "RESOURCE_CONFLICT":
757
+ return errors.resourceConflict(
758
+ resourceName || "unknown resource",
759
+ resourceType
760
+ );
761
+ case "STACK_LOCKED":
762
+ return errors.stackLocked();
763
+ case "SES_PERMISSION_DENIED":
764
+ return errors.sesPermissionDenied(iamAction || "unknown");
765
+ case "DYNAMODB_PERMISSION_DENIED":
766
+ return errors.dynamoDBPermissionDenied();
767
+ case "LAMBDA_PERMISSION_DENIED":
768
+ return errors.lambdaPermissionDenied();
769
+ case "EVENTBRIDGE_PERMISSION_DENIED":
770
+ return errors.eventBridgePermissionDenied();
771
+ case "SQS_PERMISSION_DENIED":
772
+ return errors.sqsPermissionDenied();
773
+ case "IAM_PERMISSION_DENIED":
774
+ return errors.iamPermissionDenied(
775
+ iamAction || "unknown",
776
+ "AWS resource",
777
+ service ? `Your IAM user/role needs ${service.toUpperCase()} permissions.` : "Ensure your IAM user/role has the required permissions."
778
+ );
779
+ default:
780
+ return errors.pulumiError(
781
+ originalMessage ? sanitizeErrorMessage(originalMessage) : "Deployment failed"
1429
782
  );
1430
- message = wrapsErr.message;
1431
- suggestion = wrapsErr.suggestion;
1432
- docsUrl = wrapsErr.docsUrl;
1433
- } else {
1434
- trackError("UNHANDLED_ERROR", cmdContext, {
1435
- errorType: error instanceof Error ? error.constructor.name : typeof error,
1436
- message: sanitizeErrorMessage(error)
1437
- });
1438
- message = error instanceof Error ? error.message : String(error || message);
1439
- }
1440
- jsonError(cmdContext, { code, message, suggestion, docsUrl });
1441
- process.exit(1);
1442
- }
1443
- console.error("");
1444
- if (error instanceof WrapsError) {
1445
- trackError(error.code, cmdContext);
1446
- clack4.log.error(error.message);
1447
- if (error.suggestion) {
1448
- console.log(`
1449
- ${pc5.yellow("Suggestion:")}`);
1450
- const lines = error.suggestion.split("\n");
1451
- for (const line of lines) {
1452
- console.log(` ${pc5.white(line)}`);
1453
- }
1454
- console.log();
1455
- }
1456
- if (error.docsUrl) {
1457
- console.log(`${pc5.dim("Documentation:")}`);
1458
- console.log(` ${pc5.blue(error.docsUrl)}
1459
- `);
1460
- }
1461
- process.exit(1);
1462
783
  }
1463
- if (isAWSError(error)) {
1464
- const { code, action } = parseAWSError(error);
1465
- trackError(`AWS_${code}`, cmdContext, { action });
1466
- const wrapsError = awsErrorToWrapsError(code, action);
1467
- clack4.log.error(wrapsError.message);
1468
- if (wrapsError.suggestion) {
1469
- console.log(`
1470
- ${pc5.yellow("Suggestion:")}`);
1471
- const lines = wrapsError.suggestion.split("\n");
1472
- for (const line of lines) {
1473
- console.log(` ${pc5.white(line)}`);
1474
- }
1475
- console.log();
1476
- }
1477
- if (wrapsError.docsUrl) {
1478
- console.log(`${pc5.dim("Documentation:")}`);
1479
- console.log(` ${pc5.blue(wrapsError.docsUrl)}
1480
- `);
1481
- }
1482
- process.exit(1);
1483
- }
1484
- if (isPulumiError(error)) {
1485
- const { code, iamAction, service, resourceName, resourceType } = parsePulumiError(error);
1486
- trackError(`PULUMI_${code}`, cmdContext, {
1487
- iamAction,
1488
- service,
1489
- errorType: error?.constructor?.name
1490
- });
1491
- const wrapsError = pulumiErrorToWrapsError(
1492
- code,
1493
- iamAction,
1494
- service,
1495
- resourceName,
1496
- resourceType
1497
- );
1498
- clack4.log.error(wrapsError.message);
1499
- if (wrapsError.suggestion) {
1500
- console.log(`
1501
- ${pc5.yellow("Suggestion:")}`);
1502
- const lines = wrapsError.suggestion.split("\n");
1503
- for (const line of lines) {
1504
- console.log(` ${pc5.white(line)}`);
1505
- }
1506
- console.log();
1507
- }
1508
- if (wrapsError.docsUrl) {
1509
- console.log(`${pc5.dim("Documentation:")}`);
1510
- console.log(` ${pc5.blue(wrapsError.docsUrl)}
1511
- `);
1512
- }
1513
- process.exit(1);
1514
- }
1515
- trackError("UNHANDLED_ERROR", cmdContext, {
1516
- errorType: error instanceof Error ? error.constructor.name : typeof error,
1517
- message: sanitizeErrorMessage(error)
1518
- });
1519
- clack4.log.error("An unexpected error occurred");
1520
- if (error instanceof Error) {
1521
- console.error(pc5.dim(error.message));
1522
- } else if (typeof error === "string") {
1523
- console.error(error);
1524
- }
1525
- console.log(`
1526
- ${pc5.dim("If this persists, please report at:")}`);
1527
- console.log(` ${pc5.blue("https://github.com/wraps-team/wraps/issues")}
1528
- `);
1529
- process.exit(1);
1530
- }
1531
- function awsErrorToWrapsError(code, action) {
1532
- switch (code) {
1533
- case "ExpiredTokenException":
1534
- case "TokenRefreshRequired":
1535
- case "SSOTokenExpired":
1536
- return errors.sessionTokenExpired();
1537
- case "InvalidClientTokenId":
1538
- case "InvalidAccessKeyId":
1539
- case "SignatureDoesNotMatch":
1540
- return errors.accessKeyInvalid();
1541
- case "AccessDenied":
1542
- case "AccessDeniedException":
1543
- case "UnauthorizedAccess":
1544
- return errors.iamPermissionDenied(
1545
- action || "unknown",
1546
- "AWS resource",
1547
- "Ensure your IAM user/role has the required permissions."
1548
- );
1549
- default:
1550
- return errors.noAWSCredentials();
1551
- }
1552
- }
1553
- function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourceType) {
1554
- switch (code) {
1555
- case "RESOURCE_CONFLICT":
1556
- return errors.resourceConflict(
1557
- resourceName || "unknown resource",
1558
- resourceType
1559
- );
1560
- case "STACK_LOCKED":
1561
- return errors.stackLocked();
1562
- case "SES_PERMISSION_DENIED":
1563
- return errors.sesPermissionDenied(iamAction || "unknown");
1564
- case "DYNAMODB_PERMISSION_DENIED":
1565
- return errors.dynamoDBPermissionDenied();
1566
- case "LAMBDA_PERMISSION_DENIED":
1567
- return errors.lambdaPermissionDenied();
1568
- case "EVENTBRIDGE_PERMISSION_DENIED":
1569
- return errors.eventBridgePermissionDenied();
1570
- case "SQS_PERMISSION_DENIED":
1571
- return errors.sqsPermissionDenied();
1572
- case "IAM_PERMISSION_DENIED":
1573
- return errors.iamPermissionDenied(
1574
- iamAction || "unknown",
1575
- "AWS resource",
1576
- service ? `Your IAM user/role needs ${service.toUpperCase()} permissions.` : "Ensure your IAM user/role has the required permissions."
1577
- );
1578
- default:
1579
- return errors.pulumiError("Deployment failed");
1580
- }
1581
- }
1582
- var WrapsError, errors;
1583
- var init_errors = __esm({
1584
- "src/utils/shared/errors.ts"() {
1585
- "use strict";
1586
- init_esm_shims();
1587
- init_events();
1588
- init_json_output();
1589
- WrapsError = class extends Error {
1590
- constructor(message, code, suggestion, docsUrl) {
1591
- super(message);
1592
- this.code = code;
1593
- this.suggestion = suggestion;
1594
- this.docsUrl = docsUrl;
1595
- this.name = "WrapsError";
784
+ }
785
+ var WrapsError, errors;
786
+ var init_errors = __esm({
787
+ "src/utils/shared/errors.ts"() {
788
+ "use strict";
789
+ init_esm_shims();
790
+ init_events();
791
+ init_json_output();
792
+ WrapsError = class extends Error {
793
+ constructor(message, code, suggestion, docsUrl) {
794
+ super(message);
795
+ this.code = code;
796
+ this.suggestion = suggestion;
797
+ this.docsUrl = docsUrl;
798
+ this.name = "WrapsError";
1596
799
  }
1597
800
  };
1598
801
  errors = {
@@ -1747,6 +950,61 @@ View required SES permissions:
1747
950
  wraps permissions --service email --json`,
1748
951
  "https://wraps.dev/docs/guides/aws-setup/permissions"
1749
952
  ),
953
+ // SES SendEmail rejection errors — request reached SES but the send failed
954
+ // for a reason unrelated to credentials.
955
+ sesMessageRejected: (detail) => new WrapsError(
956
+ `SES rejected the message: ${detail}`,
957
+ "SES_MESSAGE_REJECTED",
958
+ "Common causes:\n \u2022 Account is in the SES sandbox and the recipient is not a verified address\n \u2022 Sender identity (domain or email) is not verified for sending\n \u2022 The sender domain is verified for receiving but not for sending\n\nCheck status:\n wraps email status\n wraps email doctor\n\nRequest production access (exit sandbox):\n https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html",
959
+ "https://wraps.dev/docs/guides/email/troubleshooting"
960
+ ),
961
+ sesMailFromNotVerified: (detail) => new WrapsError(
962
+ `SES MAIL FROM domain is not verified: ${detail}`,
963
+ "SES_MAIL_FROM_NOT_VERIFIED",
964
+ "The custom MAIL FROM domain configured for this identity is not fully verified.\n\nCheck DNS records:\n wraps email verify\n\nOr remove the custom MAIL FROM domain in the SES console and retry.",
965
+ "https://docs.aws.amazon.com/ses/latest/dg/mail-from.html"
966
+ ),
967
+ sesAccountSendingPaused: () => new WrapsError(
968
+ "SES account-level sending is paused",
969
+ "SES_ACCOUNT_SENDING_PAUSED",
970
+ "Your SES account is currently paused from sending email. This is usually caused by:\n \u2022 A high bounce or complaint rate\n \u2022 An AWS-initiated review\n\nCheck the SES console \u2192 Reputation Dashboard for details, then resume sending once the issue is resolved.",
971
+ "https://docs.aws.amazon.com/ses/latest/dg/reputationdashboard.html"
972
+ ),
973
+ sesConfigSetSendingPaused: () => new WrapsError(
974
+ "SES configuration set sending is paused",
975
+ "SES_CONFIG_SET_SENDING_PAUSED",
976
+ "The configuration set used for this send has sending paused. Resume it in the SES console under Configuration Sets, or send without specifying the paused configuration set.",
977
+ "https://docs.aws.amazon.com/ses/latest/dg/using-configuration-sets.html"
978
+ ),
979
+ sesConfigSetMissing: (detail) => new WrapsError(
980
+ `SES configuration set does not exist: ${detail}`,
981
+ "SES_CONFIG_SET_MISSING",
982
+ "The configuration set referenced by this send does not exist in the current region. Create it in the SES console, switch regions, or remove the ConfigurationSetName from the request.",
983
+ "https://docs.aws.amazon.com/ses/latest/dg/using-configuration-sets.html"
984
+ ),
985
+ // Generic AWS error fallbacks — used by awsErrorToWrapsError when no specific
986
+ // mapping exists. These NEVER claim credentials are missing.
987
+ awsThrottled: (action) => new WrapsError(
988
+ `AWS request was throttled${action ? ` (${action})` : ""}`,
989
+ "AWS_THROTTLED",
990
+ "AWS is rate-limiting requests to this API. Wait a moment and retry.\n\nIf this happens repeatedly, request a service quota increase in the AWS console.",
991
+ "https://docs.aws.amazon.com/general/latest/gr/api-retries.html"
992
+ ),
993
+ awsLimitExceeded: (action, detail) => new WrapsError(
994
+ `AWS service limit exceeded${action ? ` (${action})` : ""}${detail ? `: ${detail}` : ""}`,
995
+ "AWS_LIMIT_EXCEEDED",
996
+ "You've hit a service quota for this AWS API.\n\nRequest a quota increase in the AWS console:\n Service Quotas \u2192 AWS Services \u2192 (your service)",
997
+ "https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html"
998
+ ),
999
+ awsUnknownError: (code, action, detail) => new WrapsError(
1000
+ `AWS API error: ${code}${action ? ` (${action})` : ""}${detail ? ` \u2014 ${detail}` : ""}`,
1001
+ `AWS_${code}`,
1002
+ `This is an AWS API error, not a credentials problem. Look up "${code}" in the AWS documentation for the failing service.
1003
+
1004
+ If you believe this is a Wraps bug, report it at:
1005
+ https://github.com/wraps-team/wraps/issues`,
1006
+ "https://wraps.dev/docs/guides/aws-setup/troubleshooting"
1007
+ ),
1750
1008
  dynamoDBPermissionDenied: () => new WrapsError(
1751
1009
  "DynamoDB permission denied",
1752
1010
  "DYNAMODB_PERMISSION_DENIED",
@@ -1862,6 +1120,7 @@ __export(aws_exports, {
1862
1120
  getSESAccountStatus: () => getSESAccountStatus,
1863
1121
  isSESSandbox: () => isSESSandbox,
1864
1122
  listSESDomains: () => listSESDomains,
1123
+ resolveAWSCredentialsToEnv: () => resolveAWSCredentialsToEnv,
1865
1124
  validateAWSCredentials: () => validateAWSCredentials,
1866
1125
  validateAWSCredentialsWithDetails: () => validateAWSCredentialsWithDetails
1867
1126
  });
@@ -1929,127 +1188,999 @@ async function validateAWSCredentialsWithDetails() {
1929
1188
  case "UnrecognizedClientException":
1930
1189
  throw errors.accessKeyInvalid();
1931
1190
  }
1932
- }
1933
- throw errors.noAWSCredentials();
1191
+ }
1192
+ throw errors.noAWSCredentials();
1193
+ }
1194
+ }
1195
+ async function resolveAWSCredentialsToEnv() {
1196
+ delete process.env.AWS_PROFILE;
1197
+ if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
1198
+ return;
1199
+ }
1200
+ const sts = new STSClient2({});
1201
+ const provider = sts.config.credentials;
1202
+ if (!provider) {
1203
+ throw errors.noAWSCredentials();
1204
+ }
1205
+ let creds;
1206
+ try {
1207
+ creds = typeof provider === "function" ? await provider() : provider;
1208
+ } catch (error) {
1209
+ if (error instanceof Error) {
1210
+ if (error.name === "ExpiredTokenException" || error.name === "TokenRefreshRequired" || error.name === "SSOTokenExpired") {
1211
+ throw errors.sessionTokenExpired();
1212
+ }
1213
+ if (error.message?.includes("Could not load credentials")) {
1214
+ throw errors.credentialsFileMissing();
1215
+ }
1216
+ }
1217
+ throw errors.noAWSCredentials();
1218
+ }
1219
+ process.env.AWS_ACCESS_KEY_ID = creds.accessKeyId;
1220
+ process.env.AWS_SECRET_ACCESS_KEY = creds.secretAccessKey;
1221
+ if (creds.sessionToken) {
1222
+ process.env.AWS_SESSION_TOKEN = creds.sessionToken;
1223
+ }
1224
+ }
1225
+ async function checkRegion(region) {
1226
+ return SES_REGIONS.includes(region);
1227
+ }
1228
+ async function getAWSRegion() {
1229
+ if (process.env.AWS_REGION) {
1230
+ return process.env.AWS_REGION;
1231
+ }
1232
+ if (process.env.AWS_DEFAULT_REGION) {
1233
+ return process.env.AWS_DEFAULT_REGION;
1234
+ }
1235
+ return "us-east-1";
1236
+ }
1237
+ async function listSESDomains(region) {
1238
+ const ses = new SESClient({ region });
1239
+ try {
1240
+ const identitiesResponse = await ses.send(
1241
+ new ListIdentitiesCommand({
1242
+ IdentityType: "Domain"
1243
+ })
1244
+ );
1245
+ const identities = identitiesResponse.Identities || [];
1246
+ if (identities.length === 0) {
1247
+ return [];
1248
+ }
1249
+ const attributesResponse = await ses.send(
1250
+ new GetIdentityVerificationAttributesCommand({
1251
+ Identities: identities
1252
+ })
1253
+ );
1254
+ const attributes = attributesResponse.VerificationAttributes || {};
1255
+ return identities.map((domain) => ({
1256
+ domain,
1257
+ verified: attributes[domain]?.VerificationStatus === "Success"
1258
+ }));
1259
+ } catch {
1260
+ return [];
1261
+ }
1262
+ }
1263
+ async function getSESAccountStatus(region) {
1264
+ const sesv22 = new SESv2Client({ region });
1265
+ try {
1266
+ const response = await sesv22.send(new GetAccountCommand({}));
1267
+ return {
1268
+ isSandbox: !response.ProductionAccessEnabled,
1269
+ sendQuota: response.SendQuota ? {
1270
+ max24HourSend: response.SendQuota.Max24HourSend ?? 0,
1271
+ maxSendRate: response.SendQuota.MaxSendRate ?? 0,
1272
+ sentLast24Hours: response.SendQuota.SentLast24Hours ?? 0
1273
+ } : void 0,
1274
+ enforcementStatus: response.EnforcementStatus
1275
+ };
1276
+ } catch {
1277
+ return { isSandbox: true, sandboxUncertain: true };
1278
+ }
1279
+ }
1280
+ async function isSESSandbox(region) {
1281
+ const status2 = await getSESAccountStatus(region);
1282
+ return status2.isSandbox;
1283
+ }
1284
+ async function getACMCertificateStatus(certificateArn) {
1285
+ const acm3 = new ACMClient({ region: "us-east-1" });
1286
+ try {
1287
+ const response = await acm3.send(
1288
+ new DescribeCertificateCommand({
1289
+ CertificateArn: certificateArn
1290
+ })
1291
+ );
1292
+ const certificate = response.Certificate;
1293
+ if (!certificate) {
1294
+ return null;
1295
+ }
1296
+ const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
1297
+ name: option.ResourceRecord?.Name || "",
1298
+ type: option.ResourceRecord?.Type || "",
1299
+ value: option.ResourceRecord?.Value || ""
1300
+ })) || [];
1301
+ return {
1302
+ status: certificate.Status || "UNKNOWN",
1303
+ domainName: certificate.DomainName || "",
1304
+ validationRecords
1305
+ };
1306
+ } catch (error) {
1307
+ console.error("Error getting ACM certificate status:", error);
1308
+ return null;
1309
+ }
1310
+ }
1311
+ var SES_REGIONS;
1312
+ var init_aws = __esm({
1313
+ "src/utils/shared/aws.ts"() {
1314
+ "use strict";
1315
+ init_esm_shims();
1316
+ init_aws_detection();
1317
+ init_errors();
1318
+ SES_REGIONS = [
1319
+ "us-east-1",
1320
+ "us-east-2",
1321
+ "us-west-1",
1322
+ "us-west-2",
1323
+ "af-south-1",
1324
+ "ap-east-1",
1325
+ "ap-south-1",
1326
+ "ap-northeast-1",
1327
+ "ap-northeast-2",
1328
+ "ap-northeast-3",
1329
+ "ap-southeast-1",
1330
+ "ap-southeast-2",
1331
+ "ap-southeast-3",
1332
+ "ca-central-1",
1333
+ "eu-central-1",
1334
+ "eu-west-1",
1335
+ "eu-west-2",
1336
+ "eu-west-3",
1337
+ "eu-south-1",
1338
+ "eu-north-1",
1339
+ "me-south-1",
1340
+ "sa-east-1"
1341
+ ];
1342
+ }
1343
+ });
1344
+
1345
+ // src/utils/shared/s3-state.ts
1346
+ var s3_state_exports = {};
1347
+ __export(s3_state_exports, {
1348
+ clearS3StackLocks: () => clearS3StackLocks,
1349
+ deleteMetadata: () => deleteMetadata,
1350
+ downloadMetadata: () => downloadMetadata,
1351
+ ensureStateBucket: () => ensureStateBucket,
1352
+ getS3BackendUrl: () => getS3BackendUrl,
1353
+ getStateBucketName: () => getStateBucketName,
1354
+ migrateLocalPulumiState: () => migrateLocalPulumiState,
1355
+ needsMigration: () => needsMigration,
1356
+ stateBucketExists: () => stateBucketExists,
1357
+ uploadMetadata: () => uploadMetadata
1358
+ });
1359
+ import { existsSync as existsSync2, statSync } from "fs";
1360
+ import { readdir, writeFile } from "fs/promises";
1361
+ import { join as join2 } from "path";
1362
+ function has404StatusCode(error) {
1363
+ if (!(error instanceof Error)) {
1364
+ return false;
1365
+ }
1366
+ const metadataError = error;
1367
+ return metadataError.$metadata?.httpStatusCode === 404;
1368
+ }
1369
+ function getStateBucketName(accountId, region) {
1370
+ return `wraps-state-${accountId}-${region}`;
1371
+ }
1372
+ function getS3BackendUrl(accountId, region) {
1373
+ return `s3://${getStateBucketName(accountId, region)}`;
1374
+ }
1375
+ async function stateBucketExists(accountId, region) {
1376
+ const { S3Client: S3Client2, HeadBucketCommand } = await import("@aws-sdk/client-s3");
1377
+ const client = new S3Client2({ region });
1378
+ const bucketName = getStateBucketName(accountId, region);
1379
+ try {
1380
+ await client.send(new HeadBucketCommand({ Bucket: bucketName }));
1381
+ return true;
1382
+ } catch (error) {
1383
+ if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error))) {
1384
+ return false;
1385
+ }
1386
+ throw error;
1387
+ }
1388
+ }
1389
+ async function ensureStateBucket(accountId, region) {
1390
+ const {
1391
+ S3Client: S3Client2,
1392
+ HeadBucketCommand,
1393
+ CreateBucketCommand,
1394
+ PutBucketEncryptionCommand,
1395
+ PutBucketVersioningCommand,
1396
+ PutPublicAccessBlockCommand,
1397
+ PutBucketTaggingCommand
1398
+ } = await import("@aws-sdk/client-s3");
1399
+ const client = new S3Client2({ region });
1400
+ const bucketName = getStateBucketName(accountId, region);
1401
+ try {
1402
+ await client.send(new HeadBucketCommand({ Bucket: bucketName }));
1403
+ return bucketName;
1404
+ } catch (error) {
1405
+ const isNotFound = error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error));
1406
+ if (!isNotFound) {
1407
+ throw error;
1408
+ }
1409
+ }
1410
+ const createParams = { Bucket: bucketName };
1411
+ if (region !== "us-east-1") {
1412
+ createParams.CreateBucketConfiguration = {
1413
+ LocationConstraint: region
1414
+ };
1415
+ }
1416
+ await client.send(new CreateBucketCommand(createParams));
1417
+ await client.send(
1418
+ new PutBucketEncryptionCommand({
1419
+ Bucket: bucketName,
1420
+ ServerSideEncryptionConfiguration: {
1421
+ Rules: [
1422
+ {
1423
+ ApplyServerSideEncryptionByDefault: {
1424
+ SSEAlgorithm: "AES256"
1425
+ }
1426
+ }
1427
+ ]
1428
+ }
1429
+ })
1430
+ );
1431
+ await client.send(
1432
+ new PutBucketVersioningCommand({
1433
+ Bucket: bucketName,
1434
+ VersioningConfiguration: {
1435
+ Status: "Enabled"
1436
+ }
1437
+ })
1438
+ );
1439
+ await client.send(
1440
+ new PutPublicAccessBlockCommand({
1441
+ Bucket: bucketName,
1442
+ PublicAccessBlockConfiguration: {
1443
+ BlockPublicAcls: true,
1444
+ BlockPublicPolicy: true,
1445
+ IgnorePublicAcls: true,
1446
+ RestrictPublicBuckets: true
1447
+ }
1448
+ })
1449
+ );
1450
+ await client.send(
1451
+ new PutBucketTaggingCommand({
1452
+ Bucket: bucketName,
1453
+ Tagging: {
1454
+ TagSet: [
1455
+ { Key: "ManagedBy", Value: "wraps-cli" },
1456
+ { Key: "Purpose", Value: "state" }
1457
+ ]
1458
+ }
1459
+ })
1460
+ );
1461
+ return bucketName;
1462
+ }
1463
+ async function uploadMetadata(bucketName, metadata) {
1464
+ const { S3Client: S3Client2, PutObjectCommand } = await import("@aws-sdk/client-s3");
1465
+ const client = new S3Client2({ region: metadata.region });
1466
+ const key = `metadata/${metadata.accountId}-${metadata.region}.json`;
1467
+ await client.send(
1468
+ new PutObjectCommand({
1469
+ Bucket: bucketName,
1470
+ Key: key,
1471
+ Body: JSON.stringify(metadata, null, 2),
1472
+ ContentType: "application/json"
1473
+ })
1474
+ );
1475
+ }
1476
+ async function deleteMetadata(bucketName, accountId, region) {
1477
+ const { S3Client: S3Client2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
1478
+ const client = new S3Client2({ region });
1479
+ const key = `metadata/${accountId}-${region}.json`;
1480
+ await client.send(
1481
+ new DeleteObjectCommand({
1482
+ Bucket: bucketName,
1483
+ Key: key
1484
+ })
1485
+ );
1486
+ }
1487
+ async function clearS3StackLocks(accountId, region) {
1488
+ const { S3Client: S3Client2, ListObjectsV2Command: ListObjectsV2Command2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
1489
+ const client = new S3Client2({ region });
1490
+ const bucketName = getStateBucketName(accountId, region);
1491
+ const prefix = ".pulumi/locks/";
1492
+ const response = await client.send(
1493
+ new ListObjectsV2Command2({ Bucket: bucketName, Prefix: prefix })
1494
+ );
1495
+ const lockObjects = response.Contents ?? [];
1496
+ if (lockObjects.length === 0) {
1497
+ return 0;
1498
+ }
1499
+ for (const obj of lockObjects) {
1500
+ if (obj.Key) {
1501
+ await client.send(
1502
+ new DeleteObjectCommand({ Bucket: bucketName, Key: obj.Key })
1503
+ );
1504
+ }
1505
+ }
1506
+ return lockObjects.length;
1507
+ }
1508
+ async function downloadMetadata(bucketName, accountId, region) {
1509
+ const { S3Client: S3Client2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
1510
+ const client = new S3Client2({ region });
1511
+ const key = `metadata/${accountId}-${region}.json`;
1512
+ try {
1513
+ const response = await client.send(
1514
+ new GetObjectCommand2({
1515
+ Bucket: bucketName,
1516
+ Key: key
1517
+ })
1518
+ );
1519
+ const body = await response.Body?.transformToString();
1520
+ if (!body) {
1521
+ return null;
1522
+ }
1523
+ return JSON.parse(body);
1524
+ } catch (error) {
1525
+ if (error instanceof Error && (error.name === "NoSuchKey" || has404StatusCode(error))) {
1526
+ return null;
1527
+ }
1528
+ throw error;
1529
+ }
1530
+ }
1531
+ async function needsMigration(localPulumiDir, accountId, region) {
1532
+ const markerPath = join2(localPulumiDir, `.migrated-${accountId}-${region}`);
1533
+ if (existsSync2(markerPath)) {
1534
+ return false;
1535
+ }
1536
+ const stacksDir = join2(localPulumiDir, ".pulumi", "stacks");
1537
+ if (!existsSync2(stacksDir)) {
1538
+ return false;
1539
+ }
1540
+ try {
1541
+ const entries = await readdir(stacksDir);
1542
+ for (const entry of entries) {
1543
+ const entryPath = join2(stacksDir, entry);
1544
+ if (statSync(entryPath).isDirectory()) {
1545
+ const files = await readdir(entryPath);
1546
+ const matching = files.filter(
1547
+ (f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
1548
+ );
1549
+ if (matching.length > 0) {
1550
+ return true;
1551
+ }
1552
+ }
1553
+ }
1554
+ return false;
1555
+ } catch {
1556
+ return false;
1557
+ }
1558
+ }
1559
+ async function migrateLocalPulumiState(localPulumiDir, bucketName, accountId, region) {
1560
+ const pulumi31 = await import("@pulumi/pulumi/automation/index.js");
1561
+ const stacksDir = join2(localPulumiDir, ".pulumi", "stacks");
1562
+ const entries = await readdir(stacksDir);
1563
+ for (const entry of entries) {
1564
+ const entryPath = join2(stacksDir, entry);
1565
+ if (!statSync(entryPath).isDirectory()) {
1566
+ continue;
1567
+ }
1568
+ const projectName = entry;
1569
+ const files = await readdir(entryPath);
1570
+ const stackFiles = files.filter(
1571
+ (f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
1572
+ );
1573
+ for (const stackFile of stackFiles) {
1574
+ const stackName = stackFile.replace(".json", "");
1575
+ try {
1576
+ const localStack = await pulumi31.LocalWorkspace.selectStack({
1577
+ stackName,
1578
+ workDir: localPulumiDir
1579
+ });
1580
+ const state = await localStack.exportStack();
1581
+ const s3Stack = await pulumi31.LocalWorkspace.createOrSelectStack(
1582
+ {
1583
+ stackName,
1584
+ projectName,
1585
+ program: async () => ({})
1586
+ },
1587
+ {
1588
+ workDir: localPulumiDir,
1589
+ envVars: {
1590
+ PULUMI_BACKEND_URL: `s3://${bucketName}`,
1591
+ PULUMI_CONFIG_PASSPHRASE: ""
1592
+ }
1593
+ }
1594
+ );
1595
+ await s3Stack.importStack(state);
1596
+ } catch (error) {
1597
+ console.error(
1598
+ `Warning: Failed to migrate stack ${stackName}: ${error instanceof Error ? error.message : error}`
1599
+ );
1600
+ }
1601
+ }
1602
+ }
1603
+ const markerPath = join2(localPulumiDir, `.migrated-${accountId}-${region}`);
1604
+ await writeFile(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
1605
+ }
1606
+ var init_s3_state = __esm({
1607
+ "src/utils/shared/s3-state.ts"() {
1608
+ "use strict";
1609
+ init_esm_shims();
1610
+ }
1611
+ });
1612
+
1613
+ // src/utils/shared/fs.ts
1614
+ var fs_exports = {};
1615
+ __export(fs_exports, {
1616
+ clearLocalStackLocks: () => clearLocalStackLocks,
1617
+ ensurePulumiWorkDir: () => ensurePulumiWorkDir,
1618
+ ensureWrapsDir: () => ensureWrapsDir,
1619
+ getPulumiWorkDir: () => getPulumiWorkDir,
1620
+ getWrapsDir: () => getWrapsDir
1621
+ });
1622
+ import { existsSync as existsSync3 } from "fs";
1623
+ import { mkdir, readdir as readdir2, rm } from "fs/promises";
1624
+ import { homedir as homedir2 } from "os";
1625
+ import { join as join3 } from "path";
1626
+ function getWrapsDir() {
1627
+ return join3(homedir2(), ".wraps");
1628
+ }
1629
+ function getPulumiWorkDir() {
1630
+ return join3(getWrapsDir(), "pulumi");
1631
+ }
1632
+ async function ensureWrapsDir() {
1633
+ const wrapsDir = getWrapsDir();
1634
+ if (!existsSync3(wrapsDir)) {
1635
+ await mkdir(wrapsDir, { recursive: true });
1636
+ }
1637
+ }
1638
+ async function clearLocalStackLocks() {
1639
+ const locksDir = join3(getPulumiWorkDir(), ".pulumi", "locks");
1640
+ if (!existsSync3(locksDir)) {
1641
+ return 0;
1642
+ }
1643
+ let count = 0;
1644
+ async function walkAndDelete(dir) {
1645
+ const entries = await readdir2(dir, { withFileTypes: true });
1646
+ for (const entry of entries) {
1647
+ const fullPath = join3(dir, entry.name);
1648
+ if (entry.isDirectory()) {
1649
+ await walkAndDelete(fullPath);
1650
+ } else if (entry.name.endsWith(".json")) {
1651
+ await rm(fullPath);
1652
+ count++;
1653
+ }
1654
+ }
1655
+ }
1656
+ await walkAndDelete(locksDir);
1657
+ return count;
1658
+ }
1659
+ async function ensurePulumiWorkDir(options) {
1660
+ await ensureWrapsDir();
1661
+ const pulumiDir = getPulumiWorkDir();
1662
+ if (!existsSync3(pulumiDir)) {
1663
+ await mkdir(pulumiDir, { recursive: true });
1664
+ }
1665
+ process.env.PULUMI_CONFIG_PASSPHRASE = "";
1666
+ if (options?.accountId && options?.region) {
1667
+ const { resolveAWSCredentialsToEnv: resolveAWSCredentialsToEnv2 } = await Promise.resolve().then(() => (init_aws(), aws_exports));
1668
+ await resolveAWSCredentialsToEnv2();
1669
+ }
1670
+ const useS3 = options?.accountId && options?.region && process.env.WRAPS_LOCAL_ONLY !== "1";
1671
+ if (useS3) {
1672
+ try {
1673
+ const {
1674
+ ensureStateBucket: ensureStateBucket2,
1675
+ getS3BackendUrl: getS3BackendUrl2,
1676
+ needsMigration: needsMigration2,
1677
+ migrateLocalPulumiState: migrateLocalPulumiState2
1678
+ } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
1679
+ const bucketName = await ensureStateBucket2(
1680
+ options.accountId,
1681
+ options.region
1682
+ );
1683
+ const shouldMigrate = await needsMigration2(
1684
+ pulumiDir,
1685
+ options.accountId,
1686
+ options.region
1687
+ );
1688
+ if (shouldMigrate) {
1689
+ process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
1690
+ await migrateLocalPulumiState2(
1691
+ pulumiDir,
1692
+ bucketName,
1693
+ options.accountId,
1694
+ options.region
1695
+ );
1696
+ }
1697
+ process.env.PULUMI_BACKEND_URL = getS3BackendUrl2(
1698
+ options.accountId,
1699
+ options.region
1700
+ );
1701
+ return;
1702
+ } catch (error) {
1703
+ const clack54 = await import("@clack/prompts");
1704
+ clack54.log.warn(
1705
+ `S3 state backend unavailable (${error instanceof Error ? error.message : error}). Using local state.`
1706
+ );
1707
+ }
1708
+ }
1709
+ process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
1710
+ }
1711
+ var init_fs = __esm({
1712
+ "src/utils/shared/fs.ts"() {
1713
+ "use strict";
1714
+ init_esm_shims();
1715
+ }
1716
+ });
1717
+
1718
+ // src/utils/shared/config.ts
1719
+ import { existsSync as existsSync4 } from "fs";
1720
+ import { chmod, readFile, writeFile as writeFile2 } from "fs/promises";
1721
+ import { join as join4 } from "path";
1722
+ function getApiBaseUrl() {
1723
+ return process.env.WRAPS_API_URL || "https://api.wraps.dev";
1724
+ }
1725
+ function getAppBaseUrl() {
1726
+ return process.env.WRAPS_APP_URL || "https://app.wraps.dev";
1727
+ }
1728
+ function getConfigPath() {
1729
+ return join4(getWrapsDir(), CONFIG_FILE);
1730
+ }
1731
+ async function readAuthConfig() {
1732
+ const path3 = getConfigPath();
1733
+ if (!existsSync4(path3)) {
1734
+ return null;
1735
+ }
1736
+ try {
1737
+ const content = await readFile(path3, "utf-8");
1738
+ return JSON.parse(content);
1739
+ } catch {
1740
+ return null;
1741
+ }
1742
+ }
1743
+ async function saveAuthConfig(config2) {
1744
+ await ensureWrapsDir();
1745
+ const path3 = getConfigPath();
1746
+ const existing = await readAuthConfig();
1747
+ const merged = existing ? { ...existing, ...config2 } : config2;
1748
+ await writeFile2(path3, JSON.stringify(merged, null, 2), "utf-8");
1749
+ await chmod(path3, 384);
1750
+ }
1751
+ async function clearAuthConfig() {
1752
+ const existing = await readAuthConfig();
1753
+ if (existing) {
1754
+ existing.auth = void 0;
1755
+ await saveAuthConfig(existing);
1756
+ }
1757
+ }
1758
+ function resolveToken(flags2) {
1759
+ return flags2?.token || process.env.WRAPS_API_KEY || null;
1760
+ }
1761
+ async function resolveTokenAsync(flags2) {
1762
+ const sync = resolveToken(flags2);
1763
+ if (sync) {
1764
+ return sync;
1765
+ }
1766
+ const config2 = await readAuthConfig();
1767
+ if (!config2?.auth?.token) {
1768
+ return null;
1769
+ }
1770
+ if (config2.auth.expiresAt && new Date(config2.auth.expiresAt) <= /* @__PURE__ */ new Date()) {
1771
+ return null;
1772
+ }
1773
+ return config2.auth.token;
1774
+ }
1775
+ var CONFIG_FILE;
1776
+ var init_config = __esm({
1777
+ "src/utils/shared/config.ts"() {
1778
+ "use strict";
1779
+ init_esm_shims();
1780
+ init_fs();
1781
+ CONFIG_FILE = "config.json";
1782
+ }
1783
+ });
1784
+
1785
+ // src/telemetry/config.ts
1786
+ import Conf from "conf";
1787
+ import { v4 as uuidv4 } from "uuid";
1788
+ var CONFIG_DEFAULTS, TelemetryConfigManager;
1789
+ var init_config2 = __esm({
1790
+ "src/telemetry/config.ts"() {
1791
+ "use strict";
1792
+ init_esm_shims();
1793
+ CONFIG_DEFAULTS = {
1794
+ enabled: true,
1795
+ anonymousId: uuidv4(),
1796
+ notificationShown: false
1797
+ };
1798
+ TelemetryConfigManager = class {
1799
+ config;
1800
+ constructor(options) {
1801
+ this.config = new Conf({
1802
+ projectName: "wraps",
1803
+ configName: "telemetry",
1804
+ defaults: CONFIG_DEFAULTS,
1805
+ cwd: options?.cwd
1806
+ });
1807
+ }
1808
+ /**
1809
+ * Check if telemetry is enabled
1810
+ */
1811
+ isEnabled() {
1812
+ return this.config.get("enabled");
1813
+ }
1814
+ /**
1815
+ * Enable or disable telemetry
1816
+ */
1817
+ setEnabled(enabled) {
1818
+ this.config.set("enabled", enabled);
1819
+ }
1820
+ /**
1821
+ * Get the anonymous user ID
1822
+ */
1823
+ getAnonymousId() {
1824
+ return this.config.get("anonymousId");
1825
+ }
1826
+ /**
1827
+ * Check if the first-run notification has been shown
1828
+ */
1829
+ hasShownNotification() {
1830
+ return this.config.get("notificationShown");
1831
+ }
1832
+ /**
1833
+ * Mark the first-run notification as shown
1834
+ */
1835
+ markNotificationShown() {
1836
+ this.config.set("notificationShown", true);
1837
+ }
1838
+ /**
1839
+ * Get the full path to the configuration file
1840
+ */
1841
+ getConfigPath() {
1842
+ return this.config.path;
1843
+ }
1844
+ /**
1845
+ * Reset configuration to defaults
1846
+ */
1847
+ reset() {
1848
+ this.config.clear();
1849
+ this.config.set({
1850
+ ...CONFIG_DEFAULTS,
1851
+ anonymousId: uuidv4()
1852
+ });
1853
+ }
1854
+ };
1855
+ }
1856
+ });
1857
+
1858
+ // src/telemetry/client.ts
1859
+ import { readFileSync as readFileSync2 } from "fs";
1860
+ import { dirname, join as join5 } from "path";
1861
+ import { fileURLToPath as fileURLToPath2 } from "url";
1862
+ import pc2 from "picocolors";
1863
+ function getTelemetryClient() {
1864
+ if (!telemetryInstance) {
1865
+ telemetryInstance = new TelemetryClient();
1866
+ }
1867
+ return telemetryInstance;
1868
+ }
1869
+ var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, TelemetryClient, telemetryInstance;
1870
+ var init_client = __esm({
1871
+ "src/telemetry/client.ts"() {
1872
+ "use strict";
1873
+ init_esm_shims();
1874
+ init_ci_detection();
1875
+ init_config();
1876
+ init_config2();
1877
+ DEFAULT_ENDPOINT = process.env.WRAPS_TELEMETRY_URL || "https://wraps.dev/api/telemetry";
1878
+ DEFAULT_TIMEOUT = 2e3;
1879
+ TelemetryClient = class {
1880
+ config;
1881
+ endpoint;
1882
+ timeout;
1883
+ debug;
1884
+ enabled;
1885
+ eventQueue = [];
1886
+ flushTimer;
1887
+ hasShownFooter = false;
1888
+ userId;
1889
+ userIdResolved = false;
1890
+ constructor(options = {}) {
1891
+ this.config = new TelemetryConfigManager();
1892
+ this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
1893
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
1894
+ this.debug = options.debug || process.env.WRAPS_TELEMETRY_DEBUG === "1";
1895
+ this.enabled = this.shouldBeEnabled();
1896
+ this.resolveUserId();
1897
+ }
1898
+ /**
1899
+ * Resolve authenticated user identity from CLI auth config.
1900
+ * Uses the first organization ID as the user identifier,
1901
+ * linking CLI telemetry to the same org tracked on the web dashboard.
1902
+ */
1903
+ async resolveUserId() {
1904
+ try {
1905
+ const config2 = await readAuthConfig();
1906
+ if (config2?.auth?.token && config2.auth.organizations?.length) {
1907
+ this.userId = config2.auth.organizations[0].id;
1908
+ }
1909
+ } catch {
1910
+ } finally {
1911
+ this.userIdResolved = true;
1912
+ }
1913
+ }
1914
+ /**
1915
+ * Determine if telemetry should be enabled based on environment and config
1916
+ */
1917
+ shouldBeEnabled() {
1918
+ if (process.env.DO_NOT_TRACK === "1") {
1919
+ return false;
1920
+ }
1921
+ if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
1922
+ return false;
1923
+ }
1924
+ if (isCI()) {
1925
+ return false;
1926
+ }
1927
+ if (!this.config.isEnabled()) {
1928
+ return false;
1929
+ }
1930
+ return true;
1931
+ }
1932
+ /**
1933
+ * Track an event
1934
+ *
1935
+ * @param event - Event name in format "category:action" (e.g., "command:init")
1936
+ * @param properties - Additional event properties (no PII)
1937
+ */
1938
+ track(event, properties) {
1939
+ const telemetryEvent = {
1940
+ event,
1941
+ properties: {
1942
+ ...properties,
1943
+ cli_version: this.getCLIVersion(),
1944
+ os: process.platform,
1945
+ node_version: process.version,
1946
+ ci: isCI()
1947
+ },
1948
+ anonymousId: this.config.getAnonymousId(),
1949
+ ...this.userId ? { userId: this.userId } : {},
1950
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1951
+ };
1952
+ if (this.debug) {
1953
+ console.log(
1954
+ "[Telemetry Debug] Event:",
1955
+ JSON.stringify(telemetryEvent, null, 2)
1956
+ );
1957
+ return;
1958
+ }
1959
+ if (!this.enabled) {
1960
+ return;
1961
+ }
1962
+ this.eventQueue.push(telemetryEvent);
1963
+ if (this.flushTimer) {
1964
+ clearTimeout(this.flushTimer);
1965
+ }
1966
+ this.flushTimer = setTimeout(() => this.flush(), 100);
1967
+ }
1968
+ /**
1969
+ * Flush queued events to server
1970
+ */
1971
+ async flush() {
1972
+ if (this.eventQueue.length === 0) {
1973
+ return;
1974
+ }
1975
+ const eventsToSend = [...this.eventQueue];
1976
+ this.eventQueue = [];
1977
+ try {
1978
+ const controller = new AbortController();
1979
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1980
+ const requestBody = {
1981
+ events: eventsToSend,
1982
+ batch: true
1983
+ };
1984
+ await fetch(this.endpoint, {
1985
+ method: "POST",
1986
+ headers: {
1987
+ "Content-Type": "application/json"
1988
+ },
1989
+ body: JSON.stringify(requestBody),
1990
+ signal: controller.signal
1991
+ });
1992
+ clearTimeout(timeoutId);
1993
+ } catch (error) {
1994
+ if (this.debug) {
1995
+ console.error("[Telemetry Debug] Failed to send events:", error);
1996
+ }
1997
+ }
1998
+ }
1999
+ /**
2000
+ * Flush and wait for all events to be sent
2001
+ * Should be called before CLI exits
2002
+ */
2003
+ async shutdown() {
2004
+ if (this.flushTimer) {
2005
+ clearTimeout(this.flushTimer);
2006
+ }
2007
+ if (!this.userIdResolved) {
2008
+ await new Promise((resolve) => {
2009
+ const check2 = () => {
2010
+ if (this.userIdResolved) {
2011
+ return resolve();
2012
+ }
2013
+ setTimeout(check2, 10);
2014
+ };
2015
+ check2();
2016
+ setTimeout(resolve, 100);
2017
+ });
2018
+ }
2019
+ if (this.userId) {
2020
+ for (const evt of this.eventQueue) {
2021
+ if (!evt.userId) {
2022
+ evt.userId = this.userId;
2023
+ }
2024
+ }
2025
+ }
2026
+ await this.flush();
2027
+ }
2028
+ /**
2029
+ * Enable telemetry.
2030
+ * Returns null if enabled, or a string describing why an env override prevented it.
2031
+ */
2032
+ enable() {
2033
+ this.config.setEnabled(true);
2034
+ const override = this.getEnvOverride();
2035
+ if (override) {
2036
+ this.enabled = false;
2037
+ return override;
2038
+ }
2039
+ this.enabled = true;
2040
+ return null;
2041
+ }
2042
+ /**
2043
+ * Check if an environment variable is overriding the config.
2044
+ * Returns a human-readable reason, or null if no override.
2045
+ */
2046
+ getEnvOverride() {
2047
+ if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
2048
+ return "DO_NOT_TRACK environment variable is set";
2049
+ }
2050
+ if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
2051
+ return "WRAPS_TELEMETRY_DISABLED environment variable is set";
2052
+ }
2053
+ if (isCI()) {
2054
+ return "CI environment detected";
2055
+ }
2056
+ return null;
2057
+ }
2058
+ /**
2059
+ * Disable telemetry
2060
+ */
2061
+ disable() {
2062
+ this.config.setEnabled(false);
2063
+ this.enabled = false;
2064
+ this.eventQueue = [];
2065
+ }
2066
+ /**
2067
+ * Check if telemetry is enabled
2068
+ */
2069
+ isEnabled() {
2070
+ return this.enabled;
2071
+ }
2072
+ /**
2073
+ * Get config file path
2074
+ */
2075
+ getConfigPath() {
2076
+ return this.config.getConfigPath();
2077
+ }
2078
+ /**
2079
+ * Show first-run notification
2080
+ */
2081
+ shouldShowNotification() {
2082
+ return this.enabled && !this.config.hasShownNotification();
2083
+ }
2084
+ /**
2085
+ * Mark notification as shown
2086
+ */
2087
+ markNotificationShown() {
2088
+ this.config.markNotificationShown();
2089
+ }
2090
+ /**
2091
+ * Show promotional footer once per CLI session.
2092
+ * Call this after successful completion of status/list commands.
2093
+ * Returns true if footer was shown, false if already shown this session.
2094
+ */
2095
+ showFooterOnce() {
2096
+ if (this.hasShownFooter) {
2097
+ return false;
2098
+ }
2099
+ this.hasShownFooter = true;
2100
+ console.log();
2101
+ console.log(pc2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2102
+ console.log("\u{1F4CA} Wraps Platform \u2014 analytics, templates, automations");
2103
+ console.log(` From $10/mo \u2192 ${pc2.cyan("https://wraps.dev/platform")}`);
2104
+ console.log();
2105
+ console.log(`\u{1F4AC} ${pc2.cyan("hey@wraps.sh")}`);
2106
+ console.log(pc2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2107
+ return true;
2108
+ }
2109
+ /**
2110
+ * Get CLI version from package.json
2111
+ */
2112
+ getCLIVersion() {
2113
+ try {
2114
+ const __filename3 = fileURLToPath2(import.meta.url);
2115
+ const __dirname4 = dirname(__filename3);
2116
+ const pkg = JSON.parse(
2117
+ readFileSync2(join5(__dirname4, "../package.json"), "utf-8")
2118
+ );
2119
+ return pkg.version;
2120
+ } catch {
2121
+ return "unknown";
2122
+ }
2123
+ }
2124
+ };
2125
+ telemetryInstance = null;
1934
2126
  }
2127
+ });
2128
+
2129
+ // src/telemetry/events.ts
2130
+ function trackCommand(command, metadata) {
2131
+ const client = getTelemetryClient();
2132
+ const sanitized = metadata ? { ...metadata } : {};
2133
+ sanitized.domain = void 0;
2134
+ sanitized.accountId = void 0;
2135
+ sanitized.email = void 0;
2136
+ client.track(`command:${command}`, sanitized);
1935
2137
  }
1936
- async function checkRegion(region) {
1937
- return SES_REGIONS.includes(region);
2138
+ function trackServiceInit(service, success, metadata) {
2139
+ const client = getTelemetryClient();
2140
+ client.track("service:init", {
2141
+ service,
2142
+ success,
2143
+ ...metadata
2144
+ });
1938
2145
  }
1939
- async function getAWSRegion() {
1940
- if (process.env.AWS_REGION) {
1941
- return process.env.AWS_REGION;
1942
- }
1943
- if (process.env.AWS_DEFAULT_REGION) {
1944
- return process.env.AWS_DEFAULT_REGION;
1945
- }
1946
- return "us-east-1";
2146
+ function trackServiceDeployed(service, metadata) {
2147
+ const client = getTelemetryClient();
2148
+ client.track("service:deployed", {
2149
+ service,
2150
+ ...metadata
2151
+ });
1947
2152
  }
1948
- async function listSESDomains(region) {
1949
- const ses = new SESClient({ region });
1950
- try {
1951
- const identitiesResponse = await ses.send(
1952
- new ListIdentitiesCommand({
1953
- IdentityType: "Domain"
1954
- })
1955
- );
1956
- const identities = identitiesResponse.Identities || [];
1957
- if (identities.length === 0) {
1958
- return [];
1959
- }
1960
- const attributesResponse = await ses.send(
1961
- new GetIdentityVerificationAttributesCommand({
1962
- Identities: identities
1963
- })
1964
- );
1965
- const attributes = attributesResponse.VerificationAttributes || {};
1966
- return identities.map((domain) => ({
1967
- domain,
1968
- verified: attributes[domain]?.VerificationStatus === "Success"
1969
- }));
1970
- } catch {
1971
- return [];
1972
- }
2153
+ function trackError(errorCode, command, metadata) {
2154
+ const client = getTelemetryClient();
2155
+ client.track("error:occurred", {
2156
+ error_code: errorCode,
2157
+ command,
2158
+ ...metadata
2159
+ });
1973
2160
  }
1974
- async function getSESAccountStatus(region) {
1975
- const sesv22 = new SESv2Client({ region });
1976
- try {
1977
- const response = await sesv22.send(new GetAccountCommand({}));
1978
- return {
1979
- isSandbox: !response.ProductionAccessEnabled,
1980
- sendQuota: response.SendQuota ? {
1981
- max24HourSend: response.SendQuota.Max24HourSend ?? 0,
1982
- maxSendRate: response.SendQuota.MaxSendRate ?? 0,
1983
- sentLast24Hours: response.SendQuota.SentLast24Hours ?? 0
1984
- } : void 0,
1985
- enforcementStatus: response.EnforcementStatus
1986
- };
1987
- } catch {
1988
- return { isSandbox: true, sandboxUncertain: true };
1989
- }
2161
+ function trackFeature(feature, metadata) {
2162
+ const client = getTelemetryClient();
2163
+ client.track(`feature:${feature}`, metadata || {});
1990
2164
  }
1991
- async function isSESSandbox(region) {
1992
- const status2 = await getSESAccountStatus(region);
1993
- return status2.isSandbox;
2165
+ function trackServiceUpgrade(service, metadata) {
2166
+ const client = getTelemetryClient();
2167
+ client.track("service:upgraded", {
2168
+ service,
2169
+ ...metadata
2170
+ });
1994
2171
  }
1995
- async function getACMCertificateStatus(certificateArn) {
1996
- const acm3 = new ACMClient({ region: "us-east-1" });
1997
- try {
1998
- const response = await acm3.send(
1999
- new DescribeCertificateCommand({
2000
- CertificateArn: certificateArn
2001
- })
2002
- );
2003
- const certificate = response.Certificate;
2004
- if (!certificate) {
2005
- return null;
2006
- }
2007
- const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
2008
- name: option.ResourceRecord?.Name || "",
2009
- type: option.ResourceRecord?.Type || "",
2010
- value: option.ResourceRecord?.Value || ""
2011
- })) || [];
2012
- return {
2013
- status: certificate.Status || "UNKNOWN",
2014
- domainName: certificate.DomainName || "",
2015
- validationRecords
2016
- };
2017
- } catch (error) {
2018
- console.error("Error getting ACM certificate status:", error);
2019
- return null;
2020
- }
2172
+ function trackServiceRemoved(service, metadata) {
2173
+ const client = getTelemetryClient();
2174
+ client.track("service:removed", {
2175
+ service,
2176
+ ...metadata
2177
+ });
2021
2178
  }
2022
- var SES_REGIONS;
2023
- var init_aws = __esm({
2024
- "src/utils/shared/aws.ts"() {
2179
+ var init_events = __esm({
2180
+ "src/telemetry/events.ts"() {
2025
2181
  "use strict";
2026
2182
  init_esm_shims();
2027
- init_aws_detection();
2028
- init_errors();
2029
- SES_REGIONS = [
2030
- "us-east-1",
2031
- "us-east-2",
2032
- "us-west-1",
2033
- "us-west-2",
2034
- "af-south-1",
2035
- "ap-east-1",
2036
- "ap-south-1",
2037
- "ap-northeast-1",
2038
- "ap-northeast-2",
2039
- "ap-northeast-3",
2040
- "ap-southeast-1",
2041
- "ap-southeast-2",
2042
- "ap-southeast-3",
2043
- "ca-central-1",
2044
- "eu-central-1",
2045
- "eu-west-1",
2046
- "eu-west-2",
2047
- "eu-west-3",
2048
- "eu-south-1",
2049
- "eu-north-1",
2050
- "me-south-1",
2051
- "sa-east-1"
2052
- ];
2183
+ init_client();
2053
2184
  }
2054
2185
  });
2055
2186
 
@@ -9896,14 +10027,14 @@ init_esm_shims();
9896
10027
  init_events();
9897
10028
  init_config();
9898
10029
  init_json_output();
9899
- import * as clack from "@clack/prompts";
10030
+ import * as clack2 from "@clack/prompts";
9900
10031
  import { createAuthClient } from "better-auth/client";
9901
10032
  import {
9902
10033
  deviceAuthorizationClient,
9903
10034
  organizationClient
9904
10035
  } from "better-auth/client/plugins";
9905
10036
  import open from "open";
9906
- import pc2 from "picocolors";
10037
+ import pc3 from "picocolors";
9907
10038
  function createCliAuthClient(baseURL) {
9908
10039
  return createAuthClient({
9909
10040
  baseURL,
@@ -9947,14 +10078,14 @@ async function login(options) {
9947
10078
  if (isJsonMode()) {
9948
10079
  jsonSuccess("auth.login", { tokenType: "api-key" });
9949
10080
  } else {
9950
- clack.log.success("API key saved.");
10081
+ clack2.log.success("API key saved.");
9951
10082
  }
9952
10083
  return;
9953
10084
  }
9954
- clack.intro(pc2.bold("Wraps \u203A Sign In"));
10085
+ clack2.intro(pc3.bold("Wraps \u203A Sign In"));
9955
10086
  const baseURL = getAppBaseUrl();
9956
10087
  const authClient = createCliAuthClient(baseURL);
9957
- const spinner10 = clack.spinner();
10088
+ const spinner10 = clack2.spinner();
9958
10089
  const { data: codeData, error: codeError } = await authClient.device.code({
9959
10090
  client_id: "wraps-cli"
9960
10091
  });
@@ -9965,7 +10096,7 @@ async function login(options) {
9965
10096
  method: "device"
9966
10097
  });
9967
10098
  trackError("DEVICE_AUTH_FAILED", "auth:login", { step: "request_code" });
9968
- clack.log.error("Failed to start device authorization.");
10099
+ clack2.log.error("Failed to start device authorization.");
9969
10100
  throw new Error("Failed to start device authorization.");
9970
10101
  }
9971
10102
  const {
@@ -9976,11 +10107,11 @@ async function login(options) {
9976
10107
  expires_in
9977
10108
  } = codeData;
9978
10109
  const formatted = `${user_code.slice(0, 4)}-${user_code.slice(4)}`;
9979
- clack.log.info(`Your code: ${pc2.bold(pc2.cyan(formatted))}`);
9980
- clack.log.info(`Visit: ${pc2.underline(`${baseURL}/device`)}`);
10110
+ clack2.log.info(`Your code: ${pc3.bold(pc3.cyan(formatted))}`);
10111
+ clack2.log.info(`Visit: ${pc3.underline(`${baseURL}/device`)}`);
9981
10112
  try {
9982
10113
  await open(`${baseURL}/device?user_code=${user_code}`);
9983
- clack.log.info("Opening browser...");
10114
+ clack2.log.info("Opening browser...");
9984
10115
  } catch {
9985
10116
  }
9986
10117
  spinner10.start("Waiting for approval...");
@@ -10012,14 +10143,14 @@ async function login(options) {
10012
10143
  duration_ms: Date.now() - startTime,
10013
10144
  method: "device"
10014
10145
  });
10015
- clack.log.success("Signed in successfully.");
10146
+ clack2.log.success("Signed in successfully.");
10016
10147
  if (organizations.length === 1) {
10017
- clack.log.info(`Organization: ${pc2.cyan(organizations[0].name)}`);
10148
+ clack2.log.info(`Organization: ${pc3.cyan(organizations[0].name)}`);
10018
10149
  } else if (organizations.length > 1) {
10019
- clack.log.info(`${organizations.length} organizations available`);
10150
+ clack2.log.info(`${organizations.length} organizations available`);
10020
10151
  } else {
10021
- clack.log.info(
10022
- `No organizations found. Create one at ${pc2.underline(`${baseURL}/onboarding`)} and run ${pc2.cyan("wraps auth login")} again.`
10152
+ clack2.log.info(
10153
+ `No organizations found. Create one at ${pc3.underline(`${baseURL}/onboarding`)} and run ${pc3.cyan("wraps auth login")} again.`
10023
10154
  );
10024
10155
  }
10025
10156
  if (isJsonMode()) {
@@ -10048,7 +10179,7 @@ async function login(options) {
10048
10179
  });
10049
10180
  trackError("ACCESS_DENIED", "auth:login", { step: "poll_token" });
10050
10181
  spinner10.stop("Denied.");
10051
- clack.log.error("Authorization was denied.");
10182
+ clack2.log.error("Authorization was denied.");
10052
10183
  throw new Error("Authorization was denied.");
10053
10184
  }
10054
10185
  if (errorCode === "expired_token") {
@@ -10063,7 +10194,7 @@ async function login(options) {
10063
10194
  });
10064
10195
  trackError("DEVICE_CODE_EXPIRED", "auth:login", { step: "poll_token" });
10065
10196
  spinner10.stop("Expired.");
10066
- clack.log.error("Device code expired. Run `wraps auth login` to try again.");
10197
+ clack2.log.error("Device code expired. Run `wraps auth login` to try again.");
10067
10198
  throw new Error("Device code expired.");
10068
10199
  }
10069
10200
 
@@ -10072,11 +10203,11 @@ init_esm_shims();
10072
10203
  init_events();
10073
10204
  init_config();
10074
10205
  init_json_output();
10075
- import * as clack2 from "@clack/prompts";
10076
- import pc3 from "picocolors";
10206
+ import * as clack3 from "@clack/prompts";
10207
+ import pc4 from "picocolors";
10077
10208
  async function logout() {
10078
10209
  if (!isJsonMode()) {
10079
- clack2.intro(pc3.bold("Wraps \u203A Sign Out"));
10210
+ clack3.intro(pc4.bold("Wraps \u203A Sign Out"));
10080
10211
  }
10081
10212
  const config2 = await readAuthConfig();
10082
10213
  if (!config2?.auth?.token) {
@@ -10085,7 +10216,7 @@ async function logout() {
10085
10216
  jsonSuccess("auth.logout", { loggedOut: false, alreadyLoggedOut: true });
10086
10217
  return;
10087
10218
  }
10088
- clack2.log.info("Not signed in.");
10219
+ clack3.log.info("Not signed in.");
10089
10220
  return;
10090
10221
  }
10091
10222
  await clearAuthConfig();
@@ -10094,7 +10225,7 @@ async function logout() {
10094
10225
  jsonSuccess("auth.logout", { loggedOut: true });
10095
10226
  return;
10096
10227
  }
10097
- clack2.log.success("Signed out. Token removed from ~/.wraps/config.json");
10228
+ clack3.log.success("Signed out. Token removed from ~/.wraps/config.json");
10098
10229
  }
10099
10230
 
10100
10231
  // src/commands/auth/status.ts
@@ -10102,8 +10233,8 @@ init_esm_shims();
10102
10233
  init_events();
10103
10234
  init_config();
10104
10235
  init_json_output();
10105
- import * as clack3 from "@clack/prompts";
10106
- import pc4 from "picocolors";
10236
+ import * as clack4 from "@clack/prompts";
10237
+ import pc5 from "picocolors";
10107
10238
  async function authStatus(_options = {}) {
10108
10239
  const config2 = await readAuthConfig();
10109
10240
  if (!config2?.auth?.token) {
@@ -10111,8 +10242,8 @@ async function authStatus(_options = {}) {
10111
10242
  if (isJsonMode()) {
10112
10243
  jsonSuccess("auth.status", { authenticated: false });
10113
10244
  } else {
10114
- clack3.intro(pc4.bold("Wraps \u203A Auth Status"));
10115
- clack3.log.info("Not signed in. Run `wraps auth login` to authenticate.");
10245
+ clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
10246
+ clack4.log.info("Not signed in. Run `wraps auth login` to authenticate.");
10116
10247
  }
10117
10248
  return;
10118
10249
  }
@@ -10126,10 +10257,10 @@ async function authStatus(_options = {}) {
10126
10257
  expiresAt: expiresAt || null
10127
10258
  });
10128
10259
  } else {
10129
- clack3.intro(pc4.bold("Wraps \u203A Auth Status"));
10130
- clack3.log.info(`Token: ${masked} (${tokenType})`);
10260
+ clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
10261
+ clack4.log.info(`Token: ${masked} (${tokenType})`);
10131
10262
  if (expiresAt) {
10132
- clack3.log.info(`Expires: ${new Date(expiresAt).toLocaleDateString()}`);
10263
+ clack4.log.info(`Expires: ${new Date(expiresAt).toLocaleDateString()}`);
10133
10264
  }
10134
10265
  }
10135
10266
  trackCommand("auth:status", { success: true, authenticated: true });
@@ -17766,10 +17897,18 @@ async function createSMTPCredentials(config2) {
17766
17897
 
17767
17898
  // src/infrastructure/resources/sqs.ts
17768
17899
  init_esm_shims();
17900
+ init_resource_checks();
17769
17901
  import * as aws11 from "@pulumi/aws";
17902
+ var SQS_TIMEOUTS = {
17903
+ customTimeouts: { create: "2m", update: "2m", delete: "2m" }
17904
+ };
17770
17905
  async function createSQSResources() {
17771
- const dlq = new aws11.sqs.Queue("wraps-email-events-dlq", {
17772
- name: "wraps-email-events-dlq",
17906
+ const dlqName = "wraps-email-events-dlq";
17907
+ const queueName = "wraps-email-events";
17908
+ const dlqUrl = await sqsQueueExists(dlqName);
17909
+ const queueUrl = await sqsQueueExists(queueName);
17910
+ const dlqConfig = {
17911
+ name: dlqName,
17773
17912
  messageRetentionSeconds: 1209600,
17774
17913
  // 14 days
17775
17914
  tags: {
@@ -17777,9 +17916,13 @@ async function createSQSResources() {
17777
17916
  Service: "email",
17778
17917
  Description: "Dead letter queue for failed SES event processing"
17779
17918
  }
17919
+ };
17920
+ const dlq = new aws11.sqs.Queue(dlqName, dlqConfig, {
17921
+ ...SQS_TIMEOUTS,
17922
+ ...dlqUrl ? { import: dlqUrl } : {}
17780
17923
  });
17781
- const queue = new aws11.sqs.Queue("wraps-email-events", {
17782
- name: "wraps-email-events",
17924
+ const queueConfig = {
17925
+ name: queueName,
17783
17926
  visibilityTimeoutSeconds: 300,
17784
17927
  // Must be >= Lambda timeout (5 minutes)
17785
17928
  messageRetentionSeconds: 345600,
@@ -17798,6 +17941,10 @@ async function createSQSResources() {
17798
17941
  Service: "email",
17799
17942
  Description: "Queue for SES email events from EventBridge"
17800
17943
  }
17944
+ };
17945
+ const queue = new aws11.sqs.Queue(queueName, queueConfig, {
17946
+ ...SQS_TIMEOUTS,
17947
+ ...queueUrl ? { import: queueUrl } : {}
17801
17948
  });
17802
17949
  return {
17803
17950
  queue,
@@ -18551,6 +18698,8 @@ async function connect2(options) {
18551
18698
  throw new Error(`Preview failed: ${msg}`);
18552
18699
  }
18553
18700
  }
18701
+ let pulumiLog = [];
18702
+ const debugPulumi = process.env.WRAPS_DEBUG === "1";
18554
18703
  let outputs;
18555
18704
  try {
18556
18705
  outputs = await progress.execute(
@@ -18588,8 +18737,14 @@ async function connect2(options) {
18588
18737
  `wraps-${identity.accountId}-${region}`
18589
18738
  );
18590
18739
  await stack.setConfig("aws:region", { value: region });
18591
- const upResult = await stack.up({ onOutput: () => {
18592
- } });
18740
+ const upResult = await stack.up({
18741
+ onOutput: (msg) => {
18742
+ pulumiLog.push(msg);
18743
+ if (debugPulumi) {
18744
+ process.stderr.write(msg);
18745
+ }
18746
+ }
18747
+ });
18593
18748
  const pulumiOutputs = upResult.outputs;
18594
18749
  return {
18595
18750
  roleArn: pulumiOutputs.roleArn?.value,
@@ -18614,9 +18769,22 @@ async function connect2(options) {
18614
18769
  trackError("STACK_LOCKED", "email:connect", { step: "deploy" });
18615
18770
  throw errors.stackLocked();
18616
18771
  }
18772
+ if (pulumiLog.length > 0) {
18773
+ const tail = pulumiLog.join("").split("\n").slice(-60).join("\n");
18774
+ const redactedTail = redactSensitiveValues(tail);
18775
+ process.stderr.write(
18776
+ `
18777
+ ${pc19.dim("\u2500\u2500\u2500 Pulumi output (last 60 lines, redacted) \u2500\u2500\u2500")}
18778
+ ${redactedTail}
18779
+ ${pc19.dim("\u2500\u2500\u2500 end Pulumi output \u2500\u2500\u2500")}
18780
+
18781
+ `
18782
+ );
18783
+ }
18617
18784
  trackError("DEPLOYMENT_FAILED", "email:connect", { step: "deploy" });
18618
18785
  throw new Error(`Pulumi deployment failed: ${msg}`);
18619
18786
  }
18787
+ pulumiLog = [];
18620
18788
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
18621
18789
  const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
18622
18790
  const hostedZone = await findHostedZone2(outputs.domain, region);
@@ -21089,6 +21257,77 @@ Enable it: ${pc24.cyan("wraps email inbound init")}
21089
21257
  }
21090
21258
  console.log();
21091
21259
  }
21260
+ var NOT_VERIFIED_PATTERN = /not verified/i;
21261
+ function mapInboundTestSendError(error, ctx) {
21262
+ if (error instanceof WrapsError) {
21263
+ return error;
21264
+ }
21265
+ if (!(error instanceof Error)) {
21266
+ return new WrapsError(
21267
+ `Failed to send inbound test email to ${ctx.recipient}`,
21268
+ "INBOUND_TEST_SEND_FAILED",
21269
+ "An unexpected error occurred while sending the test email.\n\nCheck infrastructure status:\n wraps email status\n wraps email doctor",
21270
+ "https://wraps.dev/docs/guides/email/troubleshooting"
21271
+ );
21272
+ }
21273
+ const name = error.name;
21274
+ const message = error.message || "";
21275
+ if (name === "MailFromDomainNotVerifiedException") {
21276
+ return new WrapsError(
21277
+ `Custom MAIL FROM domain is not verified for ${ctx.domain}`,
21278
+ "INBOUND_TEST_MAIL_FROM_NOT_VERIFIED",
21279
+ `The MAIL FROM domain configured for "${ctx.domain}" is not fully verified.
21280
+
21281
+ Verify DNS records:
21282
+ wraps email verify
21283
+
21284
+ Or remove the custom MAIL FROM domain in the SES console and retry.`,
21285
+ "https://docs.aws.amazon.com/ses/latest/dg/mail-from.html"
21286
+ );
21287
+ }
21288
+ if (name === "MessageRejected" || name === "InvalidParameterValue" || NOT_VERIFIED_PATTERN.test(message)) {
21289
+ return new WrapsError(
21290
+ `SES rejected the inbound test send: ${message || name}`,
21291
+ "INBOUND_TEST_MESSAGE_REJECTED",
21292
+ [
21293
+ `Tried to send: ${ctx.source} \u2192 ${ctx.recipient} (region ${ctx.region})`,
21294
+ "",
21295
+ "Most likely causes:",
21296
+ ` \u2022 Your SES account is in the sandbox and ${ctx.recipient} is not a verified address`,
21297
+ ` \u2022 The sender domain "${ctx.domain}" is not verified for sending in ${ctx.region}`,
21298
+ ` \u2022 The receiving domain "${ctx.receivingDomain}" is verified for receiving (MX) but not for sending`,
21299
+ "",
21300
+ "Check status:",
21301
+ " wraps email status",
21302
+ " wraps email doctor",
21303
+ "",
21304
+ "Exit the SES sandbox to send to any address:",
21305
+ " https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html"
21306
+ ].join("\n"),
21307
+ "https://wraps.dev/docs/guides/email/troubleshooting"
21308
+ );
21309
+ }
21310
+ if (name === "AccountSendingPausedException" || name === "ConfigurationSetSendingPausedException") {
21311
+ return new WrapsError(
21312
+ "SES sending is paused for this account",
21313
+ "INBOUND_TEST_SENDING_PAUSED",
21314
+ "Your SES account or configuration set has sending paused, usually due to a high bounce or complaint rate.\n\nCheck the SES Reputation Dashboard in the AWS console and resume sending once the issue is resolved.",
21315
+ "https://docs.aws.amazon.com/ses/latest/dg/reputationdashboard.html"
21316
+ );
21317
+ }
21318
+ if (name === "AccessDeniedException" || name === "AccessDenied") {
21319
+ return new WrapsError(
21320
+ `IAM permission denied: ses:SendEmail in ${ctx.region}`,
21321
+ "INBOUND_TEST_PERMISSION_DENIED",
21322
+ `Your AWS credentials lack the "ses:SendEmail" permission in region ${ctx.region}.
21323
+
21324
+ View required SES permissions:
21325
+ wraps permissions --service email --json`,
21326
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
21327
+ );
21328
+ }
21329
+ return error;
21330
+ }
21092
21331
  async function inboundTest(options) {
21093
21332
  if (!isJsonMode()) {
21094
21333
  clack22.intro(pc24.bold("Inbound Email Test"));
@@ -21112,34 +21351,45 @@ Enable it: ${pc24.cyan("wraps email inbound init")}
21112
21351
  const receivingDomain = inbound.receivingDomain || (inbound.subdomain ? `${inbound.subdomain}.${emailConfig.domain}` : emailConfig.domain || "");
21113
21352
  const bucketName = inbound.bucketName || `wraps-inbound-${identity.accountId}-${region}`;
21114
21353
  const testRecipient = `test@${receivingDomain}`;
21354
+ const testSource = `test@${emailConfig.domain}`;
21115
21355
  const testSubject = `Wraps Inbound Test - ${(/* @__PURE__ */ new Date()).toISOString()}`;
21116
21356
  await progress.execute(`Sending test email to ${testRecipient}`, async () => {
21117
21357
  const { SESClient: SESClient7, SendEmailCommand: SendEmailCommand2 } = await import("@aws-sdk/client-ses");
21118
21358
  const ses = new SESClient7({ region });
21119
- await ses.send(
21120
- new SendEmailCommand2({
21121
- Source: `test@${emailConfig.domain}`,
21122
- Destination: {
21123
- ToAddresses: [testRecipient]
21124
- },
21125
- Message: {
21126
- Subject: { Data: testSubject },
21127
- Body: {
21128
- Text: {
21129
- Data: "This is a test email from Wraps CLI to verify inbound email processing."
21130
- },
21131
- Html: {
21132
- Data: "<h1>Wraps Inbound Test</h1><p>This email was sent to verify inbound email processing is working correctly.</p>"
21359
+ try {
21360
+ await ses.send(
21361
+ new SendEmailCommand2({
21362
+ Source: testSource,
21363
+ Destination: {
21364
+ ToAddresses: [testRecipient]
21365
+ },
21366
+ Message: {
21367
+ Subject: { Data: testSubject },
21368
+ Body: {
21369
+ Text: {
21370
+ Data: "This is a test email from Wraps CLI to verify inbound email processing."
21371
+ },
21372
+ Html: {
21373
+ Data: "<h1>Wraps Inbound Test</h1><p>This email was sent to verify inbound email processing is working correctly.</p>"
21374
+ }
21133
21375
  }
21134
21376
  }
21135
- }
21136
- })
21137
- );
21377
+ })
21378
+ );
21379
+ } catch (error) {
21380
+ throw mapInboundTestSendError(error, {
21381
+ source: testSource,
21382
+ recipient: testRecipient,
21383
+ domain: emailConfig.domain || "",
21384
+ receivingDomain,
21385
+ region
21386
+ });
21387
+ }
21138
21388
  });
21139
- const spinner10 = clack22.spinner();
21140
- spinner10.start("Waiting for email to be processed...");
21141
21389
  const { S3Client: S3Client2, ListObjectsV2Command: ListObjectsV2Command2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
21142
21390
  const s34 = new S3Client2({ region });
21391
+ const spinner10 = clack22.spinner();
21392
+ spinner10.start("Waiting for email to be processed...");
21143
21393
  let found = false;
21144
21394
  const startTime = Date.now();
21145
21395
  const timeout = 3e4;