@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.
- package/lib/spec-config.js +6 -0
- package/lib/watch-ui.js +12 -15
- package/lib/watch-widgets.js +5 -1
- package/lib/watch.js +33 -3
- package/package.json +1 -1
package/lib/spec-config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
141
|
-
|
|
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
|
}
|
package/lib/watch-widgets.js
CHANGED
|
@@ -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
|
-
?
|
|
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,
|