@techninja/clearstack 0.4.5 → 0.4.7

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.
@@ -96,6 +96,12 @@ export function buildCmds(projectDir, opts) {
96
96
  prettier: `${runner} prettier --config .configs/.prettierrc --check src scripts`,
97
97
  mdlint: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc "docs/**/*.md" "*.md"`,
98
98
  audit,
99
+ fix: {
100
+ es: `${runner} eslint --config .configs/eslint.config.js . --fix`,
101
+ css: `${runner} stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix`,
102
+ prettier: `${runner} prettier --config .configs/.prettierrc --write src scripts`,
103
+ md: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"`,
104
+ },
99
105
  };
100
106
  return {
101
107
  lint: `${runner} eslint --config .configs/eslint.config.js . --fix`,
package/lib/watch-ui.js CHANGED
@@ -64,7 +64,11 @@ export function extractViolations(row, projectDir) {
64
64
  });
65
65
  }
66
66
  if (row.result?.errors?.length) {
67
- return [[(row.label ?? row.name ?? row.key), row.result.errors.slice(0, 3).join('\n')]];
67
+ const errors = row.result.errors;
68
+ // Prettier: [warn] src/file.js — expand each into its own entry
69
+ const prettierFiles = errors.map((l) => l.match(/^\[warn\]\s+(.+\.\w+)$/)?.[1]).filter(Boolean);
70
+ if (prettierFiles.length) return prettierFiles.map((f) => [f, 'formatting — run npm run format']);
71
+ return [[row.label ?? row.name ?? row.key, errors.slice(0, 5).join('\n')]];
68
72
  }
69
73
  return [];
70
74
  }
@@ -84,6 +88,9 @@ function rowIcon(r) {
84
88
  return '{red-fg}!{/} ';
85
89
  }
86
90
 
91
+ /** @param {object} r @param {number} w */
92
+ const rowLine = (r, w) => { const n = (r.label ?? r.name ?? r.key).padEnd(w); return ` ${rowIcon(r)} ${r.pass === false ? `{red-fg}${n}{/}` : n} ${r.detail ? `{grey-fg}${r.detail}{/}` : ''}`; };
93
+
87
94
  /**
88
95
  * Render the dashboard.
89
96
  * @param {object[]} rows
@@ -92,13 +99,8 @@ function rowIcon(r) {
92
99
  * @param {string[][]} violations
93
100
  */
