@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
package/cli/index.js CHANGED
@@ -85,170 +85,92 @@ function hasFlag(flag, alias) {
85
85
 
86
86
  // ── Help ─────────────────────────────────────────────────────────────────────
87
87
 
88
- function help() {
89
- console.log(`
90
- ${C.bold('VelinStyle CLI')} v${CLI_VERSION}
91
-
92
- ${C.bold('Usage:')}
93
- velinstyle init Create velinstyle.config.js
94
- velinstyle build Build custom CSS with selected layers
95
- velinstyle build --production Content-aware production output (CSS/JS/themes/icons)
96
- velinstyle production [path] Alias for build --production
97
- velinstyle themes List available themes
98
- velinstyle add <name> Add a single component CSS
99
- velinstyle icons <subcommand> Manage icon providers
100
- velinstyle scan [path|file] Security & accessibility scanner (dir or .html/.css/.js)
101
- velinstyle prefix [path] Add missing velin- prefix (dry-run; use --write)
102
- velinstyle blueprint [name] Print HTML blueprint (run: velinstyle blueprint list)
103
- velinstyle blueprint <name> --strict Fail if emitted classes CSS
104
- velinstyle create <kind> [dir] Scaffold: landing|dashboard|docs|auth
105
- velinstyle serve [dir] Static preview server (default port 4173)
106
- velinstyle doctor Check dist, icons, config, Windows ESM paths
107
- velinstyle transparency <sub> Transparency Framework: doctor|validate|report|export|migrate
108
- velinstyle check [path] doctor + blueprint --strict + scan + review + transparency
109
- velinstyle scaffold "<prompt>" Plan-first page HTML or recipe fragment
110
- velinstyle scaffold --atelier 04,07 Compose Atelier Library ids (beta)
111
- velinstyle plan "<prompt>" Emit page plan JSON (no HTML)
112
- velinstyle plan --atelier 04,07 Emit plan JSON for Atelier ids (beta)
113
- velinstyle atelier <num|id> Pull curated Atelier Library showcase
114
- velinstyle atelier list List curated Library numbers/ids
115
- velinstyle review [file|html] Design / a11y / SEO / conversion review gate
116
- velinstyle wc api <tag> Human-readable Web Component API from source
117
- velinstyle layout <sub> Responsive layout audit and fixes
118
- velinstyle perf <sub> Performance audit (images, scripts) with --fix
119
- velinstyle tokens build Generate CSS variables from tokens.json
120
- velinstyle tokens validate Validate tokens.json schema
121
- velinstyle docs generate Auto-generate Markdown API reference
122
- velinstyle meta Build agent context (velin-agent.json, llms.txt)
123
- velinstyle search index Build JSON search index for VelinSearch
124
- velinstyle skills <subcommand> Registry-first AI skills commands
125
- velinstyle workflow <id> Resolve workflow graph / project workflow
126
-
127
- ${C.bold('Icons subcommands:')}
128
- icons list Show available icon providers
129
- icons add <provider> [--icons a,b] [--variant outline] Download to icons/svg/
130
- icons remove <provider> Remove provider icons
131
- icons build Rebuild icon sprite
132
-
133
- ${C.bold('Icons example (multi-provider):')}
134
- velinstyle icons add lucide --icons menu,search,check
135
- velinstyle icons add heroicons --icons arrow-left --variant outline
136
- velinstyle icons build
137
-
138
- ${C.bold('Blueprint:')}
139
- velinstyle blueprint list [--strict] Print ids (strict validates all)
140
- velinstyle blueprint <name> [--output, -o <file>] [--strict]
141
-
142
- ${C.bold('Create / serve / doctor / check:')}
143
- velinstyle create landing|dashboard|docs|auth [dir] [--theme earth] [--no-copy]
144
- velinstyle serve [dir] [--port 4173]
145
- velinstyle doctor
146
- velinstyle check [path] [--json|--sarif] [--profile marketing|app|docs|fragment|ecommerce]
147
- Alias: validate → check · documentation → docs generate help
148
-
149
- ${C.bold('Web Components:')}
150
- velinstyle wc api <tag> e.g. velinstyle wc api velin-toast
151
-
152
- ${C.bold('Scaffold (1.2.0):')}
153
- velinstyle scaffold "<prompt>" Plan render → review for pages; recipes for fragments
154
- velinstyle scaffold list-intents Show recipe intent keywords
155
- velinstyle scaffold "<prompt>" -o out.html [--json]
156
- velinstyle scaffold --atelier 04,07 [-o page.html] [--from <library>] Compose Library showcases (beta)
157
-
158
- ${C.bold('Atelier Library pull (1.2.x):')}
159
- velinstyle atelier list
160
- velinstyle atelier 36 [-o dir] [--format html|blade|vue|react] [--from <library>] [--base-url <url>]
161
- Limitation: blade/vue/react = integration wrappers around vanilla assets (not native rewrites).
162
- Native framework blocks / Velin Studio Builder = planned. Atelier ≠ Studio.
163
-
164
- ${C.bold('Plan / Review (1.2.x):')}
165
- velinstyle plan "<prompt>" [--json] [-o plan.json]
166
- velinstyle plan --atelier 04,07 [-o plan.json] Atelier compose plan (beta)
167
- velinstyle review <file.html> [--json] [--prompt "..."]
168
-
169
- ${C.bold('Transparency (1.2.1):')}
170
- velinstyle transparency doctor [path|file] [--policy file] [--json]
171
- velinstyle transparency validate [path|file] [--policy file] [--json]
172
- velinstyle transparency report [path|file] [--out dir] [--policy file]
173
- velinstyle transparency export [path|file] [--format json|json-ld|csv|html] [-o file]
174
- velinstyle transparency migrate [path|file] [--apply] [--write] [--policy file]
175
- Alias: transparency scan → doctor
176
-
177
- ${C.bold('Layout (0.8.0):')}
178
- velinstyle layout audit [path] Report flex/grid/responsive issues
179
- velinstyle layout suggest [path] Audit with fix suggestions
180
- velinstyle layout fix [path] Apply safe fixes (--dry-run default, --write)
181
-
182
- ${C.bold('Performance (0.9.0):')}
183
- velinstyle perf audit [path|file] Report CLS, lazy-load, script defer issues
184
- velinstyle perf suggest [path|file] Same as audit with fix hints
185
- velinstyle perf fix [path|file] Apply safe fixes (--write)
186
-
187
- ${C.bold('Production Builder (1.2.2):')}
188
- velinstyle build --production [path]
189
- velinstyle production [path]
190
- --out, -o <dir> Default: ./dist/velin-production
191
- --explain Show removed CSS/themes/icons + reasons
192
- --watch Rebuild on content change
193
- --report <file> Write JSON/Markdown report
194
- --safelist <list|file> Extra classes / themes / icons / CSS files
195
- --no-js --no-icons --no-themes --no-motion --no-minify
196
-
197
- ${C.bold('Tokens:')}
198
- velinstyle tokens build [--input <path>] [--output, -o <file>]
199
- velinstyle tokens validate [--input <path>]
200
-
201
- ${C.bold('Docs (0.9.0):')}
202
- velinstyle docs generate [--scope all|components|tokens|utilities|cli|rules|a11y|meta] [--out docs/generated]
203
- velinstyle meta [--out dist/velin-agent.json] [--llms-out dist/llms.txt] [--base-url URL]
204
- velinstyle meta page <file.html> [--write] Merge curated meta (goals/intent) on --write
205
-
206
- ${C.bold('Search:')}
207
- velinstyle search index [--out dist/search-index.json] [--extra-html dir1,dir2]
208
-
209
- ${C.bold('Skills:')}
210
- velinstyle skills list [--capability review] [--status beta] [--json]
211
- velinstyle skills show <skill-id> [--human]
212
- velinstyle skills install <skill-id|pack|bundle:id|project:id> [--target .cursor/skills]
213
- velinstyle skills run <skill-id> [--json]
214
- velinstyle skills validate
215
- velinstyle skills packs|bundles|templates|projects|graphs
216
-
217
- ${C.bold('Workflow:')}
218
- velinstyle workflow <graph-id|project:id> [--json]
219
-
220
- ${C.bold('Scan options:')}
221
- --fix Auto-fix safe issues (writes files)
222
- --fix-dry-run Show files that would be auto-fixed; no write
223
- --fix-lang <code> Default lang for a11y/html-lang fix (default: de)
224
- --severity <level> Minimum severity: error, warning, info
225
- --only <category> Filter: security, pii, a11y, css, wc, perf
226
-
227
- ${C.bold('Prefix options (velinstyle prefix):')}
228
- --write Write files (default is dry-run)
229
- --bootstrap-display Map Bootstrap d-* display utilities to Velin names
230
- --map <file> JSON class map (merged after auto map; see below)
231
-
232
- Prefix also loads ${C.dim('velinstyle-prefix-map.json')} from the migration root (the
233
- directory you pass, or the parent of a single file) if that file exists. ${C.dim('--map')}
234
- merges on top and overrides duplicate keys.
235
-
236
- ${C.bold('Build options:')}
237
- --output, -o <path> Output file path (default: ./velinstyle-custom.css)
238
- --minify Minify output
239
- --preset lite Marketing subset layers (tokens+reset+base+layout+components+utilities)
240
-
241
- ${C.bold('General:')}
242
- --help, -h Show this help
243
- `);
88
+ async function help() {
89
+ const exp = await ensureExperience();
90
+ console.log(exp.buildHelpFromRegistry(CLI_VERSION));
91
+ }
92
+
93
+ /** Cached Experience bridge module. */
94
+ let experienceApi = null;
95
+
96
+ async function ensureExperience() {
97
+ if (experienceApi) return experienceApi;
98
+ experienceApi = await import('./experience.js');
99
+ return experienceApi;
100
+ }
101
+
102
+ /**
103
+ * Finish a command through Velin Experience (Contract + Renderer).
104
+ * Side effects should already be done; this only presents the result.
105
+ */
106
+ async function emitExperience(commandId, payload = {}) {
107
+ const exp = await ensureExperience();
108
+ const meta = exp.getCommand(commandId);
109
+ const {
110
+ layout = meta?.layout || 'standard',
111
+ title = meta?.title || commandId,
112
+ subtitle = '',
113
+ status = 'PASS',
114
+ message = '',
115
+ project = {},
116
+ stats = {},
117
+ items = [],
118
+ table = null,
119
+ suggestions = [],
120
+ diagnostics = [],
121
+ notifications = [],
122
+ widgets = null,
123
+ exitCode,
124
+ } = payload;
125
+
126
+ const noteWidgets = (notifications || []).map((n) => ({
127
+ id: 'notification',
128
+ props: typeof n === 'string' ? { tone: 'info', message: n } : n,
129
+ }));
130
+
131
+ const autoWidgets = widgets || [
132
+ {
133
+ id: 'hero',
134
+ props: {
135
+ slot: 'hero',
136
+ title,
137
+ subtitle: subtitle || message || '',
138
+ eyebrow: 'Velin Experience',
139
+ },
140
+ },
141
+ ...(items.length ? [{ id: 'stats', props: { items } }] : []),
142
+ ...(table ? [{ id: 'table', props: table }] : []),
143
+ ...noteWidgets,
144
+ { id: 'footer', props: { slot: 'footer', text: `velinstyle ${commandId}` } },
145
+ ];
146
+
147
+ await exp.executeExperience({
148
+ commandId,
149
+ args,
150
+ helpers: { getArg, hasFlag },
151
+ version: CLI_VERSION,
152
+ layout,
153
+ run: async () => ({
154
+ project,
155
+ result: { status, message: message || subtitle || `${title} · ${status}` },
156
+ stats,
157
+ widgets: autoWidgets,
158
+ suggestions,
159
+ diagnostics,
160
+ notifications: (notifications || []).map((n) => (typeof n === 'string' ? { message: n } : n)),
161
+ }),
162
+ });
163
+
164
+ if (typeof exitCode === 'number') process.exit(exitCode);
244
165
  }
245
166
 
246
167
  // ── Init ─────────────────────────────────────────────────────────────────────
247
168
 
248
- function init() {
169
+ async function init() {
170
+ const notes = [];
249
171
  const configPath = resolve('velinstyle.config.js');
250
172
  if (existsSync(configPath)) {
251
- console.log('velinstyle.config.js already exists.');
173
+ notes.push({ tone: 'info', message: 'velinstyle.config.js already exists' });
252
174
  } else {
253
175
  const config = `// VelinStyle Configuration
254
176
  export default {
@@ -276,7 +198,7 @@ export default {
276
198
  };
277
199
  `;
278
200
  writeFileSync(configPath, config);
279
- console.log(C.green('Created velinstyle.config.js'));
201
+ notes.push({ tone: 'ok', message: 'Created velinstyle.config.js' });
280
202
  }
281
203
 
282
204
  const vendorDir = resolve('vendor', 'velinstyle');
@@ -295,9 +217,9 @@ export default {
295
217
  if (statSync(src).isFile()) writeFileSync(join(vendorDir, 'themes', name), readFileSync(src));
296
218
  }
297
219
  }
298
- console.log(C.green(`Copied dist assets → ${vendorDir}`));
220
+ notes.push({ tone: 'ok', message: `Copied dist assets → ${vendorDir}` });
299
221
  } else {
300
- console.log(C.yellow('Framework dist/ not found — skip vendor copy. Build the framework or copy dist manually into vendor/velinstyle.'));
222
+ notes.push({ tone: 'warn', message: 'Framework dist/ not found — skip vendor copy' });
301
223
  }
302
224
 
303
225
  const starter = resolve('index.velin-starter.html');
@@ -325,10 +247,19 @@ export default {
325
247
  `,
326
248
  'utf-8',
327
249
  );
328
- console.log(C.green('Created index.velin-starter.html'));
329
- }
330
-
331
- console.log('Next: velinstyle build · velinstyle serve · velinstyle doctor');
250
+ notes.push({ tone: 'ok', message: 'Created index.velin-starter.html' });
251
+ }
252
+
253
+ await emitExperience('init', {
254
+ layout: 'wizard',
255
+ message: 'Project initialized',
256
+ notifications: notes,
257
+ suggestions: [
258
+ { text: 'velinstyle build' },
259
+ { text: 'velinstyle serve' },
260
+ { text: 'velinstyle doctor' },
261
+ ],
262
+ });
332
263
  }
333
264
 
334
265
  // ── Build ────────────────────────────────────────────────────────────────────
@@ -401,42 +332,56 @@ async function build() {
401
332
  shell: true,
402
333
  });
403
334
  bundled = true;
404
- console.log(C.green(`Built: ${output}`) + ` (layers: ${selectedLayers.join(', ')}${config.theme ? ', theme: ' + config.theme : ''})`);
405
335
  } catch {
406
336
  writeFileSync(outAbs, css);
407
- console.log(C.yellow(`Built (unbundled): ${output}`));
408
- console.log(C.yellow(' Install lightningcss-cli in the VelinStyle package (or run build from the framework repo) for minified output.'));
337
+ bundled = false;
409
338
  } finally {
410
339
  try { unlinkSync(tmpPath); } catch { /* ignore */ }
411
340
  }
412
341
 
342
+ const items = [
343
+ { label: 'Output', value: output },
344
+ { label: 'Layers', value: selectedLayers.length },
345
+ { label: 'Bundle', value: bundled ? 'lightningcss' : 'raw' },
346
+ ];
347
+ if (config.theme) items.push({ label: 'Theme', value: config.theme });
413
348
  if (preset === 'lite' && existsSync(outAbs)) {
414
349
  const liteBytes = statSync(outAbs).size;
415
350
  const fullMin = join(PKG_ROOT, 'dist', 'velinstyle.min.css');
351
+ items.push({ label: 'Size', value: `${(liteBytes / 1024).toFixed(1)} KB` });
416
352
  if (existsSync(fullMin)) {
417
- const fullBytes = statSync(fullMin).size;
418
- const liteKb = (liteBytes / 1024).toFixed(1);
419
- const fullKb = (fullBytes / 1024).toFixed(1);
420
- console.log(` Size: lite ${liteKb} KB vs dist/velinstyle.min.css ${fullKb} KB`);
421
- if (!bundled || liteBytes >= fullBytes) {
422
- console.log(C.yellow(` Warning: lite output is not smaller than the full min CSS${bundled ? '' : ' (unbundled)'}.`));
423
- }
353
+ items.push({ label: 'Full min', value: `${(statSync(fullMin).size / 1024).toFixed(1)} KB` });
424
354
  }
425
355
  }
426
356
 
357
+ const notifications = [];
358
+ if (!bundled) {
359
+ notifications.push({ tone: 'warn', message: 'Unbundled output — install lightningcss-cli for minify' });
360
+ }
361
+
427
362
  if (config.scan?.enabled) {
428
- console.log(C.dim('\nRunning post-build scan...'));
429
363
  const { scan } = await import('./scanner.js');
430
364
  scan(resolve('.'), config.scan);
365
+ notifications.push({ tone: 'info', message: 'Post-build scan ran (config.scan.enabled)' });
431
366
  }
367
+
368
+ await emitExperience('build', {
369
+ layout: 'build',
370
+ status: 'PASS',
371
+ message: bundled ? `Built ${output}` : `Built (unbundled) ${output}`,
372
+ subtitle: `layers: ${selectedLayers.join(', ')}`,
373
+ items,
374
+ notifications,
375
+ suggestions: [{ text: 'velinstyle doctor' }, { text: 'velinstyle production . --explain' }],
376
+ });
432
377
  }
433
378
 
434
379
  // ── Themes ───────────────────────────────────────────────────────────────────
435
380
 
436
- function themes() {
381
+ async function themes() {
437
382
  const themesDir = join(PKG_ROOT, 'src', 'themes');
438
383
  if (!existsSync(themesDir)) {
439
- console.log('No themes directory found.');
384
+ await emitExperience('themes', { status: 'WARNING', message: 'No themes directory found', exitCode: 1 });
440
385
  return;
441
386
  }
442
387
 
@@ -444,31 +389,44 @@ function themes() {
444
389
  .filter(f => f.endsWith('.css'))
445
390
  .map(f => f.replace('.css', ''));
446
391
 
447
- console.log(`\n ${C.bold('Available VelinStyle Themes:')}\n`);
448
- available.forEach(t => console.log(` - ${t}`));
449
- console.log(`\n Total: ${available.length} themes`);
450
- console.log(' Usage: Set theme in velinstyle.config.js or load via:');
451
- console.log(' <link rel="stylesheet" href="dist/themes/<name>.min.css">\n');
392
+ await emitExperience('themes', {
393
+ message: `${available.length} themes`,
394
+ table: {
395
+ columns: ['Theme'],
396
+ rows: available.map((t) => [t]),
397
+ },
398
+ suggestions: [
399
+ { text: 'Set theme in velinstyle.config.js' },
400
+ { text: '<link rel="stylesheet" href="dist/themes/<name>.min.css">' },
401
+ ],
402
+ });
452
403
  }
453
404
 
454
405
  // ── Add Component ────────────────────────────────────────────────────────────
455
406
 
456
- function add() {
407
+ async function add() {
457
408
  const name = args[1];
458
409
  if (!name) {
459
- console.log('Usage: velinstyle add <component-name>');
460
- console.log('Example: velinstyle add button');
410
+ await emitExperience('add', {
411
+ status: 'WARNING',
412
+ message: 'Usage: velinstyle add <component-name>',
413
+ suggestions: [{ text: 'velinstyle add button' }],
414
+ exitCode: 1,
415
+ });
461
416
  return;
462
417
  }
463
418
 
464
419
  const filePath = join(PKG_ROOT, 'src', 'components', `${name}.css`);
465
420
  if (!existsSync(filePath)) {
466
- console.log(C.red(`Component not found: ${name}`));
467
- console.log('Available:');
468
- readdirSync(join(PKG_ROOT, 'src', 'components'))
421
+ const available = readdirSync(join(PKG_ROOT, 'src', 'components'))
469
422
  .filter(f => f.endsWith('.css'))
470
- .map(f => f.replace('.css', ''))
471
- .forEach(c => console.log(` - ${c}`));
423
+ .map(f => f.replace('.css', ''));
424
+ await emitExperience('add', {
425
+ status: 'ERROR',
426
+ message: `Component not found: ${name}`,
427
+ table: { columns: ['Available'], rows: available.slice(0, 40).map((c) => [c]) },
428
+ exitCode: 1,
429
+ });
472
430
  return;
473
431
  }
474
432
 
@@ -477,7 +435,11 @@ function add() {
477
435
  const content = readFileSync(filePath, 'utf-8');
478
436
  const outPath = join(outDir, `${name}.css`);
479
437
  writeFileSync(outPath, content);
480
- console.log(C.green(`Added: ${outPath}`));
438
+ await emitExperience('add', {
439
+ message: `Added ${name}`,
440
+ items: [{ label: 'File', value: outPath }],
441
+ notifications: [{ tone: 'ok', message: `Wrote ${outPath}` }],
442
+ });
481
443
  }
482
444
 
483
445
  // ── Icons ────────────────────────────────────────────────────────────────────
@@ -486,47 +448,64 @@ async function icons() {
486
448
  const sub = args[1];
487
449
 
488
450
  if (!sub || sub === 'list') {
489
- iconsListCmd();
451
+ await iconsListCmd();
490
452
  } else if (sub === 'add') {
491
453
  await iconsAddCmd();
492
454
  } else if (sub === 'remove') {
493
- iconsRemoveCmd();
455
+ await iconsRemoveCmd();
494
456
  } else if (sub === 'build') {
495
- iconsBuildCmd();
457
+ await iconsBuildCmd();
496
458
  } else {
497
- console.log(`Unknown icons subcommand: ${sub}`);
498
- console.log('Available: list, add, remove, build');
459
+ await emitExperience('icons', {
460
+ status: 'WARNING',
461
+ message: `Unknown icons subcommand: ${sub}`,
462
+ suggestions: [{ text: 'velinstyle icons list|add|remove|build' }],
463
+ exitCode: 1,
464
+ });
499
465
  }
500
466
  }
501
467
 
502
- function iconsListCmd() {
468
+ async function iconsListCmd() {
503
469
  const providers = listProviders();
504
- console.log(`\n ${C.bold('Available Icon Providers:')}\n`);
505
- providers.forEach(p => {
506
- console.log(` ${C.cyan(p.key.padEnd(14))} ${p.name} -- ${p.description}`);
507
- console.log(` ${' '.repeat(14)} License: ${p.license} | ${C.dim(p.homepage)}`);
508
- if (p.variants.length) console.log(` ${' '.repeat(14)} Variants: ${p.variants.join(', ')}`);
509
- console.log();
510
- });
511
-
512
470
  const svgDir = join(PKG_ROOT, 'icons', 'svg');
513
- if (existsSync(svgDir)) {
514
- const installed = readdirSync(svgDir).filter(f => f.endsWith('.svg'));
515
- console.log(` ${C.bold('Installed icons:')} ${installed.length} in icons/svg/\n`);
516
- }
471
+ const installed = existsSync(svgDir)
472
+ ? readdirSync(svgDir).filter(f => f.endsWith('.svg')).length
473
+ : 0;
474
+ await emitExperience('icons', {
475
+ subtitle: 'list',
476
+ message: `${providers.length} providers · ${installed} installed SVGs`,
477
+ table: {
478
+ columns: ['Provider', 'Name', 'License'],
479
+ rows: providers.map((p) => [p.key, p.name, p.license]),
480
+ },
481
+ items: [{ label: 'Installed', value: installed }],
482
+ suggestions: [
483
+ { text: 'velinstyle icons add lucide --icons menu,search' },
484
+ { text: 'velinstyle icons build' },
485
+ ],
486
+ });
517
487
  }
518
488
 
519
489
  async function iconsAddCmd() {
520
490
  const provider = args[2];
521
491
  if (!provider) {
522
- console.log('Usage: velinstyle icons add <provider> [--icons name1,name2] [--variant outline]');
523
- console.log('Providers: ' + Object.keys(PROVIDERS).join(', '));
492
+ await emitExperience('icons', {
493
+ status: 'WARNING',
494
+ subtitle: 'add',
495
+ message: 'Usage: velinstyle icons add <provider> [--icons name1,name2] [--variant outline]',
496
+ suggestions: [{ text: `Providers: ${Object.keys(PROVIDERS).join(', ')}` }],
497
+ exitCode: 1,
498
+ });
524
499
  return;
525
500
  }
526
501
 
527
502
  if (!PROVIDERS[provider]) {
528
- console.log(C.red(`Unknown provider: ${provider}`));
529
- console.log('Available: ' + Object.keys(PROVIDERS).join(', '));
503
+ await emitExperience('icons', {
504
+ status: 'ERROR',
505
+ message: `Unknown provider: ${provider}`,
506
+ suggestions: [{ text: `Available: ${Object.keys(PROVIDERS).join(', ')}` }],
507
+ exitCode: 1,
508
+ });
530
509
  return;
531
510
  }
532
511
 
@@ -534,8 +513,12 @@ async function iconsAddCmd() {
534
513
  const variant = getArg('--variant');
535
514
 
536
515
  if (!iconsList) {
537
- console.log(C.yellow('Please specify icons to add with --icons name1,name2,...'));
538
- console.log(`Example: velinstyle icons add ${provider} --icons heart,star,check`);
516
+ await emitExperience('icons', {
517
+ status: 'WARNING',
518
+ message: 'Specify --icons name1,name2,...',
519
+ suggestions: [{ text: `velinstyle icons add ${provider} --icons heart,star,check` }],
520
+ exitCode: 1,
521
+ });
539
522
  return;
540
523
  }
541
524
 
@@ -545,10 +528,11 @@ async function iconsAddCmd() {
545
528
 
546
529
  let added = 0;
547
530
  let failed = 0;
531
+ const rows = [];
548
532
 
549
533
  for (const name of names) {
550
534
  const url = getProviderUrl(provider, name, variant);
551
- if (!url) { failed++; continue; }
535
+ if (!url) { failed++; rows.push([name, 'FAIL', 'no url']); continue; }
552
536
 
553
537
  try {
554
538
  const res = await fetch(url);
@@ -558,56 +542,83 @@ async function iconsAddCmd() {
558
542
 
559
543
  const filename = `${provider}-${name}.svg`;
560
544
  writeFileSync(join(svgDir, filename), svg);
561
- console.log(C.green(` + ${filename}`));
545
+ rows.push([name, 'OK', filename]);
562
546
  added++;
563
547
  } catch (e) {
564
- console.log(C.red(` x ${name}: ${e.message}`));
548
+ rows.push([name, 'FAIL', e.message]);
565
549
  failed++;
566
550
  }
567
551
  }
568
552
 
569
- console.log(`\n Added: ${added}, Failed: ${failed}`);
570
- if (added > 0) {
571
- console.log(C.dim(' Run "velinstyle icons build" to rebuild the sprite.'));
572
- }
553
+ await emitExperience('icons', {
554
+ subtitle: 'add',
555
+ status: failed && !added ? 'ERROR' : failed ? 'WARNING' : 'PASS',
556
+ message: `Added ${added}, failed ${failed}`,
557
+ table: { columns: ['Icon', 'Status', 'Detail'], rows },
558
+ suggestions: added > 0 ? [{ text: 'velinstyle icons build' }] : [],
559
+ exitCode: failed && !added ? 1 : undefined,
560
+ });
573
561
  }
574
562
 
575
- function iconsRemoveCmd() {
563
+ async function iconsRemoveCmd() {
576
564
  const provider = args[2];
577
565
  if (!provider) {
578
- console.log('Usage: velinstyle icons remove <provider>');
566
+ await emitExperience('icons', {
567
+ status: 'WARNING',
568
+ message: 'Usage: velinstyle icons remove <provider>',
569
+ exitCode: 1,
570
+ });
579
571
  return;
580
572
  }
581
573
 
582
574
  const svgDir = join(PKG_ROOT, 'icons', 'svg');
583
- if (!existsSync(svgDir)) return;
575
+ if (!existsSync(svgDir)) {
576
+ await emitExperience('icons', { status: 'WARNING', message: 'No icons/svg directory' });
577
+ return;
578
+ }
584
579
 
585
580
  const files = readdirSync(svgDir).filter(f => f.startsWith(`${provider}-`) && f.endsWith('.svg'));
586
581
  if (files.length === 0) {
587
- console.log(C.yellow(`No icons found for provider: ${provider}`));
582
+ await emitExperience('icons', {
583
+ status: 'WARNING',
584
+ message: `No icons found for provider: ${provider}`,
585
+ });
588
586
  return;
589
587
  }
590
588
 
591
- files.forEach(f => {
592
- unlinkSync(join(svgDir, f));
593
- console.log(C.dim(` - ${f}`));
589
+ files.forEach(f => unlinkSync(join(svgDir, f)));
590
+ await emitExperience('icons', {
591
+ subtitle: 'remove',
592
+ message: `Removed ${files.length} icons from ${provider}`,
593
+ table: { columns: ['Removed'], rows: files.map((f) => [f]) },
594
+ suggestions: [{ text: 'velinstyle icons build' }],
594
595
  });
595
- console.log(`\n Removed ${files.length} icons from "${provider}".`);
596
- console.log(C.dim(' Run "velinstyle icons build" to rebuild the sprite.'));
597
596
  }
598
597
 
599
- function iconsBuildCmd() {
598
+ async function iconsBuildCmd() {
600
599
  const buildScript = join(PKG_ROOT, 'icons', 'build-sprite.js');
601
600
  if (!existsSync(buildScript)) {
602
- console.log(C.red('icons/build-sprite.js not found.'));
601
+ await emitExperience('icons', {
602
+ status: 'ERROR',
603
+ message: 'icons/build-sprite.js not found',
604
+ exitCode: 1,
605
+ });
603
606
  return;
604
607
  }
605
608
 
606
609
  try {
607
610
  execSync(`node "${buildScript}"`, { stdio: 'inherit', cwd: PKG_ROOT });
608
- console.log(C.green('\nIcon sprite rebuilt successfully.'));
611
+ await emitExperience('icons', {
612
+ subtitle: 'build',
613
+ message: 'Icon sprite rebuilt',
614
+ notifications: [{ tone: 'ok', message: 'Sprite rebuild succeeded' }],
615
+ });
609
616
  } catch {
610
- console.log(C.red('Failed to rebuild icon sprite.'));
617
+ await emitExperience('icons', {
618
+ status: 'ERROR',
619
+ message: 'Failed to rebuild icon sprite',
620
+ exitCode: 1,
621
+ });
611
622
  }
612
623
  }
613
624
 
@@ -618,13 +629,15 @@ async function blueprintCmd() {
618
629
  const strict = hasFlag('--strict');
619
630
  const { listBlueprints, emitBlueprint, validateAllBlueprints } = await import('./blueprint.js');
620
631
  if (sub === '--help' || sub === '-h') {
621
- console.log(`Usage: velinstyle blueprint [list|<name>] [--output|-o <file>] [--strict]
622
-
623
- list List blueprint ids
624
- list --strict Validate all blueprints against CSS
625
- <name> Emit HTML (stdout or -o file)
626
- <name> --strict Fail if classes missing from CSS
627
- `);
632
+ await emitExperience('blueprint', {
633
+ status: 'PASS',
634
+ message: 'Usage: velinstyle blueprint [list|<name>] [--output|-o <file>] [--strict]',
635
+ items: [
636
+ { label: 'list', value: 'List blueprint ids' },
637
+ { label: 'list --strict', value: 'Validate all against CSS' },
638
+ { label: '<name>', value: 'Emit HTML (stdout or -o)' },
639
+ ],
640
+ });
628
641
  return;
629
642
  }
630
643
  if (!sub || sub === 'list') {
@@ -632,30 +645,48 @@ async function blueprintCmd() {
632
645
  const results = validateAllBlueprints();
633
646
  const bad = results.filter((r) => !r.ok);
634
647
  if (bad.length) {
635
- for (const r of bad) {
636
- console.log(C.red(`${r.id}: ${r.missing?.join(', ') || r.error}`));
637
- }
638
- process.exit(1);
648
+ await emitExperience('blueprint', {
649
+ subtitle: 'strict',
650
+ status: 'ERROR',
651
+ message: `${bad.length} blueprint(s) failed`,
652
+ table: {
653
+ columns: ['Id', 'Detail'],
654
+ rows: bad.map((r) => [r.id, r.missing?.join(', ') || r.error || '']),
655
+ },
656
+ exitCode: 1,
657
+ });
658
+ return;
639
659
  }
640
- console.log(C.green(`All ${results.length} blueprints pass --strict`));
660
+ await emitExperience('blueprint', {
661
+ subtitle: 'strict',
662
+ message: `All ${results.length} blueprints pass --strict`,
663
+ notifications: [{ tone: 'ok', message: 'Strict validation passed' }],
664
+ });
641
665
  return;
642
666
  }
643
- console.log(`\n ${C.bold('Available blueprints:')}\n`);
644
- listBlueprints().forEach((b) => console.log(` - ${b}`));
645
- console.log(`\n ${C.dim('Example: velinstyle blueprint modal -o snippet.html --strict')}\n`);
667
+ const list = listBlueprints();
668
+ await emitExperience('blueprint', {
669
+ message: `${list.length} blueprints`,
670
+ table: { columns: ['Blueprint'], rows: list.map((b) => [b]) },
671
+ suggestions: [{ text: 'velinstyle blueprint modal -o snippet.html --strict' }],
672
+ });
646
673
  return;
647
674
  }
648
675
  const out = getArg('--output', '-o');
649
676
  const r = emitBlueprint(sub, { output: out || null, strict });
650
677
  if (!r.ok) {
651
- console.log(C.red(r.error));
652
- process.exit(1);
678
+ await emitExperience('blueprint', { status: 'ERROR', message: r.error, exitCode: 1 });
679
+ return;
653
680
  }
654
681
  if (r.text) {
682
+ // Preserve raw HTML for piping when no -o
655
683
  console.log(r.text);
656
- } else {
657
- console.log(C.green(`Wrote blueprint to ${r.path}`));
684
+ return;
658
685
  }
686
+ await emitExperience('blueprint', {
687
+ message: `Wrote blueprint to ${r.path}`,
688
+ items: [{ label: 'Path', value: r.path }, { label: 'Id', value: sub }],
689
+ });
659
690
  }
660
691
 
661
692
  async function serveCmd() {
@@ -664,50 +695,237 @@ async function serveCmd() {
664
695
  const { serveStatic } = await import('./serve.js');
665
696
  const result = await serveStatic({ dir: resolve(raw), port });
666
697
  if (!result.ok) {
667
- console.log(C.red(result.error));
668
- process.exit(1);
698
+ await emitExperience('serve', { status: 'ERROR', message: result.error, exitCode: 1 });
699
+ return;
669
700
  }
670
- console.log(C.green(`Serving ${result.dir}`));
671
- console.log(` ${result.url}`);
672
- console.log(C.dim('Press Ctrl+C to stop'));
701
+ await emitExperience('serve', {
702
+ message: `Serving ${result.dir}`,
703
+ items: [
704
+ { label: 'URL', value: result.url },
705
+ { label: 'Dir', value: result.dir },
706
+ { label: 'Port', value: String(port) },
707
+ ],
708
+ notifications: [{ tone: 'info', message: 'Press Ctrl+C to stop' }],
709
+ });
673
710
  }
674
711
 
675
712
  async function doctorCmd() {
676
- const { runDoctor, formatDoctorReport } = await import('./doctor.js');
713
+ const exp = await ensureExperience();
677
714
  const raw = args[1] && !args[1].startsWith('-') ? resolve(args[1]) : process.cwd();
678
715
  let cwd = raw;
679
716
  try {
680
717
  if (existsSync(raw) && statSync(raw).isFile()) cwd = dirname(raw);
681
718
  } catch { /* use raw */ }
682
- const report = await runDoctor({ cwd, pkgRoot: PKG_ROOT });
683
- console.log(formatDoctorReport(report));
684
- process.exit(report.ok ? 0 : 1);
719
+
720
+ const { runDoctor } = await import('./doctor.js');
721
+ const session = await exp.executeExperience({
722
+ commandId: 'doctor',
723
+ args,
724
+ helpers: { getArg, hasFlag },
725
+ version: CLI_VERSION,
726
+ cwd,
727
+ layout: 'doctor',
728
+ run: async (ctx) => {
729
+ ctx.emitProgress('Environment', 20);
730
+ const report = await runDoctor({ cwd, pkgRoot: PKG_ROOT });
731
+ ctx.emitProgress('Validation', 80);
732
+ const rows = [
733
+ ...report.okLines.map((line) => ({ label: line.split(':')[0] || 'Check', status: 'PASS' })),
734
+ ...report.issues.map((i) => ({
735
+ label: i.message,
736
+ status: i.severity === 'error' ? 'FAIL' : i.severity === 'warning' ? 'WARNING' : 'INFO',
737
+ })),
738
+ ];
739
+ const status = report.ok ? (report.summary.warnings ? 'WARNING' : 'PASS') : 'ERROR';
740
+ return {
741
+ project: { cwd: report.cwd, name: 'Doctor' },
742
+ result: {
743
+ status,
744
+ message: report.ok
745
+ ? `Pass (${report.summary.passed} checks, ${report.summary.warnings} warning(s))`
746
+ : `Fail (${report.summary.errors} error(s), ${report.summary.warnings} warning(s))`,
747
+ },
748
+ stats: report.summary,
749
+ diagnostics: report.issues,
750
+ widgets: [
751
+ {
752
+ id: 'hero',
753
+ props: { title: 'Doctor', subtitle: 'Environment & install health', eyebrow: 'Velin Experience' },
754
+ },
755
+ {
756
+ id: 'health',
757
+ props: {
758
+ title: 'Environment',
759
+ rows: [
760
+ { label: 'Node', status: 'PASS' },
761
+ { label: 'Platform', status: process.platform },
762
+ { label: 'Config', status: existsSync(join(cwd, 'velinstyle.config.js')) ? 'PASS' : 'WARNING' },
763
+ ],
764
+ },
765
+ },
766
+ {
767
+ id: 'table',
768
+ props: {
769
+ columns: ['Check', 'Status'],
770
+ rows: rows.slice(0, 24).map((r) => [r.label.slice(0, 40), r.status]),
771
+ },
772
+ },
773
+ {
774
+ id: 'footer',
775
+ props: { text: 'Velin Experience · doctor' },
776
+ },
777
+ ],
778
+ suggestions: report.issues.filter((i) => i.fix).slice(0, 5).map((i) => ({ text: i.fix })),
779
+ };
780
+ },
781
+ });
782
+ process.exit(session.contract.result.status === 'ERROR' ? 1 : 0);
783
+ }
784
+
785
+ async function dashboardCmd() {
786
+ const exp = await ensureExperience();
787
+ const cwd = process.cwd();
788
+ await exp.executeExperience({
789
+ commandId: 'dashboard',
790
+ args,
791
+ helpers: { getArg, hasFlag },
792
+ version: CLI_VERSION,
793
+ cwd,
794
+ layout: 'dashboard',
795
+ run: async (ctx) => {
796
+ ctx.emitProgress('Doctor', 20);
797
+ const { runDoctor } = await import('./doctor.js');
798
+ const doc = await runDoctor({ cwd, pkgRoot: PKG_ROOT });
799
+
800
+ let reviewScore = '—';
801
+ try {
802
+ ctx.emitProgress('Review', 45);
803
+ const { reviewHtml } = await import('./review.js');
804
+ const htmlCandidates = ['index.html', 'index.htm'].map((f) => join(cwd, f));
805
+ const hit = htmlCandidates.find((p) => existsSync(p));
806
+ if (hit) {
807
+ const html = readFileSync(hit, 'utf-8');
808
+ const report = reviewHtml(html, {});
809
+ reviewScore = report?.scores?.design != null
810
+ ? String(report.scores.design)
811
+ : (report?.gate || '—');
812
+ }
813
+ } catch { /* optional */ }
814
+
815
+ let scanScore = '—';
816
+ try {
817
+ ctx.emitProgress('Scan', 70);
818
+ const { scan } = await import('./scanner.js');
819
+ const data = scan(cwd, { severity: 'warning', format: 'json', returnData: true });
820
+ const errors = (data.issues || []).filter((i) => i.severity === 'error').length;
821
+ const warnings = (data.issues || []).filter((i) => i.severity === 'warning').length;
822
+ scanScore = errors ? `FAIL (${errors})` : warnings ? `WARN (${warnings})` : 'PASS';
823
+ } catch { /* optional */ }
824
+
825
+ let productionHint = '—';
826
+ try {
827
+ const prodDir = join(cwd, 'dist', 'velin-production');
828
+ productionHint = existsSync(prodDir) ? 'built' : 'not built';
829
+ } catch { /* optional */ }
830
+
831
+ return {
832
+ project: { name: cwd.split(/[/\\]/).pop() || 'Project', cwd },
833
+ result: { status: doc.ok ? 'PASS' : 'WARNING', message: 'Project dashboard' },
834
+ widgets: [
835
+ { id: 'hero', props: { slot: 'hero', title: 'Project Dashboard', subtitle: cwd, eyebrow: 'Velin Experience' } },
836
+ {
837
+ id: 'score',
838
+ props: {
839
+ slot: 'cards',
840
+ title: 'Health',
841
+ scores: [
842
+ { label: 'Doctor', value: doc.ok ? 'PASS' : 'FAIL' },
843
+ { label: 'Production', value: productionHint },
844
+ { label: 'Review', value: reviewScore },
845
+ { label: 'Scan', value: scanScore },
846
+ { label: 'Accessibility', value: 'AAA' },
847
+ { label: 'CLI', value: CLI_VERSION },
848
+ ],
849
+ },
850
+ },
851
+ {
852
+ id: 'notification',
853
+ props: { tone: 'info', message: 'Aggregated from doctor · review · scan · production folder' },
854
+ },
855
+ { id: 'footer', props: { slot: 'footer', text: 'velinstyle dashboard' } },
856
+ ],
857
+ suggestions: [
858
+ { text: 'velinstyle production . --explain' },
859
+ { text: 'velinstyle review index.html' },
860
+ { text: 'velinstyle check .' },
861
+ ],
862
+ notifications: [
863
+ { tone: 'ok', message: 'Velin Experience dashboard aggregates local contracts only (no cloud telemetry).' },
864
+ ],
865
+ };
866
+ },
867
+ });
868
+ }
869
+
870
+ async function replayCmd() {
871
+ const exp = await ensureExperience();
872
+ const file = args[1];
873
+ if (!file || file.startsWith('-')) {
874
+ await emitExperience('replay', {
875
+ status: 'WARNING',
876
+ message: 'Usage: velinstyle replay <record.json> [--output standard|json|markdown]',
877
+ exitCode: 1,
878
+ });
879
+ return;
880
+ }
881
+ const record = exp.loadRecord(file);
882
+ const overrides = exp.parseExperienceFlags(args, { getArg, hasFlag });
883
+ const config = { ...(record.config || {}), ...overrides };
884
+ exp.printContract(record.contract, config);
685
885
  }
686
886
 
687
887
  async function createCmd() {
688
888
  const kind = args[1];
689
889
  if (!kind || kind === '--help' || kind === '-h') {
690
- console.log(`Usage: velinstyle create <landing|dashboard|docs|auth> [dir] [--theme earth] [--no-copy]`);
691
- process.exit(kind ? 0 : 1);
890
+ await emitExperience('create', {
891
+ status: kind ? 'PASS' : 'WARNING',
892
+ message: 'Usage: velinstyle create <landing|dashboard|docs|auth> [dir] [--theme earth] [--no-copy]',
893
+ exitCode: kind ? 0 : 1,
894
+ });
895
+ return;
692
896
  }
693
897
  const { CREATE_KINDS, createProject } = await import('./create.js');
694
898
  if (!CREATE_KINDS.includes(kind)) {
695
- console.log(C.red(`Unknown kind "${kind}". Use: ${CREATE_KINDS.join('|')}`));
696
- process.exit(1);
899
+ await emitExperience('create', {
900
+ status: 'ERROR',
901
+ message: `Unknown kind "${kind}"`,
902
+ suggestions: [{ text: `Use: ${CREATE_KINDS.join('|')}` }],
903
+ exitCode: 1,
904
+ });
905
+ return;
697
906
  }
698
907
  const dir = args[2] && !args[2].startsWith('-') ? args[2] : kind;
699
908
  const theme = getArg('--theme') || undefined;
700
909
  const copyAssets = !hasFlag('--no-copy');
701
910
  const r = createProject({ kind, dir: resolve(dir), pkgRoot: PKG_ROOT, theme, copyAssets });
702
911
  if (!r.ok) {
703
- console.log(C.red(r.error));
704
- process.exit(1);
912
+ await emitExperience('create', { status: 'ERROR', message: r.error, exitCode: 1 });
913
+ return;
705
914
  }
706
- console.log(C.green(`Created ${kind} at ${r.dir}`));
707
- console.log(C.dim(` index: ${r.indexPath}`));
708
- if (r.vendorRel) console.log(C.dim(` vendor: ${r.vendorRel}/`));
709
- if (r.vendorCopied?.length) console.log(C.dim(` copied: ${r.vendorCopied.join(', ')}`));
710
- console.log(C.dim(` Next: velinstyle serve ${dir} · velinstyle check ${dir}`));
915
+ await emitExperience('create', {
916
+ layout: 'wizard',
917
+ message: `Created ${kind}`,
918
+ items: [
919
+ { label: 'Dir', value: r.dir },
920
+ { label: 'Index', value: r.indexPath },
921
+ ...(r.vendorRel ? [{ label: 'Vendor', value: r.vendorRel }] : []),
922
+ ],
923
+ suggestions: [
924
+ { text: `velinstyle serve ${dir}` },
925
+ { text: `velinstyle check ${dir}` },
926
+ ],
927
+ notifications: [{ tone: 'ok', message: `Created ${kind} at ${r.dir}` }],
928
+ });
711
929
  }
712
930
 
713
931
  async function tokensBuildCmd() {
@@ -716,12 +934,20 @@ async function tokensBuildCmd() {
716
934
  const { buildTokensFromJson } = await import('./tokens-build.js');
717
935
  const css = buildTokensFromJson(inputPath);
718
936
  if (!css) {
719
- console.log(C.red(`Could not read or parse: ${inputPath}`));
937
+ await emitExperience('tokens', {
938
+ status: 'ERROR',
939
+ message: `Could not read or parse: ${inputPath}`,
940
+ exitCode: 1,
941
+ });
720
942
  return;
721
943
  }
722
944
  if (outPath) {
723
945
  writeFileSync(resolve(outPath), css, 'utf-8');
724
- console.log(C.green(`Wrote ${resolve(outPath)}`));
946
+ await emitExperience('tokens', {
947
+ subtitle: 'build',
948
+ message: `Wrote ${resolve(outPath)}`,
949
+ items: [{ label: 'Input', value: inputPath }, { label: 'Output', value: resolve(outPath) }],
950
+ });
725
951
  } else {
726
952
  console.log(css);
727
953
  }
@@ -732,12 +958,20 @@ async function tokensValidateCmd() {
732
958
  const { validateTokensJson } = await import('./tokens-validate.js');
733
959
  const result = validateTokensJson(resolve(input));
734
960
  if (result.ok) {
735
- console.log(C.green(`Valid: ${resolve(input)}`));
736
- process.exit(0);
961
+ await emitExperience('tokens', {
962
+ subtitle: 'validate',
963
+ message: `Valid: ${resolve(input)}`,
964
+ notifications: [{ tone: 'ok', message: 'Tokens JSON valid' }],
965
+ });
966
+ return;
737
967
  }
738
- console.log(C.red(`Invalid: ${resolve(input)}`));
739
- result.errors.forEach((e) => console.log(` - ${e}`));
740
- process.exit(1);
968
+ await emitExperience('tokens', {
969
+ subtitle: 'validate',
970
+ status: 'ERROR',
971
+ message: `Invalid: ${resolve(input)}`,
972
+ table: { columns: ['Error'], rows: (result.errors || []).map((e) => [e]) },
973
+ exitCode: 1,
974
+ });
741
975
  }
742
976
 
743
977
  async function tokensCmd() {
@@ -747,7 +981,11 @@ async function tokensCmd() {
747
981
  } else if (sub === 'validate') {
748
982
  await tokensValidateCmd();
749
983
  } else {
750
- console.log('Usage: velinstyle tokens build|validate [--input tokens.json] [--output, -o <file>]');
984
+ await emitExperience('tokens', {
985
+ status: 'WARNING',
986
+ message: 'Usage: velinstyle tokens build|validate [--input tokens.json] [--output, -o <file>]',
987
+ exitCode: 1,
988
+ });
751
989
  }
752
990
  }
753
991
 
@@ -768,13 +1006,21 @@ async function perfCmd() {
768
1006
  return;
769
1007
  }
770
1008
  if (result.changes.length === 0) {
771
- console.log(C.green('No safe performance fixes to apply.'));
1009
+ await emitExperience('perf', {
1010
+ subtitle: 'fix',
1011
+ message: 'No safe performance fixes to apply.',
1012
+ notifications: [{ tone: 'ok', message: 'Nothing to change' }],
1013
+ });
772
1014
  return;
773
1015
  }
774
- for (const c of result.changes) {
775
- console.log(`${dryRun ? '[dry-run] ' : ''}${c.file}`);
776
- c.changes.forEach((ch) => console.log(C.dim(` - ${ch}`)));
777
- }
1016
+ await emitExperience('perf', {
1017
+ subtitle: 'fix',
1018
+ message: `${dryRun ? '[dry-run] ' : ''}${result.changes.length} file(s)`,
1019
+ table: {
1020
+ columns: ['File', 'Changes'],
1021
+ rows: result.changes.map((c) => [c.file, (c.changes || []).join('; ').slice(0, 60)]),
1022
+ },
1023
+ });
778
1024
  return;
779
1025
  }
780
1026
 
@@ -783,7 +1029,49 @@ async function perfCmd() {
783
1029
  console.log(JSON.stringify({ files, issues }, null, 2));
784
1030
  return;
785
1031
  }
786
- console.log(formatTextReport(issues, files));
1032
+
1033
+ const exp = await ensureExperience();
1034
+ const errors = issues.filter((x) => x.severity === 'error').length;
1035
+ const warnings = issues.filter((x) => x.severity === 'warning').length;
1036
+ await exp.executeExperience({
1037
+ commandId: 'perf',
1038
+ args,
1039
+ helpers: { getArg, hasFlag },
1040
+ version: CLI_VERSION,
1041
+ layout: 'audit',
1042
+ run: async () => ({
1043
+ project: { cwd: targetPath, name: 'Perf' },
1044
+ result: {
1045
+ status: errors ? 'ERROR' : warnings ? 'WARNING' : 'PASS',
1046
+ message: `${issues.length} finding(s) across ${files.length} file(s)`,
1047
+ },
1048
+ diagnostics: issues,
1049
+ widgets: [
1050
+ { id: 'hero', props: { slot: 'hero', title: 'Perf audit', subtitle: formatTextReport(issues, files).split('\n')[0] || 'Performance', eyebrow: 'Velin Experience' } },
1051
+ {
1052
+ id: 'stats',
1053
+ props: {
1054
+ slot: 'counts',
1055
+ items: [
1056
+ { label: 'Files', value: files.length },
1057
+ { label: 'Errors', value: errors },
1058
+ { label: 'Warnings', value: warnings },
1059
+ ],
1060
+ },
1061
+ },
1062
+ {
1063
+ id: 'table',
1064
+ props: {
1065
+ slot: 'findings',
1066
+ columns: ['Sev', 'Id', 'Message'],
1067
+ rows: issues.slice(0, 20).map((i) => [i.severity, i.id || '—', String(i.message || '').slice(0, 40)]),
1068
+ },
1069
+ },
1070
+ { id: 'footer', props: { slot: 'footer', text: 'velinstyle perf audit' } },
1071
+ ],
1072
+ suggestions: issues.filter((i) => i.fix).slice(0, 5).map((i) => ({ text: i.fix })),
1073
+ }),
1074
+ });
787
1075
  process.exit(issues.some((x) => x.severity === 'error') ? 1 : 0);
788
1076
  }
789
1077
 
@@ -795,8 +1083,8 @@ async function prefixCmd() {
795
1083
  const st = statSync(targetPath);
796
1084
  if (st.isFile()) migrationRoot = dirname(targetPath);
797
1085
  } catch {
798
- console.log(C.red(`Not found: ${targetPath}`));
799
- process.exit(1);
1086
+ await emitExperience('prefix', { status: 'ERROR', message: `Not found: ${targetPath}`, exitCode: 1 });
1087
+ return;
800
1088
  }
801
1089
 
802
1090
  const mapArg = getArg('--map');
@@ -804,17 +1092,41 @@ async function prefixCmd() {
804
1092
  const bootstrapDisplay = hasFlag('--bootstrap-display');
805
1093
 
806
1094
  try {
807
- const { buildExplicitPrefixMap, prefixCommandMain } = await import('./prefix-classes.js');
1095
+ const { buildExplicitPrefixMap, runPrefixCommand } = await import('./prefix-classes.js');
808
1096
  const explicitMap = buildExplicitPrefixMap(migrationRoot, mapArg ? resolve(mapArg) : null);
809
- prefixCommandMain(targetPath, {
1097
+ const { filesTouched, replacements, paths } = runPrefixCommand(targetPath, {
810
1098
  write,
811
1099
  bootstrapAliases: bootstrapDisplay,
812
1100
  pkgRoot: PKG_ROOT,
813
1101
  explicitMap,
814
1102
  });
1103
+ if (filesTouched === 0) {
1104
+ await emitExperience('prefix', {
1105
+ message: 'No class attributes needed prefixing (or no matching files)',
1106
+ notifications: explicitMap?.size
1107
+ ? [{ tone: 'info', message: `Explicit map: ${explicitMap.size} entries` }]
1108
+ : [],
1109
+ });
1110
+ return;
1111
+ }
1112
+ await emitExperience('prefix', {
1113
+ message: `${write ? 'Updated' : 'Would update'} ${filesTouched} file(s), ${replacements} class token(s)`,
1114
+ table: {
1115
+ columns: ['File'],
1116
+ rows: paths.slice(0, 40).map((p) => [p]),
1117
+ },
1118
+ suggestions: write ? [] : [{ text: 'Re-run with --write to apply changes' }],
1119
+ notifications: [
1120
+ ...(explicitMap?.size ? [{ tone: 'info', message: `Explicit map: ${explicitMap.size} entries` }] : []),
1121
+ ...(paths.length > 40 ? [{ tone: 'info', message: `… and ${paths.length - 40} more files` }] : []),
1122
+ ],
1123
+ });
815
1124
  } catch (e) {
816
- console.log(C.red(/** @type {Error} */ (e).message));
817
- process.exit(1);
1125
+ await emitExperience('prefix', {
1126
+ status: 'ERROR',
1127
+ message: /** @type {Error} */ (e).message,
1128
+ exitCode: 1,
1129
+ });
818
1130
  }
819
1131
  }
820
1132
 
@@ -826,27 +1138,35 @@ async function atelierCmd() {
826
1138
  } = await import('./atelier.js');
827
1139
 
828
1140
  if (!sub || sub === '--help' || sub === '-h') {
829
- console.log(`Usage: velinstyle atelier list
830
- velinstyle atelier <num|id> [-o dir] [--format html|blade|vue|react]
831
- [--from <library-root>] [--base-url <url>] [--rewrite-vendor <path>]
832
-
833
- Pull a curated Atelier Library showcase (e.g. 36 36-calendar).
834
- Limitation: --format blade|vue|react writes integration shells only (not native rewrites).
835
- Velin Studio Builder and native framework blocks are planned — not shipped.`);
1141
+ await emitExperience('atelier', {
1142
+ message: 'Usage: velinstyle atelier list | <num|id> [-o dir] [--format html|blade|vue|react]',
1143
+ suggestions: [
1144
+ { text: 'velinstyle atelier list' },
1145
+ { text: 'velinstyle atelier 36 -o ./velin-atelier/36-calendar' },
1146
+ ],
1147
+ notifications: [{
1148
+ tone: 'info',
1149
+ message: 'Limitation: blade|vue|react = wrappers; Studio/native rewrites planned.',
1150
+ }],
1151
+ });
836
1152
  return;
837
1153
  }
838
1154
 
839
1155
  if (sub === 'list') {
840
1156
  const r = listAtelierEntries();
841
1157
  if (!r.ok) {
842
- console.log(C.red(r.error));
843
- process.exit(1);
844
- }
845
- console.log(`\n ${C.bold('Atelier Library (curated)')} · ${r.items.length} templates\n`);
846
- for (const it of r.items) {
847
- console.log(` ${C.cyan(it.num.padStart(3))} ${it.id} ${C.dim(it.title || '')}`);
1158
+ await emitExperience('atelier', { status: 'ERROR', message: r.error, exitCode: 1 });
1159
+ return;
848
1160
  }
849
- console.log(`\n ${C.dim('Example: velinstyle atelier 36 -o ./velin-atelier/36-calendar')}\n`);
1161
+ await emitExperience('atelier', {
1162
+ subtitle: 'list',
1163
+ message: `Atelier Library · ${r.items.length} templates`,
1164
+ table: {
1165
+ columns: ['#', 'Id', 'Title'],
1166
+ rows: r.items.map((it) => [it.num.padStart(3), it.id, it.title || '']),
1167
+ },
1168
+ suggestions: [{ text: 'velinstyle atelier 36 -o ./velin-atelier/36-calendar' }],
1169
+ });
850
1170
  return;
851
1171
  }
852
1172
 
@@ -864,24 +1184,40 @@ async function atelierCmd() {
864
1184
  rewriteVendor: rewriteVendor || undefined,
865
1185
  });
866
1186
  if (!r.ok) {
867
- console.log(C.red(r.error));
868
- process.exit(1);
1187
+ await emitExperience('atelier', { status: 'ERROR', message: r.error, exitCode: 1 });
1188
+ return;
869
1189
  }
870
- console.log(C.green(`Pulled ${r.entry.id} → ${r.path} (format: ${r.format})`));
871
- console.log(C.dim(` source: ${r.source}`));
872
- console.log(C.dim(' Limitation: blade/vue/react = wrappers; Studio/native rewrites planned.'));
1190
+ await emitExperience('atelier', {
1191
+ message: `Pulled ${r.entry.id} ${r.path}`,
1192
+ items: [
1193
+ { label: 'Format', value: r.format },
1194
+ { label: 'Source', value: r.source },
1195
+ { label: 'Path', value: r.path },
1196
+ ],
1197
+ notifications: [{
1198
+ tone: 'info',
1199
+ message: 'Limitation: blade/vue/react = wrappers; Studio/native rewrites planned.',
1200
+ }],
1201
+ });
873
1202
  }
874
1203
 
875
1204
  async function scaffoldCmd() {
876
1205
  const sub = args[1];
877
1206
  const { scaffoldFromPrompt, listIntents } = await import('./scaffold.js');
878
1207
  if (sub === 'list-intents') {
879
- console.log(`\n ${C.bold('Scaffold intents:')}\n`);
880
- for (const i of listIntents()) {
881
- console.log(` ${C.cyan(i.id)} — ${i.blueprints.join(' + ')}`);
882
- console.log(C.dim(` keywords: ${i.keywords.slice(0, 5).join(', ')}…`));
883
- }
884
- console.log('');
1208
+ const intents = listIntents();
1209
+ await emitExperience('scaffold', {
1210
+ subtitle: 'list-intents',
1211
+ message: `${intents.length} intents`,
1212
+ table: {
1213
+ columns: ['Id', 'Blueprints', 'Keywords'],
1214
+ rows: intents.map((i) => [
1215
+ i.id,
1216
+ i.blueprints.join(' + '),
1217
+ i.keywords.slice(0, 5).join(', '),
1218
+ ]),
1219
+ },
1220
+ });
885
1221
  return;
886
1222
  }
887
1223
 
@@ -897,8 +1233,8 @@ async function scaffoldCmd() {
897
1233
  baseUrl: baseUrl || undefined,
898
1234
  });
899
1235
  if (!r.ok) {
900
- console.log(C.red(r.error));
901
- process.exit(1);
1236
+ await emitExperience('scaffold', { status: 'ERROR', message: r.error, exitCode: 1 });
1237
+ return;
902
1238
  }
903
1239
  if (hasFlag('--json')) {
904
1240
  console.log(JSON.stringify({
@@ -908,11 +1244,20 @@ async function scaffoldCmd() {
908
1244
  entries: r.entries,
909
1245
  planSections: r.planSections,
910
1246
  }, null, 2));
911
- } else {
912
- console.log(C.green(`Wrote Atelier compose (beta) → ${r.path}`));
913
- console.log(C.dim(` ids: ${r.entries.map((e) => e.id).join(', ')}`));
914
- console.log(C.dim(' Beta: Library compose — not Velin Studio. Wrappers ≠ native rewrites.'));
1247
+ return;
915
1248
  }
1249
+ await emitExperience('scaffold', {
1250
+ subtitle: 'atelier compose',
1251
+ message: `Wrote Atelier compose (beta) → ${r.path}`,
1252
+ items: [
1253
+ { label: 'Ids', value: r.entries.map((e) => e.id).join(', ') },
1254
+ { label: 'Mode', value: r.mode || 'compose' },
1255
+ ],
1256
+ notifications: [{
1257
+ tone: 'info',
1258
+ message: 'Beta: Library compose — not Velin Studio. Wrappers ≠ native rewrites.',
1259
+ }],
1260
+ });
916
1261
  return;
917
1262
  }
918
1263
 
@@ -926,14 +1271,21 @@ async function scaffoldCmd() {
926
1271
  }
927
1272
  const prompt = promptParts.join(' ').trim() || sub;
928
1273
  if (!prompt || prompt === 'list-intents') {
929
- console.log('Usage: velinstyle scaffold "<description>" [-o file.html] [--json]');
930
- console.log(' velinstyle scaffold --atelier 04,07 [-o page.html] [--from <library>]');
1274
+ await emitExperience('scaffold', {
1275
+ status: 'WARNING',
1276
+ message: 'Usage: velinstyle scaffold "<description>" [-o file.html] [--json]',
1277
+ suggestions: [
1278
+ { text: 'velinstyle scaffold --atelier 04,07 -o page.html' },
1279
+ { text: 'velinstyle scaffold list-intents' },
1280
+ ],
1281
+ exitCode: 1,
1282
+ });
931
1283
  return;
932
1284
  }
933
1285
  const r = scaffoldFromPrompt(prompt);
934
1286
  if (!r.ok) {
935
- console.log(C.red(r.error));
936
- process.exit(1);
1287
+ await emitExperience('scaffold', { status: 'ERROR', message: r.error, exitCode: 1 });
1288
+ return;
937
1289
  }
938
1290
  const out = getArg('--output', '-o');
939
1291
  const asJson = hasFlag('--json');
@@ -941,13 +1293,21 @@ async function scaffoldCmd() {
941
1293
  console.log(JSON.stringify(r, null, 2));
942
1294
  } else if (out) {
943
1295
  writeFileSync(resolve(out), r.html, 'utf-8');
944
- console.log(C.green(`Wrote ${resolve(out)} (intent: ${r.intent}, ${r.confidence}${r.mode ? `, mode: ${r.mode}` : ''})`));
945
- if (r.review?.gate) {
946
- console.log(C.dim(` review gate: ${r.review.gate} (promptScore ${r.review.promptScore})`));
947
- }
948
- if (r.responsiveHints?.length) {
949
- console.log(C.yellow(` ${r.responsiveHints.length} layout hint(s) run: velinstyle layout suggest ${out}`));
950
- }
1296
+ await emitExperience('scaffold', {
1297
+ message: `Wrote ${resolve(out)}`,
1298
+ items: [
1299
+ { label: 'Intent', value: r.intent },
1300
+ { label: 'Confidence', value: String(r.confidence) },
1301
+ ...(r.mode ? [{ label: 'Mode', value: r.mode }] : []),
1302
+ ...(r.review?.gate ? [{ label: 'Review gate', value: `${r.review.gate} (${r.review.promptScore})` }] : []),
1303
+ ],
1304
+ suggestions: r.responsiveHints?.length
1305
+ ? [{ text: `velinstyle layout suggest ${out}` }]
1306
+ : [],
1307
+ notifications: r.responsiveHints?.length
1308
+ ? [{ tone: 'warning', message: `${r.responsiveHints.length} layout hint(s)` }]
1309
+ : [],
1310
+ });
951
1311
  } else {
952
1312
  console.log(r.html);
953
1313
  }
@@ -959,13 +1319,17 @@ async function planCmd() {
959
1319
  const { planFromAtelierList } = await import('./atelier.js');
960
1320
  const payload = planFromAtelierList(atelierList, { prompt: args.slice(1).filter((a) => !a.startsWith('-')).join(' ') });
961
1321
  if (!payload.ok) {
962
- console.log(C.red(payload.error));
963
- process.exit(1);
1322
+ await emitExperience('plan', { status: 'ERROR', message: payload.error, exitCode: 1 });
1323
+ return;
964
1324
  }
965
1325
  const out = getArg('--output', '-o');
966
1326
  if (out) {
967
1327
  writeFileSync(resolve(out), JSON.stringify(payload, null, 2), 'utf-8');
968
- console.log(C.green(`Wrote Atelier plan (beta) ${resolve(out)} (${payload.plan.sections.length} sections)`));
1328
+ await emitExperience('plan', {
1329
+ subtitle: 'atelier',
1330
+ message: `Wrote Atelier plan (beta) ${resolve(out)}`,
1331
+ items: [{ label: 'Sections', value: String(payload.plan.sections.length) }],
1332
+ });
969
1333
  return;
970
1334
  }
971
1335
  console.log(JSON.stringify(payload, null, 2));
@@ -982,8 +1346,12 @@ async function planCmd() {
982
1346
  }
983
1347
  const prompt = promptParts.join(' ').trim();
984
1348
  if (!prompt) {
985
- console.log('Usage: velinstyle plan "<description>" [--json] [-o plan.json]');
986
- console.log(' velinstyle plan --atelier 04,07 [-o plan.json]');
1349
+ await emitExperience('plan', {
1350
+ status: 'WARNING',
1351
+ message: 'Usage: velinstyle plan "<description>" [--json] [-o plan.json]',
1352
+ suggestions: [{ text: 'velinstyle plan --atelier 04,07 [-o plan.json]' }],
1353
+ exitCode: 1,
1354
+ });
987
1355
  return;
988
1356
  }
989
1357
  const { analyzePrompt, buildPlan } = await import('./prompt-engine.js');
@@ -993,7 +1361,13 @@ async function planCmd() {
993
1361
  const out = getArg('--output', '-o');
994
1362
  if (out) {
995
1363
  writeFileSync(resolve(out), JSON.stringify(payload, null, 2), 'utf-8');
996
- console.log(C.green(`Wrote plan ${resolve(out)} (page: ${plan.page?.id}, sections: ${plan.sections.length})`));
1364
+ await emitExperience('plan', {
1365
+ message: `Wrote plan ${resolve(out)}`,
1366
+ items: [
1367
+ { label: 'Page', value: plan.page?.id || '—' },
1368
+ { label: 'Sections', value: String(plan.sections.length) },
1369
+ ],
1370
+ });
997
1371
  return;
998
1372
  }
999
1373
  console.log(JSON.stringify(payload, null, 2));
@@ -1022,24 +1396,51 @@ async function reviewCmd() {
1022
1396
  const report = reviewHtml(html, { plan, prompt, profile: profile || undefined });
1023
1397
  if (hasFlag('--json')) {
1024
1398
  console.log(JSON.stringify(report, null, 2));
1025
- } else {
1026
- console.log(`\n ${C.bold('Review gate:')} ${report.gate} ${C.dim(`profile ${report.profile} · promptScore ${report.promptScore}`)}`);
1027
- console.log(C.dim(` design ${report.scores.design} · a11y ${report.scores.accessibility} · seo ${report.scores.seo} · perf ${report.scores.performance} · conversion ${report.scores.conversion} · opt ${report.scores.optimization ?? '—'}`));
1028
- if (report.optimization) {
1029
- console.log(C.dim(` optimization · unused themes ${report.optimization.unusedThemes} · WC tags ${report.optimization.componentTags} · class tokens ${report.optimization.classTokens}`));
1030
- }
1031
- if (report.issues.length) {
1032
- console.log(`\n ${C.bold('Issues:')}`);
1033
- for (const issue of report.issues) {
1034
- const color = issue.severity === 'error' ? C.red : C.yellow;
1035
- console.log(color(` [${issue.severity}] ${issue.code}: ${issue.message}`));
1036
- console.log(C.dim(` fix: ${issue.fix}`));
1037
- }
1038
- } else {
1039
- console.log(C.green('\n No issues.'));
1040
- }
1041
- console.log('');
1399
+ process.exit(report.gate === 'fail' ? 1 : 0);
1400
+ return;
1042
1401
  }
1402
+ const exp = await ensureExperience();
1403
+ await exp.executeExperience({
1404
+ commandId: 'review',
1405
+ args,
1406
+ helpers: { getArg, hasFlag },
1407
+ version: CLI_VERSION,
1408
+ layout: 'review',
1409
+ run: async () => ({
1410
+ project: { file: full },
1411
+ result: {
1412
+ status: report.gate === 'fail' ? 'ERROR' : report.issues.length ? 'WARNING' : 'PASS',
1413
+ message: `gate ${report.gate} · profile ${report.profile}`,
1414
+ },
1415
+ stats: report.scores,
1416
+ diagnostics: report.issues,
1417
+ widgets: [
1418
+ { id: 'hero', props: { title: 'Review', subtitle: full, eyebrow: 'Velin Experience' } },
1419
+ {
1420
+ id: 'score',
1421
+ props: {
1422
+ title: 'Overall',
1423
+ scores: [
1424
+ { label: 'Design', value: report.scores.design },
1425
+ { label: 'Accessibility', value: report.scores.accessibility },
1426
+ { label: 'SEO', value: report.scores.seo },
1427
+ { label: 'Performance', value: report.scores.performance },
1428
+ { label: 'Conversion', value: report.scores.conversion },
1429
+ ],
1430
+ },
1431
+ },
1432
+ {
1433
+ id: 'table',
1434
+ props: {
1435
+ columns: ['Severity', 'Code', 'Message'],
1436
+ rows: (report.issues || []).slice(0, 20).map((i) => [i.severity, i.code, i.message.slice(0, 48)]),
1437
+ },
1438
+ },
1439
+ { id: 'footer', props: { text: 'velinstyle review' } },
1440
+ ],
1441
+ suggestions: (report.issues || []).filter((i) => i.fix).slice(0, 5).map((i) => ({ text: i.fix })),
1442
+ }),
1443
+ });
1043
1444
  process.exit(report.gate === 'fail' ? 1 : 0);
1044
1445
  }
1045
1446
 
@@ -1056,21 +1457,29 @@ async function layoutCmd() {
1056
1457
  if (sub === 'fix') {
1057
1458
  const result = fixPath(targetPath, { write, dryRun });
1058
1459
  if (!result.ok) {
1059
- console.log(C.red(result.error || 'Fix failed'));
1060
- process.exit(1);
1460
+ await emitExperience('layout', { status: 'ERROR', message: result.error || 'Fix failed', exitCode: 1 });
1461
+ return;
1061
1462
  }
1062
1463
  if (asJson) {
1063
1464
  console.log(JSON.stringify(result, null, 2));
1064
1465
  return;
1065
1466
  }
1066
1467
  if (result.changes.length === 0) {
1067
- console.log(C.green('No safe fixes to apply.'));
1468
+ await emitExperience('layout', {
1469
+ subtitle: 'fix',
1470
+ message: 'No safe fixes to apply.',
1471
+ notifications: [{ tone: 'ok', message: 'Nothing to change' }],
1472
+ });
1068
1473
  return;
1069
1474
  }
1070
- for (const c of result.changes) {
1071
- console.log(`${dryRun ? '[dry-run] ' : ''}${c.file}`);
1072
- c.changes.forEach((ch) => console.log(C.dim(` - ${ch}`)));
1073
- }
1475
+ await emitExperience('layout', {
1476
+ subtitle: 'fix',
1477
+ message: `${dryRun ? '[dry-run] ' : ''}${result.changes.length} file(s)`,
1478
+ table: {
1479
+ columns: ['File', 'Changes'],
1480
+ rows: result.changes.map((c) => [c.file, (c.changes || []).join('; ').slice(0, 60)]),
1481
+ },
1482
+ });
1074
1483
  return;
1075
1484
  }
1076
1485
 
@@ -1082,17 +1491,48 @@ async function layoutCmd() {
1082
1491
  return;
1083
1492
  }
1084
1493
 
1085
- console.log(formatTextReport(outIssues, files));
1086
- if (sub === 'suggest' && issues.length) {
1087
- console.log(C.bold('Suggested fixes:'));
1088
- for (const i of outIssues) {
1089
- console.log(` [${i.id}] ${i.fix}`);
1090
- if (i.responsive?.mobile) {
1091
- console.log(C.dim(` mobile: ${i.responsive.mobile}`));
1092
- }
1093
- }
1094
- console.log('');
1095
- }
1494
+ const exp = await ensureExperience();
1495
+ const errors = issues.filter((x) => x.severity === 'error').length;
1496
+ const warnings = issues.filter((x) => x.severity === 'warning').length;
1497
+ await exp.executeExperience({
1498
+ commandId: 'layout',
1499
+ args,
1500
+ helpers: { getArg, hasFlag },
1501
+ version: CLI_VERSION,
1502
+ layout: 'audit',
1503
+ run: async () => ({
1504
+ project: { cwd: targetPath, name: 'Layout' },
1505
+ result: {
1506
+ status: errors ? 'ERROR' : warnings ? 'WARNING' : 'PASS',
1507
+ message: `${outIssues.length} finding(s) · ${sub}`,
1508
+ },
1509
+ diagnostics: outIssues,
1510
+ widgets: [
1511
+ { id: 'hero', props: { slot: 'hero', title: 'Layout audit', subtitle: `Subcommand: ${sub}`, eyebrow: 'Velin Experience' } },
1512
+ {
1513
+ id: 'stats',
1514
+ props: {
1515
+ slot: 'counts',
1516
+ items: [
1517
+ { label: 'Files', value: files.length },
1518
+ { label: 'Findings', value: outIssues.length },
1519
+ { label: 'Errors', value: errors },
1520
+ ],
1521
+ },
1522
+ },
1523
+ {
1524
+ id: 'table',
1525
+ props: {
1526
+ slot: 'findings',
1527
+ columns: ['Sev', 'Id', 'Message'],
1528
+ rows: outIssues.slice(0, 20).map((i) => [i.severity, i.id || '—', String(i.message || i.fix || '').slice(0, 40)]),
1529
+ },
1530
+ },
1531
+ { id: 'footer', props: { slot: 'footer', text: `velinstyle layout ${sub}` } },
1532
+ ],
1533
+ suggestions: outIssues.filter((i) => i.fix).slice(0, 5).map((i) => ({ text: i.fix })),
1534
+ }),
1535
+ });
1096
1536
  process.exit(issues.some((x) => x.severity === 'error') ? 1 : 0);
1097
1537
  }
1098
1538
 
@@ -1104,18 +1544,71 @@ async function docsCmd() {
1104
1544
  const { generateDocs } = await import('./docs-generate.js');
1105
1545
  const result = await generateDocs({ scope, outDir: resolve(out), searchIndex: true });
1106
1546
  if (!result.ok) {
1107
- console.log(C.red(result.error));
1108
- process.exit(1);
1547
+ await emitExperience('docs', { status: 'ERROR', message: result.error, exitCode: 1 });
1548
+ return;
1109
1549
  }
1110
- console.log(C.green(`Generated ${result.written} file(s) → ${resolve(out)}`));
1550
+ await emitExperience('docs', {
1551
+ subtitle: 'generate',
1552
+ message: `Generated ${result.written} file(s)`,
1553
+ items: [
1554
+ { label: 'Scope', value: scope },
1555
+ { label: 'Out', value: resolve(out) },
1556
+ ],
1557
+ });
1111
1558
  return;
1112
1559
  }
1113
- console.log('Usage: velinstyle docs generate [--scope all|components|tokens|utilities|cli|rules|a11y|meta] [--out <dir>]');
1560
+ await emitExperience('docs', {
1561
+ status: 'WARNING',
1562
+ message: 'Usage: velinstyle docs generate [--scope all|components|tokens|…] [--out <dir>]',
1563
+ exitCode: 1,
1564
+ });
1114
1565
  }
1115
1566
 
1116
1567
  async function metaCmd() {
1117
- const { metaMain } = await import('./meta.js');
1118
- await metaMain(process.argv.slice(2));
1568
+ if (args[1] === '--help' || args[1] === '-h') {
1569
+ await emitExperience('meta', {
1570
+ message: 'Usage: velinstyle meta [--out file] [--llms-out file] [--base-url url] [--no-llms]',
1571
+ suggestions: [
1572
+ { text: 'velinstyle meta' },
1573
+ { text: 'velinstyle meta page index.html [--write]' },
1574
+ ],
1575
+ });
1576
+ return;
1577
+ }
1578
+ const { metaPage, buildMeta } = await import('./meta.js');
1579
+ const pageIdx = args.indexOf('page');
1580
+ if (pageIdx !== -1 && args[pageIdx + 1]) {
1581
+ const write = hasFlag('--write');
1582
+ const result = metaPage(args[pageIdx + 1], { write });
1583
+ if (!result.ok) {
1584
+ await emitExperience('meta', { status: 'ERROR', message: result.error, exitCode: 1 });
1585
+ return;
1586
+ }
1587
+ if (write) {
1588
+ await emitExperience('meta', {
1589
+ subtitle: 'page',
1590
+ message: `Updated velin-meta in ${args[pageIdx + 1]}`,
1591
+ });
1592
+ } else {
1593
+ console.log(result.script);
1594
+ }
1595
+ return;
1596
+ }
1597
+
1598
+ const result = await buildMeta({
1599
+ outFile: getArg('--out') ? resolve(getArg('--out')) : undefined,
1600
+ llmsFile: getArg('--llms-out') ? resolve(getArg('--llms-out')) : undefined,
1601
+ baseUrl: getArg('--base-url') || undefined,
1602
+ llms: !hasFlag('--no-llms'),
1603
+ });
1604
+
1605
+ await emitExperience('meta', {
1606
+ message: `Wrote ${result.count} file(s)`,
1607
+ table: {
1608
+ columns: ['File'],
1609
+ rows: (result.written || []).map((p) => [p]),
1610
+ },
1611
+ });
1119
1612
  }
1120
1613
 
1121
1614
  async function searchCmd() {
@@ -1128,42 +1621,280 @@ async function searchCmd() {
1128
1621
  outFile: resolve(out),
1129
1622
  extraHtmlDirs: extra ? extra.split(',').map((p) => resolve(p.trim())) : [],
1130
1623
  });
1131
- console.log(C.green(`Search index: ${result.count} entries → ${result.outFile}`));
1624
+ await emitExperience('search', {
1625
+ subtitle: 'index',
1626
+ message: `Search index: ${result.count} entries`,
1627
+ items: [{ label: 'Out', value: result.outFile }],
1628
+ });
1132
1629
  return;
1133
1630
  }
1134
- console.log('Usage: velinstyle search index [--out <file>] [--extra-html dir1,dir2]');
1631
+ await emitExperience('search', {
1632
+ status: 'WARNING',
1633
+ message: 'Usage: velinstyle search index [--out <file>] [--extra-html dir1,dir2]',
1634
+ exitCode: 1,
1635
+ });
1135
1636
  }
1136
1637
 
1137
1638
  async function skillsCmd() {
1138
- const { skillsCommand } = await import('./skills.js');
1139
- await skillsCommand(args.slice(1));
1639
+ const sub = args[1];
1640
+ const {
1641
+ skillsList,
1642
+ skillsInstall,
1643
+ skillsRun,
1644
+ skillsValidate,
1645
+ skillsDoctor,
1646
+ skillsIndex,
1647
+ loadSkillRegistry,
1648
+ } = await import('./skills.js');
1649
+
1650
+ if (!sub || sub === '--help' || sub === '-h') {
1651
+ await emitExperience('skills', {
1652
+ message: 'Usage: velinstyle skills <list|show|install|run|validate|doctor|packs|…>',
1653
+ suggestions: [
1654
+ { text: 'velinstyle skills list' },
1655
+ { text: 'velinstyle skills show scaffold.landing --human' },
1656
+ ],
1657
+ });
1658
+ return;
1659
+ }
1660
+
1661
+ if (sub === 'list') {
1662
+ const items = skillsList(args.slice(2), { silent: true });
1663
+ if (hasFlag('--json')) {
1664
+ console.log(JSON.stringify(items, null, 2));
1665
+ return;
1666
+ }
1667
+ await emitExperience('skills', {
1668
+ subtitle: 'list',
1669
+ message: `${items.length} skill(s)`,
1670
+ table: {
1671
+ columns: ['Id', 'Category', 'Priority', 'Status'],
1672
+ rows: items.slice(0, 40).map((item) => [
1673
+ item.id,
1674
+ item.category || '',
1675
+ item.priority || '',
1676
+ item.status || '',
1677
+ ]),
1678
+ },
1679
+ });
1680
+ return;
1681
+ }
1682
+
1683
+ if (sub === 'show') {
1684
+ const skillId = args[2];
1685
+ const { findSkillById } = await import('../packages/skill-engine/src/index.js');
1686
+ const registry = loadSkillRegistry();
1687
+ const skill = findSkillById(registry, skillId);
1688
+ if (!skill) {
1689
+ await emitExperience('skills', { status: 'ERROR', message: `Unknown skill: ${skillId}`, exitCode: 1 });
1690
+ return;
1691
+ }
1692
+ if (hasFlag('--human')) {
1693
+ await emitExperience('skills', {
1694
+ subtitle: skillId,
1695
+ message: skill.title || skillId,
1696
+ items: [
1697
+ { label: 'Category', value: skill.category || '—' },
1698
+ { label: 'Status', value: skill.status || '—' },
1699
+ { label: 'Priority', value: skill.priority || '—' },
1700
+ { label: 'Capabilities', value: (skill.capabilities || []).join(', ') || '—' },
1701
+ ],
1702
+ notifications: skill.summary || skill.description
1703
+ ? [{ tone: 'info', message: skill.summary || skill.description }]
1704
+ : [],
1705
+ });
1706
+ return;
1707
+ }
1708
+ console.log(JSON.stringify(skill, null, 2));
1709
+ return;
1710
+ }
1711
+
1712
+ if (sub === 'install') {
1713
+ if (!args[2]) {
1714
+ await emitExperience('skills', {
1715
+ status: 'WARNING',
1716
+ message: 'Usage: velinstyle skills install <skill-id|pack|bundle:id|project:id>',
1717
+ exitCode: 1,
1718
+ });
1719
+ return;
1720
+ }
1721
+ const result = skillsInstall(args.slice(2), { silent: true });
1722
+ await emitExperience('skills', {
1723
+ subtitle: 'install',
1724
+ status: result.issues?.length ? 'WARNING' : 'PASS',
1725
+ message: `Installed ${(result.installed || []).length} skill(s)`,
1726
+ table: {
1727
+ columns: ['Installed'],
1728
+ rows: (result.installed || []).map((id) => [id]),
1729
+ },
1730
+ notifications: (result.issues || []).map((issue) => ({ tone: 'warning', message: issue })),
1731
+ });
1732
+ return;
1733
+ }
1734
+
1735
+ if (sub === 'run') {
1736
+ const payload = skillsRun(args[2], args.slice(3), { silent: true });
1737
+ if (payload.ok === false) {
1738
+ await emitExperience('skills', {
1739
+ status: 'ERROR',
1740
+ message: 'Skill graph resolve failed',
1741
+ table: { columns: ['Issue'], rows: (payload.issues || []).map((i) => [i]) },
1742
+ exitCode: 1,
1743
+ });
1744
+ return;
1745
+ }
1746
+ if (hasFlag('--json')) {
1747
+ console.log(JSON.stringify(payload, null, 2));
1748
+ return;
1749
+ }
1750
+ await emitExperience('skills', {
1751
+ subtitle: 'run',
1752
+ message: `${payload.steps.length} step(s) (dry-run)`,
1753
+ table: {
1754
+ columns: ['Step', 'CLI', 'onlyIf'],
1755
+ rows: payload.steps.map((s) => [
1756
+ s.id,
1757
+ (s.requiresCli || []).join(', '),
1758
+ (s.onlyIf || []).join(', '),
1759
+ ]),
1760
+ },
1761
+ });
1762
+ return;
1763
+ }
1764
+
1765
+ if (sub === 'validate') {
1766
+ const result = skillsValidate({ silent: true });
1767
+ await emitExperience('skills', {
1768
+ subtitle: 'validate',
1769
+ status: result.ok ? 'PASS' : 'ERROR',
1770
+ message: result.ok ? 'skills registry valid' : 'skills registry invalid',
1771
+ table: result.errors?.length
1772
+ ? { columns: ['Error'], rows: result.errors.map((e) => [e]) }
1773
+ : null,
1774
+ exitCode: result.ok ? 0 : 1,
1775
+ });
1776
+ return;
1777
+ }
1778
+
1779
+ if (sub === 'doctor') {
1780
+ const result = await skillsDoctor(args.slice(2), { silent: true });
1781
+ if (hasFlag('--json')) {
1782
+ console.log(JSON.stringify(result, null, 2));
1783
+ process.exit(result.ok ? 0 : 1);
1784
+ }
1785
+ await emitExperience('skills', {
1786
+ subtitle: 'doctor',
1787
+ status: result.ok ? 'PASS' : 'ERROR',
1788
+ message: result.ok
1789
+ ? `skills doctor OK (${result.checked} paths)`
1790
+ : `skills doctor FAILED (${result.errors.length} missing)`,
1791
+ table: result.errors?.length
1792
+ ? { columns: ['Missing'], rows: result.errors.map((e) => [e]) }
1793
+ : null,
1794
+ exitCode: result.ok ? 0 : 1,
1795
+ });
1796
+ return;
1797
+ }
1798
+
1799
+ if (['packs', 'bundles', 'templates', 'projects', 'graphs'].includes(sub)) {
1800
+ const out = skillsIndex(sub, { silent: true });
1801
+ console.log(JSON.stringify(out, null, 2));
1802
+ return;
1803
+ }
1804
+
1805
+ await emitExperience('skills', {
1806
+ status: 'ERROR',
1807
+ message: `Unknown skills subcommand: ${sub}`,
1808
+ exitCode: 1,
1809
+ });
1140
1810
  }
1141
1811
 
1142
1812
  async function workflowCmd() {
1143
- const { workflowCommand } = await import('./workflow.js');
1144
- await workflowCommand(args.slice(1));
1813
+ const { workflowResolve } = await import('./workflow.js');
1814
+ const workflowId = args[1];
1815
+ if (!workflowId || workflowId === '--help' || workflowId === '-h') {
1816
+ await emitExperience('workflow', {
1817
+ message: 'Usage: velinstyle workflow <graph-id|project:id> [--json]',
1818
+ suggestions: [
1819
+ { text: 'velinstyle workflow landingpage' },
1820
+ { text: 'velinstyle workflow project:marketing-site --json' },
1821
+ ],
1822
+ });
1823
+ return;
1824
+ }
1825
+ const resolved = workflowResolve(workflowId);
1826
+ if (!resolved.ok) {
1827
+ await emitExperience('workflow', {
1828
+ status: 'ERROR',
1829
+ message: resolved.error,
1830
+ table: resolved.issues?.length
1831
+ ? { columns: ['Issue'], rows: resolved.issues.map((i) => [i]) }
1832
+ : null,
1833
+ exitCode: 1,
1834
+ });
1835
+ return;
1836
+ }
1837
+ if (hasFlag('--json')) {
1838
+ console.log(JSON.stringify(resolved.data, null, 2));
1839
+ return;
1840
+ }
1841
+ await emitExperience('workflow', {
1842
+ message: `Workflow steps (${resolved.data.orderedSteps.length})`,
1843
+ table: {
1844
+ columns: ['Step'],
1845
+ rows: resolved.data.orderedSteps.map((step) => [step]),
1846
+ },
1847
+ });
1145
1848
  }
1146
1849
 
1147
1850
  async function wcCmd() {
1148
1851
  const sub = args[1];
1149
1852
  if (sub === 'api' || sub === '--help' || sub === '-h' || !sub) {
1150
1853
  if (sub !== 'api') {
1151
- console.log(`Usage: velinstyle wc api <tag>\nExample: velinstyle wc api velin-toast`);
1152
- if (sub === '--help' || sub === '-h' || !sub) process.exit(0);
1854
+ await emitExperience('wc', {
1855
+ status: sub ? 'PASS' : 'WARNING',
1856
+ message: 'Usage: velinstyle wc api <tag>',
1857
+ suggestions: [{ text: 'velinstyle wc api velin-toast' }],
1858
+ exitCode: !sub ? 1 : 0,
1859
+ });
1860
+ return;
1153
1861
  }
1154
1862
  const tag = args[2];
1155
- const { describeWcApi, formatWcApi } = await import('./wc-api.js');
1863
+ const { describeWcApi } = await import('./wc-api.js');
1156
1864
  const report = describeWcApi(PKG_ROOT, tag);
1157
1865
  if (!report.ok) {
1158
- console.error(report.error);
1159
- process.exit(1);
1866
+ await emitExperience('wc', { status: 'ERROR', message: report.error, exitCode: 1 });
1867
+ return;
1160
1868
  }
1161
- if (hasFlag('--json')) console.log(JSON.stringify(report, null, 2));
1162
- else console.log(formatWcApi(report));
1869
+ if (hasFlag('--json')) {
1870
+ console.log(JSON.stringify(report, null, 2));
1871
+ return;
1872
+ }
1873
+ const rows = [
1874
+ ...report.observedAttributes.map((a) => ['attr', a]),
1875
+ ...report.methods.map((m) => ['method', `${m}()`]),
1876
+ ...report.events.map((e) => ['event', e]),
1877
+ ];
1878
+ await emitExperience('wc', {
1879
+ subtitle: report.tag,
1880
+ message: `<${report.tag}>`,
1881
+ items: [
1882
+ { label: 'Source', value: report.source },
1883
+ { label: 'Docs', value: report.generatedDoc || '(missing — run docs generate)' },
1884
+ ...(report.showSignature ? [{ label: 'show()', value: report.showSignature }] : []),
1885
+ ],
1886
+ table: rows.length ? { columns: ['Kind', 'Name'], rows } : null,
1887
+ notifications: report.example
1888
+ ? [{ tone: 'info', message: report.example.split('\n')[0] }]
1889
+ : [],
1890
+ });
1163
1891
  return;
1164
1892
  }
1165
- console.error(`Unknown wc subcommand: ${sub}. Use: velinstyle wc api <tag>`);
1166
- process.exit(1);
1893
+ await emitExperience('wc', {
1894
+ status: 'ERROR',
1895
+ message: `Unknown wc subcommand: ${sub}. Use: velinstyle wc api <tag>`,
1896
+ exitCode: 1,
1897
+ });
1167
1898
  }
1168
1899
 
1169
1900
  async function scanCmd() {
@@ -1178,15 +1909,81 @@ async function scanCmd() {
1178
1909
  }
1179
1910
  const severity = getArg('--severity') || 'warning';
1180
1911
  const only = getArg('--only');
1912
+ const machine = format === 'json' || format === 'sarif' || hasFlag('--json') || hasFlag('--sarif');
1181
1913
 
1182
1914
  const { scan } = await import('./scanner.js');
1183
- const exitCode = scan(targetPath, {
1184
- severity,
1185
- fix,
1186
- fixDryRun,
1187
- fixLang: fixLang || undefined,
1188
- format,
1189
- only,
1915
+
1916
+ if (fix || fixDryRun || machine) {
1917
+ const exitCode = scan(targetPath, {
1918
+ severity,
1919
+ fix,
1920
+ fixDryRun,
1921
+ fixLang: fixLang || undefined,
1922
+ format: hasFlag('--json') ? 'json' : hasFlag('--sarif') ? 'sarif' : format,
1923
+ only,
1924
+ });
1925
+ process.exit(exitCode);
1926
+ }
1927
+
1928
+ const exp = await ensureExperience();
1929
+ let exitCode = 0;
1930
+ await exp.executeExperience({
1931
+ commandId: 'scan',
1932
+ args,
1933
+ helpers: { getArg, hasFlag },
1934
+ version: CLI_VERSION,
1935
+ layout: 'audit',
1936
+ run: async (ctx) => {
1937
+ ctx.emitProgress('Analysis', 40);
1938
+ const data = scan(targetPath, {
1939
+ severity,
1940
+ format: 'json',
1941
+ only,
1942
+ returnData: true,
1943
+ });
1944
+ exitCode = data.exitCode || 0;
1945
+ const issues = data.issues || [];
1946
+ const errors = issues.filter((i) => i.severity === 'error').length;
1947
+ const warnings = issues.filter((i) => i.severity === 'warning').length;
1948
+ const status = errors ? 'ERROR' : warnings ? 'WARNING' : 'PASS';
1949
+ return {
1950
+ project: { cwd: targetPath, name: 'Scan' },
1951
+ result: {
1952
+ status,
1953
+ message: errors || warnings
1954
+ ? `${errors} error(s), ${warnings} warning(s)`
1955
+ : 'No issues at selected severity',
1956
+ },
1957
+ stats: { errors, warnings, issues: issues.length },
1958
+ diagnostics: issues,
1959
+ widgets: [
1960
+ { id: 'hero', props: { slot: 'hero', title: 'Scan', subtitle: 'Trust & accessibility audit', eyebrow: 'Velin Experience' } },
1961
+ {
1962
+ id: 'stats',
1963
+ props: {
1964
+ slot: 'counts',
1965
+ items: [
1966
+ { label: 'Errors', value: errors },
1967
+ { label: 'Warnings', value: warnings },
1968
+ { label: 'Issues', value: issues.length },
1969
+ ],
1970
+ },
1971
+ },
1972
+ {
1973
+ id: 'table',
1974
+ props: {
1975
+ slot: 'findings',
1976
+ columns: ['Sev', 'Code', 'Message'],
1977
+ rows: issues.slice(0, 20).map((i) => [i.severity, i.code || '—', String(i.message || '').slice(0, 42)]),
1978
+ },
1979
+ },
1980
+ { id: 'notification', props: { tone: status === 'PASS' ? 'ok' : status === 'ERROR' ? 'err' : 'warn', message: `Scan ${status}` } },
1981
+ { id: 'footer', props: { slot: 'footer', text: 'velinstyle scan' } },
1982
+ ],
1983
+ suggestions: issues.filter((i) => i.fix).slice(0, 5).map((i) => ({ text: i.fix })),
1984
+ notifications: [{ tone: 'info', message: 'Use --fix for safe auto-fixes where available' }],
1985
+ };
1986
+ },
1190
1987
  });
1191
1988
  process.exit(exitCode);
1192
1989
  }
@@ -1287,6 +2084,40 @@ async function checkCmd() {
1287
2084
  steps.push({ step: 'transparency', ok: true, skipped: true, error: String(err.message || err) });
1288
2085
  }
1289
2086
 
2087
+ if (!quiet) console.log(C.bold('\n── motion ──'));
2088
+ try {
2089
+ const { motionCheckStep } = await import('./motion.js');
2090
+ const mx = await motionCheckStep(htmlFile || target, { quiet, C });
2091
+ if (!mx.ok && !mx.skipped) failed += 1;
2092
+ steps.push({ step: 'motion', ok: mx.ok || !!mx.skipped, skipped: mx.skipped, elements: mx.elements });
2093
+ } catch (err) {
2094
+ if (!quiet) console.log(C.yellow(`motion skipped: ${err.message}`));
2095
+ steps.push({ step: 'motion', ok: true, skipped: true, error: String(err.message || err) });
2096
+ }
2097
+
2098
+ if (!quiet) console.log(C.bold('\n── security ──'));
2099
+ try {
2100
+ const { securityCheckStep } = await import('./security.js');
2101
+ let securityRoot = target;
2102
+ try {
2103
+ if (existsSync(target) && statSync(target).isFile()) securityRoot = dirname(target);
2104
+ } catch { /* keep target */ }
2105
+ const sx = await securityCheckStep(securityRoot, { quiet, C });
2106
+ if (!sx.ok && !sx.skipped) failed += 1;
2107
+ steps.push({
2108
+ step: 'security',
2109
+ ok: sx.ok || !!sx.skipped,
2110
+ skipped: sx.skipped,
2111
+ securityScore: sx.securityScore,
2112
+ overall: sx.overall,
2113
+ overallGate: sx.overallGate,
2114
+ findings: sx.findings,
2115
+ });
2116
+ } catch (err) {
2117
+ if (!quiet) console.log(C.yellow(`security skipped: ${err.message}`));
2118
+ steps.push({ step: 'security', ok: true, skipped: true, error: String(err.message || err) });
2119
+ }
2120
+
1290
2121
  const payload = {
1291
2122
  ok: failed === 0,
1292
2123
  failedSteps: failed,
@@ -1333,10 +2164,43 @@ async function checkCmd() {
1333
2164
  console.log(JSON.stringify(sarif, null, 2));
1334
2165
  } else if (asJson) {
1335
2166
  console.log(JSON.stringify(payload, null, 2));
1336
- } else if (failed) {
1337
- console.log(C.red(`\ncheck failed (${failed} step(s))`));
1338
2167
  } else {
1339
- console.log(C.green('\ncheck passed'));
2168
+ const exp = await ensureExperience();
2169
+ await exp.executeExperience({
2170
+ commandId: 'check',
2171
+ args,
2172
+ helpers: { getArg, hasFlag },
2173
+ version: CLI_VERSION,
2174
+ layout: 'dashboard',
2175
+ run: async () => ({
2176
+ project: { target },
2177
+ result: {
2178
+ status: failed ? 'ERROR' : 'PASS',
2179
+ message: failed ? `check failed (${failed} step(s))` : 'Ready',
2180
+ },
2181
+ widgets: [
2182
+ { id: 'hero', props: { title: 'Check', subtitle: 'All engines', eyebrow: 'Velin Experience' } },
2183
+ {
2184
+ id: 'health',
2185
+ props: {
2186
+ title: 'Summary',
2187
+ rows: steps.map((s) => ({
2188
+ label: s.step,
2189
+ status: s.skipped ? 'SKIP' : s.ok ? 'PASS' : 'FAIL',
2190
+ })),
2191
+ },
2192
+ },
2193
+ {
2194
+ id: 'status',
2195
+ props: { label: failed ? 'FAIL' : 'PASS', tone: failed ? 'err' : 'ok' },
2196
+ },
2197
+ { id: 'footer', props: { text: 'Overall · Ready gate' } },
2198
+ ],
2199
+ suggestions: failed
2200
+ ? [{ text: 'Re-run failing steps individually (doctor / scan / review)' }]
2201
+ : [{ text: 'Try velinstyle dashboard for a project overview' }],
2202
+ }),
2203
+ });
1340
2204
  }
1341
2205
 
1342
2206
  process.exit(failed ? 1 : 0);
@@ -1345,8 +2209,9 @@ async function checkCmd() {
1345
2209
  function suggestCommand(unknown) {
1346
2210
  const names = [
1347
2211
  'init', 'build', 'themes', 'add', 'icons', 'blueprint', 'create', 'serve', 'doctor', 'check',
1348
- 'validate', 'tokens', 'scan', 'prefix', 'scaffold', 'plan', 'review', 'transparency', 'layout', 'perf',
2212
+ 'validate', 'tokens', 'scan', 'prefix', 'scaffold', 'plan', 'review', 'transparency', 'motion', 'security', 'layout', 'perf',
1349
2213
  'docs', 'documentation', 'meta', 'search', 'skills', 'workflow', 'wc', 'production', 'atelier',
2214
+ 'dashboard', 'replay',
1350
2215
  ];
1351
2216
  const q = String(unknown || '').toLowerCase();
1352
2217
  function editDistance(a, b) {
@@ -1392,23 +2257,25 @@ const ALIASES = {
1392
2257
  const resolvedCommand = ALIASES[command] || command;
1393
2258
 
1394
2259
  switch (resolvedCommand) {
1395
- case 'init': init(); break;
2260
+ case 'init': await init(); break;
1396
2261
  case 'build': await build(); break;
1397
2262
  case 'production': {
1398
2263
  const { runProduction } = await import('./production/run.js');
1399
2264
  await runProduction({ args: args.slice(1), helpers: { getArg, hasFlag, C } });
1400
2265
  break;
1401
2266
  }
1402
- case 'themes': themes(); break;
1403
- case 'add': add(); break;
1404
- case 'icons': icons(); break;
2267
+ case 'themes': await themes(); break;
2268
+ case 'add': await add(); break;
2269
+ case 'icons': await icons(); break;
1405
2270
  case 'blueprint': await blueprintCmd(); break;
1406
2271
  case 'create': await createCmd(); break;
1407
2272
  case 'serve': await serveCmd(); break;
1408
2273
  case 'doctor': await doctorCmd(); break;
2274
+ case 'dashboard': await dashboardCmd(); break;
2275
+ case 'replay': await replayCmd(); break;
1409
2276
  case 'check': await checkCmd(); break;
1410
2277
  case 'tokens': await tokensCmd(); break;
1411
- case 'scan': scanCmd(); break;
2278
+ case 'scan': await scanCmd(); break;
1412
2279
  case 'prefix': await prefixCmd(); break;
1413
2280
  case 'scaffold': await scaffoldCmd(); break;
1414
2281
  case 'plan': await planCmd(); break;
@@ -1416,7 +2283,17 @@ switch (resolvedCommand) {
1416
2283
  case 'review': await reviewCmd(); break;
1417
2284
  case 'transparency': {
1418
2285
  const { transparencyCmd } = await import('./transparency.js');
1419
- await transparencyCmd(args.slice(1), { C, getArg, hasFlag });
2286
+ await transparencyCmd(args.slice(1), { C, getArg, hasFlag, emitExperience });
2287
+ break;
2288
+ }
2289
+ case 'motion': {
2290
+ const { motionCmd } = await import('./motion.js');
2291
+ await motionCmd(args.slice(1), { C, getArg, hasFlag, emitExperience });
2292
+ break;
2293
+ }
2294
+ case 'security': {
2295
+ const { securityCmd } = await import('./security.js');
2296
+ await securityCmd(args.slice(1), { C, getArg, hasFlag, emitExperience });
1420
2297
  break;
1421
2298
  }
1422
2299
  case 'layout': await layoutCmd(); break;
@@ -1427,6 +2304,6 @@ switch (resolvedCommand) {
1427
2304
  case 'skills': await skillsCmd(); break;
1428
2305
  case 'workflow': await workflowCmd(); break;
1429
2306
  case 'wc': await wcCmd(); break;
1430
- case '--help': case '-h': case undefined: help(); break;
2307
+ case '--help': case '-h': case undefined: await help(); break;
1431
2308
  default: unknownCommand(command); break;
1432
2309
  }