distyll 0.1.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 (243) hide show
  1. package/CONTRIBUTING.md +159 -0
  2. package/POSTMORTEM.json +60 -0
  3. package/README.md +218 -0
  4. package/SETUP.md +79 -0
  5. package/action.yml +37 -0
  6. package/dist/cache.d.ts +26 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +115 -0
  9. package/dist/cache.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +153 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/ci.d.ts +7 -0
  15. package/dist/commands/ci.d.ts.map +1 -0
  16. package/dist/commands/ci.js +101 -0
  17. package/dist/commands/ci.js.map +1 -0
  18. package/dist/commands/diff.d.ts +10 -0
  19. package/dist/commands/diff.d.ts.map +1 -0
  20. package/dist/commands/diff.js +95 -0
  21. package/dist/commands/diff.js.map +1 -0
  22. package/dist/commands/fingerprint.d.ts +2 -0
  23. package/dist/commands/fingerprint.d.ts.map +1 -0
  24. package/dist/commands/fingerprint.js +77 -0
  25. package/dist/commands/fingerprint.js.map +1 -0
  26. package/dist/commands/hook.d.ts +3 -0
  27. package/dist/commands/hook.d.ts.map +1 -0
  28. package/dist/commands/hook.js +110 -0
  29. package/dist/commands/hook.js.map +1 -0
  30. package/dist/commands/init.d.ts +2 -0
  31. package/dist/commands/init.d.ts.map +1 -0
  32. package/dist/commands/init.js +75 -0
  33. package/dist/commands/init.js.map +1 -0
  34. package/dist/config.d.ts +7 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +100 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/errors.d.ts +30 -0
  39. package/dist/errors.d.ts.map +1 -0
  40. package/dist/errors.js +133 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/fingerprint/analyzer.d.ts +3 -0
  43. package/dist/fingerprint/analyzer.d.ts.map +1 -0
  44. package/dist/fingerprint/analyzer.js +230 -0
  45. package/dist/fingerprint/analyzer.js.map +1 -0
  46. package/dist/fingerprint/comparator.d.ts +4 -0
  47. package/dist/fingerprint/comparator.d.ts.map +1 -0
  48. package/dist/fingerprint/comparator.js +78 -0
  49. package/dist/fingerprint/comparator.js.map +1 -0
  50. package/dist/fingerprint/profile.d.ts +5 -0
  51. package/dist/fingerprint/profile.d.ts.map +1 -0
  52. package/dist/fingerprint/profile.js +68 -0
  53. package/dist/fingerprint/profile.js.map +1 -0
  54. package/dist/fixes/index.d.ts +12 -0
  55. package/dist/fixes/index.d.ts.map +1 -0
  56. package/dist/fixes/index.js +42 -0
  57. package/dist/fixes/index.js.map +1 -0
  58. package/dist/fixes/single-use-wrapper.d.ts +8 -0
  59. package/dist/fixes/single-use-wrapper.d.ts.map +1 -0
  60. package/dist/fixes/single-use-wrapper.js +54 -0
  61. package/dist/fixes/single-use-wrapper.js.map +1 -0
  62. package/dist/fixes/unnecessary-try-catch.d.ts +8 -0
  63. package/dist/fixes/unnecessary-try-catch.d.ts.map +1 -0
  64. package/dist/fixes/unnecessary-try-catch.js +37 -0
  65. package/dist/fixes/unnecessary-try-catch.js.map +1 -0
  66. package/dist/fixes/unused-imports.d.ts +7 -0
  67. package/dist/fixes/unused-imports.d.ts.map +1 -0
  68. package/dist/fixes/unused-imports.js +41 -0
  69. package/dist/fixes/unused-imports.js.map +1 -0
  70. package/dist/fixes/verbose-comments.d.ts +7 -0
  71. package/dist/fixes/verbose-comments.d.ts.map +1 -0
  72. package/dist/fixes/verbose-comments.js +29 -0
  73. package/dist/fixes/verbose-comments.js.map +1 -0
  74. package/dist/formatter.d.ts +4 -0
  75. package/dist/formatter.d.ts.map +1 -0
  76. package/dist/formatter.js +72 -0
  77. package/dist/formatter.js.map +1 -0
  78. package/dist/git.d.ts +22 -0
  79. package/dist/git.d.ts.map +1 -0
  80. package/dist/git.js +130 -0
  81. package/dist/git.js.map +1 -0
  82. package/dist/index.d.ts +16 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +40 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/languages/index.d.ts +8 -0
  87. package/dist/languages/index.d.ts.map +1 -0
  88. package/dist/languages/index.js +50 -0
  89. package/dist/languages/index.js.map +1 -0
  90. package/dist/languages/javascript.d.ts +6 -0
  91. package/dist/languages/javascript.d.ts.map +1 -0
  92. package/dist/languages/javascript.js +39 -0
  93. package/dist/languages/javascript.js.map +1 -0
  94. package/dist/languages/python.d.ts +6 -0
  95. package/dist/languages/python.d.ts.map +1 -0
  96. package/dist/languages/python.js +50 -0
  97. package/dist/languages/python.js.map +1 -0
  98. package/dist/parser.d.ts +8 -0
  99. package/dist/parser.d.ts.map +1 -0
  100. package/dist/parser.js +55 -0
  101. package/dist/parser.js.map +1 -0
  102. package/dist/reporters/github.d.ts +4 -0
  103. package/dist/reporters/github.d.ts.map +1 -0
  104. package/dist/reporters/github.js +70 -0
  105. package/dist/reporters/github.js.map +1 -0
  106. package/dist/reporters/terminal.d.ts +4 -0
  107. package/dist/reporters/terminal.d.ts.map +1 -0
  108. package/dist/reporters/terminal.js +59 -0
  109. package/dist/reporters/terminal.js.map +1 -0
  110. package/dist/rules/dead-code-paths.d.ts +3 -0
  111. package/dist/rules/dead-code-paths.d.ts.map +1 -0
  112. package/dist/rules/dead-code-paths.js +57 -0
  113. package/dist/rules/dead-code-paths.js.map +1 -0
  114. package/dist/rules/excessive-comments.d.ts +3 -0
  115. package/dist/rules/excessive-comments.d.ts.map +1 -0
  116. package/dist/rules/excessive-comments.js +86 -0
  117. package/dist/rules/excessive-comments.js.map +1 -0
  118. package/dist/rules/hallucinated-imports.d.ts +3 -0
  119. package/dist/rules/hallucinated-imports.d.ts.map +1 -0
  120. package/dist/rules/hallucinated-imports.js +228 -0
  121. package/dist/rules/hallucinated-imports.js.map +1 -0
  122. package/dist/rules/index.d.ts +4 -0
  123. package/dist/rules/index.d.ts.map +1 -0
  124. package/dist/rules/index.js +34 -0
  125. package/dist/rules/index.js.map +1 -0
  126. package/dist/rules/magic-values.d.ts +3 -0
  127. package/dist/rules/magic-values.d.ts.map +1 -0
  128. package/dist/rules/magic-values.js +168 -0
  129. package/dist/rules/magic-values.js.map +1 -0
  130. package/dist/rules/near-duplicate-functions.d.ts +3 -0
  131. package/dist/rules/near-duplicate-functions.d.ts.map +1 -0
  132. package/dist/rules/near-duplicate-functions.js +78 -0
  133. package/dist/rules/near-duplicate-functions.js.map +1 -0
  134. package/dist/rules/over-defensive-nulls.d.ts +3 -0
  135. package/dist/rules/over-defensive-nulls.d.ts.map +1 -0
  136. package/dist/rules/over-defensive-nulls.js +129 -0
  137. package/dist/rules/over-defensive-nulls.js.map +1 -0
  138. package/dist/rules/redundant-else-return.d.ts +3 -0
  139. package/dist/rules/redundant-else-return.d.ts.map +1 -0
  140. package/dist/rules/redundant-else-return.js +57 -0
  141. package/dist/rules/redundant-else-return.js.map +1 -0
  142. package/dist/rules/single-option-object.d.ts +3 -0
  143. package/dist/rules/single-option-object.d.ts.map +1 -0
  144. package/dist/rules/single-option-object.js +88 -0
  145. package/dist/rules/single-option-object.js.map +1 -0
  146. package/dist/rules/single-use-wrapper.d.ts +3 -0
  147. package/dist/rules/single-use-wrapper.d.ts.map +1 -0
  148. package/dist/rules/single-use-wrapper.js +172 -0
  149. package/dist/rules/single-use-wrapper.js.map +1 -0
  150. package/dist/rules/unnecessary-try-catch.d.ts +3 -0
  151. package/dist/rules/unnecessary-try-catch.d.ts.map +1 -0
  152. package/dist/rules/unnecessary-try-catch.js +116 -0
  153. package/dist/rules/unnecessary-try-catch.js.map +1 -0
  154. package/dist/rules/unused-imports.d.ts +3 -0
  155. package/dist/rules/unused-imports.d.ts.map +1 -0
  156. package/dist/rules/unused-imports.js +103 -0
  157. package/dist/rules/unused-imports.js.map +1 -0
  158. package/dist/rules/verbose-comments.d.ts +3 -0
  159. package/dist/rules/verbose-comments.d.ts.map +1 -0
  160. package/dist/rules/verbose-comments.js +100 -0
  161. package/dist/rules/verbose-comments.js.map +1 -0
  162. package/dist/scanner.d.ts +11 -0
  163. package/dist/scanner.d.ts.map +1 -0
  164. package/dist/scanner.js +196 -0
  165. package/dist/scanner.js.map +1 -0
  166. package/dist/scorer.d.ts +3 -0
  167. package/dist/scorer.d.ts.map +1 -0
  168. package/dist/scorer.js +23 -0
  169. package/dist/scorer.js.map +1 -0
  170. package/dist/types.d.ts +62 -0
  171. package/dist/types.d.ts.map +1 -0
  172. package/dist/types.js +3 -0
  173. package/dist/types.js.map +1 -0
  174. package/hn_post.md +13 -0
  175. package/marketing/COMPETITIVE_ANALYSIS.md +62 -0
  176. package/marketing/EMAIL_ANNOUNCEMENT.md +91 -0
  177. package/marketing/LANDING_PAGE_COPY.md +123 -0
  178. package/marketing/LAUNCH_POST.md +68 -0
  179. package/marketing/PRODUCT_HUNT.md +39 -0
  180. package/marketing/TWITTER_THREAD.md +70 -0
  181. package/package.json +44 -0
  182. package/producthunt.md +52 -0
  183. package/reddit_post.md +39 -0
  184. package/site/favicon.svg +10 -0
  185. package/site/index.html +281 -0
  186. package/site/script.js +82 -0
  187. package/site/style.css +516 -0
  188. package/src/cache.ts +114 -0
  189. package/src/cli.ts +169 -0
  190. package/src/commands/ci.ts +111 -0
  191. package/src/commands/diff.ts +108 -0
  192. package/src/commands/fingerprint.ts +47 -0
  193. package/src/commands/hook.ts +85 -0
  194. package/src/commands/init.ts +42 -0
  195. package/src/config.ts +75 -0
  196. package/src/errors.ts +105 -0
  197. package/src/fingerprint/analyzer.ts +214 -0
  198. package/src/fingerprint/comparator.ts +93 -0
  199. package/src/fingerprint/profile.ts +32 -0
  200. package/src/fixes/index.ts +58 -0
  201. package/src/fixes/single-use-wrapper.ts +60 -0
  202. package/src/fixes/unnecessary-try-catch.ts +43 -0
  203. package/src/fixes/unused-imports.ts +53 -0
  204. package/src/fixes/verbose-comments.ts +35 -0
  205. package/src/formatter.ts +79 -0
  206. package/src/git.ts +115 -0
  207. package/src/index.ts +15 -0
  208. package/src/languages/index.ts +50 -0
  209. package/src/languages/javascript.ts +36 -0
  210. package/src/languages/python.ts +47 -0
  211. package/src/parser.ts +52 -0
  212. package/src/reporters/github.ts +75 -0
  213. package/src/reporters/terminal.ts +67 -0
  214. package/src/rules/dead-code-paths.ts +62 -0
  215. package/src/rules/excessive-comments.ts +94 -0
  216. package/src/rules/hallucinated-imports.ts +195 -0
  217. package/src/rules/index.ts +32 -0
  218. package/src/rules/magic-values.ts +167 -0
  219. package/src/rules/near-duplicate-functions.ts +89 -0
  220. package/src/rules/over-defensive-nulls.ts +137 -0
  221. package/src/rules/redundant-else-return.ts +61 -0
  222. package/src/rules/single-option-object.ts +97 -0
  223. package/src/rules/single-use-wrapper.ts +184 -0
  224. package/src/rules/unnecessary-try-catch.ts +121 -0
  225. package/src/rules/unused-imports.ts +115 -0
  226. package/src/rules/verbose-comments.ts +105 -0
  227. package/src/scanner.ts +184 -0
  228. package/src/scorer.ts +26 -0
  229. package/src/types.ts +70 -0
  230. package/tests/commands/diff.test.ts +107 -0
  231. package/tests/config.test.ts +69 -0
  232. package/tests/e2e.test.ts +163 -0
  233. package/tests/edge-cases.test.ts +167 -0
  234. package/tests/fingerprint/analyzer.test.ts +131 -0
  235. package/tests/fixes/unnecessary-try-catch.test.ts +62 -0
  236. package/tests/git.test.ts +79 -0
  237. package/tests/rules/hallucinated-imports.test.ts +59 -0
  238. package/tests/rules/near-duplicate-functions.test.ts +90 -0
  239. package/tests/rules/unnecessary-try-catch.test.ts +81 -0
  240. package/tests/scanner.test.ts +88 -0
  241. package/tsconfig.json +20 -0
  242. package/twitter_thread.md +46 -0
  243. package/vitest.config.ts +7 -0
