@birdapi/velinstyle 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.de.md +148 -11
  2. package/README.md +174 -16
  3. package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
  4. package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
  5. package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
  6. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
  7. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
  8. package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
  9. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
  10. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
  11. package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
  12. package/cli/atelier-catalog.json +1267 -0
  13. package/cli/atelier-formats.js +159 -0
  14. package/cli/atelier.js +382 -0
  15. package/cli/blueprints/empty-state.html +3 -5
  16. package/cli/cli-manifest.json +441 -161
  17. package/cli/docgen/extract-cli.js +2 -1
  18. package/cli/docs-generate.js +9 -0
  19. package/cli/experience.js +173 -0
  20. package/cli/index.js +1384 -361
  21. package/cli/motion.js +738 -0
  22. package/cli/production/component-graph.json +166 -0
  23. package/cli/production/explain.js +52 -0
  24. package/cli/production/extract.js +278 -0
  25. package/cli/production/graph.js +102 -0
  26. package/cli/production/report.js +168 -0
  27. package/cli/production/run.js +355 -0
  28. package/cli/production/trim-css.js +177 -0
  29. package/cli/production/trim-fonts.js +23 -0
  30. package/cli/production/trim-icons.js +38 -0
  31. package/cli/production/trim-js.js +48 -0
  32. package/cli/production/trim-motion.js +22 -0
  33. package/cli/production/trim-themes.js +50 -0
  34. package/cli/production/watch.js +38 -0
  35. package/cli/review.js +48 -1
  36. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  37. package/cli/security.js +190 -0
  38. package/cli/skills.js +53 -10
  39. package/cli/transparency.js +140 -41
  40. package/cli/workflow.js +32 -17
  41. package/components/index.js +3 -0
  42. package/components/runtime/component-loaders.js +3 -0
  43. package/components/velin-drawer.js +43 -4
  44. package/components/velin-empty-state.js +83 -0
  45. package/components/velin-modal.js +76 -15
  46. package/components/velin-otp-input.js +220 -0
  47. package/components/velin-password-strength.js +147 -0
  48. package/components/velin-sheet.js +49 -4
  49. package/components/velin-theme-toggle.js +15 -1
  50. package/core/a11y/component-contracts.json +391 -255
  51. package/core/attributes/registry.js +1 -1
  52. package/core/meta/knowledge/components.json +75 -3
  53. package/core/motion/analyze.js +38 -0
  54. package/core/motion/blueprint.js +51 -0
  55. package/core/motion/composer.js +114 -0
  56. package/core/motion/data/registry.json +352 -0
  57. package/core/motion/effects.js +17 -1
  58. package/core/motion/index.js +112 -9
  59. package/core/motion/optimize.js +44 -0
  60. package/core/motion/policy.js +77 -0
  61. package/core/motion/registry.js +131 -0
  62. package/core/motion/review.js +59 -0
  63. package/core/motion/scan.js +150 -0
  64. package/core/motion/timeline.js +68 -0
  65. package/core/motion/transitions.js +49 -0
  66. package/core/motion/triggers.js +168 -0
  67. package/core/security/checks/fs.js +92 -0
  68. package/core/security/detect.js +35 -0
  69. package/core/security/engine.js +116 -0
  70. package/core/security/index.js +18 -0
  71. package/core/security/registry.js +85 -0
  72. package/core/security/report/report.js +113 -0
  73. package/core/security/rules/ci/index.js +146 -0
  74. package/core/security/rules/consumer/index.js +174 -0
  75. package/core/security/rules/deps/index.js +182 -0
  76. package/core/security/rules/index.js +24 -0
  77. package/core/security/rules/publish/index.js +230 -0
  78. package/core/security/rules/repo/index.js +143 -0
  79. package/core/security/rules/secrets/index.js +133 -0
  80. package/core/security/score/dependency-health.js +53 -0
  81. package/core/security/score/release-health.js +66 -0
  82. package/core/security/score/score.js +102 -0
  83. package/core/security/timeline/timeline.js +76 -0
  84. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  85. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  86. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  87. package/dist/chunks/chunk-MYKUI364.js +116 -0
  88. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  89. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  90. package/dist/chunks/runtime-entry.js +1 -1
  91. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  92. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  93. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  94. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  95. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  96. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  97. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  98. package/dist/llms.txt +4 -4
  99. package/dist/search-index.json +232 -5
  100. package/dist/velin-agent.json +433 -46
  101. package/dist/velinstyle-components.iife.js +3695 -2272
  102. package/dist/velinstyle-components.js +3698 -2271
  103. package/dist/velinstyle-components.min.js +262 -120
  104. package/dist/velinstyle.css +96 -13
  105. package/dist/velinstyle.d.ts +3 -0
  106. package/dist/velinstyle.min.css +1 -1
  107. package/package.json +19 -4
  108. package/packages/velinstyle-cli-core/package.json +11 -0
  109. package/packages/velinstyle-cli-core/src/config.js +84 -0
  110. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  111. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  112. package/packages/velinstyle-cli-core/src/index.js +18 -0
  113. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  114. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  115. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  116. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  117. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  118. package/packages/velinstyle-cli-registry/package.json +13 -0
  119. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  120. package/packages/velinstyle-cli-renderer/package.json +8 -0
  121. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  122. package/packages/velinstyle-cli-theme/package.json +8 -0
  123. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  124. package/packages/velinstyle-cli-ui/package.json +8 -0
  125. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  126. package/packages/velinstyle-motion/README.md +81 -0
  127. package/packages/velinstyle-motion/package.json +37 -0
  128. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  129. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  130. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  131. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  132. package/packages/velinstyle-motion/src/config.js +257 -0
  133. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  134. package/packages/velinstyle-motion/src/index.js +304 -0
  135. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  136. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  137. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  138. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  139. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  140. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  141. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  142. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  143. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  144. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  145. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  146. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  147. package/packages/velinstyle-skills/catalog.json +1 -1
  148. package/packages/velinstyle-skills/registry.json +1 -142
  149. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  150. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  151. package/src/components/data-table.css +19 -0
  152. package/src/components/empty-auth.css +33 -0
  153. package/src/components/table.css +16 -6
  154. package/src/tokens/motion.css +7 -0
  155. package/src/utilities/scroll-animation.css +66 -0
  156. package/src/velinstyle.css +1 -0
