@wraps.dev/cli 2.18.7 → 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,1564 +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
- };
677
+ console.log();
1157
678
  }
1158
- } catch {
679
+ if (wrapsError.docsUrl) {
680
+ console.log(`${pc.dim("Documentation:")}`);
681
+ console.log(` ${pc.blue(wrapsError.docsUrl)}
682
+ `);
683
+ }
684
+ process.exit(1);
1159
685
  }
1160
- return {
1161
- valid: false,
1162
- expiresAt: null,
1163
- expired: true,
1164
- minutesRemaining: null,
1165
- startUrl: null
1166
- };
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);
695
+ }
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);
1167
701
  }
1168
- function getActiveSSOProfile(profiles) {
1169
- const currentProfile = process.env.AWS_PROFILE || "default";
1170
- return profiles.find((p) => p.name === currentProfile) || null;
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
+ );
752
+ }
1171
753
  }
1172
- function getSSOLoginCommand(profile) {
1173
- if (profile && profile !== "default") {
1174
- return `aws sso login --profile ${profile}`;
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"
782
+ );
1175
783
  }
1176
- return "aws sso login";
1177
784
  }
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
1211
- }
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]);
1230
- }
1231
- }
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
- }
1241
- }
1242
- return profiles;
1243
- }
1244
- var init_aws_detection = __esm({
1245
- "src/utils/shared/aws-detection.ts"() {
785
+ var WrapsError, errors;
786
+ var init_errors = __esm({
787
+ "src/utils/shared/errors.ts"() {
1246
788
  "use strict";
1247
789
  init_esm_shims();
1248
- }
1249
- });
1250
-
1251
- // src/utils/shared/errors.ts
1252
- var errors_exports = {};
1253
- __export(errors_exports, {
1254
- WrapsError: () => WrapsError,
1255
- awsErrorToWrapsError: () => awsErrorToWrapsError,
1256
- classifyDNSError: () => classifyDNSError,
1257
- errors: () => errors,
1258
- handleCLIError: () => handleCLIError,
1259
- isAWSError: () => isAWSError,
1260
- isAWSNotFoundError: () => isAWSNotFoundError,
1261
- isPulumiError: () => isPulumiError,
1262
- parseAWSError: () => parseAWSError,
1263
- parsePulumiError: () => parsePulumiError,
1264
- sanitizeErrorMessage: () => sanitizeErrorMessage
1265
- });
1266
- import * as clack4 from "@clack/prompts";
1267
- import pc5 from "picocolors";
1268
- function isAWSError(error) {
1269
- if (!(error instanceof Error)) {
1270
- return false;
1271
- }
1272
- const awsErrorNames = [
1273
- "ExpiredTokenException",
1274
- "InvalidClientTokenId",
1275
- "AccessDenied",
1276
- "AccessDeniedException",
1277
- "UnauthorizedAccess",
1278
- "InvalidAccessKeyId",
1279
- "SignatureDoesNotMatch",
1280
- "UnrecognizedClientException",
1281
- "CredentialsError",
1282
- "TokenRefreshRequired",
1283
- "SSOTokenExpired"
1284
- ];
1285
- return awsErrorNames.includes(error.name) || "$metadata" in error;
1286
- }
1287
- function classifyDNSError(error) {
1288
- if (!(error instanceof Error)) {
1289
- return "unknown";
1290
- }
1291
- const code = error.code;
1292
- if (code === "ENOTFOUND" || code === "ENODATA") {
1293
- return "missing";
1294
- }
1295
- if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
1296
- return "network";
1297
- }
1298
- return "unknown";
1299
- }
1300
- function isAWSNotFoundError(error) {
1301
- if (!(error instanceof Error)) return false;
1302
- const awsError = error;
1303
- return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || awsError.$metadata?.httpStatusCode === 404;
1304
- }
1305
- function isPulumiError(error) {
1306
- if (!(error instanceof Error)) {
1307
- return false;
1308
- }
1309
- return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
1310
- }
1311
- function parseAWSError(error) {
1312
- const errorName = error.name || "UnknownError";
1313
- const actionMatch = error.message?.match(/when calling the (\w+) operation/i);
1314
- const action = actionMatch?.[1];
1315
- const resourceMatch = error.message?.match(/resource[:\s]+([^\s,]+)/i);
1316
- const resource = resourceMatch?.[1];
1317
- return { code: errorName, action, resource };
1318
- }
1319
- function parsePulumiError(error) {
1320
- const message = error.message || "";
1321
- if (message.includes("AccessDenied") || message.includes("access denied")) {
1322
- const actionMatch = message.match(
1323
- /(?:action|operation)[:\s]+["']?(\w+:\w+)["']?/i
1324
- );
1325
- if (actionMatch) {
1326
- const [service] = actionMatch[1].split(":");
1327
- return {
1328
- code: "IAM_PERMISSION_DENIED",
1329
- iamAction: actionMatch[1],
1330
- service
1331
- };
1332
- }
1333
- if (message.includes("ses:") || message.includes("SES")) {
1334
- return { code: "SES_PERMISSION_DENIED", service: "ses" };
1335
- }
1336
- if (message.includes("dynamodb:") || message.includes("DynamoDB")) {
1337
- return { code: "DYNAMODB_PERMISSION_DENIED", service: "dynamodb" };
1338
- }
1339
- if (message.includes("lambda:") || message.includes("Lambda")) {
1340
- return { code: "LAMBDA_PERMISSION_DENIED", service: "lambda" };
1341
- }
1342
- if (message.includes("events:") || message.includes("EventBridge")) {
1343
- return { code: "EVENTBRIDGE_PERMISSION_DENIED", service: "events" };
1344
- }
1345
- if (message.includes("sqs:") || message.includes("SQS")) {
1346
- return { code: "SQS_PERMISSION_DENIED", service: "sqs" };
1347
- }
1348
- if (message.includes("iam:") || message.includes("IAM")) {
1349
- return { code: "IAM_PERMISSION_DENIED", service: "iam" };
1350
- }
1351
- return { code: "IAM_PERMISSION_DENIED" };
1352
- }
1353
- if (message.includes("AlreadyExists") || message.includes("already exists") || message.includes("already exist") || message.includes("ResourceConflictException") || message.includes("ResourceInUse") || message.includes("EntityAlreadyExists")) {
1354
- const nameMatch = message.match(/error creating '([^']+)'/);
1355
- const typeMatch = message.match(/\((aws:[^)]+)\)/);
1356
- return {
1357
- code: "RESOURCE_CONFLICT",
1358
- resourceName: nameMatch?.[1],
1359
- resourceType: typeMatch?.[1]
1360
- };
1361
- }
1362
- if (message.includes("stack is currently locked")) {
1363
- return { code: "STACK_LOCKED" };
1364
- }
1365
- return { code: "PULUMI_ERROR" };
1366
- }
1367
- function sanitizeErrorMessage(error) {
1368
- if (!error) {
1369
- return "Unknown error";
1370
- }
1371
- let message = error instanceof Error ? error.message : String(error);
1372
- message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
1373
- message = message.replace(
1374
- /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
1375
- "[EMAIL]"
1376
- );
1377
- message = message.replace(
1378
- /(?<!\.amazonaws\.com|\.aws\.amazon\.com)\b[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b/g,
1379
- (match) => {
1380
- if (match.includes("amazonaws") || match.includes("aws.amazon")) {
1381
- return match;
1382
- }
1383
- return "[DOMAIN]";
1384
- }
1385
- );
1386
- message = message.replace(
1387
- /arn:aws:[^:]+:[^:]*:\d{12}:/g,
1388
- "arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
1389
- );
1390
- if (message.length > 500) {
1391
- message = `${message.slice(0, 500)}...`;
1392
- }
1393
- return message;
1394
- }
1395
- function handleCLIError(error, command) {
1396
- const cmdContext = command || "unknown";
1397
- if (isJsonMode()) {
1398
- let code = "UNKNOWN_ERROR";
1399
- let message = "An unexpected error occurred";
1400
- let suggestion;
1401
- let docsUrl;
1402
- if (error instanceof WrapsError) {
1403
- trackError(error.code, cmdContext);
1404
- code = error.code;
1405
- message = error.message;
1406
- suggestion = error.suggestion;
1407
- docsUrl = error.docsUrl;
1408
- } else if (isAWSError(error)) {
1409
- const parsed = parseAWSError(error);
1410
- code = `AWS_${parsed.code}`;
1411
- trackError(code, cmdContext, { action: parsed.action });
1412
- const wrapsErr = awsErrorToWrapsError(parsed.code, parsed.action, error);
1413
- message = wrapsErr.message;
1414
- suggestion = wrapsErr.suggestion;
1415
- docsUrl = wrapsErr.docsUrl;
1416
- } else if (isPulumiError(error)) {
1417
- const parsed = parsePulumiError(error);
1418
- code = `PULUMI_${parsed.code}`;
1419
- trackError(code, cmdContext, {
1420
- iamAction: parsed.iamAction,
1421
- service: parsed.service,
1422
- errorType: error?.constructor?.name
1423
- });
1424
- const wrapsErr = pulumiErrorToWrapsError(
1425
- parsed.code,
1426
- parsed.iamAction,
1427
- parsed.service,
1428
- parsed.resourceName,
1429
- parsed.resourceType
1430
- );
1431
- message = wrapsErr.message;
1432
- suggestion = wrapsErr.suggestion;
1433
- docsUrl = wrapsErr.docsUrl;
1434
- } else {
1435
- trackError("UNHANDLED_ERROR", cmdContext, {
1436
- errorType: error instanceof Error ? error.constructor.name : typeof error,
1437
- message: sanitizeErrorMessage(error)
1438
- });
1439
- message = error instanceof Error ? error.message : String(error || message);
1440
- }
1441
- jsonError(cmdContext, { code, message, suggestion, docsUrl });
1442
- process.exit(1);
1443
- }
1444
- console.error("");
1445
- if (error instanceof WrapsError) {
1446
- trackError(error.code, cmdContext);
1447
- clack4.log.error(error.message);
1448
- if (error.suggestion) {
1449
- console.log(`
1450
- ${pc5.yellow("Suggestion:")}`);
1451
- const lines = error.suggestion.split("\n");
1452
- for (const line of lines) {
1453
- console.log(` ${pc5.white(line)}`);
1454
- }
1455
- console.log();
1456
- }
1457
- if (error.docsUrl) {
1458
- console.log(`${pc5.dim("Documentation:")}`);
1459
- console.log(` ${pc5.blue(error.docsUrl)}
1460
- `);
1461
- }
1462
- process.exit(1);
1463
- }
1464
- if (isAWSError(error)) {
1465
- const { code, action } = parseAWSError(error);
1466
- trackError(`AWS_${code}`, cmdContext, { action });
1467
- const wrapsError = awsErrorToWrapsError(code, action, error);
1468
- clack4.log.error(wrapsError.message);
1469
- if (wrapsError.suggestion) {
1470
- console.log(`
1471
- ${pc5.yellow("Suggestion:")}`);
1472
- const lines = wrapsError.suggestion.split("\n");
1473
- for (const line of lines) {
1474
- console.log(` ${pc5.white(line)}`);
1475
- }
1476
- console.log();
1477
- }
1478
- if (wrapsError.docsUrl) {
1479
- console.log(`${pc5.dim("Documentation:")}`);
1480
- console.log(` ${pc5.blue(wrapsError.docsUrl)}
1481
- `);
1482
- }
1483
- process.exit(1);
1484
- }
1485
- if (isPulumiError(error)) {
1486
- const { code, iamAction, service, resourceName, resourceType } = parsePulumiError(error);
1487
- trackError(`PULUMI_${code}`, cmdContext, {
1488
- iamAction,
1489
- service,
1490
- errorType: error?.constructor?.name
1491
- });
1492
- const wrapsError = pulumiErrorToWrapsError(
1493
- code,
1494
- iamAction,
1495
- service,
1496
- resourceName,
1497
- resourceType
1498
- );
1499
- clack4.log.error(wrapsError.message);
1500
- if (wrapsError.suggestion) {
1501
- console.log(`
1502
- ${pc5.yellow("Suggestion:")}`);
1503
- const lines = wrapsError.suggestion.split("\n");
1504
- for (const line of lines) {
1505
- console.log(` ${pc5.white(line)}`);
1506
- }
1507
- console.log();
1508
- }
1509
- if (wrapsError.docsUrl) {
1510
- console.log(`${pc5.dim("Documentation:")}`);
1511
- console.log(` ${pc5.blue(wrapsError.docsUrl)}
1512
- `);
1513
- }
1514
- process.exit(1);
1515
- }
1516
- trackError("UNHANDLED_ERROR", cmdContext, {
1517
- errorType: error instanceof Error ? error.constructor.name : typeof error,
1518
- message: sanitizeErrorMessage(error)
1519
- });
1520
- clack4.log.error("An unexpected error occurred");
1521
- if (error instanceof Error) {
1522
- console.error(pc5.dim(error.message));
1523
- } else if (typeof error === "string") {
1524
- console.error(error);
1525
- }
1526
- console.log(`
1527
- ${pc5.dim("If this persists, please report at:")}`);
1528
- console.log(` ${pc5.blue("https://github.com/wraps-team/wraps/issues")}
1529
- `);
1530
- process.exit(1);
1531
- }
1532
- function awsErrorToWrapsError(code, action, originalError) {
1533
- switch (code) {
1534
- // Credential / token errors — these mean the request never reached the API
1535
- case "ExpiredTokenException":
1536
- case "TokenRefreshRequired":
1537
- case "SSOTokenExpired":
1538
- return errors.sessionTokenExpired();
1539
- case "InvalidClientTokenId":
1540
- case "InvalidAccessKeyId":
1541
- case "SignatureDoesNotMatch":
1542
- case "UnrecognizedClientException":
1543
- return errors.accessKeyInvalid();
1544
- // IAM permission errors — request reached AWS but was denied
1545
- case "AccessDenied":
1546
- case "AccessDeniedException":
1547
- case "UnauthorizedAccess":
1548
- return errors.iamPermissionDenied(
1549
- action || "unknown",
1550
- "AWS resource",
1551
- "Ensure your IAM user/role has the required permissions."
1552
- );
1553
- // SES SendEmail errors — request reached SES but was rejected
1554
- case "MessageRejected":
1555
- return errors.sesMessageRejected(sanitizeErrorMessage(originalError));
1556
- case "MailFromDomainNotVerifiedException":
1557
- return errors.sesMailFromNotVerified(sanitizeErrorMessage(originalError));
1558
- case "AccountSendingPausedException":
1559
- return errors.sesAccountSendingPaused();
1560
- case "ConfigurationSetSendingPausedException":
1561
- return errors.sesConfigSetSendingPaused();
1562
- case "ConfigurationSetDoesNotExistException":
1563
- return errors.sesConfigSetMissing(sanitizeErrorMessage(originalError));
1564
- // Throughput / quota errors
1565
- case "Throttling":
1566
- case "ThrottlingException":
1567
- case "TooManyRequestsException":
1568
- return errors.awsThrottled(action);
1569
- case "LimitExceededException":
1570
- case "ServiceQuotaExceededException":
1571
- return errors.awsLimitExceeded(
1572
- action,
1573
- sanitizeErrorMessage(originalError)
1574
- );
1575
- // Anything else — surface the real error instead of lying about credentials
1576
- default:
1577
- return errors.awsUnknownError(
1578
- code,
1579
- action,
1580
- sanitizeErrorMessage(originalError)
1581
- );
1582
- }
1583
- }
1584
- function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourceType) {
1585
- switch (code) {
1586
- case "RESOURCE_CONFLICT":
1587
- return errors.resourceConflict(
1588
- resourceName || "unknown resource",
1589
- resourceType
1590
- );
1591
- case "STACK_LOCKED":
1592
- return errors.stackLocked();
1593
- case "SES_PERMISSION_DENIED":
1594
- return errors.sesPermissionDenied(iamAction || "unknown");
1595
- case "DYNAMODB_PERMISSION_DENIED":
1596
- return errors.dynamoDBPermissionDenied();
1597
- case "LAMBDA_PERMISSION_DENIED":
1598
- return errors.lambdaPermissionDenied();
1599
- case "EVENTBRIDGE_PERMISSION_DENIED":
1600
- return errors.eventBridgePermissionDenied();
1601
- case "SQS_PERMISSION_DENIED":
1602
- return errors.sqsPermissionDenied();
1603
- case "IAM_PERMISSION_DENIED":
1604
- return errors.iamPermissionDenied(
1605
- iamAction || "unknown",
1606
- "AWS resource",
1607
- service ? `Your IAM user/role needs ${service.toUpperCase()} permissions.` : "Ensure your IAM user/role has the required permissions."
1608
- );
1609
- default:
1610
- return errors.pulumiError("Deployment failed");
1611
- }
1612
- }
1613
- var WrapsError, errors;
1614
- var init_errors = __esm({
1615
- "src/utils/shared/errors.ts"() {
1616
- "use strict";
1617
- init_esm_shims();
1618
- init_events();
1619
- init_json_output();
1620
- WrapsError = class extends Error {
1621
- constructor(message, code, suggestion, docsUrl) {
1622
- super(message);
1623
- this.code = code;
1624
- this.suggestion = suggestion;
1625
- this.docsUrl = docsUrl;
1626
- this.name = "WrapsError";
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";
1627
799
  }
1628
800
  };
1629
801
  errors = {
@@ -1938,204 +1110,1077 @@ You may need to merge your existing rules into the wraps rule set.`,
1938
1110
  }
1939
1111
  });
1940
1112
 
1941
- // src/utils/shared/aws.ts
1942
- var aws_exports = {};
1943
- __export(aws_exports, {
1944
- SES_REGIONS: () => SES_REGIONS,
1945
- checkRegion: () => checkRegion,
1946
- getACMCertificateStatus: () => getACMCertificateStatus,
1947
- getAWSRegion: () => getAWSRegion,
1948
- getSESAccountStatus: () => getSESAccountStatus,
1949
- isSESSandbox: () => isSESSandbox,
1950
- listSESDomains: () => listSESDomains,
1951
- validateAWSCredentials: () => validateAWSCredentials,
1952
- validateAWSCredentialsWithDetails: () => validateAWSCredentialsWithDetails
1113
+ // src/utils/shared/aws.ts
1114
+ var aws_exports = {};
1115
+ __export(aws_exports, {
1116
+ SES_REGIONS: () => SES_REGIONS,
1117
+ checkRegion: () => checkRegion,
1118
+ getACMCertificateStatus: () => getACMCertificateStatus,
1119
+ getAWSRegion: () => getAWSRegion,
1120
+ getSESAccountStatus: () => getSESAccountStatus,
1121
+ isSESSandbox: () => isSESSandbox,
1122
+ listSESDomains: () => listSESDomains,
1123
+ resolveAWSCredentialsToEnv: () => resolveAWSCredentialsToEnv,
1124
+ validateAWSCredentials: () => validateAWSCredentials,
1125
+ validateAWSCredentialsWithDetails: () => validateAWSCredentialsWithDetails
1126
+ });
1127
+ import { ACMClient, DescribeCertificateCommand } from "@aws-sdk/client-acm";
1128
+ import {
1129
+ GetIdentityVerificationAttributesCommand,
1130
+ ListIdentitiesCommand,
1131
+ SESClient
1132
+ } from "@aws-sdk/client-ses";
1133
+ import { GetAccountCommand, SESv2Client } from "@aws-sdk/client-sesv2";
1134
+ import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
1135
+ async function validateAWSCredentials() {
1136
+ const result = await validateAWSCredentialsWithDetails();
1137
+ return result.identity;
1138
+ }
1139
+ async function validateAWSCredentialsWithDetails() {
1140
+ const state = await detectAWSState();
1141
+ const warnings = [];
1142
+ if (state.sso.configured && state.sso.tokenStatus?.expired) {
1143
+ const profile = state.sso.activeProfile?.name;
1144
+ throw errors.ssoSessionExpired(profile);
1145
+ }
1146
+ if (state.sso.configured && state.sso.tokenStatus?.valid && state.sso.tokenStatus.minutesRemaining !== null && state.sso.tokenStatus.minutesRemaining < 15) {
1147
+ const minutes = state.sso.tokenStatus.minutesRemaining;
1148
+ const loginCmd = getSSOLoginCommand(state.sso.activeProfile?.name);
1149
+ warnings.push(
1150
+ `SSO session expires in ${minutes} minute${minutes !== 1 ? "s" : ""}. Run "${loginCmd}" to refresh.`
1151
+ );
1152
+ }
1153
+ const currentProfile = getCurrentProfile();
1154
+ if (currentProfile && currentProfile !== "default") {
1155
+ const availableProfiles = getConfiguredProfiles();
1156
+ if (!availableProfiles.includes(currentProfile)) {
1157
+ throw errors.profileNotFound(currentProfile, availableProfiles);
1158
+ }
1159
+ }
1160
+ const sts = new STSClient2({ region: "us-east-1" });
1161
+ try {
1162
+ const identity = await sts.send(new GetCallerIdentityCommand2({}));
1163
+ return {
1164
+ identity: {
1165
+ accountId: identity.Account,
1166
+ userId: identity.UserId,
1167
+ arn: identity.Arn
1168
+ },
1169
+ credentialSource: state.credentialSource,
1170
+ warnings
1171
+ };
1172
+ } catch (error) {
1173
+ if (error instanceof Error) {
1174
+ switch (error.name) {
1175
+ case "ExpiredTokenException":
1176
+ case "TokenRefreshRequired":
1177
+ throw errors.sessionTokenExpired();
1178
+ case "InvalidClientTokenId":
1179
+ case "InvalidAccessKeyId":
1180
+ case "SignatureDoesNotMatch":
1181
+ throw errors.accessKeyInvalid();
1182
+ case "CredentialsError":
1183
+ case "CredentialsProviderError":
1184
+ if (error.message?.includes("Could not load credentials")) {
1185
+ throw errors.credentialsFileMissing();
1186
+ }
1187
+ break;
1188
+ case "UnrecognizedClientException":
1189
+ throw errors.accessKeyInvalid();
1190
+ }
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
1953
1621
  });
1954
- import { ACMClient, DescribeCertificateCommand } from "@aws-sdk/client-acm";
1955
- import {
1956
- GetIdentityVerificationAttributesCommand,
1957
- ListIdentitiesCommand,
1958
- SESClient
1959
- } from "@aws-sdk/client-ses";
1960
- import { GetAccountCommand, SESv2Client } from "@aws-sdk/client-sesv2";
1961
- import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
1962
- async function validateAWSCredentials() {
1963
- const result = await validateAWSCredentialsWithDetails();
1964
- return result.identity;
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");
1965
1628
  }
1966
- async function validateAWSCredentialsWithDetails() {
1967
- const state = await detectAWSState();
1968
- const warnings = [];
1969
- if (state.sso.configured && state.sso.tokenStatus?.expired) {
1970
- const profile = state.sso.activeProfile?.name;
1971
- throw errors.ssoSessionExpired(profile);
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 });
1972
1636
  }
1973
- if (state.sso.configured && state.sso.tokenStatus?.valid && state.sso.tokenStatus.minutesRemaining !== null && state.sso.tokenStatus.minutesRemaining < 15) {
1974
- const minutes = state.sso.tokenStatus.minutesRemaining;
1975
- const loginCmd = getSSOLoginCommand(state.sso.activeProfile?.name);
1976
- warnings.push(
1977
- `SSO session expires in ${minutes} minute${minutes !== 1 ? "s" : ""}. Run "${loginCmd}" to refresh.`
1978
- );
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
+ };
1979
1855
  }
1980
- const currentProfile = getCurrentProfile();
1981
- if (currentProfile && currentProfile !== "default") {
1982
- const availableProfiles = getConfiguredProfiles();
1983
- if (!availableProfiles.includes(currentProfile)) {
1984
- throw errors.profileNotFound(currentProfile, availableProfiles);
1985
- }
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();
1986
1866
  }
1987
- const sts = new STSClient2({ region: "us-east-1" });
1988
- try {
1989
- const identity = await sts.send(new GetCallerIdentityCommand2({}));
1990
- return {
1991
- identity: {
1992
- accountId: identity.Account,
1993
- userId: identity.UserId,
1994
- arn: identity.Arn
1995
- },
1996
- credentialSource: state.credentialSource,
1997
- warnings
1998
- };
1999
- } catch (error) {
2000
- if (error instanceof Error) {
2001
- switch (error.name) {
2002
- case "ExpiredTokenException":
2003
- case "TokenRefreshRequired":
2004
- throw errors.sessionTokenExpired();
2005
- case "InvalidClientTokenId":
2006
- case "InvalidAccessKeyId":
2007
- case "SignatureDoesNotMatch":
2008
- throw errors.accessKeyInvalid();
2009
- case "CredentialsError":
2010
- case "CredentialsProviderError":
2011
- if (error.message?.includes("Could not load credentials")) {
2012
- throw errors.credentialsFileMissing();
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;
2013
1908
  }
2014
- break;
2015
- case "UnrecognizedClientException":
2016
- throw errors.accessKeyInvalid();
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;
2017
2057
  }
2018
- }
2019
- throw errors.noAWSCredentials();
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;
2020
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);
2021
2137
  }
2022
- async function checkRegion(region) {
2023
- 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
+ });
2024
2145
  }
2025
- async function getAWSRegion() {
2026
- if (process.env.AWS_REGION) {
2027
- return process.env.AWS_REGION;
2028
- }
2029
- if (process.env.AWS_DEFAULT_REGION) {
2030
- return process.env.AWS_DEFAULT_REGION;
2031
- }
2032
- return "us-east-1";
2146
+ function trackServiceDeployed(service, metadata) {
2147
+ const client = getTelemetryClient();
2148
+ client.track("service:deployed", {
2149
+ service,
2150
+ ...metadata
2151
+ });
2033
2152
  }
2034
- async function listSESDomains(region) {
2035
- const ses = new SESClient({ region });
2036
- try {
2037
- const identitiesResponse = await ses.send(
2038
- new ListIdentitiesCommand({
2039
- IdentityType: "Domain"
2040
- })
2041
- );
2042
- const identities = identitiesResponse.Identities || [];
2043
- if (identities.length === 0) {
2044
- return [];
2045
- }
2046
- const attributesResponse = await ses.send(
2047
- new GetIdentityVerificationAttributesCommand({
2048
- Identities: identities
2049
- })
2050
- );
2051
- const attributes = attributesResponse.VerificationAttributes || {};
2052
- return identities.map((domain) => ({
2053
- domain,
2054
- verified: attributes[domain]?.VerificationStatus === "Success"
2055
- }));
2056
- } catch {
2057
- return [];
2058
- }
2153
+ function trackError(errorCode, command, metadata) {
2154
+ const client = getTelemetryClient();
2155
+ client.track("error:occurred", {
2156
+ error_code: errorCode,
2157
+ command,
2158
+ ...metadata
2159
+ });
2059
2160
  }
2060
- async function getSESAccountStatus(region) {
2061
- const sesv22 = new SESv2Client({ region });
2062
- try {
2063
- const response = await sesv22.send(new GetAccountCommand({}));
2064
- return {
2065
- isSandbox: !response.ProductionAccessEnabled,
2066
- sendQuota: response.SendQuota ? {
2067
- max24HourSend: response.SendQuota.Max24HourSend ?? 0,
2068
- maxSendRate: response.SendQuota.MaxSendRate ?? 0,
2069
- sentLast24Hours: response.SendQuota.SentLast24Hours ?? 0
2070
- } : void 0,
2071
- enforcementStatus: response.EnforcementStatus
2072
- };
2073
- } catch {
2074
- return { isSandbox: true, sandboxUncertain: true };
2075
- }
2161
+ function trackFeature(feature, metadata) {
2162
+ const client = getTelemetryClient();
2163
+ client.track(`feature:${feature}`, metadata || {});
2076
2164
  }
2077
- async function isSESSandbox(region) {
2078
- const status2 = await getSESAccountStatus(region);
2079
- return status2.isSandbox;
2165
+ function trackServiceUpgrade(service, metadata) {
2166
+ const client = getTelemetryClient();
2167
+ client.track("service:upgraded", {
2168
+ service,
2169
+ ...metadata
2170
+ });
2080
2171
  }
2081
- async function getACMCertificateStatus(certificateArn) {
2082
- const acm3 = new ACMClient({ region: "us-east-1" });
2083
- try {
2084
- const response = await acm3.send(
2085
- new DescribeCertificateCommand({
2086
- CertificateArn: certificateArn
2087
- })
2088
- );
2089
- const certificate = response.Certificate;
2090
- if (!certificate) {
2091
- return null;
2092
- }
2093
- const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
2094
- name: option.ResourceRecord?.Name || "",
2095
- type: option.ResourceRecord?.Type || "",
2096
- value: option.ResourceRecord?.Value || ""
2097
- })) || [];
2098
- return {
2099
- status: certificate.Status || "UNKNOWN",
2100
- domainName: certificate.DomainName || "",
2101
- validationRecords
2102
- };
2103
- } catch (error) {
2104
- console.error("Error getting ACM certificate status:", error);
2105
- return null;
2106
- }
2172
+ function trackServiceRemoved(service, metadata) {
2173
+ const client = getTelemetryClient();
2174
+ client.track("service:removed", {
2175
+ service,
2176
+ ...metadata
2177
+ });
2107
2178
  }
2108
- var SES_REGIONS;
2109
- var init_aws = __esm({
2110
- "src/utils/shared/aws.ts"() {
2179
+ var init_events = __esm({
2180
+ "src/telemetry/events.ts"() {
2111
2181
  "use strict";
2112
2182
  init_esm_shims();
2113
- init_aws_detection();
2114
- init_errors();
2115
- SES_REGIONS = [
2116
- "us-east-1",
2117
- "us-east-2",
2118
- "us-west-1",
2119
- "us-west-2",
2120
- "af-south-1",
2121
- "ap-east-1",
2122
- "ap-south-1",
2123
- "ap-northeast-1",
2124
- "ap-northeast-2",
2125
- "ap-northeast-3",
2126
- "ap-southeast-1",
2127
- "ap-southeast-2",
2128
- "ap-southeast-3",
2129
- "ca-central-1",
2130
- "eu-central-1",
2131
- "eu-west-1",
2132
- "eu-west-2",
2133
- "eu-west-3",
2134
- "eu-south-1",
2135
- "eu-north-1",
2136
- "me-south-1",
2137
- "sa-east-1"
2138
- ];
2183
+ init_client();
2139
2184
  }
2140
2185
  });
2141
2186
 
@@ -9982,14 +10027,14 @@ init_esm_shims();
9982
10027
  init_events();
9983
10028
  init_config();
9984
10029
  init_json_output();
9985
- import * as clack from "@clack/prompts";
10030
+ import * as clack2 from "@clack/prompts";
9986
10031
  import { createAuthClient } from "better-auth/client";
9987
10032
  import {
9988
10033
  deviceAuthorizationClient,
9989
10034
  organizationClient
9990
10035
  } from "better-auth/client/plugins";
9991
10036
  import open from "open";
9992
- import pc2 from "picocolors";
10037
+ import pc3 from "picocolors";
9993
10038
  function createCliAuthClient(baseURL) {
9994
10039
  return createAuthClient({
9995
10040
  baseURL,
@@ -10033,14 +10078,14 @@ async function login(options) {
10033
10078
  if (isJsonMode()) {
10034
10079
  jsonSuccess("auth.login", { tokenType: "api-key" });
10035
10080
  } else {
10036
- clack.log.success("API key saved.");
10081
+ clack2.log.success("API key saved.");
10037
10082
  }
10038
10083
  return;
10039
10084
  }
10040
- clack.intro(pc2.bold("Wraps \u203A Sign In"));
10085
+ clack2.intro(pc3.bold("Wraps \u203A Sign In"));
10041
10086
  const baseURL = getAppBaseUrl();
10042
10087
  const authClient = createCliAuthClient(baseURL);
10043
- const spinner10 = clack.spinner();
10088
+ const spinner10 = clack2.spinner();
10044
10089
  const { data: codeData, error: codeError } = await authClient.device.code({
10045
10090
  client_id: "wraps-cli"
10046
10091
  });
@@ -10051,7 +10096,7 @@ async function login(options) {
10051
10096
  method: "device"
10052
10097
  });
10053
10098
  trackError("DEVICE_AUTH_FAILED", "auth:login", { step: "request_code" });
10054
- clack.log.error("Failed to start device authorization.");
10099
+ clack2.log.error("Failed to start device authorization.");
10055
10100
  throw new Error("Failed to start device authorization.");
10056
10101
  }
10057
10102
  const {
@@ -10062,11 +10107,11 @@ async function login(options) {
10062
10107
  expires_in
10063
10108
  } = codeData;
10064
10109
  const formatted = `${user_code.slice(0, 4)}-${user_code.slice(4)}`;
10065
- clack.log.info(`Your code: ${pc2.bold(pc2.cyan(formatted))}`);
10066
- 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`)}`);
10067
10112
  try {
10068
10113
  await open(`${baseURL}/device?user_code=${user_code}`);
10069
- clack.log.info("Opening browser...");
10114
+ clack2.log.info("Opening browser...");
10070
10115
  } catch {
10071
10116
  }
10072
10117
  spinner10.start("Waiting for approval...");
@@ -10098,14 +10143,14 @@ async function login(options) {
10098
10143
  duration_ms: Date.now() - startTime,
10099
10144
  method: "device"
10100
10145
  });
10101
- clack.log.success("Signed in successfully.");
10146
+ clack2.log.success("Signed in successfully.");
10102
10147
  if (organizations.length === 1) {
10103
- clack.log.info(`Organization: ${pc2.cyan(organizations[0].name)}`);
10148
+ clack2.log.info(`Organization: ${pc3.cyan(organizations[0].name)}`);
10104
10149
  } else if (organizations.length > 1) {
10105
- clack.log.info(`${organizations.length} organizations available`);
10150
+ clack2.log.info(`${organizations.length} organizations available`);
10106
10151
  } else {
10107
- clack.log.info(
10108
- `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.`
10109
10154
  );
10110
10155
  }
10111
10156
  if (isJsonMode()) {
@@ -10134,7 +10179,7 @@ async function login(options) {
10134
10179
  });
10135
10180
  trackError("ACCESS_DENIED", "auth:login", { step: "poll_token" });
10136
10181
  spinner10.stop("Denied.");
10137
- clack.log.error("Authorization was denied.");
10182
+ clack2.log.error("Authorization was denied.");
10138
10183
  throw new Error("Authorization was denied.");
10139
10184
  }
10140
10185
  if (errorCode === "expired_token") {
@@ -10149,7 +10194,7 @@ async function login(options) {
10149
10194
  });
10150
10195
  trackError("DEVICE_CODE_EXPIRED", "auth:login", { step: "poll_token" });
10151
10196
  spinner10.stop("Expired.");
10152
- 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.");
10153
10198
  throw new Error("Device code expired.");
10154
10199
  }
10155
10200
 
@@ -10158,11 +10203,11 @@ init_esm_shims();
10158
10203
  init_events();
10159
10204
  init_config();
10160
10205
  init_json_output();
10161
- import * as clack2 from "@clack/prompts";
10162
- import pc3 from "picocolors";
10206
+ import * as clack3 from "@clack/prompts";
10207
+ import pc4 from "picocolors";
10163
10208
  async function logout() {
10164
10209
  if (!isJsonMode()) {
10165
- clack2.intro(pc3.bold("Wraps \u203A Sign Out"));
10210
+ clack3.intro(pc4.bold("Wraps \u203A Sign Out"));
10166
10211
  }
10167
10212
  const config2 = await readAuthConfig();
10168
10213
  if (!config2?.auth?.token) {
@@ -10171,7 +10216,7 @@ async function logout() {
10171
10216
  jsonSuccess("auth.logout", { loggedOut: false, alreadyLoggedOut: true });
10172
10217
  return;
10173
10218
  }
10174
- clack2.log.info("Not signed in.");
10219
+ clack3.log.info("Not signed in.");
10175
10220
  return;
10176
10221
  }
10177
10222
  await clearAuthConfig();
@@ -10180,7 +10225,7 @@ async function logout() {
10180
10225
  jsonSuccess("auth.logout", { loggedOut: true });
10181
10226
  return;
10182
10227
  }
10183
- clack2.log.success("Signed out. Token removed from ~/.wraps/config.json");
10228
+ clack3.log.success("Signed out. Token removed from ~/.wraps/config.json");
10184
10229
  }
10185
10230
 
10186
10231
  // src/commands/auth/status.ts
@@ -10188,8 +10233,8 @@ init_esm_shims();
10188
10233
  init_events();
10189
10234
  init_config();
10190
10235
  init_json_output();
10191
- import * as clack3 from "@clack/prompts";
10192
- import pc4 from "picocolors";
10236
+ import * as clack4 from "@clack/prompts";
10237
+ import pc5 from "picocolors";
10193
10238
  async function authStatus(_options = {}) {
10194
10239
  const config2 = await readAuthConfig();
10195
10240
  if (!config2?.auth?.token) {
@@ -10197,8 +10242,8 @@ async function authStatus(_options = {}) {
10197
10242
  if (isJsonMode()) {
10198
10243
  jsonSuccess("auth.status", { authenticated: false });
10199
10244
  } else {
10200
- clack3.intro(pc4.bold("Wraps \u203A Auth Status"));
10201
- 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.");
10202
10247
  }
10203
10248
  return;
10204
10249
  }
@@ -10212,10 +10257,10 @@ async function authStatus(_options = {}) {
10212
10257
  expiresAt: expiresAt || null
10213
10258
  });
10214
10259
  } else {
10215
- clack3.intro(pc4.bold("Wraps \u203A Auth Status"));
10216
- clack3.log.info(`Token: ${masked} (${tokenType})`);
10260
+ clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
10261
+ clack4.log.info(`Token: ${masked} (${tokenType})`);
10217
10262
  if (expiresAt) {
10218
- clack3.log.info(`Expires: ${new Date(expiresAt).toLocaleDateString()}`);
10263
+ clack4.log.info(`Expires: ${new Date(expiresAt).toLocaleDateString()}`);
10219
10264
  }
10220
10265
  }
10221
10266
  trackCommand("auth:status", { success: true, authenticated: true });
@@ -17852,10 +17897,18 @@ async function createSMTPCredentials(config2) {
17852
17897
 
17853
17898
  // src/infrastructure/resources/sqs.ts
17854
17899
  init_esm_shims();
17900
+ init_resource_checks();
17855
17901
  import * as aws11 from "@pulumi/aws";
17902
+ var SQS_TIMEOUTS = {
17903
+ customTimeouts: { create: "2m", update: "2m", delete: "2m" }
17904
+ };
17856
17905
  async function createSQSResources() {
17857
- const dlq = new aws11.sqs.Queue("wraps-email-events-dlq", {
17858
- 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,
17859
17912
  messageRetentionSeconds: 1209600,
17860
17913
  // 14 days
17861
17914
  tags: {
@@ -17863,9 +17916,13 @@ async function createSQSResources() {
17863
17916
  Service: "email",
17864
17917
  Description: "Dead letter queue for failed SES event processing"
17865
17918
  }
17919
+ };
17920
+ const dlq = new aws11.sqs.Queue(dlqName, dlqConfig, {
17921
+ ...SQS_TIMEOUTS,
17922
+ ...dlqUrl ? { import: dlqUrl } : {}
17866
17923
  });
17867
- const queue = new aws11.sqs.Queue("wraps-email-events", {
17868
- name: "wraps-email-events",
17924
+ const queueConfig = {
17925
+ name: queueName,
17869
17926
  visibilityTimeoutSeconds: 300,
17870
17927
  // Must be >= Lambda timeout (5 minutes)
17871
17928
  messageRetentionSeconds: 345600,
@@ -17884,6 +17941,10 @@ async function createSQSResources() {
17884
17941
  Service: "email",
17885
17942
  Description: "Queue for SES email events from EventBridge"
17886
17943
  }
17944
+ };
17945
+ const queue = new aws11.sqs.Queue(queueName, queueConfig, {
17946
+ ...SQS_TIMEOUTS,
17947
+ ...queueUrl ? { import: queueUrl } : {}
17887
17948
  });
17888
17949
  return {
17889
17950
  queue,
@@ -18637,6 +18698,8 @@ async function connect2(options) {
18637
18698
  throw new Error(`Preview failed: ${msg}`);
18638
18699
  }
18639
18700
  }
18701
+ let pulumiLog = [];
18702
+ const debugPulumi = process.env.WRAPS_DEBUG === "1";
18640
18703
  let outputs;
18641
18704
  try {
18642
18705
  outputs = await progress.execute(
@@ -18674,8 +18737,14 @@ async function connect2(options) {
18674
18737
  `wraps-${identity.accountId}-${region}`
18675
18738
  );
18676
18739
  await stack.setConfig("aws:region", { value: region });
18677
- const upResult = await stack.up({ onOutput: () => {
18678
- } });
18740
+ const upResult = await stack.up({
18741
+ onOutput: (msg) => {
18742
+ pulumiLog.push(msg);
18743
+ if (debugPulumi) {
18744
+ process.stderr.write(msg);
18745
+ }
18746
+ }
18747
+ });
18679
18748
  const pulumiOutputs = upResult.outputs;
18680
18749
  return {
18681
18750
  roleArn: pulumiOutputs.roleArn?.value,
@@ -18700,9 +18769,22 @@ async function connect2(options) {
18700
18769
  trackError("STACK_LOCKED", "email:connect", { step: "deploy" });
18701
18770
  throw errors.stackLocked();
18702
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
+ }
18703
18784
  trackError("DEPLOYMENT_FAILED", "email:connect", { step: "deploy" });
18704
18785
  throw new Error(`Pulumi deployment failed: ${msg}`);
18705
18786
  }
18787
+ pulumiLog = [];
18706
18788
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
18707
18789
  const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
18708
18790
  const hostedZone = await findHostedZone2(outputs.domain, region);