SLOPSHOPPER

workstation-lint-fold

Folds a passing run of the workstation lint (scripts/check-invariants.sh) in a Bash result to one line before Claude reads it; a failing run stays whole.

A shopper browsing a rack in a slop shop
README

workstation

Dev-environment provisioning for Linux hosts (native and WSL), plus Warp and Windows Terminal on Windows. One command per host installs the tools, system packages, /etc files, services and dotfiles.

What this is

One entry script per OS (bootstrap.sh, bootstrap.ps1) installs a pinned mise (mise.jdx.dev) and hands off to it. mise owns every tool and every piece of host state. This checkout is mise's global config directory: ~/.config/mise on Linux, %USERPROFILE%\.config\mise on Windows.

PieceRole
bootstrap.sh / bootstrap.ps1Install pinned mise, decide the system steps, run mise. Nothing is installed by hand in the scripts beyond mise (and, on Windows, SSHFS-Win).
mise toolsEvery tool is a pin in config.toml / config.linux.toml. mise ls is the tool list.
mise bootstrapHost state from [bootstrap.*] tables: dnf packages, /etc files, services, repos, dotfiles, then the tools and the bootstrap task.
mise dot ([dotfiles])Personal config under $HOME, templated per machine. Every deployed file is an independent copy, never a symlink into the checkout.
starship, zsh plugins, fzf, zoxidePrompt, completion, fuzzy find and directory jumping.
zellijPersistent sessions; runs on the remote host so a dropped SSH tab loses nothing.
helixModal editor and $EDITOR, unless the shell inherits one, as a ws open workspace's panes do (dotfiles/config/helix). It uses Catppuccin Mocha with the mauve accent and the same clangd flags as Neovim. clang-format runs on save only where the project has a .clang-format. Files auto-save 3 s after an edit, and yanks copy over OSC 52 on Linux (the Windows clipboard on Windows).
Warp, Windows Terminal, WezTermThe Windows terminals (see Terminals).

Nothing is pushed anywhere and there is no host list: each host keeps itself current with wsu.

One setup, two OS files

Every host gets the same setup. Three tracked config files hold it, plus the git-ignored config.local.toml for per-host values, and miserc.toml (git-ignored, written by scripts/lib/mise-env.sh on Linux and bootstrap.ps1 on Windows) picks the OS file:

FileLoads onHolds
config.tomlevery hosttools for both OSes (Windows-only and Linux-only entries carry os = [...]), [vars] pins, cross-platform dotfiles
config.linux.tomlLinuxthe Linux toolbelt, dnf packages, /etc/wsl.conf, hooks, Linux dotfiles
config.windows.tomlWindowsWindows dotfiles, winget GUI apps
config.local.tomlevery host, git-ignoredname, email, sudo, per-host overrides

System steps and sudo. dnf packages, /etc files and zsh as the login shell need sudo. bootstrap.sh checks once: cached sudo or a password prompt that succeeds means they run; a failed prompt saves sudo = "no" in config.local.toml, and from then on bootstrap.sh, mise run update and wsu skip them (mise bootstrap --skip packages,files) while everything user-level still installs. An unattended run with no terminal skips them for that run only. To apply them later, get sudo and re-run ./bootstrap.sh. mise run health shows the state in its "system steps" row.

A compiler without sudo (Linux). Every Linux host also gets mise's zig, which adds only zig to PATH, so a host's own gcc always wins. Neovim builds its parsers with it when there is no cc. CC=zcc CXX=zc++ uses it for your own builds: clang-based, targeting glibc 2.28, with LLVM's libc++ and no AddressSanitizer. cmake, ninja and make stay dnf-only, because mise copies would shadow dnf's. A no-sudo host that lacks them adds e.g. cmake = "4.4.4" to its config.local.toml, then runs cd ~ && mise lock --global --platform linux-x64 cmake && mise install, which writes the git-ignored mise.local.lock.

Repo layout

bootstrap.sh, bootstrap.ps1   entry points (Linux; Windows, no admin)
config.toml, config.linux.toml, config.windows.toml
                              mise tool pins, [vars] pins, [dotfiles] entries,
                              host state as [bootstrap.*] tables (Linux)
config.local.toml             git-ignored: this host's name, email, sudo
mise.lock, mise.linux.lock, locks/
                              generated lockfiles; never hand-edit
tasks/                        file tasks with real logic (bootstrap, health, update, ...);
                              one-line wrappers are [tasks] in config.toml / config.linux.toml