package/cli/index.js CHANGED
@@ -85,146 +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 themes List available themes
96
- velinstyle add <name> Add a single component CSS
97
- velinstyle icons <subcommand> Manage icon providers
98
- velinstyle scan [path|file] Security & accessibility scanner (dir or .html/.css/.js)
99
- velinstyle prefix [path] Add missing velin- prefix (dry-run; use --write)
100
- velinstyle blueprint [name] Print HTML blueprint (run: velinstyle blueprint list)
101
- velinstyle blueprint <name> --strict Fail if emitted classes ∉ CSS
102
- velinstyle create <kind> [dir] Scaffold: landing|dashboard|docs|auth
103
- velinstyle serve [dir] Static preview server (default port 4173)
104
- velinstyle doctor Check dist, icons, config, Windows ESM paths
105
- velinstyle transparency <sub> Transparency Framework: doctor|validate|report|export|migrate
106
- velinstyle check [path] doctor + blueprint --strict + scan + review + transparency
107
- velinstyle scaffold "<prompt>" Plan-first page HTML or recipe fragment
108
- velinstyle plan "<prompt>" Emit page plan JSON (no HTML)
109
- velinstyle review [file|html] Design / a11y / SEO / conversion review gate
110
- velinstyle wc api <tag> Human-readable Web Component API from source
111
- velinstyle layout <sub> Responsive layout audit and fixes
112
- velinstyle perf <sub> Performance audit (images, scripts) with --fix
113
- velinstyle tokens build Generate CSS variables from tokens.json
114
- velinstyle tokens validate Validate tokens.json schema
115
- velinstyle docs generate Auto-generate Markdown API reference
116
- velinstyle meta Build agent context (velin-agent.json, llms.txt)
117
- velinstyle search index Build JSON search index for VelinSearch
118
- velinstyle skills <subcommand> Registry-first AI skills commands
119
- velinstyle workflow <id> Resolve workflow graph / project workflow
120
-
121
- ${C.bold('Icons subcommands:')}
122
- icons list Show available icon providers
123
- icons add <provider> [--icons a,b] [--variant outline] Download to icons/svg/
124
- icons remove <provider> Remove provider icons
125
- icons build Rebuild icon sprite
126
-
127
- ${C.bold('Icons example (multi-provider):')}
128
- velinstyle icons add lucide --icons menu,search,check
129
- velinstyle icons add heroicons --icons arrow-left --variant outline
130
- velinstyle icons build
131
-
132
- ${C.bold('Blueprint:')}
133
- velinstyle blueprint list [--strict] Print ids (strict validates all)
134
- velinstyle blueprint <name> [--output, -o <file>] [--strict]
135
-
136
- ${C.bold('Create / serve / doctor / check:')}
137
- velinstyle create landing|dashboard|docs|auth [dir] [--theme earth] [--no-copy]
138
- velinstyle serve [dir] [--port 4173]
139
- velinstyle doctor
140
- velinstyle check [path] [--json|--sarif] [--profile marketing|app|docs|fragment|ecommerce]
141
- Alias: validate check · documentation docs generate help
142
-
143
- ${C.bold('Web Components:')}
144
- velinstyle wc api <tag> e.g. velinstyle wc api velin-toast
145
-
146
- ${C.bold('Scaffold (1.2.0):')}
147
- velinstyle scaffold "<prompt>" Plan → render → review for pages; recipes for fragments
148
- velinstyle scaffold list-intents Show recipe intent keywords
149
- velinstyle scaffold "<prompt>" -o out.html [--json]
150
-
151
- ${C.bold('Plan / Review (1.2.x):')}
152
- velinstyle plan "<prompt>" [--json] [-o plan.json]
153
- velinstyle review <file.html> [--json] [--prompt "..."]
154
-
155
- ${C.bold('Transparency (1.2.1):')}
156
- velinstyle transparency doctor [path|file] [--policy file] [--json]
157
- velinstyle transparency validate [path|file] [--policy file] [--json]
158
- velinstyle transparency report [path|file] [--out dir] [--policy file]
159
- velinstyle transparency export [path|file] [--format json|json-ld|csv|html] [-o file]
160
- velinstyle transparency migrate [path|file] [--apply] [--write] [--policy file]
161
- Alias: transparency scan → doctor
162
-
163
- ${C.bold('Layout (0.8.0):')}
164
- velinstyle layout audit [path] Report flex/grid/responsive issues
165
- velinstyle layout suggest [path] Audit with fix suggestions
166
- velinstyle layout fix [path] Apply safe fixes (--dry-run default, --write)
167
-
168
- ${C.bold('Performance (0.9.0):')}
169
- velinstyle perf audit [path|file] Report CLS, lazy-load, script defer issues
170
- velinstyle perf suggest [path|file] Same as audit with fix hints
171
- velinstyle perf fix [path|file] Apply safe fixes (--write)
172
-
173
- ${C.bold('Tokens:')}
174
- velinstyle tokens build [--input <path>] [--output, -o <file>]
175
- velinstyle tokens validate [--input <path>]
176
-
177
- ${C.bold('Docs (0.9.0):')}
178
- velinstyle docs generate [--scope all|components|tokens|utilities|cli|rules|a11y|meta] [--out docs/generated]
179
- velinstyle meta [--out dist/velin-agent.json] [--llms-out dist/llms.txt] [--base-url URL]
180
- velinstyle meta page <file.html> [--write] Merge curated meta (goals/intent) on --write
181
-
182
- ${C.bold('Search:')}
183
- velinstyle search index [--out dist/search-index.json] [--extra-html dir1,dir2]
184
-
185
- ${C.bold('Skills:')}
186
- velinstyle skills list [--capability review] [--status beta] [--json]
187
- velinstyle skills show <skill-id> [--human]
188
- velinstyle skills install <skill-id|pack|bundle:id|project:id> [--target .cursor/skills]
189
- velinstyle skills run <skill-id> [--json]
190
- velinstyle skills validate
191
- velinstyle skills packs|bundles|templates|projects|graphs
192
-
193
- ${C.bold('Workflow:')}
194
- velinstyle workflow <graph-id|project:id> [--json]
195
-
196
- ${C.bold('Scan options:')}
197
- --fix Auto-fix safe issues (writes files)
198
- --fix-dry-run Show files that would be auto-fixed; no write
199
- --fix-lang <code> Default lang for a11y/html-lang fix (default: de)
200
- --severity <level> Minimum severity: error, warning, info
201
- --only <category> Filter: security, pii, a11y, css, wc, perf
202
-
203
- ${C.bold('Prefix options (velinstyle prefix):')}
204
- --write Write files (default is dry-run)
205
- --bootstrap-display Map Bootstrap d-* display utilities to Velin names
206
- --map <file> JSON class map (merged after auto map; see below)
207
-
208
- Prefix also loads ${C.dim('velinstyle-prefix-map.json')} from the migration root (the
209
- directory you pass, or the parent of a single file) if that file exists. ${C.dim('--map')}
210
- merges on top and overrides duplicate keys.
211
-
212
- ${C.bold('Build options:')}
213
- --output, -o <path> Output file path (default: ./velinstyle-custom.css)
214
- --minify Minify output
215
- --preset lite Marketing subset layers (tokens+reset+base+layout+components+utilities)
216
-
217
- ${C.bold('General:')}
218
- --help, -h Show this help
219
- `);
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);
220
165
  }
221
166
 
222
167
  // ── Init ─────────────────────────────────────────────────────────────────────
223
168
 
224
- function init() {
169
+ async function init() {
170
+ const notes = [];
225
171
  const configPath = resolve('velinstyle.config.js');
226
172
  if (existsSync(configPath)) {
227
- console.log('velinstyle.config.js already exists.');
173
+ notes.push({ tone: 'info', message: 'velinstyle.config.js already exists' });
228
174
  } else {
229
175
  const config = `// VelinStyle Configuration
