@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.
- package/README.de.md +18 -4
- package/README.md +42 -7
- package/cli/cli-manifest.json +441 -174
- package/cli/docgen/extract-cli.js +2 -1
- package/cli/docs-generate.js +9 -0
- package/cli/experience.js +173 -0
- package/cli/index.js +1294 -417
- package/cli/motion.js +738 -0
- package/cli/production/extract.js +46 -6
- package/cli/production/report.js +104 -14
- package/cli/production/run.js +44 -1
- package/cli/security.js +190 -0
- package/cli/skills.js +53 -10
- package/cli/transparency.js +140 -41
- package/cli/workflow.js +32 -17
- package/components/velin-theme-toggle.js +15 -1
- package/core/a11y/component-contracts.json +391 -274
- package/core/attributes/registry.js +1 -1
- package/core/motion/analyze.js +38 -0
- package/core/motion/blueprint.js +51 -0
- package/core/motion/composer.js +114 -0
- package/core/motion/data/registry.json +352 -0
- package/core/motion/effects.js +17 -1
- package/core/motion/index.js +112 -9
- package/core/motion/optimize.js +44 -0
- package/core/motion/policy.js +77 -0
- package/core/motion/registry.js +131 -0
- package/core/motion/review.js +59 -0
- package/core/motion/scan.js +150 -0
- package/core/motion/timeline.js +68 -0
- package/core/motion/transitions.js +49 -0
- package/core/motion/triggers.js +168 -0
- package/core/security/checks/fs.js +92 -0
- package/core/security/detect.js +35 -0
- package/core/security/engine.js +116 -0
- package/core/security/index.js +18 -0
- package/core/security/registry.js +85 -0
- package/core/security/report/report.js +113 -0
- package/core/security/rules/ci/index.js +146 -0
- package/core/security/rules/consumer/index.js +174 -0
- package/core/security/rules/deps/index.js +182 -0
- package/core/security/rules/index.js +24 -0
- package/core/security/rules/publish/index.js +230 -0
- package/core/security/rules/repo/index.js +143 -0
- package/core/security/rules/secrets/index.js +133 -0
- package/core/security/score/dependency-health.js +53 -0
- package/core/security/score/release-health.js +66 -0
- package/core/security/score/score.js +102 -0
- package/core/security/timeline/timeline.js +76 -0
- package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
- package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
- package/dist/chunks/chunk-MYKUI364.js +116 -0
- package/dist/chunks/chunk-NEVBPD5T.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
- package/dist/llms.txt +2 -2
- package/dist/search-index.json +166 -2
- package/dist/velin-agent.json +156 -32
- package/dist/velinstyle-components.iife.js +862 -17
- package/dist/velinstyle-components.js +862 -17
- package/dist/velinstyle-components.min.js +126 -126
- package/dist/velinstyle.css +42 -7
- package/dist/velinstyle.min.css +1 -1
- package/package.json +158 -143
- package/packages/velinstyle-cli-core/package.json +11 -0
- package/packages/velinstyle-cli-core/src/config.js +84 -0
- package/packages/velinstyle-cli-core/src/contract.js +56 -0
- package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
- package/packages/velinstyle-cli-core/src/index.js +18 -0
- package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
- package/packages/velinstyle-cli-core/src/runner.js +129 -0
- package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
- package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
- package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
- package/packages/velinstyle-cli-registry/package.json +13 -0
- package/packages/velinstyle-cli-registry/src/index.js +88 -0
- package/packages/velinstyle-cli-renderer/package.json +8 -0
- package/packages/velinstyle-cli-renderer/src/index.js +286 -0
- package/packages/velinstyle-cli-theme/package.json +8 -0
- package/packages/velinstyle-cli-theme/src/index.js +84 -0
- package/packages/velinstyle-cli-ui/package.json +8 -0
- package/packages/velinstyle-cli-ui/src/index.js +34 -0
- package/packages/velinstyle-motion/README.md +81 -0
- package/packages/velinstyle-motion/package.json +37 -0
- package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
- package/packages/velinstyle-motion/src/ai/provider.js +59 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
- package/packages/velinstyle-motion/src/config.js +257 -0
- package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
- package/packages/velinstyle-motion/src/index.js +304 -0
- package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
- package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
- package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
- package/packages/velinstyle-motion/src/local/effects.js +139 -0
- package/packages/velinstyle-motion/src/local/engine.js +96 -0
- package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
- package/packages/velinstyle-motion/src/registry/presets.js +25 -0
- package/packages/velinstyle-motion/src/registry/presets.json +85 -0
- package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
- package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
- package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
- package/packages/velinstyle-motion/src/validate-image.js +91 -0
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/packages/velinstyle-skills/registry.json +1 -142
- package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
- package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
- package/src/tokens/motion.css +7 -0
- 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',
|
|
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 =
|
|
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
|
-
|
|
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
|
|
195
|
-
const
|
|
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);
|
package/cli/production/report.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
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:
|
|
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
|
+
}
|
package/cli/production/run.js
CHANGED
|
@@ -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
|
-
|
|
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 || '?'}`));
|
package/cli/security.js
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = []) {
|