package/site/script.js ADDED
@@ -0,0 +1,82 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ // Terminal animation
5
+ var terminalLines = [
6
+ { text: '<span class="prompt">$</span> <span class="cmd">distyll scan src/</span>', delay: 0 },
7
+ { text: '', delay: 400 },
8
+ { text: '<span class="dim">Scanning 24 files (JS/TS)...</span>', delay: 600 },
9
+ { text: '', delay: 800 },
10
+ { text: '<span class="file">src/api/handler.ts</span>', delay: 1000 },
11
+ { text: ' <span class="warn">warning</span> <span class="dim">unnecessary-try-catch</span> Try-catch wraps synchronous pure function <span class="dim">(line 14)</span>', delay: 1200 },
12
+ { text: ' <span class="warn">warning</span> <span class="dim">single-use-wrapper</span> formatResponse() called once, adds no logic <span class="dim">(line 31)</span>', delay: 1400 },
13
+ { text: '', delay: 1600 },
14
+ { text: '<span class="file">src/utils/helpers.ts</span>', delay: 1800 },
15
+ { text: ' <span class="err">error</span> <span class="dim">verbose-comments</span> Comment restates code: "increment counter by one" <span class="dim">(line 8)</span>', delay: 2000 },
16
+ { text: ' <span class="warn">warning</span> <span class="dim">redundant-else-return</span> Else block after early return is unnecessary <span class="dim">(line 22)</span>', delay: 2200 },
17
+ { text: ' <span class="warn">warning</span> <span class="dim">single-option-object</span> Options param has only 1 property used <span class="dim">(line 45)</span>', delay: 2400 },
18
+ { text: '', delay: 2600 },
19
+ { text: '<span class="dim">──────────────────────────────────────</span>', delay: 2800 },
20
+ { text: '<span class="accent">Slop Score: 34/100</span> <span class="dim">|</span> <span class="warn">5 findings</span> <span class="dim">|</span> <span class="ok">2 files clean</span>', delay: 3000 },
21
+ { text: '<span class="dim">Run</span> <span class="cmd">distyll scan --fix</span> <span class="dim">to auto-apply suggested fixes</span>', delay: 3200 },
22
+ ];
23
+
24
+ var terminalBody = document.getElementById('terminal-output');
25
+
26
+ function runAnimation() {
27
+ if (!terminalBody) return;
28
+ terminalBody.innerHTML = '';
29
+ terminalLines.forEach(function (item, i) {
30
+ setTimeout(function () {
31
+ var div = document.createElement('div');
32
+ div.className = 'terminal-line';
33
+ div.innerHTML = item.text || '&nbsp;';
34
+ div.style.animationDelay = '0s';
35
+ terminalBody.appendChild(div);
36
+ terminalBody.scrollTop = terminalBody.scrollHeight;
37
+ }, item.delay);
38
+ });
39
+ }
40
+
41
+ // Run animation on load
42
+ runAnimation();
43
+
44
+ // Re-run when terminal scrolls into view
45
+ var observer = new IntersectionObserver(function (entries) {
46
+ entries.forEach(function (entry) {
47
+ if (entry.isIntersecting) {
48
+ runAnimation();
49
+ }
50
+ });
51
+ }, { threshold: 0.5 });
52
+
53
+ var terminalEl = document.querySelector('.terminal');
54
+ if (terminalEl) observer.observe(terminalEl);
55
+
56
+ // Copy to clipboard
57
+ document.querySelectorAll('.install-cmd').forEach(function (el) {
58
+ el.addEventListener('click', function () {
59
+ var text = el.getAttribute('data-cmd') || 'npm install -g distyll';
60
+ navigator.clipboard.writeText(text).then(function () {
61
+ el.classList.add('copied');
62
+ var icon = el.querySelector('.copy-icon');
63
+ if (icon) icon.textContent = 'Copied!';
64
+ setTimeout(function () {
65
+ el.classList.remove('copied');
66
+ if (icon) icon.textContent = 'Copy';
67
+ }, 2000);
68
+ });
69
+ });
70
+ });
71
+
72
+ // Smooth scroll for nav links
73
+ document.querySelectorAll('a[href^="#"]').forEach(function (link) {
74
+ link.addEventListener('click', function (e) {
75
+ var target = document.querySelector(link.getAttribute('href'));
76
+ if (target) {
77
+ e.preventDefault();
78
+ target.scrollIntoView({ behavior: 'smooth' });
79
+ }
80
+ });
81
+ });
82
+ })();
package/site/style.css ADDED
@@ -0,0 +1,516 @@
1
+ :root {
2
+ --bg: #0b0f1a;
3
+ --bg-card: #131927;
4
+ --bg-terminal: #0d1117;
5
+ --text: #e2e8f0;
6
+ --text-muted: #94a3b8;
7
+ --text-dim: #64748b;
8
+ --accent: #6366f1;
9
+ --accent-light: #818cf8;
10
+ --accent-glow: rgba(99, 102, 241, 0.15);
11
+ --green: #22c55e;
12
+ --yellow: #eab308;
13
+ --red: #ef4444;
14
+ --cyan: #22d3ee;
15
+ --border: #1e293b;
16
+ --radius: 12px;
17
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
+ --font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
19
+ }
20
+
21
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
22
+
23
+ html { scroll-behavior: smooth; }
24
+
25
+ body {
26
+ font-family: var(--font);
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ line-height: 1.6;
30
+ -webkit-font-smoothing: antialiased;
31
+ }
32
+
33
+ a { color: var(--accent-light); text-decoration: none; }
34
+ a:hover { text-decoration: underline; }
35
+
36
+ .container {
37
+ max-width: 1100px;
38
+ margin: 0 auto;
39
+ padding: 0 24px;
40
+ }
41
+
42
+ /* NAV */
43
+ nav {
44
+ position: fixed;
45
+ top: 0;
46
+ left: 0;
47
+ right: 0;
48
+ z-index: 100;
49
+ background: rgba(11, 15, 26, 0.85);
50
+ backdrop-filter: blur(12px);
51
+ border-bottom: 1px solid var(--border);
52
+ }
53
+
54
+ nav .container {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ height: 60px;
59
+ }
60
+
61
+ .logo {
62
+ font-family: var(--font-mono);
63
+ font-size: 1.25rem;
64
+ font-weight: 700;
65
+ color: var(--text);
66
+ }
67
+
68
+ .logo span { color: var(--accent-light); }
69
+
70
+ nav ul {
71
+ list-style: none;
72
+ display: flex;
73
+ gap: 28px;
74
+ }
75
+
76
+ nav ul a {
77
+ color: var(--text-muted);
78
+ font-size: 0.9rem;
79
+ transition: color 0.2s;
80
+ }
81
+
82
+ nav ul a:hover { color: var(--text); text-decoration: none; }
83
+
84
+ /* HERO */
85
+ .hero {
86
+ padding: 140px 0 80px;
87
+ text-align: center;
88
+ }
89
+
90
+ .hero h1 {
91
+ font-size: clamp(2rem, 5vw, 3.25rem);
92
+ font-weight: 800;
93
+ line-height: 1.15;
94
+ max-width: 720px;
95
+ margin: 0 auto 16px;
96
+ letter-spacing: -0.02em;
97
+ }
98
+
99
+ .hero h1 .highlight {
100
+ background: linear-gradient(135deg, var(--accent-light), var(--cyan));
101
+ -webkit-background-clip: text;
102
+ -webkit-text-fill-color: transparent;
103
+ background-clip: text;
104
+ }
105
+
106
+ .hero .subtitle {
107
+ font-size: 1.2rem;
108
+ color: var(--text-muted);
109
+ max-width: 540px;
110
+ margin: 0 auto 40px;
111
+ }
112
+
113
+ .cta-row {
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ gap: 16px;
118
+ flex-wrap: wrap;
119
+ }
120
+
121
+ .btn {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ padding: 12px 28px;
126
+ border-radius: 8px;
127
+ font-size: 0.95rem;
128
+ font-weight: 600;
129
+ border: none;
130
+ cursor: pointer;
131
+ transition: transform 0.15s, box-shadow 0.15s;
132
+ }
133
+
134
+ .btn:hover { transform: translateY(-1px); text-decoration: none; }
135
+
136
+ .btn-primary {
137
+ background: var(--accent);
138
+ color: #fff;
139
+ box-shadow: 0 0 20px var(--accent-glow);
140
+ }
141
+
142
+ .btn-primary:hover { box-shadow: 0 0 32px var(--accent-glow); }
143
+
144
+ .btn-outline {
145
+ background: transparent;
146
+ color: var(--text-muted);
147
+ border: 1px solid var(--border);
148
+ }
149
+
150
+ .btn-outline:hover { border-color: var(--text-dim); color: var(--text); }
151
+
152
+ .install-cmd {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 12px;
156
+ background: var(--bg-card);
157
+ border: 1px solid var(--border);
158
+ border-radius: 8px;
159
+ padding: 12px 20px;
160
+ font-family: var(--font-mono);
161
+ font-size: 0.9rem;
162
+ color: var(--text-muted);
163
+ margin-top: 24px;
164
+ cursor: pointer;
165
+ transition: border-color 0.2s;
166
+ }
167
+
168
+ .install-cmd:hover { border-color: var(--accent); }
169
+
170
+ .install-cmd code { color: var(--green); }
171
+
172
+ .install-cmd .copy-icon {
173
+ color: var(--text-dim);
174
+ transition: color 0.2s;
175
+ }
176
+
177
+ .install-cmd:hover .copy-icon { color: var(--accent-light); }
178
+
179
+ .install-cmd.copied .copy-icon { color: var(--green); }
180
+
181
+ /* TERMINAL */
182
+ .terminal-wrapper {
183
+ max-width: 700px;
184
+ margin: 48px auto 0;
185
+ }
186
+
187
+ .terminal {
188
+ background: var(--bg-terminal);
189
+ border: 1px solid var(--border);
190
+ border-radius: var(--radius);
191
+ overflow: hidden;
192
+ text-align: left;
193
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
194
+ }
195
+
196
+ .terminal-bar {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 8px;
200
+ padding: 12px 16px;
201
+ background: #161b22;
202
+ border-bottom: 1px solid var(--border);
203
+ }
204
+
205
+ .terminal-dot {
206
+ width: 12px;
207
+ height: 12px;
208
+ border-radius: 50%;
209
+ }
210
+
211
+ .terminal-dot.red { background: #ff5f57; }
212
+ .terminal-dot.yellow { background: #febc2e; }
213
+ .terminal-dot.green { background: #28c840; }
214
+
215
+ .terminal-title {
216
+ flex: 1;
217
+ text-align: center;
218
+ font-size: 0.75rem;
219
+ color: var(--text-dim);
220
+ font-family: var(--font-mono);
221
+ }
222
+
223
+ .terminal-body {
224
+ padding: 20px;
225
+ font-family: var(--font-mono);
226
+ font-size: 0.82rem;
227
+ line-height: 1.7;
228
+ min-height: 260px;
229
+ }
230
+
231
+ .terminal-body .prompt { color: var(--green); }
232
+ .terminal-body .cmd { color: var(--text); }
233
+ .terminal-body .dim { color: var(--text-dim); }
234
+ .terminal-body .file { color: var(--cyan); }
235
+ .terminal-body .warn { color: var(--yellow); }
236
+ .terminal-body .err { color: var(--red); }
237
+ .terminal-body .accent { color: var(--accent-light); }
238
+ .terminal-body .ok { color: var(--green); }
239
+
240
+ .terminal-line {
241
+ opacity: 0;
242
+ animation: fadeInLine 0.15s forwards;
243
+ white-space: pre;
244
+ }
245
+
246
+ @keyframes fadeInLine {
247
+ to { opacity: 1; }
248
+ }
249
+
250
+ /* SECTION COMMON */
251
+ section {
252
+ padding: 80px 0;
253
+ }
254
+
255
+ section h2 {
256
+ font-size: 2rem;
257
+ font-weight: 700;
258
+ text-align: center;
259
+ margin-bottom: 12px;
260
+ }
261
+
262
+ section .section-sub {
263
+ text-align: center;
264
+ color: var(--text-muted);
265
+ max-width: 560px;
266
+ margin: 0 auto 48px;
267
+ font-size: 1.05rem;
268
+ }
269
+
270
+ /* FEATURES */
271
+ .features-grid {
272
+ display: grid;
273
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
274
+ gap: 20px;
275
+ }
276
+
277
+ .feature-card {
278
+ background: var(--bg-card);
279
+ border: 1px solid var(--border);
280
+ border-radius: var(--radius);
281
+ padding: 28px;
282
+ transition: border-color 0.2s;
283
+ }
284
+
285
+ .feature-card:hover { border-color: var(--accent); }
286
+
287
+ .feature-icon {
288
+ width: 40px;
289
+ height: 40px;
290
+ border-radius: 10px;
291
+ background: var(--accent-glow);
292
+ display: flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ font-size: 1.2rem;
296
+ margin-bottom: 16px;
297
+ }
298
+
299
+ .feature-card h3 {
300
+ font-size: 1.05rem;
301
+ margin-bottom: 8px;
302
+ }
303
+
304
+ .feature-card p {
305
+ color: var(--text-muted);
306
+ font-size: 0.9rem;
307
+ line-height: 1.5;
308
+ }
309
+
310
+ /* HOW IT WORKS */
311
+ .steps {
312
+ display: grid;
313
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
314
+ gap: 32px;
315
+ max-width: 900px;
316
+ margin: 0 auto;
317
+ }
318
+
319
+ .step {
320
+ text-align: center;
321
+ position: relative;
322
+ }
323
+
324
+ .step-number {
325
+ width: 48px;
326
+ height: 48px;
327
+ border-radius: 50%;
328
+ background: var(--accent-glow);
329
+ border: 2px solid var(--accent);
330
+ display: inline-flex;
331
+ align-items: center;
332
+ justify-content: center;
333
+ font-weight: 700;
334
+ font-size: 1.1rem;
335
+ color: var(--accent-light);
336
+ margin-bottom: 16px;
337
+ }
338
+
339
+ .step code {
340
+ display: block;
341
+ font-family: var(--font-mono);
342
+ font-size: 0.85rem;
343
+ color: var(--green);
344
+ background: var(--bg-card);
345
+ border: 1px solid var(--border);
346
+ border-radius: 6px;
347
+ padding: 8px 14px;
348
+ margin: 12px auto 0;
349
+ max-width: 240px;
350
+ }
351
+
352
+ .step h3 { font-size: 1.05rem; margin-bottom: 6px; }
353
+ .step p { color: var(--text-muted); font-size: 0.9rem; }
354
+
355
+ /* COMPARISON TABLE */
356
+ .comparison-wrapper {
357
+ overflow-x: auto;
358
+ -webkit-overflow-scrolling: touch;
359
+ }
360
+
361
+ .comparison {
362
+ width: 100%;
363
+ border-collapse: collapse;
364
+ min-width: 600px;
365
+ }
366
+
367
+ .comparison th,
368
+ .comparison td {
369
+ padding: 14px 18px;
370
+ text-align: center;
371
+ border-bottom: 1px solid var(--border);
372
+ font-size: 0.9rem;
373
+ }
374
+
375
+ .comparison th {
376
+ font-weight: 600;
377
+ color: var(--text);
378
+ background: var(--bg-card);
379
+ }
380
+
381
+ .comparison th:first-child,
382
+ .comparison td:first-child {
383
+ text-align: left;
384
+ font-weight: 500;
385
+ }
386
+
387
+ .comparison td { color: var(--text-muted); }
388
+
389
+ .comparison .check { color: var(--green); font-size: 1.1rem; }
390
+ .comparison .cross { color: var(--text-dim); font-size: 1.1rem; }
391
+ .comparison .partial { color: var(--yellow); font-size: 0.85rem; }
392
+
393
+ .comparison tr:hover td { background: rgba(99, 102, 241, 0.04); }
394
+
395
+ .comparison th.highlight-col,
396
+ .comparison td.highlight-col {
397
+ background: var(--accent-glow);
398
+ }
399
+
400
+ /* PRICING */
401
+ .pricing-grid {
402
+ display: grid;
403
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
404
+ gap: 24px;
405
+ max-width: 960px;
406
+ margin: 0 auto;
407
+ }
408
+
409
+ .price-card {
410
+ background: var(--bg-card);
411
+ border: 1px solid var(--border);
412
+ border-radius: var(--radius);
413
+ padding: 32px 28px;
414
+ display: flex;
415
+ flex-direction: column;
416
+ }
417
+
418
+ .price-card.featured {
419
+ border-color: var(--accent);
420
+ box-shadow: 0 0 24px var(--accent-glow);
421
+ position: relative;
422
+ }
423
+
424
+ .price-badge {
425
+ position: absolute;
426
+ top: -12px;
427
+ left: 50%;
428
+ transform: translateX(-50%);
429
+ background: var(--accent);
430
+ color: #fff;
431
+ font-size: 0.75rem;
432
+ font-weight: 600;
433
+ padding: 4px 14px;
434
+ border-radius: 20px;
435
+ }
436
+
437
+ .price-card h3 {
438
+ font-size: 1.15rem;
439
+ margin-bottom: 4px;
440
+ }
441
+
442
+ .price-card .price {
443
+ font-size: 2.25rem;
444
+ font-weight: 800;
445
+ margin: 12px 0 4px;
446
+ }
447
+
448
+ .price-card .price span {
449
+ font-size: 0.9rem;
450
+ font-weight: 400;
451
+ color: var(--text-muted);
452
+ }
453
+
454
+ .price-card .price-desc {
455
+ color: var(--text-dim);
456
+ font-size: 0.85rem;
457
+ margin-bottom: 20px;
458
+ }
459
+
460
+ .price-card ul {
461
+ list-style: none;
462
+ flex: 1;
463
+ margin-bottom: 24px;
464
+ }
465
+
466
+ .price-card ul li {
467
+ padding: 6px 0;
468
+ font-size: 0.9rem;
469
+ color: var(--text-muted);
470
+ }
471
+
472
+ .price-card ul li::before {
473
+ content: "\2713";
474
+ color: var(--green);
475
+ margin-right: 10px;
476
+ font-weight: 700;
477
+ }
478
+
479
+ .price-card .btn { width: 100%; justify-content: center; }
480
+
481
+ /* FOOTER */
482
+ footer {
483
+ border-top: 1px solid var(--border);
484
+ padding: 40px 0;
485
+ text-align: center;
486
+ color: var(--text-dim);
487
+ font-size: 0.85rem;
488
+ }
489
+
490
+ footer .footer-links {
491
+ display: flex;
492
+ justify-content: center;
493
+ gap: 24px;
494
+ margin-bottom: 16px;
495
+ }
496
+
497
+ footer .footer-links a { color: var(--text-muted); font-size: 0.85rem; }
498
+
499
+ /* MOBILE */
500
+ @media (max-width: 768px) {
501
+ nav ul { display: none; }
502
+
503
+ .hero { padding: 110px 0 60px; }
504
+ .hero h1 { font-size: 1.75rem; }
505
+
506
+ section { padding: 60px 0; }
507
+ section h2 { font-size: 1.5rem; }
508
+
509
+ .features-grid { grid-template-columns: 1fr; }
510
+ .steps { grid-template-columns: 1fr; gap: 24px; }
511
+ .pricing-grid { grid-template-columns: 1fr; max-width: 380px; }
512
+
513
+ .terminal-body { font-size: 0.72rem; padding: 14px; min-height: 200px; }
514
+
515
+ .cta-row { flex-direction: column; }
516
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,114 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ interface CacheEntry {
5
+ timestamp: string;
6
+ ref?: string;
7
+ branch?: string;
8
+ score: number;
9
+ totalFindings: number;
10
+ fileCount: number;
11
+ }
12
+
13
+ interface CacheData {
14
+ entries: CacheEntry[];
15
+ }
16
+
17
+ const CACHE_DIR = '.distyll';
18
+ const CACHE_FILE = 'cache.json';
19
+ const MAX_ENTRIES = 100;
20
+
21
+ function getCachePath(repoRoot: string): string {
22
+ return path.join(repoRoot, CACHE_DIR, CACHE_FILE);
23
+ }
24
+
25
+ function ensureCacheDir(repoRoot: string): void {
26
+ const dir = path.join(repoRoot, CACHE_DIR);
27
+ if (!fs.existsSync(dir)) {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ export function loadCache(repoRoot: string): CacheData {
33
+ const cachePath = getCachePath(repoRoot);
34
+ try {
35
+ const raw = fs.readFileSync(cachePath, 'utf-8');
36
+ const parsed = JSON.parse(raw);
37
+ if (parsed && Array.isArray(parsed.entries)) {
38
+ return parsed;
39
+ }
40
+ } catch {
41
+ // Cache doesn't exist or is corrupt — start fresh
42
+ }
43
+ return { entries: [] };
44
+ }
45
+
46
+ export function saveScore(
47
+ repoRoot: string,
48
+ score: number,
49
+ totalFindings: number,
50
+ fileCount: number,
51
+ ref?: string,
52
+ branch?: string
53
+ ): void {
54
+ ensureCacheDir(repoRoot);
55
+
56
+ const cache = loadCache(repoRoot);
57
+ const entry: CacheEntry = {
58
+ timestamp: new Date().toISOString(),
59
+ ref,
60
+ branch,
61
+ score,
62
+ totalFindings,
63
+ fileCount,
64
+ };
65
+
66
+ cache.entries.push(entry);
67
+
68
+ // Keep only the last MAX_ENTRIES
69
+ if (cache.entries.length > MAX_ENTRIES) {
70
+ cache.entries = cache.entries.slice(-MAX_ENTRIES);
71
+ }
72
+
73
+ const cachePath = getCachePath(repoRoot);
74
+ fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
75
+ }
76
+
77
+ export interface TrendSummary {
78
+ current: number;
79
+ previous: number | null;
80
+ delta: number | null;
81
+ direction: 'improving' | 'worsening' | 'stable' | 'first-scan';
82
+ history: { timestamp: string; score: number }[];
83
+ }
84
+
85
+ export function getTrend(repoRoot: string, currentScore: number): TrendSummary {
86
+ const cache = loadCache(repoRoot);
87
+ const history = cache.entries.map((e) => ({ timestamp: e.timestamp, score: e.score }));
88
+
89
+ if (cache.entries.length === 0) {
90
+ return {
91
+ current: currentScore,
92
+ previous: null,
93
+ delta: null,
94
+ direction: 'first-scan',
95
+ history,
96
+ };
97
+ }
98
+
99
+ const lastEntry = cache.entries[cache.entries.length - 1];
100
+ const delta = currentScore - lastEntry.score;
101
+
102
+ let direction: TrendSummary['direction'];
103
+ if (delta < -2) direction = 'improving';
104
+ else if (delta > 2) direction = 'worsening';
105
+ else direction = 'stable';
106
+
107
+ return {
108
+ current: currentScore,
109
+ previous: lastEntry.score,
110
+ delta,
111
+ direction,
112
+ history,
113
+ };
114
+ }