230
176
  export default {
@@ -252,7 +198,7 @@ export default {
252
198
  };
253
199
  `;
254
200
  writeFileSync(configPath, config);
255
- console.log(C.green('Created velinstyle.config.js'));
201
+ notes.push({ tone: 'ok', message: 'Created velinstyle.config.js' });
256
202
  }
257
203
 
258
204
  const vendorDir = resolve('vendor', 'velinstyle');
@@ -271,9 +217,9 @@ export default {
271
217
  if (statSync(src).isFile()) writeFileSync(join(vendorDir, 'themes', name), readFileSync(src));
272
218
  }
273
219
  }
274
- console.log(C.green(`Copied dist assets → ${vendorDir}`));
220
+ notes.push({ tone: 'ok', message: `Copied dist assets → ${vendorDir}` });
275
221
  } else {
276
- 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' });
277
223
  }
278
224
 
279
225
  const starter = resolve('index.velin-starter.html');
@@ -301,15 +247,31 @@ export default {
301
247
  `,
302
248
  'utf-8',
303
249
  );
304
- console.log(C.green('Created index.velin-starter.html'));
305
- }
306
-
307
- 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
+ });
308
263
  }
309
264
 
310
265
  // ── Build ────────────────────────────────────────────────────────────────────
311
266
 
312
267
  async function build() {
268
+ if (hasFlag('--production')) {
269
+ const { runProduction } = await import('./production/run.js');
270
+ const rest = args.slice(1).filter((a) => a !== '--production');
271
+ await runProduction({ args: rest, helpers: { getArg, hasFlag, C } });
272
+ return;
273
+ }
274
+
313
275
  const configPath = resolve('velinstyle.config.js');
314
276
  let config;
315
277
 
@@ -370,42 +332,56 @@ async function build() {
370
332
  shell: true,
371
333
  });
372
334
  bundled = true;
373
- console.log(C.green(`Built: ${output}`) + ` (layers: ${selectedLayers.join(', ')}${config.theme ? ', theme: ' + config.theme : ''})`);
374
335
  } catch {
375
336
  writeFileSync(outAbs, css);
376
- console.log(C.yellow(`Built (unbundled): ${output}`));
377
- console.log(C.yellow(' Install lightningcss-cli in the VelinStyle package (or run build from the framework repo) for minified output.'));
337
+ bundled = false;
378
338
  } finally {
379
339
  try { unlinkSync(tmpPath); } catch { /* ignore */ }
380
340
  }
381
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 });
382
348
  if (preset === 'lite' && existsSync(outAbs)) {
383
349
  const liteBytes = statSync(outAbs).size;
384
350
  const fullMin = join(PKG_ROOT, 'dist', 'velinstyle.min.css');
351
+ items.push({ label: 'Size', value: `${(liteBytes / 1024).toFixed(1)} KB` });
385
352
  if (existsSync(fullMin)) {
386
- const fullBytes = statSync(fullMin).size;
387
- const liteKb = (liteBytes / 1024).toFixed(1);
388
- const fullKb = (fullBytes / 1024).toFixed(1);
389
- console.log(` Size: lite ${liteKb} KB vs dist/velinstyle.min.css ${fullKb} KB`);
390
- if (!bundled || liteBytes >= fullBytes) {
391
- console.log(C.yellow(` Warning: lite output is not smaller than the full min CSS${bundled ? '' : ' (unbundled)'}.`));
392
- }
353
+ items.push({ label: 'Full min', value: `${(statSync(fullMin).size / 1024).toFixed(1)} KB` });
393
354
  }
394
355
  }
395
356
 
357
+ const notifications = [];
358
+ if (!bundled) {
359
+ notifications.push({ tone: 'warn', message: 'Unbundled output — install lightningcss-cli for minify' });
360
+ }
361
+
396
362
  if (config.scan?.enabled) {
397
- console.log(C.dim('\nRunning post-build scan...'));
398
363
  const { scan } = await import('./scanner.js');
399
364
  scan(resolve('.'), config.scan);
365
+ notifications.push({ tone: 'info', message: 'Post-build scan ran (config.scan.enabled)' });
400
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
+ });
401
377
  }
402
378
 
403
379
  // ── Themes ───────────────────────────────────────────────────────────────────
404
380
 
405
- function themes() {
381
+ async function themes() {
406
382
  const themesDir = join(PKG_ROOT, 'src', 'themes');
407
383
  if (!existsSync(themesDir)) {
408
- console.log('No themes directory found.');
384
+ await emitExperience('themes', { status: 'WARNING', message: 'No themes directory found', exitCode: 1 });
409
385
  return;
410
386
  }
411
387
 
@@ -413,31 +389,44 @@ function themes() {
413
389
  .filter(f => f.endsWith('.css'))
414
390
  .map(f => f.replace('.css', ''));
415
391
 
416
- console.log(`\n ${C.bold('Available VelinStyle Themes:')}\n`);
417
- available.forEach(t => console.log(` - ${t}`));
418
- console.log(`\n Total: ${available.length} themes`);
419
- console.log(' Usage: Set theme in velinstyle.config.js or load via:');
420
- 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
+ });
421
403
  }
422
404
 
423
405
  // ── Add Component ────────────────────────────────────────────────────────────
424
406
 
425
- function add() {
407
+ async function add() {
426
408
  const name = args[1];
427
409
  if (!name) {
428
- console.log('Usage: velinstyle add <component-name>');
429
- 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
+ });
430
416
  return;
431
417
  }
432
418
 
433
419
  const filePath = join(PKG_ROOT, 'src', 'components', `${name}.css`);
434
420
  if (!existsSync(filePath)) {
435
- console.log(C.red(`Component not found: ${name}`));
436
- console.log('Available:');
437
- readdirSync(join(PKG_ROOT, 'src', 'components'))
421
+ const available = readdirSync(join(PKG_ROOT, 'src', 'components'))
438
422
  .filter(f => f.endsWith('.css'))
439
- .map(f => f.replace('.css', ''))
440
- .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
+ });
441
430
  return;
