@vibecodeqa/cli 0.36.2 → 0.37.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.
@@ -125,8 +125,8 @@ export function runAccessibility(cwd) {
125
125
  }
126
126
  }
127
127
  }
128
- // 7. Check for html lang attribute in index.html
129
- const htmlPaths = ["index.html", "web/index.html", "public/index.html"];
128
+ // 7. Check for html lang attribute + viewport + mobile meta in index.html
129
+ const htmlPaths = ["index.html", "web/index.html", "public/index.html", "src/index.html"];
130
130
  for (const h of htmlPaths) {
131
131
  const full = join(cwd, h);
132
132
  if (!existsSync(full))
@@ -136,6 +136,42 @@ export function runAccessibility(cwd) {
136
136
  missingLang++;
137
137
  issues.push({ severity: "warning", message: "<html> missing lang attribute", file: h, rule: "html-lang" });
138
138
  }
139
+ // Mobile viewport
140
+ if (!/<meta[^>]*name=["']viewport["']/.test(content)) {
141
+ issues.push({ severity: "error", message: "Missing <meta name=\"viewport\"> — page won't scale on mobile", file: h, rule: "missing-viewport" });
142
+ }
143
+ // charset
144
+ if (!/<meta[^>]*charset=/i.test(content)) {
145
+ issues.push({ severity: "warning", message: "Missing <meta charset> — may cause encoding issues", file: h, rule: "missing-charset" });
146
+ }
147
+ // Touch icon for mobile bookmarks
148
+ if (!/<link[^>]*apple-touch-icon/.test(content) && !/<link[^>]*icon/.test(content)) {
149
+ issues.push({ severity: "info", message: "No favicon or apple-touch-icon — poor mobile bookmark experience", file: h, rule: "missing-icon" });
150
+ }
151
+ }
152
+ // 8. Mobile-unfriendly patterns in components
153
+ for (const f of files) {
154
+ const source = f.rawContent || f.content;
155
+ const lines = source.split("\n");
156
+ for (let i = 0; i < lines.length; i++) {
157
+ const line = lines[i];
158
+ // Fixed pixel widths that break on mobile
159
+ if (/style=.*width:\s*\d{4,}px/.test(line)) {
160
+ issues.push({ severity: "info", message: "Fixed width ≥1000px — likely breaks on mobile", file: f.path, line: i + 1, rule: "fixed-width" });
161
+ }
162
+ // Horizontal scroll containers without overflow handling
163
+ if (/overflow-x:\s*(?:scroll|auto)/.test(line) && !/\btouch\b/.test(line) && !/-webkit-overflow-scrolling/.test(line)) {
164
+ issues.push({ severity: "info", message: "Horizontal scroll without touch-action — poor mobile scroll UX", file: f.path, line: i + 1, rule: "touch-scroll" });
165
+ }
166
+ // Hover-only interactions (no touch fallback)
167
+ if (/onMouseEnter=|@mouseenter|on:mouseenter/.test(line) && !/onClick=|@click|on:click|onTouchStart|@touchstart/.test(line)) {
168
+ issues.push({ severity: "info", message: "Hover-only interaction — unreachable on touch devices", file: f.path, line: i + 1, rule: "hover-only" });
169
+ }
170
+ // Tiny touch targets
171
+ if (/(?:width|height):\s*(?:1[0-9]|[1-9])px/.test(line) && /(?:onClick|@click|on:click|button|<a )/.test(line)) {
172
+ issues.push({ severity: "info", message: "Touch target likely <44px — hard to tap on mobile (WCAG 2.5.8)", file: f.path, line: i + 1, rule: "small-touch-target" });
173
+ }
174
+ }
139
175
  }
140
176
  const errors = issues.filter((i) => i.severity === "error").length;
141
177
  const warnings = issues.filter((i) => i.severity === "warning").length;
@@ -82,8 +82,10 @@ export async function runDeadPatterns(cwd) {
82
82
  let started = false;
83
83
  for (let j = i; j < Math.min(i + 20, lines.length); j++) {
84
84
  const l = lines[j];
85
- braceDepth += (l.match(/\{/g) || []).length;
86
- braceDepth -= (l.match(/\}/g) || []).length;
85
+ // For "} catch (e) {" on one line, only count braces after "catch"
86
+ const braceText = j === i && l.includes("catch") ? l.slice(l.indexOf("catch")) : l;
87
+ braceDepth += (braceText.match(/\{/g) || []).length;
88
+ braceDepth -= (braceText.match(/\}/g) || []).length;
87
89
  if (braceDepth > 0)
88
90
  started = true;
89
91
  if (started && j > i) {
@@ -178,6 +178,36 @@ export function runPerformance(cwd) {
178
178
  });
179
179
  }
180
180
  }
181
+ // ── 7. PWA readiness (web projects only) ──
182
+ const isWebProject = !!(deps.react || deps.vue || deps.svelte || deps["@sveltejs/kit"] || deps.next || deps.nuxt);
183
+ if (isWebProject) {
184
+ const manifestPaths = ["public/manifest.json", "public/manifest.webmanifest", "manifest.json", "web/manifest.json"];
185
+ const hasManifest = manifestPaths.some((p) => existsSync(join(cwd, p)));
186
+ if (!hasManifest) {
187
+ issues.push({ severity: "info", message: "No web app manifest — can't install as PWA or add to home screen", rule: "no-manifest" });
188
+ }
189
+ const swPaths = ["public/sw.js", "public/service-worker.js", "src/service-worker.ts", "src/sw.ts"];
190
+ const hasSW = swPaths.some((p) => existsSync(join(cwd, p))) || !!(deps["workbox-webpack-plugin"] || deps["vite-plugin-pwa"] || deps["next-pwa"]);
191
+ if (!hasSW) {
192
+ issues.push({ severity: "info", message: "No service worker — app won't work offline", rule: "no-service-worker" });
193
+ }
194
+ }
195
+ // ── 8. CSS best practices ──
196
+ const cssFiles = getProductionFiles(cwd).filter((f) => f.ext === ".css");
197
+ for (const f of cssFiles) {
198
+ const lines = f.content.split("\n");
199
+ for (let i = 0; i < lines.length; i++) {
200
+ const line = lines[i];
201
+ // !important overuse
202
+ if (/!important/.test(line)) {
203
+ issues.push({ severity: "info", message: "!important — specificity escape hatch, usually a sign of CSS architecture issues", file: f.path, line: i + 1, rule: "css-important" });
204
+ }
205
+ }
206
+ // No media queries in CSS with fixed layouts
207
+ if (f.content.length > 500 && !/@media/.test(f.content) && /width:\s*\d{3,}px/.test(f.content)) {
208
+ issues.push({ severity: "info", message: "CSS with fixed widths but no @media queries — likely not responsive", file: f.path, rule: "no-media-queries" });
209
+ }
210
+ }
181
211
  // Score — proportional to codebase, capped per category
182
212
  const totalFiles = sourceFiles.length || 1;
183
213
  const barrelPenalty = Math.min(15, (barrelImports / totalFiles) * 200);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.36.2",
3
+ "version": "0.37.0",
4
4
  "description": "Code health scanner for the AI coding era. 25 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {