@birdapi/velinstyle 1.2.2 → 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 (109) hide show
  1. package/README.de.md +18 -4
  2. package/README.md +42 -7
  3. package/cli/cli-manifest.json +441 -174
  4. package/cli/docgen/extract-cli.js +2 -1
  5. package/cli/docs-generate.js +9 -0
  6. package/cli/experience.js +173 -0
  7. package/cli/index.js +1294 -417
  8. package/cli/motion.js +738 -0
  9. package/cli/production/extract.js +46 -6
  10. package/cli/production/report.js +104 -14
  11. package/cli/production/run.js +44 -1
  12. package/cli/security.js +190 -0
  13. package/cli/skills.js +53 -10
  14. package/cli/transparency.js +140 -41
  15. package/cli/workflow.js +32 -17
  16. package/components/velin-theme-toggle.js +15 -1
  17. package/core/a11y/component-contracts.json +391 -274
  18. package/core/attributes/registry.js +1 -1
  19. package/core/motion/analyze.js +38 -0
  20. package/core/motion/blueprint.js +51 -0
  21. package/core/motion/composer.js +114 -0
  22. package/core/motion/data/registry.json +352 -0
  23. package/core/motion/effects.js +17 -1
  24. package/core/motion/index.js +112 -9
  25. package/core/motion/optimize.js +44 -0
  26. package/core/motion/policy.js +77 -0
  27. package/core/motion/registry.js +131 -0
  28. package/core/motion/review.js +59 -0
  29. package/core/motion/scan.js +150 -0
  30. package/core/motion/timeline.js +68 -0
  31. package/core/motion/transitions.js +49 -0
  32. package/core/motion/triggers.js +168 -0
  33. package/core/security/checks/fs.js +92 -0
  34. package/core/security/detect.js +35 -0
  35. package/core/security/engine.js +116 -0
  36. package/core/security/index.js +18 -0
  37. package/core/security/registry.js +85 -0
  38. package/core/security/report/report.js +113 -0
  39. package/core/security/rules/ci/index.js +146 -0
  40. package/core/security/rules/consumer/index.js +174 -0
  41. package/core/security/rules/deps/index.js +182 -0
  42. package/core/security/rules/index.js +24 -0
  43. package/core/security/rules/publish/index.js +230 -0
  44. package/core/security/rules/repo/index.js +143 -0
  45. package/core/security/rules/secrets/index.js +133 -0
  46. package/core/security/score/dependency-health.js +53 -0
  47. package/core/security/score/release-health.js +66 -0
  48. package/core/security/score/score.js +102 -0
  49. package/core/security/timeline/timeline.js +76 -0
  50. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  51. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  52. package/dist/chunks/chunk-MYKUI364.js +116 -0
  53. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  54. package/dist/chunks/runtime-entry.js +1 -1
  55. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  56. package/dist/llms.txt +2 -2
  57. package/dist/search-index.json +166 -2
  58. package/dist/velin-agent.json +156 -32
  59. package/dist/velinstyle-components.iife.js +862 -17
  60. package/dist/velinstyle-components.js +862 -17
  61. package/dist/velinstyle-components.min.js +126 -126
  62. package/dist/velinstyle.css +42 -7
  63. package/dist/velinstyle.min.css +1 -1
  64. package/package.json +158 -143
  65. package/packages/velinstyle-cli-core/package.json +11 -0
  66. package/packages/velinstyle-cli-core/src/config.js +84 -0
  67. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  68. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  69. package/packages/velinstyle-cli-core/src/index.js +18 -0
  70. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  71. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  72. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  73. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  74. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  75. package/packages/velinstyle-cli-registry/package.json +13 -0
  76. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  77. package/packages/velinstyle-cli-renderer/package.json +8 -0
  78. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  79. package/packages/velinstyle-cli-theme/package.json +8 -0
  80. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  81. package/packages/velinstyle-cli-ui/package.json +8 -0
  82. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  83. package/packages/velinstyle-motion/README.md +81 -0
  84. package/packages/velinstyle-motion/package.json +37 -0
  85. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  86. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  87. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  88. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  89. package/packages/velinstyle-motion/src/config.js +257 -0
  90. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  91. package/packages/velinstyle-motion/src/index.js +304 -0
  92. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  93. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  94. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  95. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  96. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  97. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  98. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  99. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  100. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  101. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  102. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  103. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  104. package/packages/velinstyle-skills/catalog.json +1 -1
  105. package/packages/velinstyle-skills/registry.json +1 -142
  106. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  107. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  108. package/src/tokens/motion.css +7 -0
  109. package/src/utilities/scroll-animation.css +66 -0
