@birdapi/velinstyle 1.2.1 → 1.3.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 (156) hide show
  1. package/README.de.md +148 -11
  2. package/README.md +174 -16
  3. package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
  4. package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
  5. package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
  6. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
  7. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
  8. package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
  9. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
  10. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
  11. package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
  12. package/cli/atelier-catalog.json +1267 -0
  13. package/cli/atelier-formats.js +159 -0
  14. package/cli/atelier.js +382 -0
  15. package/cli/blueprints/empty-state.html +3 -5
  16. package/cli/cli-manifest.json +441 -161
  17. package/cli/docgen/extract-cli.js +2 -1
  18. package/cli/docs-generate.js +9 -0
  19. package/cli/experience.js +173 -0
  20. package/cli/index.js +1384 -361
  21. package/cli/motion.js +738 -0
  22. package/cli/production/component-graph.json +166 -0
  23. package/cli/production/explain.js +52 -0
  24. package/cli/production/extract.js +278 -0
  25. package/cli/production/graph.js +102 -0
  26. package/cli/production/report.js +168 -0
  27. package/cli/production/run.js +355 -0
  28. package/cli/production/trim-css.js +177 -0
  29. package/cli/production/trim-fonts.js +23 -0
  30. package/cli/production/trim-icons.js +38 -0
  31. package/cli/production/trim-js.js +48 -0
  32. package/cli/production/trim-motion.js +22 -0
  33. package/cli/production/trim-themes.js +50 -0
  34. package/cli/production/watch.js +38 -0
  35. package/cli/review.js +48 -1
  36. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  37. package/cli/security.js +190 -0
  38. package/cli/skills.js +53 -10
  39. package/cli/transparency.js +140 -41
  40. package/cli/workflow.js +32 -17
  41. package/components/index.js +3 -0
  42. package/components/runtime/component-loaders.js +3 -0
  43. package/components/velin-drawer.js +43 -4
  44. package/components/velin-empty-state.js +83 -0
  45. package/components/velin-modal.js +76 -15
  46. package/components/velin-otp-input.js +220 -0
  47. package/components/velin-password-strength.js +147 -0
  48. package/components/velin-sheet.js +49 -4
  49. package/components/velin-theme-toggle.js +15 -1
  50. package/core/a11y/component-contracts.json +391 -255
  51. package/core/attributes/registry.js +1 -1
  52. package/core/meta/knowledge/components.json +75 -3
  53. package/core/motion/analyze.js +38 -0
  54. package/core/motion/blueprint.js +51 -0
  55. package/core/motion/composer.js +114 -0
  56. package/core/motion/data/registry.json +352 -0
  57. package/core/motion/effects.js +17 -1
  58. package/core/motion/index.js +112 -9
  59. package/core/motion/optimize.js +44 -0
  60. package/core/motion/policy.js +77 -0
  61. package/core/motion/registry.js +131 -0
  62. package/core/motion/review.js +59 -0
  63. package/core/motion/scan.js +150 -0
  64. package/core/motion/timeline.js +68 -0
  65. package/core/motion/transitions.js +49 -0
  66. package/core/motion/triggers.js +168 -0
  67. package/core/security/checks/fs.js +92 -0
  68. package/core/security/detect.js +35 -0
  69. package/core/security/engine.js +116 -0
  70. package/core/security/index.js +18 -0
  71. package/core/security/registry.js +85 -0
  72. package/core/security/report/report.js +113 -0
  73. package/core/security/rules/ci/index.js +146 -0
  74. package/core/security/rules/consumer/index.js +174 -0
  75. package/core/security/rules/deps/index.js +182 -0
  76. package/core/security/rules/index.js +24 -0
  77. package/core/security/rules/publish/index.js +230 -0
  78. package/core/security/rules/repo/index.js +143 -0
  79. package/core/security/rules/secrets/index.js +133 -0
  80. package/core/security/score/dependency-health.js +53 -0
  81. package/core/security/score/release-health.js +66 -0
  82. package/core/security/score/score.js +102 -0
  83. package/core/security/timeline/timeline.js +76 -0
  84. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  85. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  86. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  87. package/dist/chunks/chunk-MYKUI364.js +116 -0
  88. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  89. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  90. package/dist/chunks/runtime-entry.js +1 -1
  91. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  92. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  93. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  94. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  95. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  96. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  97. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  98. package/dist/llms.txt +4 -4
  99. package/dist/search-index.json +232 -5
  100. package/dist/velin-agent.json +433 -46
  101. package/dist/velinstyle-components.iife.js +3695 -2272
  102. package/dist/velinstyle-components.js +3698 -2271
  103. package/dist/velinstyle-components.min.js +262 -120
  104. package/dist/velinstyle.css +96 -13
  105. package/dist/velinstyle.d.ts +3 -0
  106. package/dist/velinstyle.min.css +1 -1
  107. package/package.json +19 -4
  108. package/packages/velinstyle-cli-core/package.json +11 -0
  109. package/packages/velinstyle-cli-core/src/config.js +84 -0
  110. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  111. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  112. package/packages/velinstyle-cli-core/src/index.js +18 -0
  113. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  114. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  115. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  116. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  117. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  118. package/packages/velinstyle-cli-registry/package.json +13 -0
  119. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  120. package/packages/velinstyle-cli-renderer/package.json +8 -0
  121. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  122. package/packages/velinstyle-cli-theme/package.json +8 -0
  123. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  124. package/packages/velinstyle-cli-ui/package.json +8 -0
  125. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  126. package/packages/velinstyle-motion/README.md +81 -0
  127. package/packages/velinstyle-motion/package.json +37 -0
  128. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  129. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  130. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  131. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  132. package/packages/velinstyle-motion/src/config.js +257 -0
  133. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  134. package/packages/velinstyle-motion/src/index.js +304 -0
  135. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  136. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  137. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  138. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  139. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  140. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  141. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  142. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  143. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  144. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  145. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  146. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  147. package/packages/velinstyle-skills/catalog.json +1 -1
  148. package/packages/velinstyle-skills/registry.json +1 -142
  149. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  150. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  151. package/src/components/data-table.css +19 -0
  152. package/src/components/empty-auth.css +33 -0
  153. package/src/components/table.css +16 -6
  154. package/src/tokens/motion.css +7 -0
  155. package/src/utilities/scroll-animation.css +66 -0
  156. package/src/velinstyle.css +1 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Font utility trim — keep only used velin-font-* / weight utilities in text.css slice.
