lintlane 2.5.0 → 4.0.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.
- package/LICENSE +21 -0
- package/README.md +210 -512
- package/bin/check-commit.mjs +54 -137
- package/bin/checks.mjs +237 -0
- package/bin/cli.mjs +299 -220
- package/bin/commit-cli.mjs +36 -122
- package/bin/doctor.mjs +328 -0
- package/bin/hook.mjs +156 -0
- package/bin/proc.mjs +132 -0
- package/bin/project.mjs +276 -0
- package/bin/ui-server.mjs +244 -91
- package/package.json +22 -7
- package/presets/commitizen.cjs +4 -0
- package/presets/commitlint.mjs +47 -0
- package/templates/commit-msg +2 -2
- package/templates/pre-commit +2 -2
- package/templates/pre-push +2 -2
- package/templates/prepare-commit-msg +2 -2
- package/ui/index.html +210 -44
- package/bin/ci.mjs +0 -162
- package/bin/commit-config.mjs +0 -24
- package/bin/commit-lint.mjs +0 -143
- package/bin/commit-msg.mjs +0 -18
- package/bin/commit-prompt.mjs +0 -110
- package/bin/pre-push-check.mjs +0 -48
- package/bin/prepare-commit-msg.mjs +0 -46
package/ui/index.html
CHANGED
|
@@ -12,27 +12,30 @@
|
|
|
12
12
|
</script>
|
|
13
13
|
<style>
|
|
14
14
|
:root {
|
|
15
|
-
--bg: #
|
|
15
|
+
--bg: #fafafa; --panel: #ffffff; --row-hover: #f4f4f5; --row-sel: #e4e4e7; --text: #18181b; --muted: #71717a; --border: #d4d4d8;
|
|
16
16
|
--err: #b91c1c; --warn: #b45309; --info: #1d4ed8; --ok: #15803d; --err-row: #fdf2f2;
|
|
17
17
|
--add-bg: #e6ffec; --add-fg: #116329; --del-bg: #ffebe9; --del-fg: #82071e;
|
|
18
|
-
--accent: #
|
|
18
|
+
--accent: #18181b; --accent-fg: #fafafa;
|
|
19
|
+
--surface: #f4f4f5; --silver: #c4c4cc; --log-bg: #fafafa; --log-head: #f4f4f5; --log-fg: #27272a; --log-muted: #71717a;
|
|
19
20
|
}
|
|
20
21
|
:root { color-scheme: light; }
|
|
21
22
|
@media (prefers-color-scheme: dark) {
|
|
22
23
|
:root:not([data-theme="light"]) {
|
|
23
24
|
color-scheme: dark;
|
|
24
|
-
--bg: #
|
|
25
|
+
--bg: #09090b; --panel: #18181b; --row-hover: #202023; --row-sel: #27272a; --text: #f4f4f5; --muted: #a1a1aa; --border: #3f3f46;
|
|
25
26
|
--err: #f87171; --warn: #fbbf24; --info: #60a5fa; --ok: #4ade80; --err-row: #231414;
|
|
26
27
|
--add-bg: #0f2d1a; --add-fg: #7ee787; --del-bg: #3a1618; --del-fg: #ffa198;
|
|
27
|
-
--accent: #
|
|
28
|
+
--accent: #f4f4f5; --accent-fg: #09090b;
|
|
29
|
+
--surface: #18181b; --silver: #52525b; --log-bg: #101012; --log-head: #18181b; --log-fg: #e4e4e7; --log-muted: #a1a1aa;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
:root[data-theme="dark"] {
|
|
31
33
|
color-scheme: dark;
|
|
32
|
-
--bg: #
|
|
34
|
+
--bg: #09090b; --panel: #18181b; --row-hover: #202023; --row-sel: #27272a; --text: #f4f4f5; --muted: #a1a1aa; --border: #3f3f46;
|
|
33
35
|
--err: #f87171; --warn: #fbbf24; --info: #60a5fa; --ok: #4ade80; --err-row: #231414;
|
|
34
36
|
--add-bg: #0f2d1a; --add-fg: #7ee787; --del-bg: #3a1618; --del-fg: #ffa198;
|
|
35
|
-
--accent: #
|
|
37
|
+
--accent: #f4f4f5; --accent-fg: #09090b;
|
|
38
|
+
--surface: #18181b; --silver: #52525b; --log-bg: #101012; --log-head: #18181b; --log-fg: #e4e4e7; --log-muted: #a1a1aa;
|
|
36
39
|
}
|
|
37
40
|
* { box-sizing: border-box; }
|
|
38
41
|
body { margin: 0; background: var(--bg); color: var(--text);
|
|
@@ -125,12 +128,121 @@
|
|
|
125
128
|
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }
|
|
126
129
|
.log { margin: 0; padding: 14px 18px; font-size: 12.5px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
127
130
|
.error-box { background: var(--del-bg); color: var(--del-fg); padding: 12px 16px; border-radius: 10px; margin-bottom: 16px; }
|
|
131
|
+
/* Actions-style workflow workspace */
|
|
132
|
+
#ciLayout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
|
|
133
|
+
#ciSteps { background: transparent; border: 0; }
|
|
134
|
+
#ciPanel { background: var(--log-bg); color: var(--log-fg); border: 1px solid var(--silver); border-radius: 8px; overflow: hidden; }
|
|
135
|
+
#ciStats { display: flex; gap: 24px; padding: 12px 0 20px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
136
|
+
#ciStats .stat { display: flex; align-items: baseline; gap: 7px; padding: 0; background: none; border: 0; }
|
|
137
|
+
#ciStats .stat b { font-size: 16px; }
|
|
138
|
+
#ciStats .stat span { font-size: 13px; margin: 0; }
|
|
139
|
+
.ci-tools { font-size: 13px; }
|
|
140
|
+
.ci-tools button { font-size: 12px; padding: 5px 10px; }
|
|
141
|
+
.ci-jobs-title { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 10px 12px; }
|
|
142
|
+
.ci-job { display: flex; align-items: center; gap: 8px; border-radius: 6px; margin: 3px 0; padding: 4px 8px; border: 1px solid var(--silver); }
|
|
143
|
+
.ci-job.sel { background: var(--row-sel); box-shadow: inset 3px 0 var(--info); }
|
|
144
|
+
.ci-job button { flex: 1; text-align: left; border: 0; padding: 8px 2px; background: transparent; min-width: 0; }
|
|
145
|
+
.ci-job b { display: block; font-size: 13px; font-weight: 600; }
|
|
146
|
+
.ci-job small { font-size: 11px; }
|
|
147
|
+
.ci-step { border-bottom: 1px solid var(--border); }
|
|
148
|
+
.ci-step:last-child { border-bottom: 0; }
|
|
149
|
+
.ci-step-head { display: flex; align-items: center; gap: 12px; padding: 14px 18px; width: 100%; border: 0; border-radius: 0; background: var(--log-head); color: var(--log-fg); text-align: left; }
|
|
150
|
+
.ci-step-head:hover:not(:disabled) { background: var(--row-hover); }
|
|
151
|
+
.ci-step-label { flex: 1; font-weight: 600; font-size: 13px; }
|
|
152
|
+
.ci-time { color: var(--log-muted); font: 12px ui-monospace, monospace; }
|
|
153
|
+
.ci-log-toolbar { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--border); font-size: 13px; }
|
|
154
|
+
.ci-log-toolbar .ci-time { margin-left: auto; color: var(--log-muted); }
|
|
155
|
+
.ci-command { padding: 14px 24px; color: var(--log-muted); font-size: 12px; border-bottom: 1px solid var(--row-hover); overflow-wrap: anywhere; }
|
|
156
|
+
.ci-log-body { max-height: 520px; overflow: auto; padding: 12px 0; scrollbar-color: var(--silver) var(--log-bg); }
|
|
157
|
+
.ci-log-line { display: flex; font: 12px/1.8 ui-monospace, "SF Mono", Menlo, monospace; min-height: 22px; }
|
|
158
|
+
.ci-line-number { flex: 0 0 52px; text-align: right; padding-right: 16px; color: var(--log-muted); user-select: none; }
|
|
159
|
+
.ci-line-text { white-space: pre-wrap; overflow-wrap: anywhere; min-width: 0; padding-right: 24px; }
|
|
160
|
+
.ci-log-line:hover { background: var(--log-head); }
|
|
161
|
+
#ciPanel .st-pass { color: var(--ok); } #ciPanel .st-fail { color: var(--err); } #ciPanel .st-run { color: var(--info); }
|
|
162
|
+
button:focus-visible, input:focus-visible, .ci-log-body:focus-visible { outline: 2px solid var(--info); outline-offset: 3px; }
|
|
163
|
+
#ciFollow[aria-pressed="true"] { border-color: #58a6ff; color: #58a6ff; }
|
|
164
|
+
#ciRetry[hidden] { display: none; }
|
|
165
|
+
@media (max-width: 760px) { #ciStats { gap: 16px; } .ci-tools { gap: 8px; } main { padding: 20px 14px; } #ciLayout { grid-template-columns: minmax(0, 1fr); } .ci-step-head { flex-wrap: wrap; } }
|
|
166
|
+
/* Shared component polish: zinc surfaces, silver outlines, restrained density. */
|
|
167
|
+
body { font-size: 14px; -webkit-font-smoothing: antialiased; }
|
|
168
|
+
header { min-height: 72px; padding: 16px 32px; gap: 24px; }
|
|
169
|
+
.brand { display: flex; gap: 10px; align-items: center; }
|
|
170
|
+
.brand-mark { width: 30px; height: 30px; display: grid; place-items: center; border: 1px solid var(--silver); border-radius: 7px; background: var(--surface); }
|
|
171
|
+
.brand-mark svg { width: 20px; height: 20px; }
|
|
172
|
+
h1 { font-size: 15px; letter-spacing: -.04em; font-weight: 700; }
|
|
173
|
+
.brand-divider { margin: 0 4px; color: var(--silver); font-size: 21px; font-weight: 300; }
|
|
174
|
+
.workspace-label { font-size: 12px; color: var(--muted); }
|
|
175
|
+
main { max-width: 1440px; padding: 36px 32px 64px; }
|
|
176
|
+
.page-heading { display: flex; justify-content: space-between; align-items: center; gap: 24px; margin-bottom: 30px; }
|
|
177
|
+
.eyebrow { color: var(--muted); font-size: 11px; font-weight: 500; margin-bottom: 8px; }
|
|
178
|
+
h2 { font-size: 27px; font-weight: 650; letter-spacing: -.045em; line-height: 1.25; margin: 0; }
|
|
179
|
+
.page-heading p { font-size: 13px; color: var(--muted); margin: 9px 0 0; }
|
|
180
|
+
.environment { display: inline-flex; align-items: center; gap: 7px; font: 11px ui-monospace, monospace; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; padding: 5px 9px; white-space: nowrap; }
|
|
181
|
+
.environment > span { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }
|
|
182
|
+
button { font-size: 12px; font-weight: 500; border-radius: 6px; min-height: 32px; padding: 6px 12px; box-shadow: 0 1px 2px #00000006; transition: background-color .12s, border-color .12s; }
|
|
183
|
+
button:hover:not(:disabled) { background: var(--row-hover); border-color: var(--silver); }
|
|
184
|
+
button.primary:hover:not(:disabled) { background: var(--accent); opacity: .88; }
|
|
185
|
+
button.theme { width: 32px; height: 32px; font-size: 14px; padding: 0; }
|
|
186
|
+
.tabs { padding: 3px; border-radius: 7px; background: var(--surface); border: 1px solid var(--border); gap: 2px; margin: 0; }
|
|
187
|
+
.tab { padding: 4px 15px; min-height: 27px; border-radius: 4px; font-size: 12px; }
|
|
188
|
+
.tab[aria-current="page"] { background: var(--panel); box-shadow: 0 1px 3px #00000012; }
|
|
189
|
+
.badge { font-size: 10px; padding: 2px 7px; background: var(--surface); text-transform: capitalize; border-radius: 5px; }
|
|
190
|
+
.actions { gap: 8px; }
|
|
191
|
+
.check { font-size: 12px; gap: 5px; }
|
|
192
|
+
input[type="checkbox"] { accent-color: var(--text); width: 13px; height: 13px; cursor: pointer; }
|
|
193
|
+
.stats { gap: 12px; }
|
|
194
|
+
.stat { border-radius: 8px; padding: 20px; display: flex; flex-direction: column-reverse; gap: 12px; box-shadow: 0 1px 2px #00000004; }
|
|
195
|
+
.stat b { font-size: 28px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
196
|
+
.stat span { font-size: 12px; margin: 0; }
|
|
197
|
+
.card { border-radius: 8px; border-color: var(--silver); box-shadow: 0 1px 3px #00000004; }
|
|
198
|
+
.toolbar { margin-bottom: 16px; gap: 10px; }
|
|
199
|
+
.toolbar input { font-size: 12px; padding: 8px 12px; border-radius: 6px; min-width: 180px; }
|
|
200
|
+
.chips { gap: 3px; border: 1px solid var(--border); border-radius: 6px; padding: 3px; background: var(--surface); }
|
|
201
|
+
.chip { border: 0; border-radius: 4px; min-height: 26px; padding: 3px 12px; font-size: 11px; background: transparent; }
|
|
202
|
+
.chip[aria-pressed="true"] { background: var(--panel); box-shadow: 0 1px 3px #00000012; }
|
|
203
|
+
th { background: var(--surface); color: var(--muted); font-size: 11px; font-weight: 500; padding: 11px 16px; }
|
|
204
|
+
td { padding: 13px 16px; font-size: 12px; }
|
|
205
|
+
.empty { padding: 64px 20px; display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 12px; }
|
|
206
|
+
.empty strong { color: var(--text); font-size: 14px; font-weight: 500; }
|
|
207
|
+
.empty-symbol { display: grid; place-items: center; width: 40px; height: 40px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; font: 22px ui-monospace, monospace; }
|
|
208
|
+
.panel-head { padding: 12px 16px; font-size: 12px; }
|
|
209
|
+
.diag { font-size: 12px; padding: 14px 16px; }
|
|
210
|
+
.diag .meta { font-size: 10px; margin-top: 5px; }
|
|
211
|
+
.verdict { border-radius: 8px; padding: 14px 18px; font-size: 13px; border: 1px solid var(--silver); background: var(--panel); }
|
|
212
|
+
.verdict.pass { background: var(--panel); border-color: var(--silver); color: var(--ok); box-shadow: inset 3px 0 var(--ok); }
|
|
213
|
+
.verdict.fail { background: var(--panel); border-color: var(--silver); color: var(--err); box-shadow: inset 3px 0 var(--err); }
|
|
214
|
+
.verdict small { color: var(--muted); opacity: 1; font-size: 12px; margin-top: 4px; }
|
|
215
|
+
#ciStats { gap: 28px; padding: 2px 0 18px; margin-bottom: 20px; }
|
|
216
|
+
#ciStats .stat { flex-direction: row; box-shadow: none; }
|
|
217
|
+
#ciStats .stat b { font-size: 14px; }
|
|
218
|
+
#ciStats .stat span { font-size: 11px; }
|
|
219
|
+
#ciLayout { grid-template-columns: 248px minmax(0, 1fr); gap: 20px; }
|
|
220
|
+
#ciSteps { padding: 8px; border: 1px solid var(--silver); border-radius: 8px; background: var(--panel); }
|
|
221
|
+
.ci-jobs-title { padding: 8px 8px 16px; font-size: 11px; text-transform: none; letter-spacing: 0; font-weight: 600; color: var(--text); }
|
|
222
|
+
.ci-jobs-title > span { display: block; font-size: 10px; margin-top: 5px; font-weight: 400; }
|
|
223
|
+
.ci-job { border-color: transparent; margin: 2px 0; padding: 3px 8px; gap: 9px; }
|
|
224
|
+
.ci-job:hover { background: var(--row-hover); }
|
|
225
|
+
.ci-job.sel { border-color: var(--silver); background: var(--surface); box-shadow: none; }
|
|
226
|
+
.ci-job b { font-size: 12px; font-weight: 500; }
|
|
227
|
+
.ci-job small { display: block; font-size: 10px; margin-top: 5px; }
|
|
228
|
+
.ci-log-toolbar { padding: 16px 18px; font-size: 12px; background: var(--log-bg); }
|
|
229
|
+
.log-caption { display: block; font-size: 10px; color: var(--log-muted); margin-top: 4px; }
|
|
230
|
+
.ci-log-toolbar strong { font-weight: 500; }
|
|
231
|
+
.ci-step-head { padding: 15px 18px; font-size: 11px; }
|
|
232
|
+
.ci-step-label { font-size: 12px; font-weight: 500; }
|
|
233
|
+
.ci-step-head .ci-time { font-size: 10px; min-width: 45px; text-align: right; }
|
|
234
|
+
.ci-command { padding: 12px 18px; font-size: 11px; }
|
|
235
|
+
.ci-log-line { font-size: 11px; line-height: 1.9; }
|
|
236
|
+
#ciFollow[aria-pressed="true"] { border-color: var(--silver); color: var(--text); background: var(--surface); }
|
|
237
|
+
@media (max-width: 950px) { header { gap: 12px; padding: 12px 20px; } .workspace-label, .brand-divider { display: none; } .actions { flex-wrap: wrap; } }
|
|
238
|
+
@media (max-width: 760px) { main { padding: 24px 16px 48px; } #ciLayout { grid-template-columns: minmax(0, 1fr); } .page-heading { align-items: flex-start; } .environment { display: none; } .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } header { gap: 10px; } header .actions { width: 100%; order: 2; } .chips { margin-left: 0; } .ci-tools { gap: 8px; } }
|
|
239
|
+
@media (prefers-reduced-motion: reduce) { button { transition: none; } }
|
|
128
240
|
</style>
|
|
129
241
|
</head>
|
|
130
242
|
<body>
|
|
131
243
|
<header>
|
|
132
|
-
<h1>Lintlane</h1>
|
|
133
|
-
<nav class="tabs">
|
|
244
|
+
<div class="brand"><span class="brand-mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m5 8 4 4-4 4M13 16h6M13 8h6"/></svg></span><h1>Lintlane</h1><span class="brand-divider">/</span><span class="workspace-label">Workspace</span></div>
|
|
245
|
+
<nav class="tabs" aria-label="Dashboard views">
|
|
134
246
|
<button type="button" class="tab" data-view="lint" aria-current="page">Lint</button>
|
|
135
247
|
<button type="button" class="tab" data-view="ci">CI</button>
|
|
136
248
|
</nav>
|
|
@@ -148,6 +260,7 @@
|
|
|
148
260
|
<button type="button" id="theme" class="theme" aria-label="Toggle light/dark mode"></button>
|
|
149
261
|
</header>
|
|
150
262
|
<main id="view-lint">
|
|
263
|
+
<div class="page-heading"><div><div class="eyebrow">Code quality</div><h2>Lint overview</h2><p>Review diagnostics and apply fixes before your next commit.</p></div><span class="environment"><span></span>Local workspace</span></div>
|
|
151
264
|
<div id="notice"></div>
|
|
152
265
|
<div class="stats" id="stats"></div>
|
|
153
266
|
<div class="toolbar">
|
|
@@ -165,12 +278,21 @@
|
|
|
165
278
|
</main>
|
|
166
279
|
|
|
167
280
|
<main id="view-ci" hidden>
|
|
281
|
+
<div class="page-heading"><div><div class="eyebrow">Continuous integration</div><h2>Workflow runs</h2><p>Run your project checks and inspect the output, step by step.</p></div><span class="environment"><span></span>Local workspace</span></div>
|
|
168
282
|
<div id="ciNotice"></div>
|
|
169
283
|
<div id="ciVerdict"></div>
|
|
170
|
-
<div class="stats" id="ciStats"></div>
|
|
171
|
-
<div class="
|
|
172
|
-
<
|
|
173
|
-
<div class="
|
|
284
|
+
<div class="stats" id="ciStats" aria-live="polite"></div>
|
|
285
|
+
<div class="toolbar ci-tools">
|
|
286
|
+
<span class="muted">Local workflow / <strong>Quality checks</strong></span>
|
|
287
|
+
<div class="spacer"></div>
|
|
288
|
+
<button type="button" id="ciRetry" hidden>Re-run failed checks</button>
|
|
289
|
+
<button type="button" id="ciFollow" aria-pressed="true">Follow active step</button>
|
|
290
|
+
<button type="button" id="ciExpand">Expand all logs</button>
|
|
291
|
+
|
|
292
|
+
</div>
|
|
293
|
+
<div id="ciLayout">
|
|
294
|
+
<div id="ciSteps"></div>
|
|
295
|
+
<div id="ciPanel"></div>
|
|
174
296
|
</div>
|
|
175
297
|
</main>
|
|
176
298
|
|
|
@@ -293,8 +415,8 @@ function renderStats() {
|
|
|
293
415
|
function renderTable() {
|
|
294
416
|
const files = visibleFiles();
|
|
295
417
|
const card = $("#tableCard");
|
|
296
|
-
if (!state) { card.innerHTML = `<div class="empty">
|
|
297
|
-
if (state.files.length === 0) { card.innerHTML = `<div class="empty"
|
|
418
|
+
if (!state) { card.innerHTML = `<div class="empty"><span class="empty-symbol" aria-hidden="true">⌘</span><strong>Your next clean commit starts here</strong><span>Run a check to review lint and formatting diagnostics.</span></div>`; return; }
|
|
419
|
+
if (state.files.length === 0) { card.innerHTML = `<div class="empty"><span class="empty-symbol fixable" aria-hidden="true">✓</span><strong>No issues found</strong><span>Your code matches the project’s lint and formatting rules.</span></div>`; return; }
|
|
298
420
|
if (files.length === 0) { card.innerHTML = `<div class="empty">No files match your filters.</div>`; return; }
|
|
299
421
|
card.innerHTML = `<table><thead><tr><th>File</th><th>Level</th><th class="num">Issues</th><th>Fix</th></tr></thead><tbody>${files.map((f) => {
|
|
300
422
|
const w = worst(f);
|
|
@@ -471,6 +593,8 @@ for (const tab of document.querySelectorAll(".tab")) tab.addEventListener("click
|
|
|
471
593
|
|
|
472
594
|
// ---------------------------------------------------------------- CI view
|
|
473
595
|
const ci = { steps: null, enabled: new Set(), results: {}, selected: null, run: null, finished: false, startedAt: 0, seconds: null, error: null, follow: true };
|
|
596
|
+
ci.cancelled = false;
|
|
597
|
+
ci.expanded = new Set();
|
|
474
598
|
const ESC = String.fromCharCode(27);
|
|
475
599
|
const BEL = String.fromCharCode(7);
|
|
476
600
|
const ANSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "g");
|
|
@@ -479,8 +603,15 @@ const fmtMs = (ms) => (ms == null ? "–" : ms < 1000 ? `${ms}ms` : `${(ms / 100
|
|
|
479
603
|
|
|
480
604
|
async function ciLoad() {
|
|
481
605
|
try {
|
|
482
|
-
const { steps } = await api("/api/ci/steps");
|
|
606
|
+
const { steps, problems } = await api("/api/ci/steps");
|
|
483
607
|
ci.steps = steps;
|
|
608
|
+
ci.problems = [...problems];
|
|
609
|
+
try {
|
|
610
|
+
const report = await api("/api/doctor");
|
|
611
|
+
for (const f of report.findings) {
|
|
612
|
+
if (f.status === "fail") ci.problems.push(`Setup: ${f.message}${f.fix ? ` — ${f.fix}` : ""}`);
|
|
613
|
+
}
|
|
614
|
+
} catch {}
|
|
484
615
|
for (const st of steps) if (st.defaultOn) ci.enabled.add(st.id);
|
|
485
616
|
ci.selected = steps.find((st) => st.defaultOn)?.id ?? steps[0]?.id ?? null;
|
|
486
617
|
} catch (e) { ci.error = e.message; }
|
|
@@ -517,46 +648,52 @@ function ciRenderVerdict() {
|
|
|
517
648
|
if (!ci.finished) { el.innerHTML = ""; return; }
|
|
518
649
|
const res = ci.steps.filter((st) => ci.results[st.id]);
|
|
519
650
|
const failed = res.find((st) => ci.results[st.id].status === "fail");
|
|
520
|
-
const cancelled = res.some((st) => ci.results[st.id].status === "cancelled");
|
|
651
|
+
const cancelled = ci.cancelled || res.some((st) => ci.results[st.id].status === "cancelled");
|
|
521
652
|
if (cancelled) el.innerHTML = `<div class="verdict">Run stopped.</div>`;
|
|
653
|
+
else if (ci.error) el.innerHTML = `<div class="verdict fail">Run could not complete<small>${esc(ci.error)}</small></div>`;
|
|
522
654
|
else if (failed) el.innerHTML = `<div class="verdict fail">CI would fail<small>First failure: ${esc(failed.label)}. Fix it before pushing.</small></div>`;
|
|
523
|
-
else if (res.length) el.innerHTML = `<div class="verdict pass">All ${plural(res.length, "step")} passed
|
|
655
|
+
else if (res.length) el.innerHTML = `<div class="verdict pass">All ${plural(res.length, "step")} passed<small>Same checks as your CI workflow, run locally.</small></div>`;
|
|
524
656
|
}
|
|
525
657
|
|
|
526
658
|
function ciRenderTable() {
|
|
527
659
|
const card = $("#ciSteps");
|
|
528
|
-
if (!ci.steps) { card.innerHTML =
|
|
529
|
-
|
|
530
|
-
card.innerHTML = `<table><thead><tr><th></th><th>Step</th><th>Status</th><th class="num">Time</th></tr></thead><tbody>${ci.steps.map((st) => {
|
|
660
|
+
if (!ci.steps) { card.innerHTML = '<div class="empty">Loading steps…</div>'; return; }
|
|
661
|
+
card.innerHTML = '<div class="ci-jobs-title">Workflow steps<span class="muted">Select checks to include in this run</span></div>' + ci.steps.map((st) => {
|
|
531
662
|
const r = ci.results[st.id];
|
|
532
|
-
return `<
|
|
533
|
-
|
|
534
|
-
<td class="step"><b>${esc(st.label)}</b><small class="mono">${esc(st.display)}</small>${st.note ? `<small> · ${esc(st.note)}</small>` : ""}</td>
|
|
535
|
-
<td class="status">${ciStatusCell(r)}</td><td class="num muted">${fmtMs(r?.ms)}</td></tr>`;
|
|
536
|
-
}).join("")}</tbody></table>`;
|
|
663
|
+
return `<div class="ci-job ${st.id === ci.selected ? "sel" : ""}"><input type="checkbox" data-enable="${esc(st.id)}" aria-label="Include ${esc(st.label)}" ${ci.enabled.has(st.id) ? "checked" : ""} ${ci.run ? "disabled" : ""} /><button type="button" data-select="${esc(st.id)}" ${st.id === ci.selected ? 'aria-current="step"' : ""}><b>${esc(st.label)}</b><small>${ciStatusCell(r)} · ${fmtMs(r?.ms)}</small></button></div>`;
|
|
664
|
+
}).join("");
|
|
537
665
|
}
|
|
538
666
|
|
|
539
667
|
function ciRenderPanel() {
|
|
540
668
|
const panel = $("#ciPanel");
|
|
541
|
-
const
|
|
542
|
-
if (!
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
669
|
+
const scrolls = new Map([...panel.querySelectorAll(".ci-log-body")].map((el) => [el.dataset.log, el.scrollTop]));
|
|
670
|
+
if (!ci.steps) { panel.innerHTML = '<div class="empty">Loading workflow…</div>'; return; }
|
|
671
|
+
panel.innerHTML = '<div class="ci-log-toolbar"><div><strong>Execution logs</strong><span class="log-caption">Quality checks</span></div><span class="ci-time">Local run</span></div>' + ci.steps.filter((st) => ci.enabled.has(st.id) || ci.results[st.id]).map((st) => {
|
|
672
|
+
const r = ci.results[st.id];
|
|
673
|
+
const open = ci.expanded.has(st.id);
|
|
674
|
+
const text = (r?.log ?? "").replace(ANSI, "");
|
|
675
|
+
const placeholder = r?.status === "running" ? "Waiting for output…" : r?.status === "skip" ? "This step was skipped." : r?.status === "cancelled" ? "This step was stopped." : "No output recorded.";
|
|
676
|
+
return `<section class="ci-step" id="ci-step-${esc(st.id)}"><button type="button" class="ci-step-head" data-toggle="${esc(st.id)}" aria-expanded="${open}" aria-controls="ci-body-${esc(st.id)}"><span aria-hidden="true">${open ? "▾" : "▸"}</span><span class="ci-step-label">${esc(st.label)}</span>${ciStatusCell(r)}<span class="ci-time">${fmtMs(r?.ms)}</span></button>${open ? `<div id="ci-body-${esc(st.id)}"><div class="ci-command mono">$ ${esc(st.display)}</div><div class="ci-log-body" data-log="${esc(st.id)}" tabindex="0" aria-label="${esc(st.label)} output">${(text || placeholder).split("\n").map((line, i) => `<div class="ci-log-line"><span class="ci-line-number">${i + 1}</span><span class="ci-line-text">${esc(line)}</span></div>`).join("")}</div></div>` : ""}</section>`;
|
|
677
|
+
}).join("");
|
|
678
|
+
for (const el of panel.querySelectorAll(".ci-log-body")) el.scrollTop = ci.follow && ci.results[el.dataset.log]?.status === "running" ? el.scrollHeight : scrolls.get(el.dataset.log) ?? 0;
|
|
549
679
|
}
|
|
550
680
|
|
|
551
681
|
function ciRender() {
|
|
552
682
|
const busy = !!ci.run;
|
|
553
683
|
const ciRunBtn = $("#ciRun");
|
|
554
|
-
ciRunBtn.textContent = busy ? "
|
|
684
|
+
ciRunBtn.textContent = busy ? "Cancel run" : ci.finished ? "Re-run checks" : "Run checks";
|
|
555
685
|
ciRunBtn.classList.toggle("primary", !busy);
|
|
556
686
|
ciRunBtn.disabled = !ci.steps || (!busy && ci.enabled.size === 0);
|
|
557
687
|
$("#ciStop").disabled = busy;
|
|
558
|
-
$("#ciStatus").textContent = busy ? "running…" : !ci.finished ? "idle" : Object.values(ci.results).some((r) => r.status === "fail") ? "failed" : "passed";
|
|
559
|
-
$("#ciNotice").innerHTML = ci.error
|
|
688
|
+
$("#ciStatus").textContent = busy ? "running…" : !ci.finished ? "idle" : ci.cancelled ? "stopped" : ci.error ? "error" : Object.values(ci.results).some((r) => r.status === "fail") ? "failed" : "passed";
|
|
689
|
+
$("#ciNotice").innerHTML = ci.error
|
|
690
|
+
? `<div class="error-box">${esc(ci.error)}</div>`
|
|
691
|
+
: (ci.problems ?? []).map((p) => `<div class="error-box">${esc(p)}</div>`).join("");
|
|
692
|
+
const visible = ci.steps?.filter((st) => ci.enabled.has(st.id) || ci.results[st.id]) ?? [];
|
|
693
|
+
$("#ciExpand").textContent = visible.length && visible.every((st) => ci.expanded.has(st.id)) ? "Collapse all logs" : "Expand all logs";
|
|
694
|
+
$("#ciExpand").disabled = !visible.length;
|
|
695
|
+
$("#ciFollow").setAttribute("aria-pressed", String(ci.follow));
|
|
696
|
+
$("#ciRetry").hidden = busy || !Object.values(ci.results).some((r) => r.status === "fail");
|
|
560
697
|
ciRenderStats(); ciRenderVerdict(); ciRenderTable(); ciRenderPanel();
|
|
561
698
|
}
|
|
562
699
|
|
|
@@ -567,18 +704,19 @@ function ciSoftRender() {
|
|
|
567
704
|
}
|
|
568
705
|
|
|
569
706
|
function ciEvent(e) {
|
|
570
|
-
if (e.type === "start") { ciRes(e.id).status = "running"; if (ci.follow) ci.selected = e.id; ciRender(); }
|
|
571
|
-
else if (e.type === "log") { ciRes(e.id).log += e.text; if (e.id
|
|
572
|
-
else if (e.type === "end") { const r = ciRes(e.id); r.status = e.code === 0 ? "pass" : "fail"; r.ms = e.ms; if (e.code !== 0
|
|
707
|
+
if (e.type === "start") { ciRes(e.id).status = "running"; if (ci.follow) { ci.selected = e.id; ci.expanded.add(e.id); } ciRender(); }
|
|
708
|
+
else if (e.type === "log") { ciRes(e.id).log += e.text; if (ci.expanded.has(e.id)) ciSoftRender(); }
|
|
709
|
+
else if (e.type === "end") { const r = ciRes(e.id); r.status = e.code === 0 ? "pass" : "fail"; r.ms = e.ms; if (e.code !== 0) { ci.expanded.add(e.id); if (ci.follow) ci.selected = e.id; } else if (ci.follow) ci.expanded.delete(e.id); ciRender(); }
|
|
573
710
|
else if (e.type === "skip") { ciRes(e.id).status = "skip"; ciRender(); }
|
|
574
711
|
}
|
|
575
712
|
|
|
576
|
-
async function ciStart() {
|
|
577
|
-
const ids = ci.steps.filter((st) => ci.enabled.has(st.id)).map((st) => st.id);
|
|
713
|
+
async function ciStart(onlyIds) {
|
|
714
|
+
const ids = onlyIds ?? ci.steps.filter((st) => ci.enabled.has(st.id)).map((st) => st.id);
|
|
578
715
|
if (!ids.length) return;
|
|
716
|
+
ci.expanded.clear();
|
|
579
717
|
ci.results = {};
|
|
580
718
|
for (const id of ids) ciRes(id);
|
|
581
|
-
ci.finished = false; ci.error = null; ci.seconds = null; ci.follow = true; ci.startedAt = Date.now();
|
|
719
|
+
ci.finished = false; ci.cancelled = false; ci.error = null; ci.seconds = null; ci.follow = true; ci.startedAt = Date.now();
|
|
582
720
|
const ctrl = new AbortController();
|
|
583
721
|
ci.run = ctrl;
|
|
584
722
|
const tick = setInterval(ciRenderStats, 1000);
|
|
@@ -606,6 +744,7 @@ async function ciStart() {
|
|
|
606
744
|
}
|
|
607
745
|
} catch (e) {
|
|
608
746
|
if (e.name === "AbortError") {
|
|
747
|
+
ci.cancelled = true;
|
|
609
748
|
for (const r of Object.values(ci.results)) {
|
|
610
749
|
if (r.status === "running") r.status = "cancelled";
|
|
611
750
|
else if (r.status === "pending") r.status = "skip";
|
|
@@ -627,9 +766,36 @@ $("#ciSteps").addEventListener("click", (e) => {
|
|
|
627
766
|
ciRender();
|
|
628
767
|
return;
|
|
629
768
|
}
|
|
630
|
-
const
|
|
631
|
-
if (
|
|
769
|
+
const job = e.target.closest("[data-select]");
|
|
770
|
+
if (job) {
|
|
771
|
+
ci.selected = job.dataset.select; ci.expanded.add(ci.selected); ci.follow = false; ciRender();
|
|
772
|
+
document.getElementById(`ci-step-${ci.selected}`)?.scrollIntoView({ block: "nearest" });
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
$("#ciPanel").addEventListener("click", (e) => {
|
|
777
|
+
const head = e.target.closest("[data-toggle]");
|
|
778
|
+
if (!head) return;
|
|
779
|
+
const id = head.dataset.toggle;
|
|
780
|
+
if (ci.expanded.has(id)) ci.expanded.delete(id); else ci.expanded.add(id);
|
|
781
|
+
ci.selected = id; ci.follow = false; ciRender();
|
|
782
|
+
$("#ciPanel").querySelector(`[data-toggle="${id}"]`)?.focus();
|
|
783
|
+
});
|
|
784
|
+
$("#ciExpand").addEventListener("click", () => {
|
|
785
|
+
const visible = ci.steps?.filter((st) => ci.enabled.has(st.id) || ci.results[st.id]) ?? [];
|
|
786
|
+
const allOpen = visible.every((st) => ci.expanded.has(st.id));
|
|
787
|
+
for (const st of visible) { if (allOpen) ci.expanded.delete(st.id); else ci.expanded.add(st.id); }
|
|
788
|
+
ci.follow = false; ciRender();
|
|
789
|
+
});
|
|
790
|
+
$("#ciFollow").addEventListener("click", () => {
|
|
791
|
+
ci.follow = !ci.follow;
|
|
792
|
+
if (ci.follow) {
|
|
793
|
+
const active = ci.steps?.find((st) => ci.results[st.id]?.status === "running");
|
|
794
|
+
if (active) { ci.selected = active.id; ci.expanded.add(active.id); }
|
|
795
|
+
}
|
|
796
|
+
ciRender();
|
|
632
797
|
});
|
|
798
|
+
$("#ciRetry").addEventListener("click", () => ciStart(ci.steps.filter((st) => ci.results[st.id]?.status === "fail").map((st) => st.id)));
|
|
633
799
|
|
|
634
800
|
showView(location.hash === "#ci" ? "ci" : "lint");
|
|
635
801
|
</script>
|
package/bin/ci.mjs
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
const MAX_LOG_CHARS = 2_000_000;
|
|
5
|
-
|
|
6
|
-
const RUN_SCRIPT = {
|
|
7
|
-
bun: (name) => ["bun", "run", name],
|
|
8
|
-
npm: (name) => ["npm", "run", name],
|
|
9
|
-
pnpm: (name) => ["pnpm", "run", name],
|
|
10
|
-
yarn: (name) => ["yarn", "run", name],
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// Same commands the generated GitHub workflow uses for its install step.
|
|
14
|
-
const FROZEN_INSTALL = {
|
|
15
|
-
bun: ["bun", "install", "--frozen-lockfile"],
|
|
16
|
-
npm: ["npm", "ci"],
|
|
17
|
-
pnpm: ["pnpm", "install", "--frozen-lockfile"],
|
|
18
|
-
yarn: ["yarn", "install", "--frozen-lockfile"],
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const NPM_DEFAULT_TEST = /no test specified/i;
|
|
22
|
-
|
|
23
|
-
function firstScript(scripts, names) {
|
|
24
|
-
return names.find((name) => typeof scripts[name] === "string");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Work out which CI steps this project can run locally. Only commands the
|
|
29
|
-
* project itself defines (package.json scripts, its own Biome) are offered,
|
|
30
|
-
* and the browser only ever sends step ids back — never command lines.
|
|
31
|
-
*/
|
|
32
|
-
export function detectCiSteps({ cwd, biome, pmName, pkg }) {
|
|
33
|
-
const scripts = pkg?.scripts ?? {};
|
|
34
|
-
const run = RUN_SCRIPT[pmName] ?? RUN_SCRIPT.npm;
|
|
35
|
-
const steps = [];
|
|
36
|
-
|
|
37
|
-
steps.push({
|
|
38
|
-
id: "install",
|
|
39
|
-
label: "Install (frozen lockfile)",
|
|
40
|
-
cmd: FROZEN_INSTALL[pmName] ?? FROZEN_INSTALL.npm,
|
|
41
|
-
defaultOn: false,
|
|
42
|
-
note: "What CI does first. Reinstalls node_modules, so it's off by default.",
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
steps.push({
|
|
46
|
-
id: "biome",
|
|
47
|
-
label: "Biome CI (lint + format)",
|
|
48
|
-
cmd: [biome, "ci", "--colors=off", "."],
|
|
49
|
-
display: "biome ci .",
|
|
50
|
-
defaultOn: true,
|
|
51
|
-
note: "The same check the GitHub workflow runs.",
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const typecheck = firstScript(scripts, ["typecheck", "type-check", "check-types", "tsc"]);
|
|
55
|
-
if (typecheck) {
|
|
56
|
-
steps.push({
|
|
57
|
-
id: "typecheck",
|
|
58
|
-
label: "Typecheck",
|
|
59
|
-
cmd: run(typecheck),
|
|
60
|
-
defaultOn: true,
|
|
61
|
-
});
|
|
62
|
-
} else if (
|
|
63
|
-
existsSync(join(cwd, "tsconfig.json")) &&
|
|
64
|
-
existsSync(join(cwd, "node_modules", ".bin", "tsc"))
|
|
65
|
-
) {
|
|
66
|
-
steps.push({
|
|
67
|
-
id: "typecheck",
|
|
68
|
-
label: "Typecheck",
|
|
69
|
-
cmd: [join(cwd, "node_modules", ".bin", "tsc"), "--noEmit"],
|
|
70
|
-
display: "tsc --noEmit",
|
|
71
|
-
defaultOn: true,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (typeof scripts.build === "string") {
|
|
76
|
-
steps.push({ id: "build", label: "Build", cmd: run("build"), defaultOn: true });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (typeof scripts.test === "string" && !NPM_DEFAULT_TEST.test(scripts.test)) {
|
|
80
|
-
steps.push({ id: "test", label: "Tests", cmd: run("test"), defaultOn: true });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return steps.map((step) => ({ ...step, display: step.display ?? step.cmd.join(" ") }));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Run steps one after another and stream NDJSON events:
|
|
88
|
-
* start / log / end / skip / done
|
|
89
|
-
* Closing the response (the browser's Stop button) kills the running process.
|
|
90
|
-
*/
|
|
91
|
-
export function ciStream({ cwd, steps, stopOnFail, release }) {
|
|
92
|
-
const encoder = new TextEncoder();
|
|
93
|
-
let proc = null;
|
|
94
|
-
let cancelled = false;
|
|
95
|
-
|
|
96
|
-
return new ReadableStream({
|
|
97
|
-
async start(controller) {
|
|
98
|
-
const send = (event) => {
|
|
99
|
-
if (!cancelled) controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
|
|
100
|
-
};
|
|
101
|
-
let failed = false;
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
for (const step of steps) {
|
|
105
|
-
if (cancelled) break;
|
|
106
|
-
if (failed && stopOnFail) {
|
|
107
|
-
send({ type: "skip", id: step.id });
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
send({ type: "start", id: step.id });
|
|
112
|
-
const t0 = performance.now();
|
|
113
|
-
let code = 1;
|
|
114
|
-
let sent = 0;
|
|
115
|
-
let truncated = false;
|
|
116
|
-
|
|
117
|
-
const pump = async (readable) => {
|
|
118
|
-
const decoder = new TextDecoder();
|
|
119
|
-
for await (const chunk of readable) {
|
|
120
|
-
const text = decoder.decode(chunk, { stream: true });
|
|
121
|
-
if (sent < MAX_LOG_CHARS) {
|
|
122
|
-
sent += text.length;
|
|
123
|
-
send({ type: "log", id: step.id, text });
|
|
124
|
-
} else if (!truncated) {
|
|
125
|
-
truncated = true;
|
|
126
|
-
send({ type: "log", id: step.id, text: "\n… output truncated …\n" });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
proc = Bun.spawn(step.cmd, {
|
|
133
|
-
cwd,
|
|
134
|
-
env: { ...process.env, CI: "true", NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
135
|
-
stdin: "ignore",
|
|
136
|
-
stdout: "pipe",
|
|
137
|
-
stderr: "pipe",
|
|
138
|
-
});
|
|
139
|
-
await Promise.all([pump(proc.stdout), pump(proc.stderr)]);
|
|
140
|
-
code = await proc.exited;
|
|
141
|
-
} catch (error) {
|
|
142
|
-
send({ type: "log", id: step.id, text: `\n${error.message}\n` });
|
|
143
|
-
code = 127;
|
|
144
|
-
}
|
|
145
|
-
proc = null;
|
|
146
|
-
|
|
147
|
-
if (cancelled) break;
|
|
148
|
-
if (code !== 0) failed = true;
|
|
149
|
-
send({ type: "end", id: step.id, code, ms: Math.round(performance.now() - t0) });
|
|
150
|
-
}
|
|
151
|
-
send({ type: "done" });
|
|
152
|
-
} finally {
|
|
153
|
-
release();
|
|
154
|
-
if (!cancelled) controller.close();
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
cancel() {
|
|
158
|
-
cancelled = true;
|
|
159
|
-
proc?.kill();
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
}
|
package/bin/commit-config.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Reads the optional commit settings from the project's package.json:
|
|
2
|
-
//
|
|
3
|
-
// "lintlane": {
|
|
4
|
-
// "commit": {
|
|
5
|
-
// "types": ["feat", "fix", "chore"],
|
|
6
|
-
// "messageLengthLimit": 72,
|
|
7
|
-
// "allowedPrefixes": ["WIP"],
|
|
8
|
-
// "retryAfterFailure": true
|
|
9
|
-
// }
|
|
10
|
-
// }
|
|
11
|
-
//
|
|
12
|
-
// Plain Node, never throws: a missing or broken file just means defaults.
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
|
|
16
|
-
export function loadCommitConfig(cwd = process.cwd()) {
|
|
17
|
-
try {
|
|
18
|
-
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
19
|
-
const commit = pkg?.lintlane?.commit;
|
|
20
|
-
return commit && typeof commit === "object" ? commit : {};
|
|
21
|
-
} catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
}
|