442
431
  }
443
432
 
@@ -446,7 +435,11 @@ function add() {
446
435
  const content = readFileSync(filePath, 'utf-8');
447
436
  const outPath = join(outDir, `${name}.css`);
448
437
  writeFileSync(outPath, content);
449
- 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
+ });
450
443
  }
451
444
 
452
445
  // ── Icons ────────────────────────────────────────────────────────────────────
@@ -455,47 +448,64 @@ async function icons() {
455
448
  const sub = args[1];
456
449
 
457
450
  if (!sub || sub === 'list') {
458
- iconsListCmd();
451
+ await iconsListCmd();
459
452
  } else if (sub === 'add') {
460
453
  await iconsAddCmd();
461
454
  } else if (sub === 'remove') {
462
- iconsRemoveCmd();
455
+ await iconsRemoveCmd();
463
456
  } else if (sub === 'build') {
464
- iconsBuildCmd();
457
+ await iconsBuildCmd();
465
458
  } else {
466
- console.log(`Unknown icons subcommand: ${sub}`);
467
- 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
+ });
468
465
  }
469
466
  }
470
467
 
471
- function iconsListCmd() {
468
+ async function iconsListCmd() {
472
469
  const providers = listProviders();
473
- console.log(`\n ${C.bold('Available Icon Providers:')}\n`);
474
- providers.forEach(p => {
475
- console.log(` ${C.cyan(p.key.padEnd(14))} ${p.name} -- ${p.description}`);
476
- console.log(` ${' '.repeat(14)} License: ${p.license} | ${C.dim(p.homepage)}`);
477
- if (p.variants.length) console.log(` ${' '.repeat(14)} Variants: ${p.variants.join(', ')}`);
478
- console.log();
479
- });
480
-
481
470
  const svgDir = join(PKG_ROOT, 'icons', 'svg');
482
- if (existsSync(svgDir)) {
483
- const installed = readdirSync(svgDir).filter(f => f.endsWith('.svg'));
484
- console.log(` ${C.bold('Installed icons:')} ${installed.length} in icons/svg/\n`);
485
- }
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
+ });
486
487
  }
487
488
 
488
489
  async function iconsAddCmd() {
489
490
  const provider = args[2];
490
491
  if (!provider) {
491
- console.log('Usage: velinstyle icons add <provider> [--icons name1,name2] [--variant outline]');
492
- 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
+ });
493
499
  return;
494
500
  }
495
501
 
496
502
  if (!PROVIDERS[provider]) {
497
- console.log(C.red(`Unknown provider: ${provider}`));
498
- 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
+ });
499
509
  return;
500
510
  }
501
511
 
@@ -503,8 +513,12 @@ async function iconsAddCmd() {
503
513
  const variant = getArg('--variant');
504
514
 
505
515
  if (!iconsList) {
506
- console.log(C.yellow('Please specify icons to add with --icons name1,name2,...'));
507
- 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
+ });
508
522
  return;
509
523
  }
510
524
 
@@ -514,10 +528,11 @@ async function iconsAddCmd() {
514
528
 
515
529
  let added = 0;
516
530
  let failed = 0;
531
+ const rows = [];
517
532
 
518
533
  for (const name of names) {
519
534
  const url = getProviderUrl(provider, name, variant);
520
- if (!url) { failed++; continue; }
535
+ if (!url) { failed++; rows.push([name, 'FAIL', 'no url']); continue; }
521
536
 
522
537
  try {
523
538
  const res = await fetch(url);
@@ -527,56 +542,83 @@ async function iconsAddCmd() {
527
542
 
528
543
  const filename = `${provider}-${name}.svg`;
529
544
  writeFileSync(join(svgDir, filename), svg);
530
- console.log(C.green(` + ${filename}`));
545
+ rows.push([name, 'OK', filename]);
531
546
  added++;
532
547
  } catch (e) {
533
- console.log(C.red(` x ${name}: ${e.message}`));
548
+ rows.push([name, 'FAIL', e.message]);
534
549
  failed++;
535
550
  }
536
551
  }
537
552
 
538
- console.log(`\n Added: ${added}, Failed: ${failed}`);
539
- if (added > 0) {
540
- console.log(C.dim(' Run "velinstyle icons build" to rebuild the sprite.'));
541
- }
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
+ });
542
561
  }
543
562
 
544
- function iconsRemoveCmd() {
563
+ async function iconsRemoveCmd() {
545
564
  const provider = args[2];
546
565
  if (!provider) {
547
- 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
+ });
548
571
  return;
549
572
  }
550
573
 
551
574
  const svgDir = join(PKG_ROOT, 'icons', 'svg');
552
- if (!existsSync(svgDir)) return;
575
+ if (!existsSync(svgDir)) {
576
+ await emitExperience('icons', { status: 'WARNING', message: 'No icons/svg directory' });
577
+ return;
578
+ }
553
579
 
554
580
  const files = readdirSync(svgDir).filter(f => f.startsWith(`${provider}-`) && f.endsWith('.svg'));
555
581
  if (files.length === 0) {
556
- 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
+ });
557
586
  return;
558
587
  }
559
588
 
560
- files.forEach(f => {
561
- unlinkSync(join(svgDir, f));
562
- 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' }],
563
595
  });
564
- console.log(`\n Removed ${files.length} icons from "${provider}".`);
565
- console.log(C.dim(' Run "velinstyle icons build" to rebuild the sprite.'));
566
596
  }
567
597
 
568
- function iconsBuildCmd() {
598
+ async function iconsBuildCmd() {
569
599
  const buildScript = join(PKG_ROOT, 'icons', 'build-sprite.js');
570
600
  if (!existsSync(buildScript)) {
571
- 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
+ });
572
606
  return;
573
607
  }
574
608
 
575
609
  try {
576
610
  execSync(`node "${buildScript}"`, { stdio: 'inherit', cwd: PKG_ROOT });
577
- 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
+ });
578
616
  } catch {
579
- 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
+ });
580
622
  }
581
623
  }
582
624
 
@@ -587,13 +629,15 @@ async function blueprintCmd() {
587
629
  const strict = hasFlag('--strict');
588
630
  const { listBlueprints, emitBlueprint, validateAllBlueprints } = await import('./blueprint.js');
589
631
  if (sub === '--help' || sub === '-h') {
590
- console.log(`Usage: velinstyle blueprint [list|<name>] [--output|-o <file>] [--strict]
591
-
592
- list List blueprint ids
593
- list --strict Validate all blueprints against CSS
594
- <name> Emit HTML (stdout or -o file)
595
- <name> --strict Fail if classes missing from CSS
596
- `);
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
+ });
597
641
  return;