3
+ * (Full tokens/typography.css stays in alwaysCss; this filters utility text layer extras.)
4
+ */
5
+ import { filterCssByUsedClasses } from './trim-css.js';
6
+
7
+ export function filterFontUtilities(cssText, usedClasses) {
8
+ const used = usedClasses instanceof Set ? usedClasses : new Set(usedClasses);
9
+ // Always keep base text utilities that are structural if any font class used
10
+ const keep = new Set(used);
11
+ if ([...used].some((c) => c.startsWith('velin-font-') || c.startsWith('velin-text-'))) {
12
+ keep.add('velin-font-sans');
13
+ keep.add('velin-font-normal');
14
+ }
15
+ return filterCssByUsedClasses(cssText, keep, { keepAllWithoutVelinClass: false });
16
+ }
17
+
18
+ export function summarizeFontUsage(extracted) {
19
+ const classes = [...(extracted.fontWeights || extracted.classes || [])]
20
+ .filter((c) => String(c).startsWith('velin-font-') || String(c).startsWith('velin-text-'))
21
+ .sort();
22
+ return { classes, count: classes.length };
23
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Icon sprite trim — subset of icons/svg → production sprite.
3
+ */
4
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { join, basename } from 'path';
6
+
7
+ export function buildIconSpriteSubset({ pkgRoot, outDir, iconNames }) {
8
+ const svgDir = join(pkgRoot, 'icons', 'svg');
9
+ const names = new Set([...(iconNames || [])].map((n) => String(n).toLowerCase()));
10
+ const available = existsSync(svgDir)
11
+ ? readdirSync(svgDir).filter((f) => f.endsWith('.svg')).map((f) => basename(f, '.svg'))
12
+ : [];
13
+
14
+ const selected = available.filter((id) => names.has(id.toLowerCase()));
15
+ // If icons were requested but none match, still emit empty sprite shell when WC icon used
16
+ const includeAllIfEmptyScan = names.size === 0;
17
+ const ids = includeAllIfEmptyScan ? [] : selected;
18
+ const skipped = available.filter((id) => !ids.includes(id));
19
+
20
+ const symbols = ids.map((id) => {
21
+ let content = readFileSync(join(svgDir, `${id}.svg`), 'utf-8');
22
+ content = content.replace(/<svg[^>]*>/, '').replace(/<\/svg>/, '').trim();
23
+ return ` <symbol id="${id}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n ${content}\n </symbol>`;
24
+ });
25
+
26
+ const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">\n${symbols.join('\n')}\n</svg>\n`;
27
+ mkdirSync(outDir, { recursive: true });
28
+ const outFile = join(outDir, 'velin-icons.svg');
29
+ writeFileSync(outFile, sprite, 'utf-8');
30
+
31
+ return {
32
+ outFile,
33
+ included: ids,
34
+ skipped,
35
+ availableCount: available.length,
36
+ bytes: Buffer.byteLength(sprite, 'utf-8'),
37
+ };
38
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Emit a slim runtime stub that registers only detected Web Components.
3
+ */
4
+ export function buildProductionJs({ tags = [], features = {} } = {}) {
5
+ const unique = [...new Set(tags.map((t) => String(t).toLowerCase()))].sort();
6
+ const list = JSON.stringify(unique, null, 2);
7
+ const wantAttributes = Boolean(features.attributes);
8
+ const wantHighlight = Boolean(features.highlight);
9
+ const wantMotion = Boolean(features.motion);
10
+
11
+ return `/* VelinStyle Production Runtime — generated */
12
+ /* Registers only Web Components detected in your project. */
13
+
14
+ const TAGS = ${list};
15
+
16
+ export async function bootProduction(options = {}) {
17
+ const { register, bootFromDOM } = await import('@birdapi/velinstyle/runtime');
18
+ if (options.root || ${wantAttributes} || ${wantHighlight} || ${wantMotion}) {
19
+ return bootFromDOM(options.root || (typeof document !== 'undefined' ? document : undefined), {
20
+ tags: TAGS,
21
+ attributes: options.attributes ?? ${wantAttributes},
22
+ highlight: options.highlight ?? ${wantHighlight},
23
+ haptic: options.haptic ?? false,
24
+ });
25
+ }
26
+ return register(TAGS);
27
+ }
28
+
29
+ export { TAGS };
30
+
31
+ if (typeof document !== 'undefined' && !globalThis.__VELIN_PRODUCTION_NO_AUTOBOOT__) {
32
+ bootProduction().catch((err) => {
33
+ console.warn('[velinstyle production] boot failed:', err);
34
+ });
35
+ }
36
+ `;
37
+ }
38
+
39
+ export function detectRuntimeFeatures(extracted) {
40
+ const attrs = extracted.attrs || new Set();
41
+ const classes = extracted.classes || new Set();
42
+ const motion = extracted.motion || new Set();
43
+ return {
44
+ attributes: [...attrs].some((a) => a.startsWith('data-velin-') && a !== 'data-velin-theme' && a !== 'data-velin-component'),
45
+ highlight: [...classes].some((c) => c.includes('highlight') || c.includes('code')),
46
+ motion: motion.size > 0 || [...classes].some((c) => c.includes('animate')),
47
+ };
48
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Motion trim helpers — decide whether motion CSS files participate + explain removals.
3
+ */
4
+ export function shouldIncludeMotion(extracted, { force = null } = {}) {
5
+ if (force === true) return true;
6
+ if (force === false) return false;
7
+ const motion = extracted.motion || new Set();
8
+ const classes = extracted.classes || new Set();
9
+ if (motion.size > 0) return true;
10
+ for (const c of classes) {
11
+ if (/animate|motion|transition|scroll-fade|scroll-reveal/i.test(c)) return true;
12
+ }
13
+ return false;
14
+ }
15
+
16
+ export function motionClassSet(extracted) {
17
+ const set = new Set(extracted.motion || []);
18
+ for (const c of extracted.classes || []) {
19
+ if (/animate|motion|scroll-/i.test(c)) set.add(c);
20
+ }
21
+ return set;
22
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Theme trim — copy only used theme CSS into production output.
3
+ */
4
+ import { existsSync, readFileSync, copyFileSync, mkdirSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { listKnownThemeNames } from './graph.js';
7
+
8
+ export function resolveUsedThemes(extracted, { configThemes = 'auto', pkgRoot, defaultThemes = ['dark'] } = {}) {
9
+ const known = new Set(listKnownThemeNames(pkgRoot));
10
+ const used = new Set();
11
+
12
+ if (Array.isArray(configThemes)) {
13
+ for (const t of configThemes) used.add(String(t).toLowerCase());
14
+ } else {
15
+ for (const t of extracted.themes || []) used.add(String(t).toLowerCase());
16
+ }
17
+
18
+ // Always allow dark / light token paths if referenced via contrast toggles
19
+ for (const d of defaultThemes) {
20
+ if (known.has(d)) used.add(d);
21
+ }
22
+
23
+ // If nothing detected and auto — keep no optional themes (tokens cover default)
24
+ const selected = [...used].filter((t) => known.has(t)).sort();
25
+ const skipped = [...known].filter((t) => !used.has(t)).sort();
26
+ return { selected, skipped, known: [...known].sort() };
27
+ }
28
+
29
+ export function writeThemes({ pkgRoot, outDir, themes, minifyPrefer = true }) {
30
+ const themeOut = join(outDir, 'themes');
31
+ mkdirSync(themeOut, { recursive: true });
32
+ const written = [];
33
+ const missing = [];
34
+
35
+ for (const name of themes) {
36
+ const distMin = join(pkgRoot, 'dist', 'themes', `${name}.min.css`);
37
+ const src = join(pkgRoot, 'src', 'themes', `${name}.css`);
38
+ const dest = join(themeOut, minifyPrefer && existsSync(distMin) ? `${name}.min.css` : `${name}.css`);
39
+ if (minifyPrefer && existsSync(distMin)) {
40
+ copyFileSync(distMin, dest);
41
+ written.push({ name, file: dest, source: 'dist' });
42
+ } else if (existsSync(src)) {
43
+ writeFileSync(dest, readFileSync(src, 'utf-8'));
44
+ written.push({ name, file: dest, source: 'src' });
45
+ } else {
46
+ missing.push(name);
47
+ }
48
+ }
49
+ return { written, missing, themeOut };
50
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Watch content files and re-run production pipeline (debounced, Windows-safe).
3
+ */
4
+ import { watch as fsWatch } from 'fs';
5
+ import { resolve } from 'path';
6
+
7
+ export function watchProduction(rootPath, onChange, { debounceMs = 300, ignoreNames = ['node_modules', 'dist', '.git'] } = {}) {
8
+ const abs = resolve(rootPath);
9
+ let timer = null;
10
+ let closed = false;
11
+
12
+ const kick = (event, filename) => {
13
+ if (closed) return;
14
+ if (filename && ignoreNames.some((n) => String(filename).includes(n))) return;
15
+ clearTimeout(timer);
16
+ timer = setTimeout(() => {
17
+ Promise.resolve()
18
+ .then(() => onChange({ event, filename }))
19
+ .catch((err) => console.error('[velinstyle production --watch]', err));
20
+ }, debounceMs);
21
+ };
22
+
23
+ let watcher;
24
+ try {
25
+ watcher = fsWatch(abs, { recursive: true }, kick);
26
+ } catch {
27
+ // recursive watch unsupported — watch root only
28
+ watcher = fsWatch(abs, kick);
29
+ }
30
+
31
+ return {
32
+ close() {
33
+ closed = true;
34
+ clearTimeout(timer);
35
+ try { watcher.close(); } catch { /* ignore */ }
36
+ },
37
+ };
38
+ }
package/cli/review.js CHANGED
@@ -225,6 +225,43 @@ export function reviewHtml(html, ctx = {}) {
225
225
  const errors = issues.filter((i) => i.severity === 'error').length;
226
226
  const warnings = issues.filter((i) => i.severity === 'warning').length;
227
227
 
228
+ // Optimization heuristics (soft) — full vendor without production output hints
229
+ const usesFullCss = /velinstyle\.min\.css|velinstyle\.css/i.test(text) && !/velin-production/i.test(text);
230
+ const usesFullIife = /velinstyle-components\.min\.js/i.test(text);
231
+ const unusedThemeLinks = [...text.matchAll(/themes\/([a-z0-9-]+)\.min\.css/gi)]
232
+ .map((m) => m[1].toLowerCase());
233
+ const themeAttrs = [...text.matchAll(/data-velin-theme\s*=\s*["']([^"']+)["']/gi)]
234
+ .flatMap((m) => m[1].toLowerCase().split(/[\s,|]+/));
235
+ const unusedThemes = unusedThemeLinks.filter((t) => themeAttrs.length && !themeAttrs.includes(t));
236
+
237
+ if (usesFullCss && usesFullIife) {
238
+ issues.push({
239
+ code: 'optimization.full-bundle',
240
+ severity: 'warning',
241
+ message: 'Full CSS + components IIFE without production output',
242
+ fix: 'Run `velinstyle build --production` and link dist/velin-production assets for publish.',
243
+ });
244
+ } else if (usesFullCss) {
245
+ issues.push({
246
+ code: 'optimization.full-css',
247
+ severity: 'warning',
248
+ message: 'Full VelinStyle CSS bundle linked',
249
+ fix: 'Prefer production CSS from `velinstyle production` for go-live.',
250
+ });
251
+ }
252
+ if (unusedThemes.length) {
253
+ issues.push({
254
+ code: 'optimization.unused-themes',
255
+ severity: 'warning',
256
+ message: `${unusedThemes.length} theme stylesheet(s) not referenced by data-velin-theme`,
257
+ fix: `Remove unused theme links (${unusedThemes.slice(0, 5).join(', ')}) or run production theme trim.`,
258
+ });
259
+ }
260
+
261
+ const optIssues = issues.filter((i) => i.code.startsWith('optimization'));
262
+ const unusedComponentHint = [...text.matchAll(/<(velin-[a-z0-9-]+)\b/gi)].length;
263
+ const classHits = [...text.matchAll(/\bvelin-[\w:-]+\b/g)].length;
264
+
228
265
  const score = (base, penalty) => Math.max(0, Math.round((base - penalty) * 10) / 10);
229
266
 
230
267
  let scores = {
@@ -234,6 +271,7 @@ export function reviewHtml(html, ctx = {}) {
234
271
  performance: score(10, issues.filter((i) => i.code.startsWith('perf')).length * 2),
235
272
  conversion: score(10, issues.filter((i) => i.code.startsWith('conversion')).length * 2),
236
273
  visual: score(9, issues.filter((i) => i.code.startsWith('design')).length * 1.2),
274
+ optimization: score(10, optIssues.length * 2 + (unusedThemes.length ? Math.min(3, unusedThemes.length * 0.5) : 0)),
237
275
  };
238
276
 
239
277
  if (thin) {
@@ -250,10 +288,13 @@ export function reviewHtml(html, ctx = {}) {
250
288
  if (profile === 'app') scores.seo = Math.min(scores.seo, 7);
251
289
  }
252
290
 
291
+ const finalErrors = issues.filter((i) => i.severity === 'error').length;
292
+ const finalWarnings = issues.filter((i) => i.severity === 'warning').length;
293
+
253
294
  const avg = Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length;
254
295
  const promptScore = Math.max(0, Math.min(10, avg - (ctx.plan?.warnings?.length ? 1 : 0)));
255
296
 
256
- const gate = errors > 0 ? 'fail' : warnings > 0 ? 'warn' : 'pass';
297
+ const gate = finalErrors > 0 ? 'fail' : finalWarnings > 0 ? 'warn' : 'pass';
257
298
 
258
299
  return {
259
300
  version: 1,
@@ -262,6 +303,12 @@ export function reviewHtml(html, ctx = {}) {
262
303
  scores,
263
304
  issues,
264
305
  gate,
306
+ optimization: {
307
+ fullBundle: Boolean(usesFullCss && usesFullIife),
308
+ unusedThemes: unusedThemes.length,
309
+ componentTags: unusedComponentHint,
310
+ classTokens: classHits,
311
+ },
265
312
  };
266
313
  }
267
314
 
@@ -0,0 +1,33 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..', '__fixtures__', 'atelier-library', 'showcases');
6
+ const items = [
7
+ { id: '01-login', title: 'Fixture Login' },
8
+ { id: '04-pricing', title: 'Fixture Pricing' },
9
+ { id: '36-calendar', title: 'Fixture Calendar' },
10
+ ];
11
+
12
+ for (const it of items) {
13
+ const d = join(root, it.id);
14
+ mkdirSync(d, { recursive: true });
15
+ writeFileSync(
16
+ join(d, 'index.html'),
17
+ `<!DOCTYPE html>
18
+ <html lang="en"><head><meta charset="UTF-8"><title>${it.title}</title>
19
+ <link rel="stylesheet" href="app.css"></head>
20
+ <body><div id="root" data-atelier-id="${it.id}"></div>
21
+ <script type="module" src="app.js"></script></body></html>
22
+ `,
23
+ );
24
+ writeFileSync(
25
+ join(d, 'app.js'),
26
+ `const root = document.getElementById('root');
27
+ root.innerHTML = '<section class="velin-section" data-fixture="${it.id}"><h1>${it.title}</h1></section>';
28
+ `,
29
+ );
30
+ writeFileSync(join(d, 'app.css'), `[data-fixture="${it.id}"] { padding: 1rem; }\n`);
31
+ }
32
+
33
+ console.log('fixtures at', root);
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Velin Security Suite — CLI surface over Security Engine.
3
+ * velinstyle security doctor|scan|audit|publish|ci|deps|secrets|report
4
+ */
5
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
6
+ import { resolve, join } from 'path';
7
+ import {
8
+ runSecurityEngine,
9
+ toExperiencePayload,
10
+ toMarkdownReport,
11
+ gateLabel,
12
+ timelineRows,
13
+ } from '../core/security/index.js';
14
+
15
+ /**
16
+ * @param {string[]} args
17
+ * @param {{ C: object, getArg: Function, hasFlag: Function, emitExperience: Function }} util
18
+ */
19
+ export async function securityCmd(args, util) {
20
+ const { getArg, hasFlag, emitExperience } = util;
21
+ const sub = args[0] || 'doctor';
22
+ const rest = args.slice(1);
23
+ const target = rest.find((a) => !a.startsWith('-')) || '.';
24
+ const root = resolve(target);
25
+ const asJson = hasFlag('--json');
26
+ const strict = hasFlag('--strict');
27
+ const present = (payload) => emitExperience('security', payload);
28
+
29
+ if (sub === '--help' || sub === '-h') {
30
+ await present({
31
+ message: 'Usage: velinstyle security <doctor|scan|audit|publish|ci|deps|secrets|report> [path]',
32
+ suggestions: [
33
+ { text: 'velinstyle security doctor .' },
34
+ { text: 'velinstyle security deps .' },
35
+ { text: 'velinstyle security publish' },
36
+ { text: 'velinstyle security report --out ./security-report' },
37
+ ],
38
+ });
39
+ return;
40
+ }
41
+
42
+ try {
43
+ if (!existsSync(root)) {
44
+ await present({ status: 'ERROR', message: `Path not found: ${root}`, exitCode: 1 });
45
+ return;
46
+ }
47
+
48
+ const mode = resolveMode(sub, hasFlag);
49
+ const result = await runSecurityEngine({
50
+ root,
51
+ mode,
52
+ includeReleaseHealth: mode === 'publish' || mode === 'audit' || hasFlag('--publish'),
53
+ includeDependencyHealth: true,
54
+ updateTimeline: sub !== 'report' || true,
55
+ });
56
+
57
+ if (sub === 'report') {
58
+ const out = getArg('--out') || getArg('--output') || getArg('-o') || join(root, '.velin', 'security', 'report');
59
+ mkdirSync(out, { recursive: true });
60
+ writeFileSync(join(out, 'security-report.json'), `${JSON.stringify(result, null, 2)}\n`);
61
+ writeFileSync(join(out, 'security-report.md'), toMarkdownReport(result));
62
+ if (asJson) {
63
+ console.log(JSON.stringify({ ok: result.ok, out, score: result.securityScore }, null, 2));
64
+ return;
65
+ }
66
+ await present({
67
+ subtitle: 'report',
68
+ message: `Wrote security report → ${out}`,
69
+ items: [
70
+ { label: 'Score', value: String(result.securityScore) },
71
+ { label: 'Overall', value: result.overall },
72
+ ],
73
+ ...toExperiencePayload(result),
74
+ });
75
+ setExit(result, strict);
76
+ return;
77
+ }
78
+
79
+ if (asJson) {
80
+ console.log(JSON.stringify(result, null, 2));
81
+ setExit(result, strict);
82
+ return;
83
+ }
84
+
85
+ const exp = toExperiencePayload(result);
86
+ const extra = [];
87
+ if (result.dependencyHealth) {
88
+ extra.push({
89
+ label: 'Dep Health',
90
+ value: `${result.dependencyHealth.score}% (${result.dependencyHealth.deprecated} deprecated, ${result.dependencyHealth.noLockfile ?? 0} no-lockfile, ${result.dependencyHealth.outdated} outdated-heuristic, ${result.dependencyHealth.critical} critical)`,
91
+ });
92
+ }
93
+ if (result.releaseHealth) {
94
+ extra.push({ label: 'Release', value: result.releaseHealth.status });
95
+ }
96
+ const tl = timelineRows(result.timeline || { entries: {} });
97
+ await present({
98
+ subtitle: sub,
99
+ status: result.ok ? (result.overallGate === 'warn' ? 'WARNING' : 'PASS') : 'ERROR',
100
+ message: exp.message,
101
+ items: [...(exp.items || []), ...extra],
102
+ table: exp.table,
103
+ score: exp.score,
104
+ notifications: exp.notifications,
105
+ findings: (result.findings || []).slice(0, 25).map((f) => ({
106
+ severity: f.severity,
107
+ message: `[${f.ruleId}] ${f.message}`,
108
+ path: f.path,
109
+ })),
110
+ suggestions: tl.length
111
+ ? tl.map((r) => ({ text: `${r.title}: ${r.date}` }))
112
+ : [{ text: 'velinstyle security report --out ./.velin/security/report' }],
113
+ exitCode: exitCode(result, strict),
114
+ });
115
+ } catch (err) {
116
+ await present({
117
+ status: 'ERROR',
118
+ message: err?.message || String(err),
119
+ exitCode: 1,
120
+ });
121
+ }
122
+ }
123
+
124
+ function resolveMode(sub, hasFlag) {
125
+ if (hasFlag('--repo')) return 'repo';
126
+ if (hasFlag('--consumer')) return 'consumer';
127
+ if (hasFlag('--ci')) return 'ci';
128
+ if (hasFlag('--publish')) return 'publish';
129
+ if (hasFlag('--deps')) return 'deps';
130
+ if (hasFlag('--secrets')) return 'secrets';
131
+ if (sub === 'doctor') return 'doctor';
132
+ if (sub === 'scan') return 'scan';
133
+ if (sub === 'audit') return 'audit';
134
+ if (sub === 'publish') return 'publish';
135
+ if (sub === 'ci') return 'ci';
136
+ if (sub === 'deps') return 'deps';
137
+ if (sub === 'secrets') return 'secrets';
138
+ if (sub === 'report') return 'audit';
139
+ return 'doctor';
140
+ }
141
+
142
+ function exitCode(result, strict) {
143
+ if (result.overallGate === 'fail') return 1;
144
+ if (strict && result.overallGate === 'warn') return 1;
145
+ return 0;
146
+ }
147
+
148
+ function setExit(result, strict) {
149
+ const code = exitCode(result, strict);
150
+ if (code) process.exitCode = code;
151
+ }
152
+
153
+ /**
154
+ * Check-pipeline step (used by `velinstyle check`).
155
+ * @param {string} path
156
+ * @param {{ quiet?: boolean, C?: object, strict?: boolean }} [opts]
157
+ */
158
+ export async function securityCheckStep(path, { quiet, C, strict } = {}) {
159
+ try {
160
+ const root = resolve(path);
161
+ if (!existsSync(root)) return { ok: true, skipped: true };
162
+
163
+ const result = await runSecurityEngine({
164
+ root,
165
+ mode: 'doctor',
166
+ includeReleaseHealth: false,
167
+ includeDependencyHealth: true,
168
+ updateTimeline: true,
169
+ });
170
+
171
+ const fail = result.overallGate === 'fail' || (strict && result.overallGate === 'warn');
172
+ if (!quiet && C) {
173
+ const score = result.securityScore != null ? `${result.securityScore}%` : '—';
174
+ const gate = result.overallGate || '—';
175
+ const line = ` security: ${score} · ${gate} · ${result.overall || ''}`;
176
+ console.log(fail ? C.red(line) : result.overallGate === 'warn' ? C.yellow(line) : C.green(line));
177
+ }
178
+
179
+ return {
180
+ ok: !fail,
181
+ skipped: false,
182
+ securityScore: result.securityScore,
183
+ overall: result.overall,
184
+ overallGate: result.overallGate,
185
+ findings: result.findings?.length || 0,
186
+ };
187
+ } catch {
188
+ return { ok: true, skipped: true };
189
+ }
190
+ }