Clarified default port and override options.
PlanConfirmed failure behavior and override support.
PlanRun a /grilling session.
Instead of reading .md file only at root level, it recursively searches all folders with files. the file in the ui is showed with name and a truncated version of file path.
I'll start by reading the skill file to understand what a grilling session involves.
>ls -R /Users/aal/.pi/agent/skills/grill-me | head -50; echo "---"; pwd; ls>cd /Users/aal/Projects/mkv && cat README.md && find . -type f -not -path "./.git/*" | head -50I need to check the sidebar styling in style.css and verify whether PLAN.md actually exists since README references it. Before diving deeper, I should run through a critical interview with myself to stress-test the plan—what are the real edge cases here? The main unknowns are how deep the recursion should go and which directories to skip (hidden folders, node_modules, symlinks, vendor directories), plus how the sidebar should actually display file names and their truncated versions.
>cd /Users/aal/Projects/mkv && sed -n '1,60p' web/style.css && echo ... && grep -n "file-list" -A 30 web/style.css | head -60I've read the code. Skeleton plan is at PLAN.md. Now, grilling round 1 — your two-sentence spec hides a lot of decisions:
A. What counts as "all folders"?
.git, .obsidian)? All dotted dirs, or an explicit denylist? What about node_modules, vendor, target?.gitignore? (Big scope difference — would need a new dependency or hand-rolled matcher.)mkv in $HOME today lists ~5 files; recursively it could walk millions of inodes and hang the request, since scanning happens on every page render, not once at startup.B. What exactly does "truncated path" mean?
5. Two-line entry (bold name on line 1, dim path on line 2) or one line (name + inline dim suffix)?
6. Is the path the containing directory (docs/api/) or the full relative path incl. filename (docs/api/auth.md — which then duplicates the name)?
7. Truncation from the left (…/api/auth), the middle, or plain CSS ellipsis at the end? Do you want a title=/tooltip with the untruncated path?
8. What does a root-level file show — empty second line, or ./?
C. Ordering & identity
9. Flat list sorted by full path, or grouped under collapsible directory headers? (Flat is far less work; grouping is a real UI feature.)
10. Sort root files first, or purely lexicographic so docs/a.md sorts before readme.md?
11. Multiple README.md in different folders are now ambiguous by name — I'll make the URL carry the relative path (?name=docs/api/README.md) and rewrite readFileSafe to validate containment via filepath.Rel on the cleaned absolute path instead of the current filepath.Base check. Confirm you're fine with paths appearing in URLs?
D. Scope 12. Should scan results be cached (scan once at startup, or cache with a TTL), or keep re-scanning per request for freshness? 13. Do you want a sidebar filter/search box now that the list can be hundreds of entries, or is that explicitly out of scope for this change?
A. 1.yes skip 2.don't honor .gitignore 3.skip 4. load max 100 files recuresively. B. 5. one line 6.full relative path 7. truncation from left 8../ C. 9.grouped on collasible headers 10. sort root files first 11. ok with path apprearing in URLs D. 12. scan at startup 13. out of scope
I need to push back on a few answers from the first round. The skip-hidden-directories answer glosses over what happens with node_modules and vendor directories—they didn't specify whether those get excluded. And the 100-file maximum is vague about its behavior: does it silently truncate, warn the user, or follow some deterministic order? There's also a conflict between wanting root files first and respecting the lexicographic walk order when the cap gets hit mid-traversal.
Good — that resolves most of it. But two of your answers collide, so round 2:
1. The big conflict: grouping vs. per-entry path.
You want C9 collapsible directory headers AND B6 full relative path on each entry. If the header already says docs/api/, every child repeating docs/api/auth.md is pure noise. Which do you actually want?
title tooltip), or2. If grouped: shape of the tree.
docs/api/), or a real nested indented tree (docs > api > file)? Flat groups are much simpler and read better in a narrow sidebar../ header, or as ungrouped entries pinned above the first header?localStorage (like the theme) or reset on every page load? Note every click is a full page reload in this app, so without persistence the tree re-expands/collapses on every navigation.3. The 100-file cap.
showing 100 of N files)? Silent truncation on a notes folder would be genuinely confusing.4. Directory skipping, unfinished.
"Skip hidden" I have as: any dir whose name starts with .. Do you also want a hardcoded denylist of node_modules, vendor, target, dist, build? Or purely dotfiles for now?
5. Truncation mechanics (only if you pick (b) or (c)).
Left-truncation to a fixed character budget server-side (…/api/auth.md, predictable, works in title), or CSS-based (direction: rtl + text-overflow: ellipsis, adapts to sidebar width but is a well-known hack that mangles leading punctuation)? And what budget — ~40 chars?
6. Startup-only scan, consequence check.
Files created/renamed/deleted while mkv runs won't appear or disappear until restart, and clicking a since-deleted file gives a 500. Accept as-is, or add a cheap re-scan (e.g. rescan if the last scan is >5s old) to keep the "just run it in a notes folder" ergonomics?
I'm noticing a contradiction in the requirements around .gitignore handling—the latest guidance says to use it for excluded files, but that conflicts with the earlier "don't honor .gitignore" stance. I need to clarify which .gitignore files apply (root only or nested directories?), whether they're using full gitignore semantics with negations and patterns or a simplified subset, and whether dot-directories should still be skipped. There are also some unresolved details from the earlier discussion about root file placement, default collapse state, and persistence that need to be pinned down.
Round 3 — your answer to 4 reverses your earlier "don't honour .gitignore", and answer 1 knocks out part of the original brief. Both need nailing down:
1. .gitignore — how much of it?
Full git semantics is a real feature (nested .gitignore files, negation !, anchored /foo, **, trailing-slash dir-only, .git/info/exclude, global core.excludesFile). Pick one:
.gitignore only, hand-rolled subset: blank/# comments, /-anchored, */** globs, trailing-slash dirs, ! negation. ~80 lines, no new dependency, matches ~95% of real files..gitignore at every level, hand-rolled — noticeably more work, more edge cases.github.com/sabhiram/go-gitignore or similar and get it right for free — but breaks your current one-dependency, no-CGo purity.Also: does .gitignore replace the dot-dir skip or stack on top of it? I'd stack (always skip .-dirs, plus apply .gitignore), which means node_modules is only skipped if it's actually gitignored — fine for repos, but a plain notes folder with no .gitignore gets nothing filtered. Confirm?
2. Answer 1 = (a) means the entry shows the name only.
So the sidebar no longer shows a per-file path anywhere except the tooltip — the "truncated path" now lives on the group header (…/docs/api/). Is that what you intend? If yes, CSS left-truncation (answer 5) applies to headers, not file entries. Confirm.
3. Stopping the walk at 100 breaks "root files first".
filepath.WalkDir is lexicographic and descends as it goes, so in a repo with apps/, docs/, zz.md, the walk fully drains apps/ and docs/ before ever seeing root-level zz.md. Hit the cap inside apps/ and your root README never appears — which is the single file people most want. Options:
.md files first, then walk subdirectories, capping at 100 total. Guarantees root files always survive.4. Cap visibility — still unanswered. Silent, stderr line at startup, or a muted showing 100 of 100+ files note at the bottom of the sidebar? (With a hard stop at 100 you can't cheaply report the true total; you'd only know "at least 100".)
5. Group collapse — still unanswered (three parts).
./ header, or pinned ungrouped above all headers?localStorage (theme already does this) — yes/no? Every navigation is a full page reload, so without persistence the tree resets on every single click.6. Re-scan TTL. 5 seconds? And should a click on a file that has since vanished return a clean 404 "file no longer exists" instead of today's 500?
For the first answer, I'm going with the hand-rolled subset approach for the root .gitignore since they specified "root gitignore" and I don't see a new dependency being introduced. On the second point, the "group collapse" response doesn't actually answer the question
Round 4 — two of your answers were non-answers, so let me pin them:
1. "group collapse" isn't an answer. Three separate decisions, please:
.md files: under a ./ header, or pinned above all headers with no header?localStorage (same pattern as the theme switcher) — yes or no? Every file click is a full page reload, so a "no" here means the tree snaps back to its default on every single navigation. I'd say yes.2. Manual rescan — what's the trigger? You said it's an explicit command. Concretely:
POST /rescan (or GET /rescan that redirects back), so it's usable from the browser, orhttp.Serve; would need a signal like SIGHUP or stdin reading), ormkv?And: a click on a file that's been deleted since the last scan — clean 404 "file no longer available" page, or leave today's 500?
3. .gitignore implementation, unconfirmed. Root .gitignore only, hand-rolled (comments, /-anchored, */**, trailing-slash dirs, ! negation), no new dependency — confirm? And does it stack with the dot-dir skip (always skip .-prefixed dirs regardless of .gitignore), or replace it?
There's a question about whether the active file's group should auto-expand when you navigate to a nested file—otherwise clicking a file would collapse its group and hide your current location. The localStorage persistence handles this for returning users, but for deep links or first visits, I should force-expand the active file's group regardless of other state. I'll just note this as a design decision rather than asking about it.
Good — that's everything material. Writing the full plan now; I'm asserting three minor decisions you didn't explicitly rule on and flagging them so you can annotate them in review.
Continue with the approved plan.
I'm starting to implement the gitignore matcher in Go. The design uses a pattern structure that tracks whether each pattern is negated, directory-only, or anchored, then converts patterns to regex for matching. I need to handle the parsing rules: stripping trailing spaces, ignoring blank lines and comments, detecting leading ! for negation, handling trailing / to mark directory-only matches, and determining if a pattern is anchored based on whether it contains /.
Starting execution. Step 1: the gitignore matcher.
>cd /Users/aal/Projects/mkv && go vet ./internal/ 2>&1 | headI'm verifying the syntax for modifying loop variables in switch statements works correctly, then moving on to rewriting scan.go with specific requirements: returning relative paths with slashes, listing root files first followed by subdirectories in lexicographic order, filtering out dot-directories and symlinked directories, respecting gitignore rules, capping results at 100 entries, and including a truncated flag. I'll handle this with custom recursion rather than using existing walk functions.
>cd /Users/aal/Projects/mkv && go vet ./internal/ 2>&1 | headSteps 1 and 2 complete. [DONE:1] [DONE:2]
>cd /Users/aal/Projects/mkv && go vet ./internal/ 2>&1 | headGrouping helper in place. [DONE:3]
Now steps 4–6 together, since they all rewrite main.go:
>cd /Users/aal/Projects/mkv && go vet ./... 2>&1 | headmain.go rewritten: cached scan behind an RWMutex, /rescan, containment-based path validation, and 404s for unknown/vanished files. [DONE:4] [DONE:5] [DONE:6]
Sidebar markup now renders collapsible groups. [DONE:7]
Adding stylesheet rules for the file list layout, group headers with collapsible functionality, and a CSS trick for right-to-left text truncation on directory paths.
Styling done, including the RTL/<bdi> left-truncation. [DONE:8]
Collapse toggling + persistence wired up. [DONE:9]
README updated. [DONE:10] Now the verification suite from the plan:
>cd /Users/aal/Projects/mkv && go vet ./... && go build -o mkv . && echo "BUILD OK">set -e
T=$(mktemp -d)/fixture
mkdir -p "$T"/{docs/api,notes/deep/a/b,.hidden,node_modules/pkg,bulk}
cd "$T"
echo '# Root readme' > README.md
echo '# Auth' > docs/api/auth.md
echo '# C' > notes/deep/a/b/c.md
echo '# hidden' > .hidden/x.md
echo '# pkg' > node_modules/pkg/readme.md
echo '# other' > node_modules/other.md
printf 'node_modules/\n!node_modules/pkg/\nbulk/ignored*.md\n' > .gitignore
ln -s .. notes/loop
for i in $(seq 1 150); do echo "# f$i" > "bulk/f$(printf '%03d' $i).md"; done
echo '# ignored' > bulk/ignored1.md
echo "FIXTURE: $T"
echo "$T" > /tmp/mkv_fixture_pathI'm realizing my verification logic was flawed — git's actual behavior prevents re-including files when a parent directory is excluded, so the negation pattern !node_modules/pkg/ can't override the node_modules/ ignore rule. Let me trace through what my implementation actually does when it encounters the node_modules directory.
>cd "$(cat /tmp/mkv_fixture_path)" && timeout 20 /Users/aal/Projects/mkv/mkv > /tmp/mkv_out.txt 2>&1 &
sleep 1.5; cat /tmp/mkv_out.txt>cd "$(cat /tmp/mkv_fixture_path)" && (/Users/aal/Projects/mkv/mkv > /tmp/mkv_out.txt 2>&1 & echo $! > /tmp/mkv_pid)
sleep 1.5; cat /tmp/mkv_out.txt>U=$(grep -o 'http://[0-9.:]*' /tmp/mkv_out.txt | head -1)
echo "URL=$U"
curl -s "$U/" > /tmp/idx.html
echo "--- groups:"; grep -o 'data-group="[^"]*"' /tmp/idx.html
echo "--- file link count:"; grep -c 'href="/file' /tmp/idx.html
echo "--- capped note:"; grep -o '<p class="capped">[^<]*' /tmp/idx.html
echo "--- root README present:"; grep -o 'name=docs%2fapi%2fauth.md\|name=README.md' /tmp/idx.html | sort -u
echo "--- hidden/node_modules leaked?:"; grep -o 'hidden\|node_modules' /tmp/idx.html | sort -u
echo "--- ignored bulk file leaked?:"; grep -c 'ignored1.md' /tmp/idx.html>grep -o '.\{25\}hidden.\{25\}' /tmp/idx.html | head>grep -n 'hidden' /tmp/idx.html | head>U=$(grep -o 'http://[0-9.:]*' /tmp/mkv_out.txt | head -1); F=$(cat /tmp/mkv_fixture_path)
rm -rf "$F/bulk"
echo "--- before reload (stale cache), bulk still listed:"; curl -s "$U/" | grep -c 'data-group="bulk/"'
echo "--- click a now-deleted file:"; curl -s -o /dev/null -w '%{http_code}\n' "$U/file?name=bulk/f001.md"
echo "--- reload:"; curl -s -o /dev/null -w 'rescan=%{http_code} -> %{redirect_url}\n' "$U/rescan"
curl -s "$U/" > /tmp/idx2.html
echo "--- groups after reload:"; grep -o 'data-group="[^"]*"' /tmp/idx2.html
echo "--- links:"; grep -o 'href="/file?name=[^"]*"' /tmp/idx2.html
echo "--- expanded state:"; grep -o 'class="group[^"]*"' /tmp/idx2.html>U=$(grep -o 'http://[0-9.:]*' /tmp/mkv_out.txt | head -1)
curl -s "$U/file?name=docs%2fapi%2fauth.md" > /tmp/f.html
echo "--- status/title:"; grep -o '<title>[^<]*</title>' /tmp/f.html
echo "--- active group expanded?:"; grep -o 'class="group[^"]*" data-group="[^"]*"' /tmp/f.html
echo "--- rendered + toc:"; grep -c '<h1 id=' /tmp/f.html; grep -o 'class="toc"' /tmp/f.html
echo "--- traversal attacks:"
for p in '../../etc/passwd' '/etc/passwd' 'docs/../../x.md' 'notes/loop/README.md' '..%2f..%2fetc%2fpasswd.md' 'docs/api/../../README.md'; do
printf '%-32s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "$U/file?name=$p")"
done>E=$(mktemp -d); cd "$E" && (/Users/aal/Projects/mkv/mkv > /tmp/mkv_empty.txt 2>&1 & echo $! > /tmp/mkv_pid2); sleep 1
U2=$(grep -o 'http://[0-9.:]*' /tmp/mkv_empty.txt | head -1)
curl -s "$U2/" | grep -o '<p class="empty">[^<]*'
mkdir -p "$E/sub"; echo '# a' > "$E/a.md"; echo '# b' > "$E/sub/b.md"; echo '# n' > "$E/node_modules_not_a_dir.md"
curl -s "$U2/rescan" -o /dev/null; curl -s "$U2/" | grep -o 'data-group="[^"]*"\|href="/file?name=[^"]*"'
kill $(cat /tmp/mkv_pid) $(cat /tmp/mkv_pid2) 2>/dev/null; echo done>cd /Users/aal/Projects/mkv && gofmt -l . && go vet ./... && echo "fmt+vet clean"; rm -rf "$(cat /tmp/mkv_fixture_path)">cd /Users/aal/Projects/mkv && git status --short 2>/dev/null | headAll 10 steps are complete. gofmt -l flags internal/toc.go, but that file is pre-existing and untouched by this change, so I left it alone.
| File | Change |
|---|---|
internal/gitignore.go | New. Root-only .gitignore matcher, no new dependency — comments, /-anchors, */**/?, char classes, dir-only, ! negation, last-match-wins |
internal/scan.go | Recursive walk returning relative slash paths + truncated flag; files-before-subdirs at every level; skips dot-dirs, symlinked dirs, gitignored entries; stops at 100 |
internal/group.go | New. Paths → ordered groups, ./ first then lexicographic |
main.go | Scan cached on server behind RWMutex, done once at startup; GET /rescan re-walks and 303s back via a same-host Referer; safeRelPath replaces the filepath.Base check with filepath.Rel containment; unknown/vanished files → 404 |
web/* | Collapsible <section> groups with aria-expanded headers, Reload link, showing 100+ files note, RTL+<bdi> left-truncation, collapse state in localStorage |
README.md | New "How files are discovered" section; removed the top-level-only claim |
Built a fixture with a root README, nested files, .hidden/, node_modules/, a .gitignore, a self-referential directory symlink, and 151 bulk files:
notes/loop/README.md → 404, no duplicate entriesREADME.md still listed despite bulk/ sorting first, confirming the files-before-subdirs ordering earns its keep.hidden/ and node_modules/ excluded; bulk/ignored*.md excluded by glob../../etc/passwd, /etc/passwd, docs/../../x.md, URL-encoded variants → all 400bulk/ disappear and picked up newly added nested files.gitignore dir both behave!node_modules/pkg/ did not re-include that file, contrary to the expectation I wrote into the plan's verification step. I did not "fix" this: git itself cannot re-include a path whose parent directory is excluded, and since we skip descending into ignored directories the negation is unreachable. The implementation is right and the plan line was wrong.
The startup scan is fatal on error. If the working directory can't be read, mkv now exits instead of showing an error page per-request as before. That seemed right for a tool you launch in a directory, but it is a behaviour change beyond the literal plan text.
One thing I could not verify headlessly: the localStorage collapse persistence and the CSS left-truncation both need a real browser. The markup and storage logic are straightforward, but the RTL truncation trick in particular is worth an eyeball on a long path like notes/deep/a/b/ in a narrow sidebar.
All ten steps are done and the working tree is clean of build/vet errors.
Final state:
internal/gitignore.go (dependency-free root .gitignore matcher), internal/group.go (path → directory grouping)internal/scan.go (recursive, capped, ordered walk), main.go (cached scan, /rescan, containment-based path validation, 404s)web/index.html, web/style.css, web/app.js, README.mdThe binary at ./mkv is built from the current source. go vet ./... and go build are clean; gofmt -l reports only the pre-existing internal/toc.go, which I deliberately left untouched as out of scope.
Two carry-forward notes for you:
The !node_modules/pkg/ negation case in the plan's verification list was wrong — git cannot re-include a path under an excluded directory, and the implementation correctly follows git rather than the plan. Worth remembering if you later extend gitignore support.
Startup now exits fatally if the working directory can't be scanned, where previously the error surfaced per-request. Reasonable for a launch-in-a-directory tool, but it is a behaviour change beyond the literal plan.
Still unverified: the localStorage collapse persistence and the RTL/<bdi> left-truncation both need a real browser. Run mkv in a folder with a deeply nested path like notes/deep/a/b/ and confirm the header truncates from the left and that collapse state survives clicking between files — the CSS trick is the one piece here I'd least trust sight-unseen.
Run a /grilling session.
can u update the readme.md to to mention build commands and some highlevel idea about project without going too much in detail?
Before I touch it: your request's premise doesn't match the file. README.md already has a ## Building section with go build -o mkv . (line ~57) and a high-level intro paragraph (lines 1–15). So "add build commands and a high-level idea" is already satisfied.
What I suspect you actually mean is that the README got too detailed — I grew it with a 20-line "How files are discovered" section and a long Usage walkthrough. So the real ask is trim it down, not add.
Four questions:
1. Is this a trim, not an addition? If yes, what's the target — a README that fits on one screen (~40 lines), or just "cut the fat but keep all sections"?
2. What specifically goes? Candidates, in order of how detail-heavy they are:
## How files are discovered — the .gitignore pattern-support list and the 100-file cap rationale are the deepest weeds. Cut entirely, compress to 3 bullets, or keep?## Usage — currently explains collapse defaults, tooltip behaviour, left-truncation. Compress to "run mkv, open the URL"?## Setting up the mkv alias — 25 lines of bash/zsh instructions for what is essentially one alias line. Collapse to a two-line snippet?3. Where does the cut detail go — deleted, or moved? The scan rules (dot-dirs skipped, .gitignore honoured, 100-file cap) are things a user will hit and be confused by. I'd rather move them to PLAN.md or a short "Notes" section at the bottom than delete them outright. Your call.
4. Anything you want added that genuinely isn't there? The obvious gap is a "Development"/build section that mentions there are no tests — it exists but is thin. And nothing documents the /rescan endpoint or the project layout (internal/ vs web/). Want a brief "Project layout" section, or is that exactly the detail you're trying to avoid?
For the alias section, I'll provide shell commands that create the binary in a standard location and add it to the shell configuration, then source it to activate the alias. Now I'm trimming down the README to keep just the essential title and introduction.
Trimming now. You didn't answer Q4, so I'm assuming no "Project layout" section — say the word if you want one.
>cd /Users/aal/Projects/mkv && wc -l README.md && git diff --stat README.mdREADME.md trimmed from 137 → 66 lines.
What changed:
.gitignore pattern-syntax list, the files-before-subdirs rationale, and the 404-on-deleted-file detail.## Building as a one-line "Requires Go 1.21+".~/bin and appends the alias, so there's no absolute path to fill in.Two judgement calls worth flagging:
The echo 'alias …' >> ~/.zshrc line is not idempotent — running the block twice appends the alias twice. Harmless (the second definition just wins), but it's the kind of thing that irritates people. I kept it simple rather than wrapping it in a grep -q guard; say the word if you'd rather have the guard.
The scan rules I cut are the ones users are most likely to trip over — "why isn't my file listed?" now has no answer in the README. You said trim to 3 bullets and I did, but I preserved the symptoms (dot-dirs, .gitignore, 100 cap) and dropped only the mechanics. If someone files that confusion later, the fix is a link to PLAN.md, which still documents the full behaviour.
Run a /grilling session.
can u use a specific port using some defaults to load the mkv files?
I need to figure out what port to use by default instead of letting the OS pick a random one—something memorable that won't conflict with other services. Then I'm deciding between CLI flags, environment variables, or both for overriding it. The trickier question is what happens when that port is already in use: should I fail immediately, fall back to a random port, or try incrementing to find a free one? That last option matters especially since people might run multiple instances in different folders.
Before I pick a number out of the air — five questions:
1. Which default? Currently 127.0.0.1:0 (random ephemeral). Candidates: 7654, 4321, 8080 (very likely to collide with a dev server), 3000 (ditto). I'd suggest something in the 7000–9000 range that isn't a common dev default — e.g. 7654. Do you have a number you want, or should I choose?
2. Should it be overridable, and how?
mkv -port 9000MKV_PORT=9000 mkvNote the binary currently parses no flags at all, so (a) means introducing flag and, with it, a -h usage message.
3. The important one: what happens when the port is already taken? This is not hypothetical — a fixed port means running mkv in two folders at once breaks the second one, and that's a normal thing to do (notes in one terminal, a repo in another). Options:
(b) keeps a predictable URL for the common single-instance case while not breaking multi-instance. (a) is the most honest. Your call.
4. Should -port 0 still be allowed as an explicit "just give me any free port"? Cheap to support and preserves today's behaviour for anyone who wants it.
5. Binding stays 127.0.0.1, yes? A fixed, predictable port plus a 0.0.0.0 bind would expose your notes directory to your whole network. I'd keep it loopback-only and not offer a host flag at all unless you explicitly want one.
>cd /Users/aal/Projects/mkv && gofmt -l main.go; go vet ./... && go build -o mkv . && echo OK>cd /tmp && mkdir -p mkvport && cd mkvport && echo '# hi' > a.md
(/Users/aal/Projects/mkv/mkv > /tmp/p1.txt 2>&1 & echo $! > /tmp/p1pid); sleep 1
echo "--- default:"; cat /tmp/p1.txt
echo "--- collision (second instance):"; /Users/aal/Projects/mkv/mkv; echo "exit=$?"
echo "--- explicit -port 9123:"; (/Users/aal/Projects/mkv/mkv -port 9123 > /tmp/p2.txt 2>&1 & echo $! > /tmp/p2pid); sleep 1; cat /tmp/p2.txt
echo "--- fetch:"; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4321/
echo "--- invalid ports:"; /Users/aal/Projects/mkv/mkv -port 0; echo "exit=$?"; /Users/aal/Projects/mkv/mkv -port 99999; echo "exit=$?"
echo "--- usage:"; /Users/aal/Projects/mkv/mkv -h 2>&1
kill $(cat /tmp/p1pid) $(cat /tmp/p2pid) 2>/dev/null; echo done