598
642
  }
599
643
  if (!sub || sub === 'list') {
@@ -601,30 +645,48 @@ async function blueprintCmd() {
601
645
  const results = validateAllBlueprints();
602
646
  const bad = results.filter((r) => !r.ok);
603
647
  if (bad.length) {
604
- for (const r of bad) {
605
- console.log(C.red(`${r.id}: ${r.missing?.join(', ') || r.error}`));
606
- }
607
- 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;
608
659
  }
609
- 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
+ });
610
665
  return;
611
666
  }
612
- console.log(`\n ${C.bold('Available blueprints:')}\n`);
613
- listBlueprints().forEach((b) => console.log(` - ${b}`));
614
- 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
+ });
615
673
  return;
616
674
  }
617
675
  const out = getArg('--output', '-o');
618
676
  const r = emitBlueprint(sub, { output: out || null, strict });
619
677
  if (!r.ok) {
620
- console.log(C.red(r.error));
621
- process.exit(1);
678
+ await emitExperience('blueprint', { status: 'ERROR', message: r.error, exitCode: 1 });
679
+ return;
622
680
  }
623
681
  if (r.text) {
682
+ // Preserve raw HTML for piping when no -o
624
683
  console.log(r.text);
625
- } else {
626
- console.log(C.green(`Wrote blueprint to ${r.path}`));
684
+ return;
627
685
  }
686
+ await emitExperience('blueprint', {
687
+ message: `Wrote blueprint to ${r.path}`,
688
+ items: [{ label: 'Path', value: r.path }, { label: 'Id', value: sub }],
689
+ });
628
690
  }
629
691
 
630
692
  async function serveCmd() {
@@ -633,50 +695,237 @@ async function serveCmd() {
633
695
  const { serveStatic } = await import('./serve.js');
634
696
  const result = await serveStatic({ dir: resolve(raw), port });
635
697
  if (!result.ok) {
636
- console.log(C.red(result.error));
637
- process.exit(1);
698
+ await emitExperience('serve', { status: 'ERROR', message: result.error, exitCode: 1 });
699
+ return;
638
700
  }
639
- console.log(C.green(`Serving ${result.dir}`));
640
- console.log(` ${result.url}`);
641
- 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
+ });
642
710
  }
643
711
 
644
712
  async function doctorCmd() {
645
- const { runDoctor, formatDoctorReport } = await import('./doctor.js');
713
+ const exp = await ensureExperience();
646
714
  const raw = args[1] && !args[1].startsWith('-') ? resolve(args[1]) : process.cwd();
647
715
  let cwd = raw;
648
716
  try {
649
717
  if (existsSync(raw) && statSync(raw).isFile()) cwd = dirname(raw);
650
718
  } catch { /* use raw */ }
651
- const report = await runDoctor({ cwd, pkgRoot: PKG_ROOT });
652
- console.log(formatDoctorReport(report));
653
- 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);
654
885
  }
655
886
 
656
887
  async function createCmd() {
657
888
  const kind = args[1];
658
889
  if (!kind || kind === '--help' || kind === '-h') {
659
- console.log(`Usage: velinstyle create <landing|dashboard|docs|auth> [dir] [--theme earth] [--no-copy]`);
660
- 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;
661
896
  }
662
897
  const { CREATE_KINDS, createProject } = await import('./create.js');
663
898
  if (!CREATE_KINDS.includes(kind)) {
664
- console.log(C.red(`Unknown kind "${kind}". Use: ${CREATE_KINDS.join('|')}`));
665
- 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;
666
906
  }
667
907
  const dir = args[2] && !args[2].startsWith('-') ? args[2] : kind;
668
908
  const theme = getArg('--theme') || undefined;
669
909
  const copyAssets = !hasFlag('--no-copy');
670
910
  const r = createProject({ kind, dir: resolve(dir), pkgRoot: PKG_ROOT, theme, copyAssets });
671
911
  if (!r.ok) {
672
- console.log(C.red(r.error));
673
- process.exit(1);
912
+ await emitExperience('create', { status: 'ERROR', message: r.error, exitCode: 1 });
913
+ return;
674
914
  }
675
- console.log(C.green(`Created ${kind} at ${r.dir}`));
676
- console.log(C.dim(` index: ${r.indexPath}`));
677
- if (r.vendorRel) console.log(C.dim(` vendor: ${r.vendorRel}/`));
678
- if (r.vendorCopied?.length) console.log(C.dim(` copied: ${r.vendorCopied.join(', ')}`));
679
- 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
+ });
680
929
  }
681
930
 
682
931
  async function tokensBuildCmd() {
@@ -685,12 +934,20 @@ async function tokensBuildCmd() {
685
934
  const { buildTokensFromJson } = await import('./tokens-build.js');
686
935
  const css = buildTokensFromJson(inputPath);
687
936
  if (!css) {
688
- 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
+ });
689
942
  return;
690
943
  }
691
944
  if (outPath) {
692
945
  writeFileSync(resolve(outPath), css, 'utf-8');
693
- 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
+ });
694
951
  } else {
695
952
  console.log(css);
696
953
  }
@@ -701,12 +958,20 @@ async function tokensValidateCmd() {
701
958
  const { validateTokensJson } = await import('./tokens-validate.js');
702
959
  const result = validateTokensJson(resolve(input));
703
960
  if (result.ok) {
704
- console.log(C.green(`Valid: ${resolve(input)}`));
705
- 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;
706
967
  }
707
- console.log(C.red(`Invalid: ${resolve(input)}`));
708
- result.errors.forEach((e) => console.log(` - ${e}`));
709
- 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
+ });
710
975
  }
711
976
 
712
977
  async function tokensCmd() {
@@ -716,7 +981,11 @@ async function tokensCmd() {
716
981
  } else if (sub === 'validate') {
717
982
  await tokensValidateCmd();
718
983
  } else {
719
- 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
+ });
720
989
  }
721
990
  }
722
991
 
@@ -737,13 +1006,21 @@ async function perfCmd() {
737
1006
  return;
738
1007
  }
739
1008
  if (result.changes.length === 0) {
740
- 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
+ });
741
1014
  return;
742
1015
  }
743
- for (const c of result.changes) {
744
- console.log(`${dryRun ? '[dry-run] ' : ''}${c.file}`);
745
- c.changes.forEach((ch) => console.log(C.dim(` - ${ch}`)));
746
- }
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
+ });
747
1024
  return;
748
1025
  }
749
1026
 
@@ -752,7 +1029,49 @@ async function perfCmd() {
752
1029
  console.log(JSON.stringify({ files, issues }, null, 2));
753
1030
  return;
754
1031
  }
