guardrail-ship 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/mock-implementation.d.ts +1 -0
  6. package/dist/mock-implementation.d.ts.map +1 -0
  7. package/dist/mock-implementation.js +2 -0
  8. package/dist/mock-implementation.js.map +1 -0
  9. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
  10. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
  11. package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
  12. package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
  13. package/dist/mockproof/import-graph-scanner.d.ts +93 -0
  14. package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
  15. package/dist/mockproof/import-graph-scanner.js +411 -0
  16. package/dist/mockproof/import-graph-scanner.js.map +1 -0
  17. package/dist/mockproof/index.d.ts +10 -0
  18. package/dist/mockproof/index.d.ts.map +1 -0
  19. package/dist/mockproof/index.js +10 -0
  20. package/dist/mockproof/index.js.map +1 -0
  21. package/dist/reality-mode/auth-enforcer.d.ts +13 -0
  22. package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
  23. package/dist/reality-mode/auth-enforcer.js +90 -0
  24. package/dist/reality-mode/auth-enforcer.js.map +1 -0
  25. package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
  26. package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
  27. package/dist/reality-mode/explorer/critical-flows.js +463 -0
  28. package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
  29. package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
  30. package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
  31. package/dist/reality-mode/explorer/flow-parser.js +250 -0
  32. package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
  33. package/dist/reality-mode/explorer/index.d.ts +11 -0
  34. package/dist/reality-mode/explorer/index.d.ts.map +1 -0
  35. package/dist/reality-mode/explorer/index.js +11 -0
  36. package/dist/reality-mode/explorer/index.js.map +1 -0
  37. package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
  38. package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
  39. package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
  40. package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
  41. package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
  42. package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
  43. package/dist/reality-mode/explorer/surface-discovery.js +357 -0
  44. package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
  45. package/dist/reality-mode/explorer/types.d.ts +275 -0
  46. package/dist/reality-mode/explorer/types.d.ts.map +1 -0
  47. package/dist/reality-mode/explorer/types.js +8 -0
  48. package/dist/reality-mode/explorer/types.js.map +1 -0
  49. package/dist/reality-mode/fake-success-detector.d.ts +10 -0
  50. package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
  51. package/dist/reality-mode/fake-success-detector.js +76 -0
  52. package/dist/reality-mode/fake-success-detector.js.map +1 -0
  53. package/dist/reality-mode/index.d.ts +14 -0
  54. package/dist/reality-mode/index.d.ts.map +1 -0
  55. package/dist/reality-mode/index.js +14 -0
  56. package/dist/reality-mode/index.js.map +1 -0
  57. package/dist/reality-mode/reality-scanner.d.ts +48 -0
  58. package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
  59. package/dist/reality-mode/reality-scanner.js +516 -0
  60. package/dist/reality-mode/reality-scanner.js.map +1 -0
  61. package/dist/reality-mode/report-generator.d.ts +11 -0
  62. package/dist/reality-mode/report-generator.d.ts.map +1 -0
  63. package/dist/reality-mode/report-generator.js +233 -0
  64. package/dist/reality-mode/report-generator.js.map +1 -0
  65. package/dist/reality-mode/traffic-classifier.d.ts +14 -0
  66. package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
  67. package/dist/reality-mode/traffic-classifier.js +131 -0
  68. package/dist/reality-mode/traffic-classifier.js.map +1 -0
  69. package/dist/reality-mode/types.d.ts +90 -0
  70. package/dist/reality-mode/types.d.ts.map +1 -0
  71. package/dist/reality-mode/types.js +2 -0
  72. package/dist/reality-mode/types.js.map +1 -0
  73. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
  74. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
  75. package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
  76. package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
  77. package/dist/ship-badge/index.d.ts +9 -0
  78. package/dist/ship-badge/index.d.ts.map +1 -0
  79. package/dist/ship-badge/index.js +9 -0
  80. package/dist/ship-badge/index.js.map +1 -0
  81. package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
  82. package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
  83. package/dist/ship-badge/ship-badge-generator.js +681 -0
  84. package/dist/ship-badge/ship-badge-generator.js.map +1 -0
  85. package/package.json +20 -0
  86. package/src/index.ts +7 -0
  87. package/src/mock-implementation.ts +0 -0
  88. package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
  89. package/src/mockproof/import-graph-scanner.d.ts +93 -0
  90. package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
  91. package/src/mockproof/import-graph-scanner.js +482 -0
  92. package/src/mockproof/import-graph-scanner.ts +540 -0
  93. package/src/mockproof/index.ts +18 -0
  94. package/src/reality-mode/auth-enforcer.ts +97 -0
  95. package/src/reality-mode/explorer/critical-flows.ts +504 -0
  96. package/src/reality-mode/explorer/flow-parser.ts +293 -0
  97. package/src/reality-mode/explorer/index.ts +22 -0
  98. package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
  99. package/src/reality-mode/explorer/surface-discovery.ts +498 -0
  100. package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
  101. package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
  102. package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
  103. package/src/reality-mode/explorer/templates/github-action.yml +132 -0
  104. package/src/reality-mode/explorer/types.ts +356 -0
  105. package/src/reality-mode/fake-success-detector.ts +89 -0
  106. package/src/reality-mode/index.ts +19 -0
  107. package/src/reality-mode/reality-scanner.d.ts +123 -0
  108. package/src/reality-mode/reality-scanner.d.ts.map +1 -0
  109. package/src/reality-mode/reality-scanner.js +526 -0
  110. package/src/reality-mode/reality-scanner.ts +576 -0
  111. package/src/reality-mode/report-generator.ts +253 -0
  112. package/src/reality-mode/traffic-classifier.ts +169 -0
  113. package/src/reality-mode/types.ts +95 -0
  114. package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
  115. package/src/ship-badge/index.ts +16 -0
  116. package/src/ship-badge/ship-badge-generator.d.ts +136 -0
  117. package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
  118. package/src/ship-badge/ship-badge-generator.js +779 -0
  119. package/src/ship-badge/ship-badge-generator.ts +873 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Ship Badge Generator