scripts/, scripts/lib/        checks (check-invariants.sh, check-templates.sh), helpers, tests
dotfiles/                     every deployed source under its real name
  *.tera                      templates (zshrc, bashrc, zshenv, gitconfig, ssh/config, ...)
  config/                     ~/.config/*: starship, helix, nvim, zellij, zsh plugins, ...
  claude/                     ~/.claude/*: CLAUDE.md, hooks, skills, settings seed and enforced keys
  local/bin/                  ~/.local/bin: batpipe, winterop, zcc, zc++
  windows/                    the Windows-only sources
configs/wsl/wsl.conf          source of /etc/wsl.conf ([bootstrap.files], not [dotfiles])
docs/claude/, .claude/        notes, hooks and settings for Claude Code
docs/windows/application_list.md   apps to hand-install on a fresh Windows machine

Dotfile modes: template for the .tera sources, copy for everything else, on every platform. A directory source carries exclude = [".vendor", ".gitkeep"]. ~/.claude/settings.json is not a dotfile entry: a jq merge (scripts/lib/claude-settings-merge.sh; on Windows scripts/claude-settings-merge.ps1, with the same filter) combines dotfiles/claude/settings.seed.json, the live file and settings.enforced.json, the last winning. wsu runs it on both OSes.

Setup

Linux

Prerequisites: curl, git, tar (a single preflight lists every missing one at once), and free space in $HOME for the tools: the linux figure in disk-budget.toml plus 1 GB, less what is already installed (about 6.9 GB fresh; none when every tool is installed). Without it the tools step stops before installing anything and lists the largest folders in your home; WORKSTATION_SKIP_DISK_CHECK=1 overrides. RHEL-family EL8 and EL9 are supported (tested on AlmaLinux 9.8 and RHEL 8.10). On EL8, bear isn't packaged and is skipped; plain gdb is pwndbg's bundled GDB 17 on every host, so GEF gets a modern gdb there too. Then, on the host:

curl -fsSL https://raw.githubusercontent.com/ArrushC/workstation/main/bootstrap.sh | bash && \
exec zsh   # or: source ~/.bashrc, if you can't chsh on this host

The repo is public, so no token or SSH key is needed. GITHUB_TOKEN is optional: mise uses it to lift GitHub's 60-requests/hour anonymous API limit. To use a (public) fork, change DOTFILES_REPO at the top of bootstrap.sh ($DotfilesRepo in bootstrap.ps1). Your git name and email are asked once and stored in config.local.toml.

What bootstrap.sh does:

  1. Preflight: curl, git, tar.
  2. Clone this repo to ~/.config/mise (or pull it), then carry on in the checkout's own bootstrap.sh, so a pull's changes apply in the same run.
  3. Install the pinned, sha256-verified mise into ~/.local/bin.
  4. Ask your name and email once (saved in config.local.toml) and decide the system steps (sudo; see above).
  5. Write the token set to miserc.toml, then run mise bootstrap --yes (packages, /etc files, services, repos, dotfiles, the tools, the bootstrap task, then the Linux-only final hook in config.linux.toml: vcpkg, claude, fonts, which skips itself under WSL, and WezTerm's terminfo entry). The tools come from its pre-tools hook (scripts/lib/mise-install.sh: the disk check, then mise install), after the dnf batch, because node needs dnf's libatomic to start. The first run passes --force-dotfiles (why).
  6. With sudo: set zsh as the login shell: sudo usermod -s for a local account, or, for a directory (AD/LDAP) account served by SSSD, a per-host sss_override (installing sssd-tools) and an sssd restart.

It is idempotent; re-run any time. Copy your SSH key from a client with ssh-copy-id <user>@<host>.

FlagMeaning
--reinstallWipe the cloned repo (including config.local.toml, so name and email are asked again), then re-bootstrap. Prompts first. Installed tools, deployed dotfiles, SSH keys and system packages are kept.
--yes, -ySkip the --reinstall confirmation.
--help, -hUsage.

Running --reinstall from inside the repo is refused (the script would delete itself); run it from outside, with the script in memory:

curl -fsSL https://raw.githubusercontent.com/ArrushC/workstation/main/bootstrap.sh | bash -s -- --reinstall

mise run check-updates runs mise outdated --bump for every pinned tool, dnf check-update on Linux hosts, and git ls-remote for the [vars] pins. Tools tracking latest (the pypi: tools) are reported as rolling. It only reports; the weekly bump workflow (see Adding things) does the bumping.

Extras. A host's first bootstrap ends by offering the Claude Code status line (ccstatusline), also under curl | bash: use the tracked config, define one for this machine only (persisted as a per-host opt-out in config.local.toml), set a new global one (committed back to dotfiles/config/ccstatusline/settings.json), or skip. Re-run any time with mise run statusline. On native (non-WSL) Linux hosts the fonts task installs JetBrainsMono Nerd Font Mono to ~/.local/share/fonts/JetBrainsMonoNerdFontMono/ (needed for glyphs in starship, eza, lazygit, yazi, helix); WSL hosts skip it because Windows Terminal reads Windows-registered fonts. The Claude Code installer, plugins, settings merge and herdr plugin run from tasks/claude, a final hook in config.linux.toml, so they run on Linux hosts only and only on a full mise bootstrap (not wsa). Re-run with mise run fonts or mise run claude.

Windows

The Windows host is a client. No admin is needed: everything installs under your user profile (%LOCALAPPDATA%\workstation, mise's %LOCALAPPDATA%\mise, the User PATH, CurrentUser PSGallery, HKCU fonts). One best-effort exception: SSHFS-Win depends on WinFsp, a kernel driver, so its first install raises UAC (two prompts on a host without WinFsp: one for WinFsp, one for SSHFS-Win); decline them or pass -SkipElevated and everything else still completes.

Prerequisites: Git (the script hard-fails with a link if it is missing; winget install Git.Git), a working curl.exe (curl.exe --version), and room on the drive holding %LOCALAPPDATA% for the tools: disk-budget.toml's windows figure plus 1 GB, less what is already installed (about 4 GB fresh; none when every tool is installed; $env:WORKSTATION_SKIP_DISK_CHECK = '1' overrides). PowerShell 5.1 and 7 are supported. Windows HTTP downloads use curl.exe with redirects, retries and checked exit codes.

$f = "$env:TEMP\bootstrap.ps1"; curl.exe -fsSL --retry 3 -o $f https://raw.githubusercontent.com/ArrushC/workstation/main/bootstrap.ps1
if ($?) { powershell -NoProfile -ExecutionPolicy Bypass -File $f }

Paste both lines into PowerShell. The script runs in its own Windows PowerShell process with the execution policy bypassed for that process only, so it works on a machine still at the default Restricted policy, and a failure leaves your window open with the error. Flags go at the end of the second line (... -File $f -SkipKeyGen). Re-run the same two lines any time, or .\bootstrap.ps1 from the clone if your execution policy allows scripts.

What bootstrap.ps1 does:

  1. Preflight: require curl.exe and git (it never installs Git).
  2. Clone this repo to %USERPROFILE%\.config\mise (or pull it), then carry on in the checkout's own bootstrap.ps1, so a pull's changes apply in the same run.
  3. Install the pinned, sha256-verified mise into %LOCALAPPDATA%\workstation\mise. mise is the only version bootstrap.ps1 pins; every CLI tool is a mise pin in config*.toml.
  4. Write miserc.toml (windows), ask once for your git name and email, check that mise loads config.windows.toml (it stops otherwise: the Windows dotfiles and apps would be skipped silently), and run mise bootstrap --only dotfiles,tools: the dotfiles plus every CLI tool (gh, Starship, Helix, Nushell, jq, OpenCode, omp, jcode, DevToys CLI, dnGrep, LogExpert, Node, Go, uv, gopls, language servers, ccstatusline, oh-my-mermaid, archify). The first run passes --force-dotfiles.
  5. After a successful tools phase: add mise's shims dir to the User PATH, reinstall node when its declaration changed (its postinstall carries the language servers), then mise prune and mise reshim. A changed .wslconfig prints the wsl --shutdown reminder. mise bootstrap's post-tools hook (config.windows.toml) runs mise run nu-init, which regenerates Nushell's init files (starship, mise, zoxide, atuin) in %APPDATA%\nushell\vendor\autoload, so wsu refreshes them too. The same hook merges ~/.claude/settings.json (scripts/claude-settings-merge.ps1), so a change to settings.enforced.json, such as the theme or a plugin, reaches Windows with wsu. Its mise.nu builds on each session's own PATH rather than the PATH mise activate nu saw, so Nushell finds what its terminal passes down, as PowerShell does.
  6. Install the missing GUI apps: mise bootstrap --only packages installs config.windows.toml's [bootstrap.packages] winget list (Windows Terminal, Warp, Obsidian, DevToys, DBeaver, WinSCP, Beyond Compare, Zed; latest, checked against the winget manifest's sha256, each self-updating). winget's own settings.json, a tracked dotfile, prefers per-user installers; Zed's only installer is machine scope but installs per-user, without admin. An app counts as installed when winget list --id <Id> --exact finds it. DevToys is the Microsoft Store build (9NBN8W1DS547). Then SSHFS-Win (UAC). Without winget (App Installer) the step warns and skips.
  7. Add Start Menu shortcuts for dnGrep and LogExpert, generate the Warp Tab Configs (local shells plus one per SSH host) and the Windows Terminal SSH fragment, seed dnGrep's settings, and install the PowerShell profile loader when Documents is redirected.
  8. Install BurntToast and Claude Code (native installer, self-updating; ~\.local\bin goes on the User PATH), merge ~/.claude/settings.json again (for -SkipToolInstall, which runs no hook) and seed settings.local.json.
  9. Build the wpy Python env (a uv venv on mise's python with the libraries in scripts/python-env.txt) and install the Nerd Font (mise's github:ryanoasis/nerd-fonts, registered per-user by scripts/install-nerd-fonts.ps1 with a logon task that re-activates it).
  10. Prompt for an SSH key, then print docs/windows/application_list.md as a hand-install checklist.

Restart the shell afterwards so the new profile loads. Nushell is the default local shell (in Windows Terminal and Zed, both through mise's shim, %LOCALAPPDATA%\mise\shims\nu.exe); PowerShell stays for .NET/COM/registry tasks. Zed's SSH remote terminals use the remote's login shell instead: Zed sends its settings to the remote, so the Nushell setting sits under its "windows" key. Nushell's command-line colours match zsh's on the Linux hosts (commands green, unknown commands red, quoted strings yellow, flags peach, paths underlined): autoload/syntax-highlight.nu overrides the vendored Catppuccin theme's, and lint keeps the two in step. One gap: Nushell gives every argument of an external command (ssh, git) one colour, so its flags and quoted strings stay plain where zsh colours them. atuin's own background jobs don't light starship's jobs gear (scripts/nu-init.nu labels them). Both shells carry the zsh toolbelt (wsh lists it):

  • z/zi (zoxide), l/la/ll/lt/lta/ltl (eza), y (yazi), fssh (pick an SSH host from ~/.ssh/config and the files it Includes, so config.local's too; the same on Linux) and hide
  • Ctrl-R history search (atuin, local only; each shell's earlier history is imported once), Up/Down for history starting with what's typed (in Nushell that includes what other tabs ran: its own history is SQLite), Alt-. for the previous line's last word
  • Ctrl-T to insert a file and Alt-C to cd into a directory (fzf, with bat/eza previews)
  • Nushell: Tab opens a completion menu with descriptions for about 1,000 CLIs (carapace, after bootstrap.ps1's own flags), as wide as the pane allows, so a long path shows the part that differs; an alias completes as its expansion, so gco <Tab> offers branches, and wse/wsd complete the tracked targets; Ctrl-O edits the command line in Helix; a command that isn't found but mise installs is named, with the way out; ls shows timestamps as ll does
  • Speed: nu-init writes both shells' tool init after every tools phase, and mise's prompt hook runs only when a config file, an installed tool or a MISE_* variable changes (WORKSTATION_MISE_GATE=0 turns that check off), so a prompt costs milliseconds rather than a mise.exe start
  • PowerShell: gc/gp/gl run git (PowerShell's built-in aliases are removed first), and the prompt shows a failed command's exit code

rm goes to the Recycle Bin (rm --permanent skips it). The theme is Catppuccin Mocha, vendored into %APPDATA%\nushell\autoload\; ls, eza and the completion menu use the Linux hosts' LS_COLORS (from dotfiles/dircolors), and PowerShell's command line has zsh's colours.

FlagMeaning
-RepoPath <dir>Clone somewhere other than %USERPROFILE%\.config\mise.
-SkipKeyGenSkip the SSH-key prompt.
-SkipToolInstallSkip mise, the mise tools phase, the winget GUI apps, the Python env and Claude Code (assume present; the post-tools hook, which runs nu-init, the Neovim plugins, the archify skill copy and the WezTerm nightly install, doesn't run either).
-SkipDotfilesClone and install tools but do not apply dotfiles. On a host's first run this also skips winget's settings.json (the per-user preference), so the GUI apps install with winget's default scope.
-SkipBurntToastSkip the BurntToast PowerShell module install.
-SkipNerdFontsSkip the Nerd Font install.
-SkipElevatedSkip SSHFS-Win/WinFsp, the one UAC prompt. Warp's VC++ runtime dependency can also raise UAC on a host without that runtime; this flag does not cover it.
-ReinstallWipe the cloned repo, then re-bootstrap. Prompts unless -Yes.
-YesSkip confirmation prompts (-Reinstall).

-Reinstall from inside the repo is refused, since the script would delete itself; the download above runs a copy from %TEMP%, so put -Reinstall at the end of its second line.

Health and updates. There is no report mode in bootstrap.ps1. Use mise doctor, mise bootstrap status and mise dot status for the tools, host state and dotfiles. wsu updates the dotfiles and every CLI tool. The GUI apps are config.windows.toml's [bootstrap.packages]: mise bootstrap packages status lists them as installed or missing, mise bootstrap packages apply --manager winget installs the missing ones, and mise bootstrap packages upgrade --manager winget updates them. To add one, add a "winget:<Id>" = "latest" line (the Id from winget search).

A mise version bump reaches Windows through wsu's pull, but the installed mise is then older than config.toml's min_version and refuses the new config, and so does every mise shim, including Windows Terminal's Nushell. Re-run .\bootstrap.ps1 from a Windows PowerShell window to install the new mise.

Windows Terminal's settings.json (Catppuccin Mocha, Nushell default profile) is a tracked copy dotfile deployed straight to the Store package's LocalState. Warp's settings.toml and keybindings.yaml (%LOCALAPPDATA%\warp\Warp\config\) and theme (%APPDATA%\warp\Warp\data\themes\) are tracked too. Both apps rewrite their own files, so record changes made in their UIs with wsr. The generated parts are separate: Warp workstation-*.toml Tab Configs in %APPDATA%\warp\Warp\data\tab_configs\, and the Windows Terminal fragment under %LOCALAPPDATA%\Microsoft\Windows Terminal\Fragments\workstation\. Your own Tab Configs and profiles are never touched.

SSH host launchers. Every concrete Host alias in the git-ignored ~/.ssh/config.local becomes an SSH: <alias> Warp Tab Config and Windows Terminal profile that runs ssh -t <alias> zellij attach --create main. Patterns and ! negations are skipped. WezTerm lists the same hosts in its launcher, and CTRL+ALT+S there adds, edits or deletes one without opening the file (see Terminals). Re-run .\bootstrap.ps1 after a change, by hand or from WezTerm, then restart Warp or Windows Terminal.

Example: Host build1 with HostName build1.example.com in %USERPROFILE%\.ssh\config.local.

WSL

WSL distros are ordinary managed Linux hosts, reached through the AlmaLinux-9 profile in Windows Terminal or Warp (or wsl.exe). bootstrap.sh detects WSL ($WSL_DISTRO_NAME, /proc/version) and the rest of the flow is unchanged. Inside the distro:

sudo dnf install -y curl git tar python3 python3-pip    # RHEL / AlmaLinux / Fedora
sudo apt install -y curl git tar python3 python3-pip   # Debian / Ubuntu
curl -fsSL https://raw.githubusercontent.com/ArrushC/workstation/main/bootstrap.sh | bash

Run it where you have sudo (the usual case); without it the system steps, /etc/wsl.conf included, are skipped. Two files configure WSL and the repo owns both:

  • /etc/wsl.conf (per-distro: systemd, automount, interop, default user, appendWindowsPath=false): deployed on Linux hosts by config.linux.toml from configs/wsl/wsl.conf (on a non-WSL host nothing reads it).
  • %USERPROFILE%\.wslconfig (VM memory, CPU, networking, all distros): deployed on the Windows host by a copy dotfile from dotfiles/wslconfig. It enables autoMemoryReclaim=gradual and pre-opts into sparseVhd=true.

Both need wsl --shutdown from a Windows terminal to take effect. bootstrap.ps1 prints that reminder when .wslconfig changes; on Linux, remember it yourself after a /etc/wsl.conf change. WSL tabs open in ~ because the WSL Terminal fragment launches wsl.exe -d AlmaLinux-9 --cd ~. Every tracked shell exports COLORTERM=truecolor, so 24-bit color works in Windows Terminal. Keep long-lived work in a zellij session.

Terminals (Warp, Windows Terminal and WezTerm)

Warp is the day-to-day Windows terminal and opens into AlmaLinux-9 (WSL zsh). Its "+" menu (CTRL+ALT+L) lists the generated Tab Configs. Windows Terminal stays fully configured because Warp cannot run Nushell (its supported shells are PowerShell, WSL2 and Git Bash) and cannot take Windows' default-terminal role. Warp skips the shell-side fzf, atuin, starship, shift-select and fzf-tab wiring (TERM_PROGRAM=WarpTerminal), since its own editor handles those. The pane mnemonics follow zellij's pane mode (d = down, r = right) so muscle memory transfers between local panes and SSH plus zellij.

WarpAction
ALT+SHIFT+D / RSplit pane down / right
ALT+SHIFT+arrowsMove between panes
CTRL+SHIFT+ZToggle pane maximize
CTRL+= / - / 0Font size bigger / smaller / reset
CTRL+RCommand history search
CTRL+↑/↓Select previous / next block
ALT+↑/↓Jump between bookmarked blocks
CTRL+SHIFT+PCommand palette
CTRL+ALT+LTab Config palette
Windows TerminalAction

| ALT+SHIFT+D / R | Spl

Source 2 files
hooks/register.ts 34 lines
1import type { Register } from 'claude-code'
2
3import { fold } from './fold'
4
5type Block = { type: string; text?: string; content?: unknown }
6
7// A text block, or a tool_result's content (a string, or text blocks), with each
8// passing lint run folded; the same object when nothing folded.
9function foldBlock(block: Block): Block {
10  if (block.type === 'text' && typeof block.text === 'string') {
11    const text = fold(block.text)
12    return text === undefined ? block : { ...block, text }
13  }
14  if (block.type !== 'tool_result') return block
15  if (typeof block.content === 'string') {
16    const content = fold(block.content)
17    return content === undefined ? block : { ...block, content }
18  }
19  if (!Array.isArray(block.content)) return block
20  const parts = (block.content as Block[]).map(foldBlock)
21  return parts.every((part, i) => part === (block.content as Block[])[i]) ? block : { ...block, content: parts }
22}
23
24// What the model reads of a Bash result: session.append stores the row the next request
25// sends, while the tool's own record (what the transcript draws, ctrl+o) keeps the run whole.
26export const register: Register = on => {
27  on('session.append', { door: 'tool-result' }, ($, e, next) => {
28    if (e.origin.kind !== 'tool' || e.origin.tool !== 'Bash') return next(e)
29    const content = (e.message.content as Block[]).map(foldBlock)
30    if (content.every((block, i) => block === e.message.content[i])) return next(e)
31    return next({ ...e, message: { ...e.message, content: content as typeof e.message.content } })
32  })
33}
34
hooks/fold.ts 27 lines
1// One run of scripts/check-invariants.sh, from its banner to its verdict, folded to a
2// line when the verdict is a pass. A run that failed, or was cut off before its verdict,
3// comes back undefined: the caller keeps the text whole. The script's own lines are
4// matched with their colour codes taken off: hdr() `==> name`, ok() `   ✓ text`,
5// bad() `   ✗ text`, note() `   · text` (a check that was skipped, kept in the fold).
6const COLOUR = /\x1b\[[0-9;]*m/g
7const BANNER = '== workstation invariant check =='
8const PASSED = '✓ all invariant checks passed'
9
10export function fold(text: string): string | undefined {
11  const lines = text.split('\n')
12  const plain = lines.map(line => line.replace(COLOUR, ''))
13  const start = plain.findIndex(line => line.trim() === BANNER)
14  if (start < 0) return undefined
15  const end = plain.findIndex((line, i) => i > start && line.trim() === PASSED)
16  if (end < 0) return undefined
17  const run = plain.slice(start + 1, end)
18  if (run.some(line => line.trimStart().startsWith('✗'))) return undefined
19
20  const checks = run.filter(line => line.trimStart().startsWith('✓ ')).length
21  const sections = run.filter(line => line.startsWith('==> ')).length
22  const notes = run.filter(line => line.trimStart().startsWith('· ')).map(line => `   ${line.trim()}`)
23  const summary = `${PASSED}: ${checks} checks in ${sections} sections (folded by workstation-lint-fold; run it again to see them)`
24
25  return [...lines.slice(0, start), summary, ...notes, ...lines.slice(end + 1)].join('\n')
26}
27