755
- 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
+ });
756
1075
  process.exit(issues.some((x) => x.severity === 'error') ? 1 : 0);
757
1076
  }
758
1077
 
@@ -764,8 +1083,8 @@ async function prefixCmd() {
764
1083
  const st = statSync(targetPath);
765
1084
  if (st.isFile()) migrationRoot = dirname(targetPath);
766
1085
  } catch {
767
- console.log(C.red(`Not found: ${targetPath}`));
768
- process.exit(1);
1086
+ await emitExperience('prefix', { status: 'ERROR', message: `Not found: ${targetPath}`, exitCode: 1 });
1087
+ return;
769
1088
  }
770
1089
 
771
1090
  const mapArg = getArg('--map');
@@ -773,32 +1092,175 @@ async function prefixCmd() {
773
1092
  const bootstrapDisplay = hasFlag('--bootstrap-display');
774
1093
 
775
1094
  try {
776
- const { buildExplicitPrefixMap, prefixCommandMain } = await import('./prefix-classes.js');
1095
+ const { buildExplicitPrefixMap, runPrefixCommand } = await import('./prefix-classes.js');
777
1096
  const explicitMap = buildExplicitPrefixMap(migrationRoot, mapArg ? resolve(mapArg) : null);
778
- prefixCommandMain(targetPath, {
1097
+ const { filesTouched, replacements, paths } = runPrefixCommand(targetPath, {
779
1098
  write,
780
1099
  bootstrapAliases: bootstrapDisplay,
781
1100
  pkgRoot: PKG_ROOT,
782
1101
  explicitMap,
783
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
+ });
784
1124
  } catch (e) {
785
- console.log(C.red(/** @type {Error} */ (e).message));
786
- process.exit(1);
1125
+ await emitExperience('prefix', {
1126
+ status: 'ERROR',
1127
+ message: /** @type {Error} */ (e).message,
1128
+ exitCode: 1,
1129
+ });
787
1130
  }
788
1131
  }
789
1132
 
1133
+ async function atelierCmd() {
1134
+ const sub = args[1];
1135
+ const {
1136
+ listAtelierEntries,
1137
+ pullAtelier,
1138
+ } = await import('./atelier.js');
1139
+
1140
+ if (!sub || sub === '--help' || sub === '-h') {
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
+ });
1152
+ return;
1153
+ }
1154
+
1155
+ if (sub === 'list') {
1156
+ const r = listAtelierEntries();
1157
+ if (!r.ok) {
1158
+ await emitExperience('atelier', { status: 'ERROR', message: r.error, exitCode: 1 });
1159
+ return;
1160
+ }
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
+ });
1170
+ return;
1171
+ }
1172
+
1173
+ const out = getArg('--output', '-o');
1174
+ const format = getArg('--format') || 'html';
1175
+ const from = getArg('--from') || process.env.VELINSTYLE_ATELIER_ROOT || null;
1176
+ const baseUrl = getArg('--base-url');
1177
+ const rewriteVendor = getArg('--rewrite-vendor');
1178
+
1179
+ const r = await pullAtelier(sub, {
1180
+ output: out || undefined,
1181
+ format,
1182
+ from: from || undefined,
1183
+ baseUrl: baseUrl || undefined,
1184
+ rewriteVendor: rewriteVendor || undefined,
1185
+ });
1186
+ if (!r.ok) {
1187
+ await emitExperience('atelier', { status: 'ERROR', message: r.error, exitCode: 1 });
1188
+ return;
1189
+ }
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
+ });
1202
+ }
1203
+
790
1204
  async function scaffoldCmd() {
791
1205
  const sub = args[1];
792
1206
  const { scaffoldFromPrompt, listIntents } = await import('./scaffold.js');
793
1207
  if (sub === 'list-intents') {
794
- console.log(`\n ${C.bold('Scaffold intents:')}\n`);
795
- for (const i of listIntents()) {
796
- console.log(` ${C.cyan(i.id)} — ${i.blueprints.join(' + ')}`);
797
- console.log(C.dim(` keywords: ${i.keywords.slice(0, 5).join(', ')}…`));
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
+ });
1221
+ return;
1222
+ }
1223
+
1224
+ const atelierList = getArg('--atelier');
1225
+ if (atelierList) {
1226
+ const { composeAtelierPage } = await import('./atelier.js');
1227
+ const out = getArg('--output', '-o') || resolve('velin-atelier/compose.html');
1228
+ const from = getArg('--from') || process.env.VELINSTYLE_ATELIER_ROOT || null;
1229
+ const baseUrl = getArg('--base-url');
1230
+ const r = await composeAtelierPage(atelierList, {
1231
+ output: out,
1232
+ from: from || undefined,
1233
+ baseUrl: baseUrl || undefined,
1234
+ });
1235
+ if (!r.ok) {
1236
+ await emitExperience('scaffold', { status: 'ERROR', message: r.error, exitCode: 1 });
1237
+ return;
1238
+ }
1239
+ if (hasFlag('--json')) {
1240
+ console.log(JSON.stringify({
1241
+ ok: true,
1242
+ mode: r.mode,
1243
+ path: r.path,
1244
+ entries: r.entries,
1245
+ planSections: r.planSections,
1246
+ }, null, 2));
1247
+ return;
798
1248
  }
799
- console.log('');
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
+ });
800
1261
  return;
801
1262
  }
1263
+
802
1264
  const promptParts = [];
803
1265
  let i = 1;
804
1266
  while (i < args.length) {
@@ -809,13 +1271,21 @@ async function scaffoldCmd() {
809
1271
  }
810
1272
  const prompt = promptParts.join(' ').trim() || sub;
811
1273
  if (!prompt || prompt === 'list-intents') {
812
- console.log('Usage: velinstyle scaffold "<description>" [-o file.html] [--json]');
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
+ });
813
1283
  return;
814
1284
  }
815
1285
  const r = scaffoldFromPrompt(prompt);
816
1286
  if (!r.ok) {
817
- console.log(C.red(r.error));
818
- process.exit(1);
1287
+ await emitExperience('scaffold', { status: 'ERROR', message: r.error, exitCode: 1 });
1288
+ return;
819
1289
  }
820
1290
  const out = getArg('--output', '-o');
821
1291
  const asJson = hasFlag('--json');