3
+ *
4
+ * "One-click shareable proof that your app is real."
5
+ *
6
+ * Generates badges + hosted permalinks for:
7
+ * ✅ No Mock Data Detected
8
+ * ✅ No Localhost/Ngrok
9
+ * ✅ All required env vars present
10
+ * ✅ Billing not simulated
11
+ * ✅ DB is real
12
+ * ✅ OAuth callbacks not localhost
13
+ *
14
+ * Vibecoders slap this on README / landing page / Product Hunt for social proof.
15
+ */
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+ import * as crypto from "crypto";
19
+ const BADGE_COLORS = {
20
+ pass: "#4ade80", // Green
21
+ fail: "#f87171", // Red
22
+ warning: "#fbbf24", // Yellow
23
+ skip: "#9ca3af", // Gray
24
+ ship: "#22c55e", // Bright green
25
+ noship: "#ef4444", // Bright red
26
+ };
27
+ export class ShipBadgeGenerator {
28
+ /**
29
+ * Run all ship checks and generate badges
30
+ */
31
+ async generateShipBadge(config) {
32
+ const projectName = config.projectName || path.basename(config.projectPath);
33
+ const projectId = this.generateProjectId(config.projectPath);
34
+ // Run all checks
35
+ const checks = await this.runAllChecks(config.projectPath);
36
+ // Calculate verdict
37
+ const { verdict, score } = this.calculateVerdict(checks);
38
+ // Generate badges
39
+ const badges = this.generateAllBadges(checks, verdict, score);
40
+ // Generate permalink (would be hosted on Guardrail servers in production)
41
+ const permalink = `https://Guardrail.dev/badge/${projectId}`;
42
+ const embedCode = this.generateEmbedCode(projectId, verdict, projectName);
43
+ // Save badges if output dir specified
44
+ if (config.outputDir) {
45
+ await this.saveBadges(badges, config.outputDir);
46
+ }
47
+ const result = {
48
+ projectId,
49
+ projectName,
50
+ verdict,
51
+ score,
52
+ checks,
53
+ badges,
54
+ timestamp: new Date().toISOString(),
55
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
56
+ permalink,
57
+ embedCode,
58
+ };
59
+ // Save result JSON
60
+ if (config.outputDir) {
61
+ await fs.promises.writeFile(path.join(config.outputDir, "ship-badge-result.json"), JSON.stringify(result, null, 2));
62
+ }
63
+ return result;
64
+ }
65
+ /**
66
+ * Run all ship-worthiness checks
67
+ */
68
+ async runAllChecks(projectPath) {
69
+ const checks = [];
70
+ // 1. No Mock Data
71
+ checks.push(await this.checkNoMockData(projectPath));
72
+ // 2. No Localhost/Ngrok
73
+ checks.push(await this.checkNoLocalhost(projectPath));
74
+ // 3. Env Vars Present
75
+ checks.push(await this.checkEnvVars(projectPath));
76
+ // 4. Real Billing (not simulated)
77
+ checks.push(await this.checkRealBilling(projectPath));
78
+ // 5. Real Database
79
+ checks.push(await this.checkRealDatabase(projectPath));
80
+ // 6. OAuth Callbacks
81
+ checks.push(await this.checkOAuthCallbacks(projectPath));
82
+ return checks;
83
+ }
84
+ /**
85
+ * Check for mock data patterns
86
+ */
87
+ async checkNoMockData(projectPath) {
88
+ const patterns = [
89
+ /MockProvider/g,
90
+ /useMock\(/g,
91
+ /mock-context/g,
92
+ /const\s+mock\w*\s*=/gi,
93
+ /lorem\s+ipsum/gi,
94
+ /john\.doe|jane\.doe/gi,
95
+ /user@example\.com/gi,
96
+ ];
97
+ const issues = [];
98
+ const files = await this.findSourceFiles(projectPath);
99
+ for (const file of files.slice(0, 100)) {
100
+ // Limit for performance
101
+ try {
102
+ const content = await fs.promises.readFile(file, "utf-8");
103
+ const relativePath = path.relative(projectPath, file);
104
+ // Skip test files
105
+ if (this.isTestFile(relativePath))
106
+ continue;
107
+ for (const pattern of patterns) {
108
+ pattern.lastIndex = 0;
109
+ if (pattern.test(content)) {
110
+ issues.push(`${relativePath}: ${pattern.source}`);
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ catch (e) {
116
+ // Skip unreadable files
117
+ }
118
+ }
119
+ return {
120
+ id: "no-mock-data",
121
+ name: "No Mock Data Detected",
122
+ shortName: "Mock Data",
123
+ status: issues.length === 0 ? "pass" : "fail",
124
+ message: issues.length === 0
125
+ ? "No mock data patterns found in production code"
126
+ : `Found ${issues.length} mock data patterns`,
127
+ details: issues.slice(0, 5),
128
+ };
129
+ }
130
+ /**
131
+ * Check for localhost/ngrok URLs
132
+ */
133
+ async checkNoLocalhost(projectPath) {
134
+ const patterns = [
135
+ /localhost:\d+/g,
136
+ /127\.0\.0\.1:\d+/g,
137
+ /\.ngrok\.io/g,
138
+ /\.ngrok-free\.app/g,
139
+ /jsonplaceholder\.typicode\.com/g,
140
+ ];
141
+ const issues = [];
142
+ const configFiles = [
143
+ ".env",
144
+ ".env.production",
145
+ "next.config.js",
146
+ "next.config.mjs",
147
+ "vite.config.ts",
148
+ "vite.config.js",
149
+ "src/config/api.ts",
150
+ "src/lib/api.ts",
151
+ ];
152
+ for (const configFile of configFiles) {
153
+ const filePath = path.join(projectPath, configFile);
154
+ if (fs.existsSync(filePath)) {
155
+ try {
156
+ const content = await fs.promises.readFile(filePath, "utf-8");
157
+ for (const pattern of patterns) {
158
+ pattern.lastIndex = 0;
159
+ const matches = content.match(pattern);
160
+ if (matches) {
161
+ issues.push(`${configFile}: ${matches[0]}`);
162
+ }
163
+ }
164
+ }
165
+ catch (e) {
166
+ // Skip
167
+ }
168
+ }
169
+ }
170
+ return {
171
+ id: "no-localhost",
172
+ name: "No Localhost/Ngrok",
173
+ shortName: "Real URLs",
174
+ status: issues.length === 0 ? "pass" : "fail",
175
+ message: issues.length === 0
176
+ ? "No localhost or temporary URLs in config"
177
+ : `Found ${issues.length} localhost/ngrok URLs`,
178
+ details: issues,
179
+ };
180
+ }
181
+ /**
182
+ * Check for required environment variables
183
+ */
184
+ async checkEnvVars(projectPath) {
185
+ const requiredVars = [
186
+ "DATABASE_URL",
187
+ "API_URL",
188
+ "NEXTAUTH_URL",
189
+ "NEXTAUTH_SECRET",
190
+ ];
191
+ const envPath = path.join(projectPath, ".env");
192
+ const envProdPath = path.join(projectPath, ".env.production");
193
+ let envContent = "";
194
+ if (fs.existsSync(envProdPath)) {
195
+ envContent = await fs.promises.readFile(envProdPath, "utf-8");
196
+ }
197
+ else if (fs.existsSync(envPath)) {
198
+ envContent = await fs.promises.readFile(envPath, "utf-8");
199
+ }
200
+ // Also check .env.example to see what's expected
201
+ const examplePath = path.join(projectPath, ".env.example");
202
+ let expectedVars = [];
203
+ if (fs.existsSync(examplePath)) {
204
+ const exampleContent = await fs.promises.readFile(examplePath, "utf-8");
205
+ expectedVars = exampleContent
206
+ .split("\n")
207
+ .filter((line) => line.includes("=") && !line.startsWith("#"))
208
+ .map((line) => line.split("=")[0]?.trim() || "");
209
+ }
210
+ const missing = [];
211
+ const varsToCheck = expectedVars.length > 0 ? expectedVars : requiredVars;
212
+ for (const varName of varsToCheck) {
213
+ const regex = new RegExp(`^${varName}=.+`, "m");
214
+ if (!regex.test(envContent)) {
215
+ missing.push(varName);
216
+ }
217
+ }
218
+ const hasEnvFile = fs.existsSync(envPath) || fs.existsSync(envProdPath);
219
+ return {
220
+ id: "env-vars",
221
+ name: "Environment Variables Present",
222
+ shortName: "Env Vars",
223
+ status: !hasEnvFile ? "skip" : missing.length === 0 ? "pass" : "warning",
224
+ message: !hasEnvFile
225
+ ? "No .env file found"
226
+ : missing.length === 0
227
+ ? "All expected environment variables are set"
228
+ : `Missing ${missing.length} environment variables`,
229
+ details: missing.slice(0, 5),
230
+ };
231
+ }
232
+ /**
233
+ * Check for real billing (not demo/test)
234
+ */
235
+ async checkRealBilling(projectPath) {
236
+ const testKeyPatterns = [
237
+ /sk_test_/g,
238
+ /pk_test_/g,
239
+ /STRIPE_TEST/g,
240
+ /demo_billing/gi,
241
+ /simulate.*payment/gi,
242
+ /fake.*billing/gi,
243
+ ];
244
+ const issues = [];
245
+ const files = await this.findSourceFiles(projectPath);
246
+ // Check for Stripe/billing related files
247
+ const billingFiles = files.filter((f) => /stripe|billing|payment|checkout/i.test(f));
248
+ if (billingFiles.length === 0) {
249
+ return {
250
+ id: "real-billing",
251
+ name: "Billing Not Simulated",
252
+ shortName: "Billing",
253
+ status: "skip",
254
+ message: "No billing code detected",
255
+ };
256
+ }
257
+ for (const file of billingFiles) {
258
+ try {
259
+ const content = await fs.promises.readFile(file, "utf-8");
260
+ const relativePath = path.relative(projectPath, file);
261
+ if (this.isTestFile(relativePath))
262
+ continue;
263
+ for (const pattern of testKeyPatterns) {
264
+ pattern.lastIndex = 0;
265
+ if (pattern.test(content)) {
266
+ issues.push(`${relativePath}: ${pattern.source}`);
267
+ }
268
+ }
269
+ }
270
+ catch (e) {
271
+ // Skip
272
+ }
273
+ }
274
+ return {
275
+ id: "real-billing",
276
+ name: "Billing Not Simulated",
277
+ shortName: "Billing",
278
+ status: issues.length === 0 ? "pass" : "fail",
279
+ message: issues.length === 0
280
+ ? "No test billing keys or demo billing code found"
281
+ : `Found ${issues.length} test/demo billing patterns`,
282
+ details: issues.slice(0, 5),
283
+ };
284
+ }
285
+ /**
286
+ * Check for real database connection
287
+ */
288
+ async checkRealDatabase(projectPath) {
289
+ const fakeDbPatterns = [
290
+ /sqlite:memory/gi,
291
+ /\.sqlite$/gi,
292
+ /mockdb/gi,
293
+ /fake.*database/gi,
294
+ /in-memory.*db/gi,
295
+ ];
296
+ const envPath = path.join(projectPath, ".env");
297
+ const envProdPath = path.join(projectPath, ".env.production");
298
+ let dbUrl = "";
299
+ for (const p of [envProdPath, envPath]) {
300
+ if (fs.existsSync(p)) {
301
+ const content = await fs.promises.readFile(p, "utf-8");
302
+ const match = content.match(/DATABASE_URL=(.+)/);
303
+ if (match) {
304
+ dbUrl = match[1] || "";
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ if (!dbUrl) {
310
+ return {
311
+ id: "real-database",
312
+ name: "Database Is Real",
313
+ shortName: "Database",
314
+ status: "skip",
315
+ message: "No DATABASE_URL found",
316
+ };
317
+ }
318
+ const isFake = fakeDbPatterns.some((p) => p.test(dbUrl)) || /localhost/.test(dbUrl);
319
+ return {
320
+ id: "real-database",
321
+ name: "Database Is Real",
322
+ shortName: "Database",
323
+ status: isFake ? "warning" : "pass",
324
+ message: isFake
325
+ ? "Database URL points to local/fake database"
326
+ : "Database URL appears to be a real hosted database",
327
+ };
328
+ }
329
+ /**
330
+ * Check OAuth callback URLs
331
+ */
332
+ async checkOAuthCallbacks(projectPath) {
333
+ const authFiles = [
334
+ "src/app/api/auth/[...nextauth]/route.ts",
335
+ "src/pages/api/auth/[...nextauth].ts",
336
+ "src/lib/auth.ts",
337
+ "src/config/auth.ts",
338
+ ];
339
+ const issues = [];
340
+ for (const authFile of authFiles) {
341
+ const filePath = path.join(projectPath, authFile);
342
+ if (fs.existsSync(filePath)) {
343
+ try {
344
+ const content = await fs.promises.readFile(filePath, "utf-8");
345
+ if (/callbackUrl.*localhost/i.test(content)) {
346
+ issues.push(`${authFile}: localhost callback URL`);
347
+ }
348
+ if (/redirect.*localhost/i.test(content)) {
349
+ issues.push(`${authFile}: localhost redirect`);
350
+ }
351
+ }
352
+ catch (e) {
353
+ // Skip
354
+ }
355
+ }
356
+ }
357
+ // Also check NEXTAUTH_URL
358
+ const envPath = path.join(projectPath, ".env");
359
+ if (fs.existsSync(envPath)) {
360
+ const content = await fs.promises.readFile(envPath, "utf-8");
361
+ const match = content.match(/NEXTAUTH_URL=(.+)/);
362
+ if (match && match[1] && /localhost/i.test(match[1])) {
363
+ issues.push(".env: NEXTAUTH_URL points to localhost");
364
+ }
365
+ }
366
+ const hasAuthCode = authFiles.some((f) => fs.existsSync(path.join(projectPath, f)));
367
+ return {
368
+ id: "oauth-callbacks",
369
+ name: "OAuth Callbacks Not Localhost",
370
+ shortName: "OAuth",
371
+ status: !hasAuthCode ? "skip" : issues.length === 0 ? "pass" : "fail",
372
+ message: !hasAuthCode
373
+ ? "No OAuth/auth code detected"
374
+ : issues.length === 0
375
+ ? "OAuth callbacks configured for production"
376
+ : `Found ${issues.length} localhost OAuth issues`,
377
+ details: issues,
378
+ };
379
+ }
380
+ /**
381
+ * Calculate overall verdict
382
+ */
383
+ calculateVerdict(checks) {
384
+ const activeChecks = checks.filter((c) => c.status !== "skip");
385
+ const passed = activeChecks.filter((c) => c.status === "pass").length;
386
+ const failed = activeChecks.filter((c) => c.status === "fail").length;
387
+ const warnings = activeChecks.filter((c) => c.status === "warning").length;
388
+ const score = activeChecks.length > 0
389
+ ? Math.round((passed / activeChecks.length) * 100)
390
+ : 100;
391
+ let verdict;
392
+ if (failed > 0) {
393
+ verdict = "no-ship";
394
+ }
395
+ else if (warnings > 0) {
396
+ verdict = "review";
397
+ }
398
+ else {
399
+ verdict = "ship";
400
+ }
401
+ return { verdict, score };
402
+ }
403
+ /**
404
+ * Generate all badge SVGs
405
+ */
406
+ generateAllBadges(checks, verdict, score) {
407
+ const mainColor = verdict === "ship"
408
+ ? BADGE_COLORS.ship
409
+ : verdict === "no-ship"
410
+ ? BADGE_COLORS.noship
411
+ : BADGE_COLORS.warning;
412
+ return {
413
+ main: this.createBadge("Guardrail", verdict === "ship"
414
+ ? "🚀 SHIP IT"
415
+ : verdict === "no-ship"
416
+ ? "🛑 NO SHIP"
417
+ : "⚠️ REVIEW", mainColor),
418
+ mockData: this.createCheckBadge(checks.find((c) => c.id === "no-mock-data")),
419
+ realApi: this.createCheckBadge(checks.find((c) => c.id === "no-localhost")),
420
+ envVars: this.createCheckBadge(checks.find((c) => c.id === "env-vars")),
421
+ billing: this.createCheckBadge(checks.find((c) => c.id === "real-billing")),
422
+ database: this.createCheckBadge(checks.find((c) => c.id === "real-database")),
423
+ oauth: this.createCheckBadge(checks.find((c) => c.id === "oauth-callbacks")),
424
+ combined: this.createCombinedBadge(checks, score),
425
+ };
426
+ }
427
+ /**
428
+ * Create a single check badge
429
+ */
430
+ createCheckBadge(check) {
431
+ const icon = check.status === "pass"
432
+ ? "✅"
433
+ : check.status === "fail"
434
+ ? "❌"
435
+ : check.status === "warning"
436
+ ? "⚠️"
437
+ : "⏭️";
438
+ const color = BADGE_COLORS[check.status];
439
+ const label = check.shortName;
440
+ const value = check.status === "pass"
441
+ ? "Pass"
442
+ : check.status === "fail"
443
+ ? "Fail"
444
+ : check.status === "warning"
445
+ ? "Warning"
446
+ : "Skip";
447
+ return this.createBadge(label, `${icon} ${value}`, color);
448
+ }
449
+ /**
450
+ * Create a combined badge strip
451
+ */
452
+ createCombinedBadge(checks, score) {
453
+ const passed = checks.filter((c) => c.status === "pass").length;
454
+ const total = checks.filter((c) => c.status !== "skip").length;
455
+ const color = score >= 80
456
+ ? BADGE_COLORS.pass
457
+ : score >= 50
458
+ ? BADGE_COLORS.warning
459
+ : BADGE_COLORS.fail;
460
+ return this.createBadge("Ship Score", `${passed}/${total} (${score}%)`, color, "for-the-badge");
461
+ }
462
+ /**
463
+ * Create SVG badge
464
+ */
465
+ createBadge(label, value, color, style = "flat") {
466
+ const labelWidth = label.length * 7 + 10;
467
+ const valueWidth = value.length * 7 + 10;
468
+ const totalWidth = labelWidth + valueWidth;
469
+ const height = style === "for-the-badge" ? 28 : 20;
470
+ const fontSize = 11;
471
+ const labelBg = "#555";
472
+ if (style === "for-the-badge") {
473
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}">
474
+ <linearGradient id="smooth" x2="0" y2="100%">
475
+ <stop offset="0" stop-color="#fff" stop-opacity=".7"/>
476
+ <stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
477
+ <stop offset=".9" stop-color="#000" stop-opacity=".3"/>
478
+ <stop offset="1" stop-color="#000" stop-opacity=".5"/>
479
+ </linearGradient>
480
+ <rect rx="4" width="${totalWidth}" height="${height}" fill="${labelBg}"/>
481
+ <rect rx="4" x="${labelWidth}" width="${valueWidth}" height="${height}" fill="${color}"/>
482
+ <rect rx="4" width="${totalWidth}" height="${height}" fill="url(#smooth)"/>
483
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="${fontSize}" font-weight="bold">
484
+ <text x="${labelWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(label)}</text>
485
+ <text x="${labelWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(label)}</text>
486
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(value)}</text>
487
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(value)}</text>
488
+ </g>
489
+ </svg>`;
490
+ }
491
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}">
492
+ <linearGradient id="smooth" x2="0" y2="100%">
493
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
494
+ <stop offset="1" stop-opacity=".1"/>
495
+ </linearGradient>
496
+ <clipPath id="round">
497
+ <rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
498
+ </clipPath>
499
+ <g clip-path="url(#round)">
500
+ <rect width="${labelWidth}" height="${height}" fill="${labelBg}"/>
501
+ <rect x="${labelWidth}" width="${valueWidth}" height="${height}" fill="${color}"/>
502
+ <rect width="${totalWidth}" height="${height}" fill="url(#smooth)"/>
503
+ </g>
504
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="${fontSize}">
505
+ <text x="${labelWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(label)}</text>
506
+ <text x="${labelWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(label)}</text>
507
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(value)}</text>
508
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(value)}</text>
509
+ </g>
510
+ </svg>`;
511
+ }
512
+ /**
513
+ * Generate embed code for README
514
+ */
515
+ generateEmbedCode(projectId, verdict, projectName) {
516
+ return `<!-- Guardrail Ship Badge -->
517
+ [![Guardrail Ship Status](https://Guardrail.dev/api/badge/${projectId}/main.svg)](https://Guardrail.dev/badge/${projectId})
518
+ [![Mock Data](https://Guardrail.dev/api/badge/${projectId}/mock-data.svg)](https://Guardrail.dev/badge/${projectId})
519
+ [![Real APIs](https://Guardrail.dev/api/badge/${projectId}/real-api.svg)](https://Guardrail.dev/badge/${projectId})
520
+ <!-- End Guardrail Ship Badge -->
521
+
522
+ ---
523
+
524
+ **${projectName}** verified by [Guardrail](https://Guardrail.dev) - Stop shipping pretend features.`;
525
+ }
526
+ /**
527
+ * Save badges to directory
528
+ */
529
+ async saveBadges(badges, outputDir) {
530
+ await fs.promises.mkdir(outputDir, { recursive: true });
531
+ const files = [
532
+ ["main", "ship-status.svg"],
533
+ ["mockData", "mock-data.svg"],
534
+ ["realApi", "real-api.svg"],
535
+ ["envVars", "env-vars.svg"],
536
+ ["billing", "billing.svg"],
537
+ ["database", "database.svg"],
538
+ ["oauth", "oauth.svg"],
539
+ ["combined", "ship-score.svg"],
540
+ ];
541
+ for (const [key, filename] of files) {
542
+ await fs.promises.writeFile(path.join(outputDir, filename), badges[key], "utf-8");
543
+ }
544
+ }
545
+ /**
546
+ * Generate project ID from path
547
+ */
548
+ generateProjectId(projectPath) {
549
+ const hash = crypto
550
+ .createHash("sha256")
551
+ .update(projectPath)
552
+ .digest("hex")
553
+ .slice(0, 12);
554
+ return hash;
555
+ }
556
+ /**
557
+ * Find source files
558
+ */
559
+ async findSourceFiles(projectPath) {
560
+ const files = [];
561
+ const extensions = [".ts", ".tsx", ".js", ".jsx"];
562
+ const excludeDirs = [
563
+ "node_modules",
564
+ ".git",
565
+ ".next",
566
+ "dist",
567
+ "build",
568
+ "coverage",
569
+ ];
570
+ const walk = async (dir) => {
571
+ try {
572
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
573
+ for (const entry of entries) {
574
+ const fullPath = path.join(dir, entry.name);
575
+ if (entry.isDirectory()) {
576
+ if (!excludeDirs.includes(entry.name) &&
577
+ !entry.name.startsWith(".")) {
578
+ await walk(fullPath);
579
+ }
580
+ }
581
+ else if (entry.isFile()) {
582
+ const ext = path.extname(entry.name);
583
+ if (extensions.includes(ext)) {
584
+ files.push(fullPath);
585
+ }
586
+ }
587
+ }
588
+ }
589
+ catch (e) {
590
+ // Skip
591
+ }
592
+ };
593
+ await walk(projectPath);
594
+ return files;
595
+ }
596
+ /**
597
+ * Check if file is a test file
598
+ */
599
+ isTestFile(filePath) {
600
+ const testPatterns = [
601
+ /__tests__/,
602
+ /\.test\./,
603
+ /\.spec\./,
604
+ /test\//,
605
+ /tests\//,
606
+ /e2e\//,
607
+ /__mocks__/,
608
+ /stories\//,
609
+ ];
610
+ return testPatterns.some((p) => p.test(filePath));
611
+ }
612
+ /**
613
+ * Escape HTML entities
614
+ */
615
+ escapeHtml(text) {
616
+ return text
617
+ .replace(/&/g, "&amp;")
618
+ .replace(/</g, "&lt;")
619
+ .replace(/>/g, "&gt;")
620
+ .replace(/"/g, "&quot;")
621
+ .replace(/'/g, "&#039;");
622
+ }
623
+ /**
624
+ * Generate human-readable report
625
+ */
626
+ generateReport(result) {
627
+ const lines = [];
628
+ lines.push("╔══════════════════════════════════════════════════════════════╗");
629
+ lines.push("║ 🚀 Guardrail Ship Badge Report 🚀 ║");
630
+ lines.push("╚══════════════════════════════════════════════════════════════╝");
631
+ lines.push("");
632
+ const verdictEmoji = result.verdict === "ship"
633
+ ? "🚀"
634
+ : result.verdict === "no-ship"
635
+ ? "🛑"
636
+ : "⚠️";
637
+ const verdictText = result.verdict === "ship"
638
+ ? "SHIP IT!"
639
+ : result.verdict === "no-ship"
640
+ ? "NO SHIP"
641
+ : "NEEDS REVIEW";
642
+ lines.push(`${verdictEmoji} VERDICT: ${verdictText}`);
643
+ lines.push(` Ship Score: ${result.score}/100`);
644
+ lines.push(` Project: ${result.projectName}`);
645
+ lines.push("");
646
+ lines.push("─".repeat(64));
647
+ lines.push("");
648
+ lines.push("CHECKS:");
649
+ lines.push("");
650
+ for (const check of result.checks) {
651
+ const icon = check.status === "pass"
652
+ ? "✅"
653
+ : check.status === "fail"
654
+ ? "❌"
655
+ : check.status === "warning"
656
+ ? "⚠️"
657
+ : "⏭️";
658
+ lines.push(`${icon} ${check.name}`);
659
+ lines.push(` ${check.message}`);
660
+ if (check.details && check.details.length > 0) {
661
+ for (const detail of check.details) {
662
+ lines.push(` • ${detail}`);
663
+ }
664
+ }
665
+ lines.push("");
666
+ }
667
+ lines.push("─".repeat(64));
668
+ lines.push("");
669
+ lines.push("ADD TO YOUR README:");
670
+ lines.push("");
671
+ lines.push(result.embedCode);
672
+ lines.push("");
673
+ lines.push("─".repeat(64));
674
+ lines.push(`Permalink: ${result.permalink}`);
675
+ lines.push(`Generated: ${result.timestamp}`);
676
+ lines.push(`Expires: ${result.expiresAt}`);
677
+ return lines.join("\n");
678
+ }
679
+ }
680
+ export const shipBadgeGenerator = new ShipBadgeGenerator();
681
+ //# sourceMappingURL=ship-badge-generator.js.map