@techninja/clearstack 0.3.37 → 0.3.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/check.js CHANGED
@@ -26,7 +26,7 @@ function findTypeConfigs(dir, runner) {
26
26
  }
27
27
  return configs.map((c) => ({
28
28
  key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
29
- run: () => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path}`, dir),
29
+ run: () => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path}`, dir).pass,
30
30
  }));
31
31
  }
32
32
 
@@ -41,24 +41,24 @@ export function buildChecks(dir, cfg, cmds) {
41
41
  const runner = detectRunner(dir);
42
42
  return [
43
43
  { key: 'es', name: 'ESLint', parent: 'lint',
44
- run: () => runCmd('ESLint', cmds.lint, dir, `${js()} files`) },
44
+ run: () => runCmd('ESLint', cmds.lint, dir, `${js()} files`).pass },
45
45
  { key: 'css', name: 'Stylelint', parent: 'lint',
46
- run: () => runCmd('Stylelint', cmds.stylelint, dir, `${css()} files`) },
46
+ run: () => runCmd('Stylelint', cmds.stylelint, dir, `${css()} files`).pass },
47
47
  { key: 'md', name: 'Markdown lint', parent: 'lint',
48
- run: () => runCmd('Markdown', cmds.mdlint, dir, `${md()} files`) },
48
+ run: () => runCmd('Markdown', cmds.mdlint, dir, `${md()} files`).pass },
49
49
  { key: 'prettier', name: 'Prettier', parent: 'format',
50
- run: () => runCmd('Prettier', cmds.prettier, dir, `${js()} files`) },
50
+ run: () => runCmd('Prettier', cmds.prettier, dir, `${js()} files`).pass },
51
51
  { key: 'code', name: `Code (max ${cfg.codeMax} lines)`,
52
- run: () => checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`, { exclude: cfg.testPattern }) },
52
+ run: () => checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`, { exclude: cfg.testPattern }).pass },
53
53
  { key: 'tests', name: `Tests (max ${cfg.testMax} lines)`,
54
- run: () => checkFileLines(dir, cfg.codeExt, cfg.testMax, cfg.ignore, `Tests (max ${cfg.testMax} lines)`, { include: cfg.testPattern }) },
54
+ run: () => checkFileLines(dir, cfg.codeExt, cfg.testMax, cfg.ignore, `Tests (max ${cfg.testMax} lines)`, { include: cfg.testPattern }).pass },
55
55
  { key: 'docs', name: `Docs (max ${cfg.docsMax} lines)`,
56
- run: () => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`) },
56
+ run: () => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`).pass },
57
57
  { key: 'imports', name: 'Import map aliases (no ../ imports)',
58
- run: () => checkImports(dir, cfg.ignore, 'Import map aliases (no ../ imports)') },
58
+ run: () => checkImports(dir, cfg.ignore, 'Import map aliases (no ../ imports)').pass },
59
59
  ...findTypeConfigs(dir, runner),
60
60
  { key: 'audit', name: 'Security audit',
61
- run: () => runCmd('Security audit', cmds.audit, dir) },
61
+ run: () => runCmd('Security audit', cmds.audit, dir).pass },
62
62
  ];
63
63
  }