@@ -9,9 +9,45 @@ export const DEFAULT_IGNORE = [
9
9
  ];
10
10
 
11
11
  export const CONTENT_EXTS = new Set([
12
- '.html', '.htm', '.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.vue', '.md', '.css',
12
+ '.html', '.htm',
13
+ '.blade.php',
14
+ '.twig',
15
+ '.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx',
16
+ '.vue',
17
+ '.astro',
18
+ '.md', '.mdx',
19
+ '.css',
13
20
  ]);
14
21
 
22
+ /** Resolve multi-dot extensions (.blade.php, .mdx) before extname. */
23
+ export function contentExtOf(filePath) {
24
+ const name = String(filePath || '').toLowerCase().replace(/\\/g, '/');
25
+ const base = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
26
+ if (base.endsWith('.blade.php')) return '.blade.php';
27
+ if (base.endsWith('.mdx')) return '.mdx';
28
+ return extname(base).toLowerCase();
29
+ }
30
+
31
+ export function isContentFile(filePath) {
32
+ return CONTENT_EXTS.has(contentExtOf(filePath));
33
+ }
34
+
35
+ /** Strip template expressions so {{ class }} / {% %} are not mistaken for tokens. */
36
+ export function stripTemplateNoise(content, ext) {
37
+ let text = String(content || '');
38
+ if (ext === '.astro') {
39
+ text = text.replace(/^---[\s\S]*?---\s*/, ' ');
40
+ }
41
+ if (ext === '.blade.php' || ext === '.twig' || ext === '.vue' || ext === '.astro' || ext === '.mdx') {
42
+ text = text
43
+ .replace(/\{\{[\s\S]*?\}\}/g, ' ')
44
+ .replace(/\{!![\s\S]*?!!\}/g, ' ')
45
+ .replace(/\{%[\s\S]*?%\}/g, ' ')
46
+ .replace(/@\{\{[\s\S]*?\}\}/g, ' ');
47
+ }
48
+ return text;
49
+ }
50
+
15
51
  const CLASS_ATTR_RE = /class(?:Name)?\s*=\s*["'`]([^"'`]+)["'`]/gi;
16
52
  const CLASS_TOKEN_RE = /\b((?:velin|expo)-[\w:-]+)\b/g;
17
53
  const TAG_RE = /<(velin-[a-z0-9-]+)\b/gi;
@@ -21,6 +57,7 @@ const THEMES_LIST_RE = /themes\s*[:=]\s*\[([^\]]+)\]/gi;
21
57
  const ICON_NAME_RE = /(?:name|icon)\s*=\s*["']([a-z0-9][a-z0-9_-]*)["']/gi;
22
58
  const ICON_HASH_RE = /velin-icons\.svg#([a-z0-9][a-z0-9_-]*)/gi;
23
59
  const MOTION_ATTR_RE = /data-velin-(?:animate|motion|scroll)\s*=\s*["']([^"']+)["']/gi;
60
+ const MOTION_BARE_RE = /\svelin-(?:motion|reveal|fade|slide|scale|parallax|hover|stagger|scroll|trigger|timeline|motion-sequence)(?:\s|=|>)/gi;
24
61
  const FONT_FACE_RE = /@font-face\s*\{([\s\S]*?)\}/gi;
25
62
  const FONT_WEIGHT_RE = /font-weight\s*:\s*([0-9]+|normal|bold)/gi;
26
63
 
@@ -44,7 +81,7 @@ export function walkContentFiles(dir, { ignore = DEFAULT_IGNORE, out = [], root
44
81
  continue;
45
82
  }
46
83
  if (!entry.isFile()) continue;
47
- const ext = extname(entry.name).toLowerCase();
84
+ const ext = contentExtOf(entry.name);
48
85
  if (!CONTENT_EXTS.has(ext)) continue;
49
86
  out.push(full);
50
87
  }
@@ -57,8 +94,7 @@ export function collectContentFiles(targetPath, { ignore = DEFAULT_IGNORE, extra
57
94
  if (!existsSync(abs)) return [];
58
95
  const st = statSync(abs);
59
96
  if (st.isFile()) {
60
- const ext = extname(abs).toLowerCase();
61
- return CONTENT_EXTS.has(ext) ? [abs] : [];
97
+ return isContentFile(abs) ? [abs] : [];
62
98
  }
63
99
  return walkContentFiles(abs, { ignore: ignoreList });
64
100
  }
@@ -119,6 +155,9 @@ export function extractFromText(content, { isCss = false } = {}) {
119
155
  if (part) motion.add(part.trim());
120
156
  }
121
157
  }
158
+ for (const m of text.matchAll(MOTION_BARE_RE)) {
159
+ motion.add(m[0].trim().replace(/=.*/, '').trim() || 'velin-motion');
160
+ }
122
161
  for (const cls of classes) {
123
162
  if (cls.includes('animate') || cls.includes('motion') || cls.startsWith('velin-scroll-')) {
124
163
  motion.add(cls);
@@ -191,8 +230,9 @@ export function extractProject(targetPath, options = {}) {
191
230
  } catch {
192
231
  continue;
193
232
  }
194
- const isCss = extname(file).toLowerCase() === '.css';
195
- const part = extractFromText(body, { isCss });
233
+ const ext = contentExtOf(file);
234
+ const isCss = ext === '.css';
235
+ const part = extractFromText(isCss ? body : stripTemplateNoise(body, ext), { isCss });
196
236
  for (const c of part.classes) merged.classes.add(c);
197
237
  for (const t of part.tags) merged.tags.add(t);
198
238
  for (const t of part.themes) merged.themes.add(t);
@@ -1,15 +1,22 @@
1
1
  /**
2
- * Production report — text + JSON.
2
+ * Production report — text + JSON with before/after breakdown.
3
3
  */
4
4
  export function buildReport(data) {
5
5
  const {
6
6
  originalBytes = 0,
7
7
  productionBytes = 0,
8
8
  breakdown = {},
9
+ categories = null,
9
10
  themes = [],
10
11
  icons = [],
11
12
  components = [],
12
13
  tags = [],
14
+ themeCountBefore = null,
15
+ themeCountAfter = null,
16
+ motionBefore = null,
17
+ motionAfter = null,
18
+ componentsBefore = null,
19
+ componentsAfter = null,
13
20
  imagesSkipped = true,
14
21
  filesScanned = 0,
15
22
  } = data;
@@ -17,23 +24,40 @@ export function buildReport(data) {
17
24
  const saved = Math.max(0, originalBytes - productionBytes);
18
25
  const pct = originalBytes > 0 ? Math.round((saved / originalBytes) * 100) : 0;
19
26
 
27
+ const rows = categories || buildCategoryRows({
28
+ breakdown,
29
+ themes,
30
+ icons,
31
+ components,
32
+ tags,
33
+ themeCountBefore,
34
+ themeCountAfter,
35
+ motionBefore,
36
+ motionAfter,
37
+ componentsBefore,
38
+ componentsAfter,
39
+ fullCss: data.fullCss,
40
+ fullJs: data.fullJs,
41
+ fullIcons: data.fullIcons,
42
+ cssBytes: data.cssBytes,
43
+ jsBytes: data.jsBytes,
44
+ iconBytes: data.iconBytes,
45
+ });
46
+
20
47
  const lines = [
21
- 'Velin Production Report',
22
- `Original ${formatKb(originalBytes)}`,
23
- '↓',
24
- `Production ${formatKb(productionBytes)}`,
25
- '↓',
26
- `${pct} % gespart`,
48
+ 'Production Report',
27
49
  '',
28
50
  ];
29
51
 
30
- for (const [key, delta] of Object.entries(breakdown)) {
31
- if (delta == null) continue;
32
- const label = key.padEnd(8);
33
- const sign = delta > 0 ? '-' : delta < 0 ? '+' : ' ';
34
- lines.push(`${label} ${sign}${formatKb(Math.abs(delta))}`);
52
+ for (const row of rows) {
53
+ const before = row.beforeCount != null ? String(row.beforeCount) : formatKb(row.before);
54
+ const after = row.afterCount != null ? String(row.afterCount) : formatKb(row.after);
55
+ lines.push(`${String(row.label).padEnd(14)}${before.padStart(8)} → ${after}`);
35
56
  }
36
57
 
58
+ lines.push('');
59
+ lines.push(`${'Savings'.padEnd(14)}${pct} %`);
60
+
37
61
  if (imagesSkipped) {
38
62
  lines.push('');
39
63
  lines.push('Images: skipped (coming soon)');
@@ -41,18 +65,19 @@ export function buildReport(data) {
41
65
 
42
66
  lines.push('');
43
67
  lines.push(`Scanned files: ${filesScanned}`);
44
- if (themes.length) lines.push(`Themes: ${themes.join(', ')}`);
68
+ if (themes.length) lines.push(`Themes kept: ${themes.join(', ')}`);
45
69
  if (icons.length) lines.push(`Icons (${icons.length}): ${icons.slice(0, 12).join(', ')}${icons.length > 12 ? '…' : ''}`);
46
70
  if (tags.length) lines.push(`Web Components: ${tags.join(', ')}`);
47
71
  if (components.length) lines.push(`CSS modules: ${components.length}`);
48
72
 
49
73
  return {
50
- version: 1,
74
+ version: 2,
51
75
  originalBytes,
52
76
  productionBytes,
53
77
  savedBytes: saved,
54
78
  savedPercent: pct,
55
79
  breakdown,
80
+ categories: rows,
56
81
  themes,
57
82
  icons,
58
83
  tags,
@@ -63,7 +88,50 @@ export function buildReport(data) {
63
88
  };
64
89
  }
65
90
 
91
+ function buildCategoryRows(opts) {
92
+ const {
93
+ fullCss = 0,
94
+ fullJs = 0,
95
+ fullIcons = 0,
96
+ cssBytes = 0,
97
+ jsBytes = 0,
98
+ iconBytes = 0,
99
+ themeCountBefore,
100
+ themeCountAfter,
101
+ motionBefore,
102
+ motionAfter,
103
+ componentsBefore,
104
+ componentsAfter,
105
+ breakdown = {},
106
+ themes = [],
107
+ components = [],
108
+ tags = [],
109
+ } = opts;
110
+
111
+ return [
112
+ { label: 'CSS', before: fullCss || (cssBytes + (breakdown.CSS || 0)), after: cssBytes },
113
+ { label: 'Runtime', before: fullJs || (jsBytes + (breakdown.JS || 0)), after: jsBytes },
114
+ { label: 'Icons', before: fullIcons || (iconBytes + (breakdown.Icons || 0)), after: iconBytes },
115
+ {
116
+ label: 'Themes',
117
+ beforeCount: themeCountBefore != null ? themeCountBefore : (themes.length + (breakdown.Themes ? 1 : 0)),
118
+ afterCount: themeCountAfter != null ? themeCountAfter : themes.length,
119
+ },
120
+ {
121
+ label: 'Motion',
122
+ beforeCount: motionBefore != null ? motionBefore : 18,
123
+ afterCount: motionAfter != null ? motionAfter : (breakdown.Motion ? 4 : 18),
124
+ },
125
+ {
126
+ label: 'Components',
127
+ beforeCount: componentsBefore != null ? componentsBefore : Math.max(components.length, tags.length, 43),
128
+ afterCount: componentsAfter != null ? componentsAfter : Math.max(tags.length, components.length ? Math.min(components.length, tags.length || components.length) : tags.length),
129
+ },
130
+ ];
131
+ }
132
+
66
133
  export function formatKb(bytes) {
134
+ if (bytes == null || Number.isNaN(bytes)) return '—';
67
135
  return `${(bytes / 1024).toFixed(bytes >= 10240 ? 0 : 1)} KB`;
68
136
  }
69
137
 
@@ -76,3 +144,25 @@ export function measureBytes(parts) {
76
144
  }
77
145
  return total;
78
146
  }
147
+
148
+ /** Map report → Experience contract widgets */
149
+ export function reportToExperienceWidgets(report) {
150
+ return [
151
+ {
152
+ id: 'hero',
153
+ props: {
154
+ title: 'Production Builder',
155
+ subtitle: 'Build only what your project actually uses',
156
+ eyebrow: 'Velin Experience',
157
+ },
158
+ },
159
+ {
160
+ id: 'bundle',
161
+ props: {
162
+ title: 'Production Report',
163
+ rows: report.categories || [],
164
+ savingsPct: report.savedPercent,
165
+ },
166
+ },
167
+ ];
168
+ }
@@ -229,6 +229,12 @@ export async function runProduction(opts = {}) {
229
229
  const report = buildReport({
230
230
  originalBytes: original || (fullCss + fullJs + fullIcons),
231
231
  productionBytes,
232
+ fullCss,
233
+ fullJs,
234
+ fullIcons,
235
+ cssBytes,
236
+ jsBytes,
237
+ iconBytes: iconMeta.bytes || 0,
232
238
  breakdown: {
233
239
  CSS: Math.max(0, fullCss - cssBytes),
234
240
  JS: Math.max(0, fullJs - jsBytes),
@@ -238,6 +244,12 @@ export async function runProduction(opts = {}) {
238
244
  Fonts: 2 * 1024,
239
245
  },
240
246
  themes: themeMeta.selected || [],
247
+ themeCountBefore: listKnownThemeNames(PKG_ROOT).length,
248
+ themeCountAfter: (themeMeta.selected || []).length,
249
+ motionBefore: 18,
250
+ motionAfter: includeMotion ? 18 : 4,
251
+ componentsBefore: 43,
252
+ componentsAfter: Math.max(closure.tags.length, 1),
241
253
  icons: iconMeta.included || [],
242
254
  components: cssResult.included,
243
255
  tags: closure.tags,
@@ -280,7 +292,38 @@ export async function runProduction(opts = {}) {
280
292
  }
281
293
 
282
294
  console.log(C.green(`\nProduction build → ${outDir}`));
283
- console.log(report.text);
295
+ try {
296
+ const { createExperienceSession, parseExperienceFlags, writeRecording, loadExperienceConfig } = await import('../experience.js');
297
+ const { reportToExperienceWidgets } = await import('./report.js');
298
+ const overrides = parseExperienceFlags(args || process.argv.slice(2), helpers || {
299
+ getArg: () => null,
300
+ hasFlag: () => false,
301
+ });
302
+ const config = loadExperienceConfig(cwd, overrides);
303
+ const session = createExperienceSession(config);
304
+ const contract = {
305
+ project: { outDir, filesScanned: extracted.files.length },
306
+ lifecycle: { stage: 'summary', stages: [] },
307
+ result: { status: 'PASS', message: `Saved ${report.savedPercent}%` },
308
+ stats: { savedPercent: report.savedPercent, productionBytes, originalBytes: report.originalBytes },
309
+ widgets: reportToExperienceWidgets(report),
310
+ suggestions: explain
311
+ ? [{ text: 'See explain.txt for removed CSS/themes/icons' }]
312
+ : [{ text: 'Add --explain for removed-asset details' }],
313
+ diagnostics: [],
314
+ notifications: [
315
+ { tone: 'ok', message: `Production savings ${report.savedPercent}%` },
316
+ ],
317
+ meta: { commandId: 'production', layout: 'build', durationMs: 0 },
318
+ };
319
+ console.log(session.render(contract));
320
+ if (config.record || overrides.record) {
321
+ const paths = writeRecording({ contract, events: [], config });
322
+ console.log(`\nRecorded → ${paths.json}`);
323
+ }
324
+ } catch {
325
+ console.log(report.text);
326
+ }
284
327
  console.log(C.dim(`\nCSS: ${cssOut}${bundled ? ' (lightningcss)' : ' (raw)'}`));
285
328
  if (jsOut) console.log(C.dim(`JS: ${jsOut} (${closure.tags.length} components)`));
286
329
  if (!noIcons) console.log(C.dim(`Icons: ${iconMeta.included.length} / ${iconMeta.availableCount || '?'}`));
@@ -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
+ }
package/cli/skills.js CHANGED
@@ -28,7 +28,11 @@ export function loadSkillRegistry() {
28
28
  return loadRegistry(REGISTRY_PATH);
29
29
  }
30
30
 
31
- export function skillsList(argv = []) {
31
+ /**
32
+ * @param {string[]} argv
33
+ * @param {{ silent?: boolean }} [opts]
34
+ */
35
+ export function skillsList(argv = [], opts = {}) {
32
36
  const registry = loadSkillRegistry();
33
37
  const items = listSkills(registry, {
34
38
  capability: argValue(argv, '--capability') || undefined,
@@ -37,14 +41,16 @@ export function skillsList(argv = []) {
37
41
  origin: argValue(argv, '--origin') || undefined,
38
42
  category: argValue(argv, '--category') || undefined,
39
43
  });
44
+ if (opts.silent) return items;
40
45
  if (hasFlag(argv, '--json')) {
41
46
  console.log(JSON.stringify(items, null, 2));
42
- return;
47
+ return items;
43
48
  }
44
49
  for (const item of items) {
45
50
  console.log(`${item.id} [${item.category}] ${item.priority}/${item.status} capabilities=${(item.capabilities || []).join(',')}`);
46
51
  }
47
52
  console.log(`\n${items.length} skill(s)`);
53
+ return items;
48
54
  }
49
55
 
50
56
  export function skillsShow(skillId, argv = []) {
@@ -56,9 +62,10 @@ export function skillsShow(skillId, argv = []) {
56
62
  }
57
63
  if (hasFlag(argv, '--human')) {
58
64
  console.log(formatSkillHuman(skill));
59
- return;
65
+ return skill;
60
66
  }
61
67
  console.log(JSON.stringify(skill, null, 2));
68
+ return skill;
62
69
  }
63
70
 
64
71
  function formatSkillHuman(skill) {
@@ -103,9 +110,14 @@ function formatSkillHuman(skill) {
103
110
  return lines.filter(Boolean).join('\n');
104
111
  }
105
112
 
106
- export function skillsInstall(argv = []) {
113
+ /**
114
+ * @param {string[]} argv
115
+ * @param {{ silent?: boolean }} [opts]
116
+ */
117
+ export function skillsInstall(argv = [], opts = {}) {
107
118
  const selection = argv[0];
108
119
  if (!selection) {
120
+ if (opts.silent) return null;
109
121
  console.error('Usage: velinstyle skills install <skill-id|pack|bundle:id|project:id>');
110
122
  process.exit(1);
111
123
  }
@@ -118,18 +130,30 @@ export function skillsInstall(argv = []) {
118
130
  targetRoot: target,
119
131
  includeDependencies: !hasFlag(argv, '--no-deps'),
120
132
  });
133
+ if (opts.silent) {
134
+ const markerPath = join(target, 'registry-source.json');
135
+ writeFileSync(markerPath, JSON.stringify({ source: REGISTRY_PATH, installed: result.installed }, null, 2), 'utf-8');
136
+ return result;
137
+ }
121
138
  for (const installed of result.installed) console.log(`Installed ${installed}`);
122
139
  if (result.issues.length) {
123
140
  for (const issue of result.issues) console.warn(`Warning: ${issue}`);
124
141
  }
125
142
  const markerPath = join(target, 'registry-source.json');
126
143
  writeFileSync(markerPath, JSON.stringify({ source: REGISTRY_PATH, installed: result.installed }, null, 2), 'utf-8');
144
+ return result;
127
145
  }
128
146
 
129
- export function skillsRun(skillId, argv = []) {
147
+ /**
148
+ * @param {string} skillId
149
+ * @param {string[]} argv
150
+ * @param {{ silent?: boolean }} [opts]
151
+ */
152
+ export function skillsRun(skillId, argv = [], opts = {}) {
130
153
  const registry = loadSkillRegistry();
131
154
  const resolved = resolveSkillGraph(registry, skillId);
132
155
  if (resolved.issues.length) {
156
+ if (opts.silent) return { ok: false, issues: resolved.issues, steps: [] };
133
157
  for (const issue of resolved.issues) console.error(issue);
134
158
  process.exit(1);
135
159
  }
@@ -139,20 +163,27 @@ export function skillsRun(skillId, argv = []) {
139
163
  if (!skill) continue;
140
164
  steps.push({ id: skill.id, requiresCli: skill.requiresCli || [], onlyIf: skill.onlyIf || [] });
141
165
  }
166
+ const payload = { steps };
167
+ if (opts.silent) return payload;
142
168
  if (hasFlag(argv, '--json')) {
143
- console.log(JSON.stringify({ steps }, null, 2));
144
- return;
169
+ console.log(JSON.stringify(payload, null, 2));
170
+ return payload;
145
171
  }
146
172
  for (const step of steps) {
147
173
  console.log(`- ${step.id}`);
148
174
  if (step.requiresCli.length) console.log(` cli: ${step.requiresCli.join(', ')}`);
149
175
  if (step.onlyIf.length) console.log(` onlyIf: ${step.onlyIf.join(', ')}`);
150
176
  }
177
+ return payload;
151
178
  }
152
179
 
153
- export function skillsValidate() {
180
+ /**
181
+ * @param {{ silent?: boolean }} [opts]
182
+ */
183
+ export function skillsValidate(opts = {}) {
154
184
  const registry = loadSkillRegistry();
155
185
  const result = validateRegistry(registry);
186
+ if (opts.silent) return result;
156
187
  if (result.errors.length) {
157
188
  for (const error of result.errors) console.error(`Error: ${error}`);
158
189
  }
@@ -163,9 +194,14 @@ export function skillsValidate() {
163
194
  process.exit(result.ok ? 0 : 1);
164
195
  }
165
196
 
166
- export async function skillsDoctor(argv = []) {
197
+ /**
198
+ * @param {string[]} argv
199
+ * @param {{ silent?: boolean }} [opts]
200
+ */
201
+ export async function skillsDoctor(argv = [], opts = {}) {
167
202
  const { doctorSkillPaths } = await import('../scripts/check-skills-paths.mjs');
168
203
  const result = doctorSkillPaths(PKG_ROOT);
204
+ if (opts.silent) return result;
169
205
  if (hasFlag(argv, '--json')) {
170
206
  console.log(JSON.stringify(result, null, 2));
171
207
  process.exit(result.ok ? 0 : 1);
@@ -176,9 +212,14 @@ export async function skillsDoctor(argv = []) {
176
212
  process.exit(1);
177
213
  }
178
214
  console.log(`skills doctor OK (${result.checked} paths).`);
215
+ return result;
179
216
  }
180
217
 
181
- export function skillsIndex(kind) {
218
+ /**
219
+ * @param {string} kind
220
+ * @param {{ silent?: boolean }} [opts]
221
+ */
222
+ export function skillsIndex(kind, opts = {}) {
182
223
  const registry = loadSkillRegistry();
183
224
  const map = {
184
225
  packs: registry.packs || [],
@@ -188,7 +229,9 @@ export function skillsIndex(kind) {
188
229
  graphs: registry.workflowGraphs || [],
189
230
  };
190
231
  const out = map[kind] || [];
232
+ if (opts.silent) return out;
191
233
  console.log(JSON.stringify(out, null, 2));
234
+ return out;
192
235
  }
193
236
 
194
237
  export function skillsCommand(argv = []) {