@@ -823,19 +1293,49 @@ async function scaffoldCmd() {
823
1293
  console.log(JSON.stringify(r, null, 2));
824
1294
  } else if (out) {
825
1295
  writeFileSync(resolve(out), r.html, 'utf-8');
826
- console.log(C.green(`Wrote ${resolve(out)} (intent: ${r.intent}, ${r.confidence}${r.mode ? `, mode: ${r.mode}` : ''})`));
827
- if (r.review?.gate) {
828
- console.log(C.dim(` review gate: ${r.review.gate} (promptScore ${r.review.promptScore})`));
829
- }
830
- if (r.responsiveHints?.length) {
831
- console.log(C.yellow(` ${r.responsiveHints.length} layout hint(s) run: velinstyle layout suggest ${out}`));
832
- }
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
+ });
833
1311
  } else {
834
1312
  console.log(r.html);
835
1313
  }
836
1314
  }
837
1315
 
838
1316
  async function planCmd() {
1317
+ const atelierList = getArg('--atelier');
1318
+ if (atelierList) {
1319
+ const { planFromAtelierList } = await import('./atelier.js');
1320
+ const payload = planFromAtelierList(atelierList, { prompt: args.slice(1).filter((a) => !a.startsWith('-')).join(' ') });
1321
+ if (!payload.ok) {
1322
+ await emitExperience('plan', { status: 'ERROR', message: payload.error, exitCode: 1 });
1323
+ return;
1324
+ }
1325
+ const out = getArg('--output', '-o');
1326
+ if (out) {
1327
+ writeFileSync(resolve(out), JSON.stringify(payload, null, 2), 'utf-8');
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
+ });
1333
+ return;
1334
+ }
1335
+ console.log(JSON.stringify(payload, null, 2));
1336
+ return;
1337
+ }
1338
+
839
1339
  const promptParts = [];
840
1340
  let i = 1;
841
1341
  while (i < args.length) {
@@ -846,7 +1346,12 @@ async function planCmd() {
846
1346
  }
847
1347
  const prompt = promptParts.join(' ').trim();
848
1348
  if (!prompt) {
849
- console.log('Usage: velinstyle plan "<description>" [--json] [-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
+ });
850
1355
  return;
851
1356
  }
852
1357
  const { analyzePrompt, buildPlan } = await import('./prompt-engine.js');
@@ -856,7 +1361,13 @@ async function planCmd() {
856
1361
  const out = getArg('--output', '-o');
857
1362
  if (out) {
858
1363
  writeFileSync(resolve(out), JSON.stringify(payload, null, 2), 'utf-8');
859
- 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
+ });
860
1371
  return;
861
1372
  }
862
1373
  console.log(JSON.stringify(payload, null, 2));
@@ -885,21 +1396,51 @@ async function reviewCmd() {
885
1396
  const report = reviewHtml(html, { plan, prompt, profile: profile || undefined });
886
1397
  if (hasFlag('--json')) {
887
1398
  console.log(JSON.stringify(report, null, 2));
888
- } else {
889
- console.log(`\n ${C.bold('Review gate:')} ${report.gate} ${C.dim(`profile ${report.profile} · promptScore ${report.promptScore}`)}`);
890
- console.log(C.dim(` design ${report.scores.design} · a11y ${report.scores.accessibility} · seo ${report.scores.seo} · perf ${report.scores.performance} · conversion ${report.scores.conversion}`));
891
- if (report.issues.length) {
892
- console.log(`\n ${C.bold('Issues:')}`);
893
- for (const issue of report.issues) {
894
- const color = issue.severity === 'error' ? C.red : C.yellow;
895
- console.log(color(` [${issue.severity}] ${issue.code}: ${issue.message}`));
896
- console.log(C.dim(` fix: ${issue.fix}`));
897
- }
898
- } else {
899
- console.log(C.green('\n No issues.'));
900
- }
901
- console.log('');
1399
+ process.exit(report.gate === 'fail' ? 1 : 0);
1400
+ return;
902
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
+ });
903
1444
  process.exit(report.gate === 'fail' ? 1 : 0);
904
1445
  }
905
1446
 
@@ -916,21 +1457,29 @@ async function layoutCmd() {
916
1457
  if (sub === 'fix') {
917
1458
  const result = fixPath(targetPath, { write, dryRun });
918
1459
  if (!result.ok) {
919
- console.log(C.red(result.error || 'Fix failed'));
920
- process.exit(1);
1460
+ await emitExperience('layout', { status: 'ERROR', message: result.error || 'Fix failed', exitCode: 1 });
1461
+ return;
921
1462
  }
922
1463
  if (asJson) {
923
1464
  console.log(JSON.stringify(result, null, 2));
924
1465
  return;
925
1466
  }
926
1467
  if (result.changes.length === 0) {
927
- 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
+ });
928
1473
  return;
929
1474
  }
930
- for (const c of result.changes) {
931
- console.log(`${dryRun ? '[dry-run] ' : ''}${c.file}`);
932
- c.changes.forEach((ch) => console.log(C.dim(` - ${ch}`)));
933
- }
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
+ });
934
1483
  return;
935
1484
  }
936
1485
 
@@ -942,17 +1491,48 @@ async function layoutCmd() {
942
1491
  return;
943
1492
  }
944
1493
 
945
- console.log(formatTextReport(outIssues, files));
946
- if (sub === 'suggest' && issues.length) {
947
- console.log(C.bold('Suggested fixes:'));
948
- for (const i of outIssues) {
949
- console.log(` [${i.id}] ${i.fix}`);
950
- if (i.responsive?.mobile) {
951
- console.log(C.dim(` mobile: ${i.responsive.mobile}`));
952
- }
953
- }
954
- console.log('');
955
- }
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
+ });
956
1536
  process.exit(issues.some((x) => x.severity === 'error') ? 1 : 0);
957
1537
  }
958
1538
 
@@ -964,18 +1544,71 @@ async function docsCmd() {
964
1544
  const { generateDocs } = await import('./docs-generate.js');
965
1545
  const result = await generateDocs({ scope, outDir: resolve(out), searchIndex: true });
966
1546
  if (!result.ok) {
967
- console.log(C.red(result.error));
968
- process.exit(1);
1547
+ await emitExperience('docs', { status: 'ERROR', message: result.error, exitCode: 1 });
1548
+ return;
969
1549
  }
970
- 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
+ });
971
1558
  return;
972
1559
  }
973
- 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
+ });
974
1565
  }
975
1566
 
976
1567
  async function metaCmd() {
977
- const { metaMain } = await import('./meta.js');
978
- 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
+ });
979
1612
  }
980
1613
 
981
1614
  async function searchCmd() {
@@ -988,42 +1621,280 @@ async function searchCmd() {
988
1621
  outFile: resolve(out),
989
1622
  extraHtmlDirs: extra ? extra.split(',').map((p) => resolve(p.trim())) : [],
990
1623
  });
991
- 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
+ });
992
1629
  return;
993
1630
  }
994
- 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
+ });
995
1636
  }
996
1637
 
997
1638
  async function skillsCmd() {
998
- const { skillsCommand } = await import('./skills.js');
999
- 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
+ });
1000
1810
  }
1001
1811
 
1002
1812
  async function workflowCmd() {
1003
- const { workflowCommand } = await import('./workflow.js');
1004
- 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
+ });
1005
1848
  }
1006
1849
 
1007
1850
  async function wcCmd() {
1008
1851
  const sub = args[1];
1009
1852
  if (sub === 'api' || sub === '--help' || sub === '-h' || !sub) {
1010
1853
  if (sub !== 'api') {
1011
- console.log(`Usage: velinstyle wc api <tag>\nExample: velinstyle wc api velin-toast`);
1012
- 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;
1013
1861
  }
1014
1862
  const tag = args[2];
1015
- const { describeWcApi, formatWcApi } = await import('./wc-api.js');
1863
+ const { describeWcApi } = await import('./wc-api.js');
1016
1864
  const report = describeWcApi(PKG_ROOT, tag);
1017
1865
  if (!report.ok) {
1018
- console.error(report.error);
1019
- process.exit(1);
1866
+ await emitExperience('wc', { status: 'ERROR', message: report.error, exitCode: 1 });
1867
+ return;
1020
1868
  }
1021
- if (hasFlag('--json')) console.log(JSON.stringify(report, null, 2));
1022
- 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
+ });
1023
1891
  return;
1024
1892
  }
1025
- console.error(`Unknown wc subcommand: ${sub}. Use: velinstyle wc api <tag>`);
1026
- 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
+ });
1027
1898
  }
1028
1899
 
1029
1900
  async function scanCmd() {
@@ -1038,15 +1909,81 @@ async function scanCmd() {
1038
1909
  }
1039
1910
  const severity = getArg('--severity') || 'warning';
1040
1911
  const only = getArg('--only');
1912
+ const machine = format === 'json' || format === 'sarif' || hasFlag('--json') || hasFlag('--sarif');
1041
1913
 
1042
1914
  const { scan } = await import('./scanner.js');
1043
- const exitCode = scan(targetPath, {
1044
- severity,
1045
- fix,
1046
- fixDryRun,
1047
- fixLang: fixLang || undefined,
1048
- format,
1049
- 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
+ },
1050
1987
  });
1051
1988
  process.exit(exitCode);
1052
1989
  }
@@ -1147,6 +2084,40 @@ async function checkCmd() {
1147
2084
  steps.push({ step: 'transparency', ok: true, skipped: true, error: String(err.message || err) });
1148
2085
  }
1149
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
+
1150
2121
  const payload = {
1151
2122
  ok: failed === 0,
1152
2123
  failedSteps: failed,
@@ -1193,10 +2164,43 @@ async function checkCmd() {
1193
2164
  console.log(JSON.stringify(sarif, null, 2));
1194
2165
  } else if (asJson) {
1195
2166
  console.log(JSON.stringify(payload, null, 2));
1196
- } else if (failed) {
1197
- console.log(C.red(`\ncheck failed (${failed} step(s))`));
1198
2167
  } else {
1199
- 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
+ });
1200
2204
  }
1201
2205
 
1202
2206
  process.exit(failed ? 1 : 0);
@@ -1205,8 +2209,9 @@ async function checkCmd() {
1205
2209
  function suggestCommand(unknown) {
1206
2210
  const names = [
1207
2211
  'init', 'build', 'themes', 'add', 'icons', 'blueprint', 'create', 'serve', 'doctor', 'check',
1208
- 'validate', 'tokens', 'scan', 'prefix', 'scaffold', 'plan', 'review', 'transparency', 'layout', 'perf',
1209
- 'docs', 'documentation', 'meta', 'search', 'skills', 'workflow', 'wc',
2212
+ 'validate', 'tokens', 'scan', 'prefix', 'scaffold', 'plan', 'review', 'transparency', 'motion', 'security', 'layout', 'perf',
2213
+ 'docs', 'documentation', 'meta', 'search', 'skills', 'workflow', 'wc', 'production', 'atelier',
2214
+ 'dashboard', 'replay',
1210
2215
  ];
1211
2216
  const q = String(unknown || '').toLowerCase();
1212
2217
  function editDistance(a, b) {
@@ -1252,25 +2257,43 @@ const ALIASES = {
1252
2257
  const resolvedCommand = ALIASES[command] || command;
1253
2258
 
1254
2259
  switch (resolvedCommand) {
1255
- case 'init': init(); break;
1256
- case 'build': build(); break;
1257
- case 'themes': themes(); break;
1258
- case 'add': add(); break;
1259
- case 'icons': icons(); break;
2260
+ case 'init': await init(); break;
2261
+ case 'build': await build(); break;
2262
+ case 'production': {
2263
+ const { runProduction } = await import('./production/run.js');
2264
+ await runProduction({ args: args.slice(1), helpers: { getArg, hasFlag, C } });
2265
+ break;
2266
+ }
2267
+ case 'themes': await themes(); break;
2268
+ case 'add': await add(); break;
2269
+ case 'icons': await icons(); break;
1260
2270
  case 'blueprint': await blueprintCmd(); break;
1261
2271
  case 'create': await createCmd(); break;
1262
2272
  case 'serve': await serveCmd(); break;
1263
2273
  case 'doctor': await doctorCmd(); break;
2274
+ case 'dashboard': await dashboardCmd(); break;
2275
+ case 'replay': await replayCmd(); break;
1264
2276
  case 'check': await checkCmd(); break;
1265
2277
  case 'tokens': await tokensCmd(); break;
1266
- case 'scan': scanCmd(); break;
2278
+ case 'scan': await scanCmd(); break;
1267
2279
  case 'prefix': await prefixCmd(); break;
1268
2280
  case 'scaffold': await scaffoldCmd(); break;
1269
2281
  case 'plan': await planCmd(); break;
2282
+ case 'atelier': await atelierCmd(); break;
1270
2283
  case 'review': await reviewCmd(); break;
1271
2284
  case 'transparency': {
1272
2285
  const { transparencyCmd } = await import('./transparency.js');
1273
- 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 });
1274
2297
  break;
1275
2298
  }
1276
2299
  case 'layout': await layoutCmd(); break;
@@ -1281,6 +2304,6 @@ switch (resolvedCommand) {
1281
2304
  case 'skills': await skillsCmd(); break;
1282
2305
  case 'workflow': await workflowCmd(); break;
1283
2306
  case 'wc': await wcCmd(); break;
1284
- case '--help': case '-h': case undefined: help(); break;
2307
+ case '--help': case '-h': case undefined: await help(); break;
1285
2308
  default: unknownCommand(command); break;
1286
2309
  }