64
64
 
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Built-in default OG image template — used when no project template exists.
3
+ * @module lib/og-default-template
4
+ */
5
+
6
+ /** @param {number} w @param {number} h */
7
+ export function builtinTemplate(w, h) {
8
+ return `<!DOCTYPE html>
9
+ <html><head><meta charset="utf-8">
10
+ <style>
11
+ :root {
12
+ --bg: {bg}; --surface: {surface}; --primary: {primary};
13
+ --accent: {accent}; --text: {text}; --text-muted: {textMuted};
14
+ }
15
+ * { margin: 0; padding: 0; box-sizing: border-box; }
16
+ body {
17
+ width: ${w}px; height: ${h}px;
18
+ display: flex; align-items: center; justify-content: center;
19
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
20
+ background: var(--bg); color: var(--text); overflow: hidden;
21
+ }
22
+ .card {
23
+ display: flex; width: 100%; height: 100%; padding: 60px;
24
+ flex-direction: column; justify-content: center; gap: 24px;
25
+ background: linear-gradient(135deg, var(--bg) 0%, var(--surface) 100%);
26
+ }
27
+ .card-with-image { flex-direction: row; align-items: center; }
28
+ .content { flex: 1; display: flex; flex-direction: column; gap: 20px; }
29
+ .title { font-size: 56px; font-weight: 800; line-height: 1.1; color: var(--primary); }
30
+ .description { font-size: 26px; line-height: 1.4; color: var(--text-muted); }
31
+ .logo { position: absolute; bottom: 40px; right: 60px; opacity: 0.7; height: 40px; }
32
+ .hero { width: 360px; height: 360px; border-radius: 24px; object-fit: cover; flex-shrink: 0; }
33
+ .badge { font-size: 18px; text-transform: uppercase; letter-spacing: 2px; color: var(--accent); font-weight: 600; }
34
+ .emoji { font-size: 80px; }
35
+ </style></head>
36
+ <body>
37
+ <div class="card {cardClass}">
38
+ <div class="content">
39
+ {badgeHtml}
40
+ {emojiHtml}
41
+ <div class="title">{title}</div>
42
+ <div class="description">{description}</div>
43
+ </div>
44
+ {imageHtml}
45
+ </div>
46
+ {logoHtml}
47
+ </body></html>`;
48
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * OG image template resolver — finds the right template for a route,
3
+ * renders it with interpolated data, ready for Playwright screenshot.
4
+ * @module lib/og-image-template
5
+ */
6
+
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+ import { interpolate } from './og-template.js';
10
+ import { builtinTemplate } from './og-default-template.js';
11
+
12
+ const WIDTH = 1200;
13
+ const HEIGHT = 630;
14
+
15
+ /**
16
+ * Resolve template HTML for a route.
17
+ * Priority: route-specific → project default → built-in.
18
+ * @param {string} projectDir
19
+ * @param {string} [templateName]
20
+ */
21
+ export function resolveTemplate(projectDir, templateName) {
22
+ if (templateName) {
23
+ const custom = resolve(projectDir, `src/og-templates/${templateName}.html`);
24
+ if (existsSync(custom)) return readFileSync(custom, 'utf-8');
25
+ }
26
+ const projectDefault = resolve(projectDir, 'src/og-templates/default.html');
27
+ if (existsSync(projectDefault)) return readFileSync(projectDefault, 'utf-8');
28
+ return builtinTemplate(WIDTH, HEIGHT);
29
+ }
30
+
31
+ /**
32
+ * Render a template with data context, inlining local assets as data URIs.
33
+ * @param {string} template
34
+ * @param {Record<string, unknown>} data
35
+ * @param {string} projectDir
36
+ */
37
+ export function renderTemplate(template, data, projectDir) {
38
+ let html = interpolate(template, data);
39
+ html = html.replace(/src="\/([^"]+)"/g, (_, p) => {
40
+ const abs = resolve(projectDir, 'src', p);
41
+ if (!existsSync(abs)) return `src=""`;
42
+ const ext = p.split('.').pop();
43
+ const mime = ext === 'svg' ? 'image/svg+xml' : `image/${ext}`;
44
+ return `src="data:${mime};base64,${readFileSync(abs).toString('base64')}"`;
45
+ });
46
+ return html;
47
+ }
48
+
49
+ /** Load CSS variables from a project's token file if available. */
50
+ export function loadTokens(projectDir) {
51
+ const paths = ['src/styles/tokens.css', 'src/styles/base.css', 'src/styles/global.css'];
52
+ for (const p of paths) {
53
+ const full = resolve(projectDir, p);
54
+ if (!existsSync(full)) continue;
55
+ const css = readFileSync(full, 'utf-8');
56
+ const vars = {};
57
+ for (const m of css.matchAll(/--([^:]+):\s*([^;]+)/g)) vars[m[1].trim()] = m[2].trim();
58
+ if (Object.keys(vars).length) return vars;
59
+ }
60
+ return {};
61
+ }
62
+
63
+ /**
64
+ * Build the full data context for template rendering.
65
+ * @param {{ title: string, description?: string, image?: string }} config
66
+ * @param {Record<string, unknown>} itemData
67
+ * @param {{ tokens?: Record<string, string>, logo?: string, siteName?: string }} site
68
+ */
69
+ export function buildContext(config, itemData, site) {
70
+ const t = site.tokens || {};
71
+ const title = interpolate(config.title, itemData);
72
+ const desc = config.description ? interpolate(config.description, itemData) : '';
73
+ const image = config.image ? interpolate(config.image, itemData) : '';
74
+ const item = itemData.item || {};
75
+ const emoji = item.emoji || itemData.emoji || '';
76
+
77
+ return {
78
+ ...itemData, ...item,
79
+ title: truncate(title, 60),
80
+ description: truncate(desc, 120),
81
+ bg: t['color-bg'] || '#0f172a',
82
+ surface: t['color-surface'] || '#1e293b',
83
+ primary: t['color-primary'] || '#818cf8',
84
+ accent: t['color-accent'] || '#34d399',
85
+ text: t['color-text'] || '#e2e8f0',
86
+ textMuted: t['color-text-muted'] || '#a8b8cc',
87
+ emoji, image,
88
+ variantsFormatted: formatNum(item.expected_variants),
89
+ uniqueFormatted: formatNum(item.estimated_unique_variants),
90
+ imageHtml: image ? `<img class="hero" src="${image}">` : '',
91
+ cardClass: image ? 'card-with-image' : '',
92
+ logoHtml: site.logo ? `<img class="logo" src="${site.logo}">` : '',
93
+ badgeHtml: site.siteName ? `<div class="badge">${site.siteName}</div>` : '',
94
+ emojiHtml: emoji ? `<div class="emoji">${emoji}</div>` : '',
95
+ };
96
+ }
97
+
98
+ /** Format large numbers with K/M suffix. */
99
+ function formatNum(n) {
100
+ if (!n) return '—';
101
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
102
+ if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
103
+ return String(n);
104
+ }
105
+
106
+ /** @param {string} str @param {number} max */
107
+ function truncate(str, max) {
108
+ return str.length > max ? str.slice(0, max - 1) + '…' : str;
109
+ }
110
+
111
+ export { WIDTH, HEIGHT };
@@ -65,9 +65,9 @@ export function injectMeta(html, meta) {
65
65
  * @returns {string}
66
66
  */
67
67
  export function interpolate(template, data) {
68
- return template.replace(/\{([^}]+)\}/g, (_, path) => {
68
+ return template.replace(/\{([\w.]+)\}/g, (match, path) => {
69
69
  const val = path.split('.').reduce((obj, key) => obj?.[key], data);
70
- return val !== null && val !== undefined ? String(val) : '';
70
+ return val !== null && val !== undefined ? String(val) : match;
71
71
  });
72
72
  }
73
73
 
package/lib/report.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Entropy report — runs the full spec pipeline, collects structured results.
3
+ * Uses the same check functions as `spec all` but in quiet mode for data.
4
+ * @module lib/report
5
+ */
6
+
7
+ import { runCmd, checkFileLines, checkImports } from './spec-utils.js';
8
+ import { loadConfig, buildCmds, detectRunner } from './spec-config.js';
9
+
10
+ /**
11
+ * Run full spec pipeline and return structured JSON results.
12
+ * @param {string} dir
13
+ * @returns {{ checks: object[], summary: object }}
14
+ */
15
+ export function collect(dir) {
16
+ const cfg = loadConfig(dir);
17
+ const cmds = buildCmds(dir);
18
+ const runner = detectRunner(dir);
19
+ const q = { quiet: true };
20
+
21
+ // Phase 1: lint/format fixes (mutating, like spec all)
22
+ const lint = runCmd('ESLint', cmds.lint, dir, undefined, q);
23
+ const stylelint = runCmd('Stylelint', cmds.stylelint, dir, undefined, q);
24
+ const prettier = runCmd('Prettier', cmds.prettier, dir, undefined, q);
25
+ const mdlint = runCmd('Markdown', cmds.mdlint, dir, undefined, q);
26
+
27
+ // Phase 2: measure post-fix state
28
+ const code = checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, 'Code', { exclude: cfg.testPattern, quiet: true });
29
+ const tests = checkFileLines(dir, cfg.codeExt, cfg.testMax, cfg.ignore, 'Tests', { include: cfg.testPattern, quiet: true });
30
+ const docs = checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, 'Docs', { quiet: true });
31
+ const imports = checkImports(dir, cfg.ignore, 'Imports', q);
32
+
33
+ // Phase 3: types
34
+ const types = runCmd('Types', `${runner} tsc --project .configs/jsconfig.json`, dir, undefined, q);
35
+
36
+ // Phase 4: audit
37
+ const audit = runCmd('Audit', cmds.audit, dir, undefined, q);
38
+
39
+ const checks = [lint, stylelint, mdlint, prettier, code, tests, docs, imports, types, audit];
40
+ const allLineViolations = [...(code.violations || []), ...(tests.violations || []), ...(docs.violations || [])];
41
+ const totalFiles = (code.files || 0) + (tests.files || 0) + (docs.files || 0);
42
+ const excessLines = allLineViolations.reduce((s, v) => s + (v.lines - v.max), 0);
43
+
44
+ return {
45
+ checks,
46
+ summary: {
47
+ totalFiles,
48
+ totalChecks: checks.length,
49
+ passed: checks.filter((c) => c.pass).length,
50
+ fileViolations: allLineViolations.length,
51
+ importViolations: imports.violations?.length || 0,
52
+ excessLines,
53
+ lintErrors: (lint.errors?.length || 0) + (stylelint.errors?.length || 0),
54
+ typeErrors: types.errors?.length || 0,
55
+ },
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Run report and print human-friendly output (or JSON with --json flag).
61
+ * @param {string} projectDir
62
+ * @param {{ json?: boolean }} [opts]
63
+ */
64
+ export function report(projectDir, opts = {}) {
65
+ console.log('\n🔧 Running full spec pipeline...\n');
66
+ const { checks, summary } = collect(projectDir);
67
+
68
+ if (opts.json) {
69
+ console.log(JSON.stringify({ checks, summary }, null, 2));
70
+ return;
71
+ }
72
+
73
+ const pct = summary.totalFiles > 0
74
+ ? ((summary.totalFiles - summary.fileViolations) / summary.totalFiles * 100).toFixed(1)
75
+ : '100.0';
76
+
77
+ console.log('📊 Clearstack Entropy Report\n');
78
+ console.log(` Checks: ${summary.passed}/${summary.totalChecks} passed`);
79
+ console.log(` Project size: ${summary.totalFiles} files`);
80
+ console.log(` Compliance: ${pct}% of files within line limits\n`);
81
+
82
+ for (const c of checks) {
83
+ const icon = c.pass ? '✅' : '❌';
84
+ console.log(` ${icon} ${c.label} (${c.time})`);
85
+ }
86
+
87
+ if (summary.excessLines > 0) {
88
+ const violations = [...(checks[4].violations || []), ...(checks[5].violations || []), ...(checks[6].violations || [])];
89
+ violations.sort((a, b) => (b.lines - b.max) - (a.lines - a.max));
90
+ console.log(`\n Total excess: ${summary.excessLines} lines over limits across ${summary.fileViolations} file(s)`);
91
+ console.log(' Top offenders:');
92
+ for (const v of violations.slice(0, 5)) console.log(` ${v.file}: ${v.lines} lines (+${v.lines - v.max})`);
93
+ const unowned = violations.filter((v) => /\b(vendor|dist|build)\b/.test(v.file));
94
+ if (unowned.length) console.log(`\n 💡 ${unowned.length} in vendor/dist — consider adding to SPEC_IGNORE_DIRS`);
95
+ }
96
+
97
+ if (summary.lintErrors > 0) console.log(`\n ⚠️ ${summary.lintErrors} unfixable lint error(s) remain`);
98
+ if (summary.typeErrors > 0) console.log(` ⚠️ ${summary.typeErrors} type error(s)`);
99
+ if (summary.importViolations > 0) console.log(` ⚠️ ${summary.importViolations} parent (../) import(s) need aliases`);
100
+
101
+ if (summary.passed === summary.totalChecks) {
102
+ console.log('\n ✨ All clear — zero entropy drift.\n');
103
+ } else {
104
+ console.log('');
105
+ }
106
+ }
@@ -13,8 +13,9 @@ export function detectRunner(projectDir) {
13
13
  return 'npx';
14
14
  }
15
15
 
16
- /** @param {string} src */
16
+ /** @param {string} src @returns {Record<string, string>} */
17
17
  function parseEnv(src) {
18
+ /** @type {Record<string, string>} */
18
19
  const env = {};
19
20
  for (const line of src.split('\n')) {
20
21
  const m = line.match(/^([^#=]+)=(.*)$/);
@@ -24,12 +25,15 @@ function parseEnv(src) {
24
25
  }
25
26
 
26
27
  /**
27
- * Load spec config from project .env.
28
+ * Load spec config from project .env, layering .env.local on top.
28
29
  * @param {string} projectDir
29
30
  */
30
31
  export function loadConfig(projectDir) {
31
32
  const envPath = resolve(projectDir, '.env');
32
- const env = existsSync(envPath) ? parseEnv(readFileSync(envPath, 'utf-8')) : {};
33
+ const localPath = resolve(projectDir, '.env.local');
34
+ const base = existsSync(envPath) ? parseEnv(readFileSync(envPath, 'utf-8')) : {};
35
+ const local = existsSync(localPath) ? parseEnv(readFileSync(localPath, 'utf-8')) : {};
36
+ const env = { ...base, ...local };
33
37
  return {
34
38
  codeMax: parseInt(env.SPEC_CODE_MAX_LINES) || 150,
35
39
  testMax: parseInt(env.SPEC_TEST_MAX_LINES) || 300,
package/lib/spec-utils.js CHANGED
@@ -35,23 +35,16 @@ export function findFiles(dir, extensions, ignoreDirs, root = dir) {
35
35
  * @param {string} root
36
36
  * @param {string[]} extensions
37
37
  * @param {string[]} ignoreDirs
38
- * @param {string[]} [dirs] - Subdirectories to scope to
39
- * @param {RegExp} [filter] - Optional regex filter on filenames
40
38
  * @returns {number}
41
39
  */
42
- export function countFiles(root, extensions, ignoreDirs, dirs, filter) {
43
- let files;
44
- if (dirs) {
45
- files = dirs.reduce((acc, d) => {
46
- try { return acc.concat(findFiles(resolve(root, d), extensions, ignoreDirs, root)); }
47
- catch { return acc; }
48
- }, []);
49
- } else {
50
- files = findFiles(root, extensions, ignoreDirs, root);
51
- }
52
- return filter ? files.filter((f) => filter.test(f)).length : files.length;
40
+ export function countFiles(root, extensions, ignoreDirs) {
41
+ return findFiles(root, extensions, ignoreDirs, root).length;
53
42
  }
54
43
 
44
+ /** @typedef {{ pass: boolean, label: string, time: string, files?: number, errors?: string[] }} CheckResult */
45
+ /** @typedef {{ file: string, lines: number, max: number }} LineViolation */
46
+ /** @typedef {{ file: string, spec: string }} ImportViolation */
47
+
55
48
  /**
56
49
  * Check file line counts with timing.
57
50
  * @param {string} root
@@ -59,8 +52,8 @@ export function countFiles(root, extensions, ignoreDirs, dirs, filter) {
59
52
  * @param {number} max
60
53
  * @param {string[]} ignoreDirs
61
54
  * @param {string} label
62
- * @param {{ exclude?: string, include?: string }} [filter]
63
- * @returns {boolean}
55
+ * @param {{ exclude?: string, include?: string, quiet?: boolean }} [filter]
56
+ * @returns {CheckResult & { violations?: LineViolation[] }}
64
57
  */
65
58
  export function checkFileLines(root, extensions, max, ignoreDirs, label, filter) {
66
59
  const start = performance.now();
@@ -74,12 +67,14 @@ export function checkFileLines(root, extensions, max, ignoreDirs, label, filter)
74
67
  }
75
68
  const time = elapsed(start);
76
69
  if (violations.length === 0) {
77
- console.log(` ✅ ${label} (${files.length} files, ${time})`);
78
- return true;
70
+ if (!filter?.quiet) console.log(` ✅ ${label} (${files.length} files, ${time})`);
71
+ return { pass: true, label, time, files: files.length };
72
+ }
73
+ if (!filter?.quiet) {
74
+ console.log(` ❌ ${label} — ${violations.length} violation(s):`);
75
+ for (const v of violations) console.log(` ${v.file}: ${v.lines} lines (max ${v.max})`);
79
76
  }
80
- console.log(` ❌ ${label} ${violations.length} violation(s):`);
81
- for (const v of violations) console.log(` ${v.file}: ${v.lines} lines (max ${v.max})`);
82
- return false;
77
+ return { pass: false, label, time, files: files.length, violations };
83
78
  }
84
79
 
85
80
  /**
@@ -88,9 +83,10 @@ export function checkFileLines(root, extensions, max, ignoreDirs, label, filter)
88
83
  * @param {string} root
89
84
  * @param {string[]} ignoreDirs
90
85
  * @param {string} label
91
- * @returns {boolean}
86
+ * @param {{ quiet?: boolean }} [opts]
87
+ * @returns {CheckResult & { violations?: ImportViolation[] }}
92
88
  */
93
- export function checkImports(root, ignoreDirs, label) {
89
+ export function checkImports(root, ignoreDirs, label, opts) {
94
90
  const start = performance.now();
95
91
  const files = findFiles(root, ['.js'], ignoreDirs, root)
96
92
  .filter((f) => f.startsWith('src/') && !f.includes('vendor/') && !f.includes('api/') && !f.endsWith('.test.js') && !f.endsWith('server.js'));
@@ -106,39 +102,45 @@ export function checkImports(root, ignoreDirs, label) {
106
102
  }
107
103
  const time = elapsed(start);
108
104
  if (violations.length === 0) {
109
- console.log(` ✅ ${label} (${files.length} files, ${time})`);
110
- return true;
105
+ if (!opts?.quiet) console.log(` ✅ ${label} (${files.length} files, ${time})`);
106
+ return { pass: true, label, time, files: files.length };
111
107
  }
112
- console.log(` ❌ ${label} — ${violations.length} violation(s):`);
113
- for (const v of violations) console.log(` ${v.file}: import '${v.spec}' → use #prefix/ alias`);
114
- return false;
108
+ if (!opts?.quiet) {
109
+ console.log(`${label} ${violations.length} violation(s):`);
110
+ for (const v of violations) console.log(` ${v.file}: import '${v.spec}' → use #prefix/ alias`);
111
+ }
112
+ return { pass: false, label, time, files: files.length, violations };
115
113
  }
116
114
 
115
+
117
116
  /**
118
117
  * Run a shell command, report pass/fail with timing.
119
118
  * @param {string} label
120
119
  * @param {string} cmd
121
120
  * @param {string} cwd
122
121
  * @param {string} [stats]
123
- * @returns {boolean}
122
+ * @param {{ quiet?: boolean }} [opts]
123
+ * @returns {CheckResult}
124
124
  */
125
- export function runCmd(label, cmd, cwd, stats) {
125
+ export function runCmd(label, cmd, cwd, stats, opts) {
126
126
  const start = performance.now();
127
127
  const suffix = (s) => s ? ` (${s}, ${elapsed(start)})` : ` (${elapsed(start)})`;
128
128
  try {
129
129
  execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf-8' });
130
- console.log(` ✅ ${label}${suffix(stats)}`);
131
- return true;
130
+ if (!opts?.quiet) console.log(` ✅ ${label}${suffix(stats)}`);
131
+ return { pass: true, label, time: elapsed(start), files: parseInt(stats) || undefined };
132
132
  } catch (err) {
133
133
  const out = (err.stdout || '') + (err.stderr || '');
134
134
  const ownErrors = out.trim().split('\n')
135
135
  .filter((l) => l.trim() && !l.includes('node_modules'));
136
136
  if (ownErrors.length === 0) {
137
- console.log(` ✅ ${label}${suffix(stats)}`);
138
- return true;
137
+ if (!opts?.quiet) console.log(` ✅ ${label}${suffix(stats)}`);
138
+ return { pass: true, label, time: elapsed(start) };
139
+ }
140
+ if (!opts?.quiet) {
141
+ console.log(` ❌ ${label}${suffix(stats)}`);
142
+ for (const line of ownErrors) console.log(` ${line}`);
139
143
  }
140
- console.log(` ❌ ${label}${suffix(stats)}`);
141
- for (const line of ownErrors) console.log(` ${line}`);
142
- return false;
144
+ return { pass: false, label, time: elapsed(start), errors: ownErrors };
143
145
  }
144
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.37",
3
+ "version": "0.3.38",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -64,8 +64,8 @@
64
64
  "express": "^5.2.1",
65
65
  "hybrids": "^9.1.22",
66
66
  "lucide-static": "^1.7.0",
67
- "markdownlint-cli2": "^0.21.0",
68
- "playwright": "^1.50.0",
67
+ "markdownlint-cli2": "^0.12.1",
68
+ "playwright": "^1.61.0",
69
69
  "prettier": "^3.8.1",
70
70
  "stylelint": "^17.6.0",
71
71
  "stylelint-config-standard": "^40.0.0",