94
101
  export function render(rows, lastCheck, watchDirs, violations) {
95
- const labelWidth = Math.max(...rows.map((r) => (r.label ?? r.name ?? r.key).length));
96
- const statusLines = rows.map((r) => {
97
- const name = (r.label ?? r.name ?? r.key).padEnd(labelWidth);
98
- const styled = r.pass === false ? `{red-fg}${name}{/}` : name;
99
- const detail = r.detail ? `{grey-fg}${r.detail}{/}` : '';
100
- return ` ${rowIcon(r)} ${styled} ${detail}`;
101
- });
102
+ const w = Math.max(...rows.map((r) => (r.label ?? r.name ?? r.key).length));
103
+ const statusLines = rows.map((r) => rowLine(r, w));
102
104
  statusLines.push('');
103
105
  statusLines.push(`{grey-fg} watching ${watchDirs.join(', ')} last check: ${lastCheck}{/}${getCopyNote()}`);
104
106
  statusBox.setContent(statusLines.join('\n'));
@@ -137,12 +139,7 @@ export function destroyScreen() {
137
139
 
138
140
  /** Refresh just the status box — used for transient notes like copy confirmation. */
139
141
  export function renderNote(rows, lastCheck, watchDirs) {
140
- const labelWidth = Math.max(...rows.map((r) => (r.label ?? r.name ?? r.key).length));
141
- const lines = [
142
- ...rows.map((r) => { const n = (r.label ?? r.name ?? r.key).padEnd(labelWidth); return ` ${rowIcon(r)} ${r.pass === false ? `{red-fg}${n}{/}` : n} ${r.detail ? `{grey-fg}${r.detail}{/}` : ''}`; }),
143
- '',
144
- `{grey-fg} watching ${watchDirs.join(', ')} last check: ${lastCheck}{/}${getCopyNote()}`,
145
- ];
146
- statusBox.setContent(lines.join('\n'));
142
+ const w = Math.max(...rows.map((r) => (r.label ?? r.name ?? r.key).length));
143
+ statusBox.setContent([...rows.map((r) => rowLine(r, w)), '', `{grey-fg} watching ${watchDirs.join(', ')} last check: ${lastCheck}{/}${getCopyNote()}`].join('\n'));
147
144
  screen.render();
148
145
  }
@@ -15,7 +15,7 @@ export const screen = blessed.screen({ smartCSR: true, title: 'Clearstack Spec W
15
15
  export const outer = blessed.box({
16
16
  top: 0, left: 0, width: '100%', height: '100%',
17
17
  border: { type: 'line' },
18
- label: { text: ' {blue-fg}♥{/blue-fg} Clearstack Spec Watch q quit ↑↓ scroll c copy {/}', side: 'left' },
18
+ label: { text: ' {blue-fg}♥{/blue-fg} Clearstack Spec Watch q quit ↑↓ scroll c copy f fix {/}', side: 'left' },
19
19
  tags: true,
20
20
  style: { border: { fg: 'cyan' }, label: { fg: 'grey', bold: false } },
21
21
  });
@@ -58,9 +58,13 @@ export const getCopyNote = () => _copyNote;
58
58
  let _onCopyNote = () => {};
59
59
  export const setOnCopyNote = (fn) => { _onCopyNote = fn; };
60
60
 
61
+ let _onFix = () => {};
62
+ export const setOnFix = (fn) => { _onFix = fn; };
63
+
61
64
  screen.key(['C-c', 'q'], () => _quit());
62
65
  screen.key(['pageup', 'up'], () => { logBox.scroll(-1); screen.render(); });
63
66
  screen.key(['pagedown', 'down'], () => { logBox.scroll(1); screen.render(); });
67
+ screen.key(['f'], () => _onFix());
64
68
  screen.key(['c'], () => {
65
69
  const raw = logBox.getContent()
66
70
  .replace(/\{[^}]+\}/g, '') // blessed tags
package/lib/watch.js CHANGED
@@ -7,11 +7,13 @@
7
7
 
8
8
  import { existsSync } from 'node:fs';
9
9
  import { resolve } from 'node:path';
10
+ import { execSync } from 'node:child_process';
10
11
  import { loadConfig, buildCmds, makeExtMap, FAST_CHECKS } from './spec-config.js';
11
12
  import { buildChecks } from './check.js';
12
13
  import { render, extractViolations, renderNote } from './watch-ui.js';
13
14
  import { spawnServer, detectServerCmd } from './server-proc.js';
14
15
  import { setupLifecycle } from './watch-lifecycle.js';
16
+ import { setOnFix } from './watch-widgets.js';
15
17
 
16
18
  /** Adapt a Check from buildChecks into a watch row (adds pass/detail/result state). */
17
19
  function toRow(check) {
@@ -84,14 +86,19 @@ export async function startWatch(projectDir) {
84
86
  return;
85
87
  }
86
88
  const row = toRun[i++];
89
+ row.pass = null;
90
+ row.detail = '';
91
+ render(rows, lastCheck, watchDirs, currentViolations);
92
+ const t0 = Date.now();
87
93
  const raw = await row.run({ quiet: true });
94
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';
88
95
  row.result = typeof raw === 'boolean' ? { pass: raw } : raw;
89
96
  row.pass = row.result.pass;
90
97
  row.detail = row.pass
91
- ? (row.result.detail ?? (row.result.files ? `${row.result.files} files` : ''))
98
+ ? `${row.result.detail ?? (row.result.files ? `${row.result.files} files` : '')} (${elapsed})`
92
99
  : (row.result.violations?.length
93
- ? `${row.result.violations.length} violation(s)`
94
- : (() => { const errs = row.result.errors ?? []; const n = errs.filter((l) => /\.(js|ts|css|md)/.test(l)).length || errs.length; return `${n} error(s)`; })());
100
+ ? `${row.result.violations.length} violation(s) (${elapsed})`
101
+ : (() => { const errs = row.result.errors ?? []; const n = errs.filter((l) => /\.(js|ts|css|md)/.test(l)).length || errs.length; return `${n} error(s) (${elapsed})`; })());
95
102
  // Rebuild violations from all currently-failed rows
96
103
  currentViolations = specRows.flatMap((r) => extractViolations(r, projectDir));
97
104
  render(rows, lastCheck, watchDirs, currentViolations);
@@ -114,15 +121,38 @@ export async function startWatch(projectDir) {
114
121
  */
115
122
  function schedule(ext) {
116
123
  for (const k of checksForExt(ext)) {
124
+ const row = specRows.find((r) => r.key === k);
125
+ if (row) row.pass = null; // show spinner while queued
117
126
  if (FAST_CHECKS.has(k)) fastPending.add(k);
118
127
  else slowPending.add(k);
119
128
  }
129
+ render(rows, lastCheck, watchDirs, currentViolations);
120
130
  if (fastPending.size) { clearTimeout(fastTimer); fastTimer = setTimeout(runFast, 50); }
121
131
  if (slowPending.size) { clearTimeout(slowTimer); slowTimer = setTimeout(runSlow, 1500); }
122
132
  }
123
133
 
124
134
  runKeys(new Set(specRows.map((r) => r.key)));
125
135
 
136
+ // f key — run fix commands for all currently-failing fixable checks
137
+ // Checks can provide a fix via cmds.fix[key] (built-ins) or row.fix() (extensions)
138
+ setOnFix(() => {
139
+ const fixable = specRows.filter((r) => !r.pass && (cmds.fix?.[r.key] || r.fix));
140
+ if (!fixable.length) return;
141
+ for (const row of fixable) {
142
+ row.pass = null; // show spinner immediately
143
+ slowPending.add(row.key);
144
+ }
145
+ render(rows, lastCheck, watchDirs, currentViolations);
146
+ for (const row of fixable) {
147
+ try {
148
+ if (row.fix) row.fix();
149
+ else execSync(cmds.fix[row.key], { cwd: projectDir, stdio: 'pipe' });
150
+ } catch { /* fixer may exit 1 even after fixing */ }
151
+ }
152
+ clearTimeout(slowTimer);
153
+ slowTimer = setTimeout(runSlow, 100);
154
+ });
155
+
126
156
  setupLifecycle({
127
157
  serverRow, specRows, projectDir, watchDirs,
128
158
  schedule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {