tuiboard 0.5.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.
Files changed (41) hide show
  1. package/.tuiboard/config.example.yaml +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +208 -0
  4. package/bin/tuiboard.ts +28 -0
  5. package/package.json +62 -0
  6. package/src/app.tsx +129 -0
  7. package/src/cli/args.test.ts +40 -0
  8. package/src/cli/args.ts +41 -0
  9. package/src/config/loader.ts +169 -0
  10. package/src/input/handleKey.ts +733 -0
  11. package/src/io/watcher.ts +85 -0
  12. package/src/io/writer.ts +92 -0
  13. package/src/parser/markdown.ts +351 -0
  14. package/src/parser/serialize.ts +97 -0
  15. package/src/scripts/agents-check.ts +24 -0
  16. package/src/scripts/parse-check.ts +124 -0
  17. package/src/scripts/roundtrip-check.ts +79 -0
  18. package/src/store/agents.test.ts +181 -0
  19. package/src/store/agents.ts +435 -0
  20. package/src/store/index.test.ts +110 -0
  21. package/src/store/index.ts +972 -0
  22. package/src/store/parsers.ts +243 -0
  23. package/src/store/timeline.test.ts +279 -0
  24. package/src/store/timeline.ts +279 -0
  25. package/src/store/virtual-panel.ts +0 -0
  26. package/src/types.ts +116 -0
  27. package/src/ui/AgentRow.tsx +79 -0
  28. package/src/ui/AgentsBar.tsx +102 -0
  29. package/src/ui/BoardView.tsx +333 -0
  30. package/src/ui/Chrome.tsx +122 -0
  31. package/src/ui/Modal.tsx +613 -0
  32. package/src/ui/TaskRow.tsx +240 -0
  33. package/src/ui/TimelineView.tsx +643 -0
  34. package/src/ui/VirtualPanel.tsx +237 -0
  35. package/src/ui/board-scroll.test.ts +63 -0
  36. package/src/ui/board-scroll.ts +49 -0
  37. package/src/ui/glyphs.ts +129 -0
  38. package/src/views/AgentsOnly.tsx +103 -0
  39. package/src/views/BoardOnly.tsx +35 -0
  40. package/src/views/Dashboard.tsx +106 -0
  41. package/src/views/TimelineOnly.tsx +12 -0
@@ -0,0 +1,32 @@
1
+ # tuiboard configuration example — copy this file to `config.yaml` (same
2
+ # directory) and edit to point at your real boards.
3
+ #
4
+ # tuiboard walks up from the cwd looking for `.tuiboard/config.yaml`, so
5
+ # placing the file at the root of a vault makes every subdirectory of that
6
+ # vault auto-configured.
7
+ #
8
+ # Without a config file, tuiboard falls back to scanning the cwd for any
9
+ # .md file containing `- [ ]` tasks.
10
+
11
+ boards:
12
+ # Each entry is a markdown file in the Obsidian Kanban Plugin format
13
+ # (frontmatter `kanban-plugin: board`, `## Column` headings, `- [ ] task`
14
+ # rows). Paths can be absolute or relative to this config file.
15
+ - path: ./Work.md
16
+ name: Work
17
+ - path: ./Personal.md
18
+ name: Personal
19
+
20
+ # Assignees available in the assign modal (`a` shortcut). Plain list of
21
+ # strings — tuiboard prefixes each with `@` when displayed.
22
+ assignees:
23
+ - Alice
24
+ - Bob
25
+
26
+ # Name of the column that counts as "done" for stats and status colors.
27
+ # Defaults to "Done".
28
+ done_column: Done
29
+
30
+ # Name of the column used by the archive action (Shift-X). If the column
31
+ # doesn't exist in a board, tuiboard creates it on the fly.
32
+ archive_column: Archive
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nazzareno Giannelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # tuiboard
2
+
3
+ A terminal dashboard that unifies **kanban**, a **Today/Tomorrow virtual
4
+ panel**, a **24-hour timeline**, and a **live agent view** for Claude Code
5
+ sessions — all on top of plain markdown task files.
6
+
7
+ Built with [OpenTUI](https://opentui.com) + SolidJS on Bun. Cross-platform
8
+ (Linux, macOS, Windows). No vendor lock-in: boards are CommonMark with
9
+ the Obsidian Tasks-plugin emoji vocabulary, so they open and edit fine in
10
+ any markdown editor.
11
+
12
+ ```
13
+ ┌─tuiboard──[1 Work · 2 Personal]──────────open · done · cols───────────────┐
14
+ │ ┌Today/Tom──┐ ┌Board──────────────────────────┐ ┌─Timeline──┐ │
15
+ │ │● Today │ │ Inbox 3 In Progress 5 Done │ │ 07 ────── │ │
16
+ │ │ ⏰ Agenda │ │ ▶ Task 1 │ │ 08 ────── │ │
17
+ │ │ ⌚09:00…│ │ Task 2 │ │ 09 ⌚ deep │ │
18
+ │ │ 🔺 Prio │ │ Task 3 │ │ 10 ────── │ │
19
+ │ └───────────┘ └────────────────────────────────┘ │ 11 ────── │ │
20
+ │ ┌Agents (live)──────────────────────────────────┐│ 12 ────── │ │
21
+ │ │● tuiboard Shadow 💬 active 📂 ... ││ 13 ⌚ call │ │
22
+ │ │ pulse Laptop 💤 3m ago 📂 ... ││ ... │ │
23
+ │ └────────────────────────────────────────────────┘└────────────┘ │
24
+ │ hjkl move · Tab board · S-Tab zone · F1/F2/F3 toggle · z zoom · ? help │
25
+ └────────────────────────────────────────────────────────────────────────────┘
26
+ ```
27
+
28
+ ## Install
29
+
30
+ Requires [Bun](https://bun.sh) ≥ 1.2 — tuiboard runs on the Bun runtime (it's
31
+ not a Node CLI). OpenTUI ships its own native renderer binaries; Bun picks the
32
+ right one for your platform automatically. Pick whichever install fits:
33
+
34
+ **Global, straight from GitHub** (no npm needed) — run `tuiboard` from anywhere:
35
+
36
+ ```bash
37
+ bun install -g github:NazzarenoGiannelli/tuiboard
38
+ tuiboard
39
+ ```
40
+
41
+ **Global, from npm** (once published):
42
+
43
+ ```bash
44
+ bun install -g tuiboard # or run once, no install: bunx tuiboard
45
+ ```
46
+
47
+ **From source** (for hacking on it):
48
+
49
+ ```bash
50
+ git clone https://github.com/NazzarenoGiannelli/tuiboard.git
51
+ cd tuiboard
52
+ bun install
53
+ bun run dev # or: bun link → then `tuiboard` globally, live-linked
54
+ ```
55
+
56
+ ## Configure
57
+
58
+ Copy `.tuiboard/config.example.yaml` to a config location and edit the
59
+ `boards:` list to point at your markdown files. tuiboard resolves the config
60
+ in this order (first hit wins):
61
+
62
+ 1. **`$TUIBOARD_CONFIG`** — explicit path to a config file.
63
+ 2. **Project-local** — `.tuiboard/config.yaml`, walking up from the cwd. Drop
64
+ a `.tuiboard/` folder at a project/vault root and it's used whenever you
65
+ launch from inside that tree.
66
+ 3. **Global** — `~/.config/tuiboard/config.yaml` (or `~/.tuiboard/config.yaml`).
67
+ Use **absolute** board paths here and `tuiboard` shows your boards from
68
+ *any* directory — the usual setup for a single-vault user.
69
+ 4. **Fallback** — scan the cwd for any `.md` file containing `- [ ]` tasks.
70
+
71
+ ```yaml
72
+ boards:
73
+ - path: ./Work.md
74
+ name: Work
75
+ - path: ./Personal.md
76
+ name: Personal
77
+
78
+ assignees: [Alice, Bob]
79
+ done_column: Done
80
+ archive_column: Archive
81
+ ```
82
+
83
+ ## Markdown board format
84
+
85
+ `tuiboard` reads and writes **plain CommonMark** with the Obsidian
86
+ Tasks-plugin emoji vocabulary. Any markdown editor renders these files
87
+ sensibly; the Obsidian Kanban plugin renders them as a kanban; we render
88
+ them as a TUI.
89
+
90
+ ### Minimal example
91
+
92
+ ```markdown
93
+ ---
94
+ kanban-plugin: board
95
+ ---
96
+
97
+ ## Today
98
+
99
+ - [ ] Fix auth flow @nazza ⏳ 2026-05-27 ⌚ 09:00-10:30 #pr-followup
100
+ - [x] Review PR #412 ✅ 2026-05-26
101
+
102
+ ## In Progress
103
+
104
+ - [ ] Migrate timeline to OpenTUI @nazza
105
+
106
+ ## Done
107
+ ```
108
+
109
+ ### Metadata vocabulary
110
+
111
+ | Symbol | Meaning | Notes |
112
+ |---|---|---|
113
+ | `## Heading` | Column name | One column per H2 heading |
114
+ | `- [ ]` / `- [x]` | Task (open / done) | Standard markdown task list |
115
+ | `@name` | Assignee | Configurable list in config.yaml |
116
+ | `#tag` | Tag | Any hashtag; passed through verbatim |
117
+ | `⏳ YYYY-MM-DD` | Scheduled date | Tasks-plugin convention |
118
+ | `📅 YYYY-MM-DD` | Due date | Tasks-plugin convention |
119
+ | `🛫 YYYY-MM-DD` | Start date | Tasks-plugin convention |
120
+ | `✅ YYYY-MM-DD` | Done date | Tasks-plugin convention |
121
+ | `⌚ HH:MM-HH:MM` | Time block | tuiboard-specific (Tasks plugin has no time-of-day) |
122
+ | `🔺 ⏫ 🔼 🔽 ⏬` | Priority | Tasks-plugin convention |
123
+
124
+ Anything else stays in the task text untouched on write-back. Roundtrip is
125
+ byte-for-byte preserving when a task hasn't been edited; structured fields
126
+ are rebuilt only after an in-app mutation.
127
+
128
+ ## Layouts
129
+
130
+ Launch `tuiboard` with no flag for the default 4-zone dashboard.
131
+
132
+ | Flag | View | Use case |
133
+ |---|---|---|
134
+ | (none) | **Dashboard** — all 4 zones | Default; everything in one terminal |
135
+ | `--view=board` | Kanban + virtual panel only | Focus mode, or a single WezTerm pane |
136
+ | `--view=timeline` | Timeline fullscreen | Wall-mounted "what's now" |
137
+ | `--view=agents` | Agent view fullscreen | Cross-machine session monitor |
138
+
139
+ The dashboard auto-collapses optional zones on narrow terminals:
140
+
141
+ | Terminal width | Default zones visible |
142
+ |---|---|
143
+ | ≥ 150 cols | virtual + board + timeline + agents |
144
+ | 120–149 | virtual + board + agents |
145
+ | 100–119 | virtual + board |
146
+ | < 100 | board only |
147
+
148
+ `F1` / `F2` / `F3` toggles override the auto-collapse for the current
149
+ session (until the next terminal resize).
150
+
151
+ ## Keyboard
152
+
153
+ ### Navigation
154
+
155
+ | Key | Action |
156
+ |---|---|
157
+ | `h j k l` / arrows | Move cursor inside the active zone |
158
+ | `Tab` | Cycle to next board |
159
+ | `1`..`9` | Jump to board N |
160
+ | `v` | Toggle Today/Tomorrow virtual panel focus |
161
+ | `Shift-Tab` | Cycle active zone (virtual → board → timeline → agents) |
162
+ | `F1` / `F2` / `F3` | Toggle visibility of Virtual / Timeline / Agents zones |
163
+ | `z` | Zoom active zone to full screen |
164
+
165
+ ### Task actions (work in board, virtual, AND timeline zones)
166
+
167
+ | Key | Action |
168
+ |---|---|
169
+ | `Enter` | Toggle done |
170
+ | `o` | Open detail view |
171
+ | `e` | Edit task text |
172
+ | `s` | Schedule date modal |
173
+ | `t` | Set scheduled = today |
174
+ | `m` | Set scheduled = tomorrow |
175
+ | `.` | Schedule **now** — time block at the next 15-min slot |
176
+ | `b` | Set time block modal |
177
+ | `p` | Cycle priority (none → 🔺 → ⏫ → 🔼 → 🔽 → ⏬ → none) |
178
+ | `a` | Set assignee |
179
+ | `d` | Delete task (with confirm) |
180
+ | `Shift-X` | Archive task → moves to Archive column |
181
+
182
+ ### Multi-select
183
+
184
+ | Key | Action |
185
+ |---|---|
186
+ | `Space` | Mark / unmark task — task actions then apply to ALL marked |
187
+ | `Esc` | Clear marks (when no modal is open) |
188
+
189
+ ### Board-only / bulk / global
190
+
191
+ | Key | Action |
192
+ |---|---|
193
+ | `n` | New task in current column (quick-add syntax) |
194
+ | `Shift-T` | Reset ALL overdue tasks (any board) to today |
195
+ | `Ctrl-Z` | Undo last mutation |
196
+ | `?` | Help modal with the full reference |
197
+ | `q` · `Ctrl-C` | Quit |
198
+
199
+ ## Status
200
+
201
+ - **v0.5** — daily-driver ready. Kanban + virtual + timeline + agents
202
+ all functional, multi-select, undo, atomic file roundtrip, mouse click,
203
+ responsive layout. Tested on Windows with WezTerm; Linux/macOS should
204
+ work via the same OpenTUI binaries (untested).
205
+
206
+ ## License
207
+
208
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Global entry point for `tuiboard` (after `bun install -g tuiboard`,
4
+ * `bunx tuiboard`, or `bun link`).
5
+ *
6
+ * OpenTUI's Solid JSX runtime must be registered via `bun --preload` BEFORE
7
+ * the module graph is parsed — otherwise app.tsx's JSX is transformed against
8
+ * the wrong runtime and bun throws `Export named 'Fragment' not found`.
9
+ * The `--preload` flag can't travel through a shebang cross-platform (Windows
10
+ * global bins are .cmd shims), so we re-exec bun with the flag here, forward
11
+ * any CLI args, and inherit stdio so the TUI keeps the real terminal.
12
+ */
13
+
14
+ import { spawnSync } from "node:child_process";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const appPath = join(here, "..", "src", "app.tsx");
20
+ const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
21
+
22
+ const result = spawnSync(
23
+ process.execPath, // the bun binary running this script
24
+ ["--preload", preload, appPath, ...process.argv.slice(2)],
25
+ { stdio: "inherit" },
26
+ );
27
+
28
+ process.exit(result.status ?? 1);
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "tuiboard",
3
+ "version": "0.5.0",
4
+ "description": "Terminal dashboard for markdown task boards. Kanban + Today/Tomorrow + 24h timeline + Claude Code agent view, all in one TUI.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Nazzareno Giannelli <nazzareno.giannelli@gmail.com>",
8
+ "homepage": "https://github.com/NazzarenoGiannelli/tuiboard#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/NazzarenoGiannelli/tuiboard.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/NazzarenoGiannelli/tuiboard/issues"
15
+ },
16
+ "keywords": [
17
+ "tui",
18
+ "kanban",
19
+ "terminal",
20
+ "task-board",
21
+ "markdown",
22
+ "obsidian",
23
+ "productivity",
24
+ "opentui",
25
+ "bun"
26
+ ],
27
+ "engines": {
28
+ "bun": ">=1.2.0"
29
+ },
30
+ "bin": {
31
+ "tuiboard": "./bin/tuiboard.ts"
32
+ },
33
+ "files": [
34
+ "src/",
35
+ "bin/",
36
+ ".tuiboard/config.example.yaml",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "dev": "bun --preload @opentui/solid/preload src/app.tsx",
42
+ "start": "bun --preload @opentui/solid/preload src/app.tsx",
43
+ "parse:check": "bun run src/scripts/parse-check.ts",
44
+ "roundtrip:check": "bun run src/scripts/roundtrip-check.ts",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "bun test",
47
+ "agents:check": "bun run src/scripts/agents-check.ts",
48
+ "prepublishOnly": "bun run typecheck && bun test"
49
+ },
50
+ "dependencies": {
51
+ "@opentui/core": "^0.2.15",
52
+ "@opentui/solid": "^0.2.15",
53
+ "chokidar": "^4.0.3",
54
+ "js-yaml": "^4.1.0",
55
+ "solid-js": "^1.9.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/bun": "latest",
59
+ "@types/js-yaml": "^4.0.9",
60
+ "typescript": "^5.7.0"
61
+ }
62
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * tuiboard — bootstrap.
3
+ *
4
+ * Loads config, builds the reactive store, parses argv, and dispatches
5
+ * to one of four root views:
6
+ * - undefined → Dashboard (all 4 zones)
7
+ * - "board" → BoardOnly (kanban + virtual fullscreen)
8
+ * - "timeline"→ TimelineOnly
9
+ * - "agents" → AgentsOnly
10
+ *
11
+ * The store and keyboard handler are shared across all four; only the
12
+ * root layout component changes.
13
+ */
14
+
15
+ import { createMemo } from "solid-js";
16
+ import { render, useKeyboard } from "@opentui/solid";
17
+
18
+ import { parseArgs, type ViewKind } from "~/cli/args";
19
+ import { loadConfig } from "~/config/loader";
20
+ import { handleKey } from "~/input/handleKey";
21
+ import {
22
+ createTuiStore,
23
+ type TuiStore,
24
+ } from "~/store/index";
25
+ import { buildVirtualItems } from "~/store/virtual-panel";
26
+ import { T } from "~/ui/glyphs";
27
+ import { TopBar, BottomBar } from "~/ui/Chrome";
28
+ import { ModalLayer } from "~/ui/Modal";
29
+ import { BoardOnly } from "~/views/BoardOnly";
30
+ import { Dashboard } from "~/views/Dashboard";
31
+ import { TimelineOnly } from "~/views/TimelineOnly";
32
+ import { AgentsOnly } from "~/views/AgentsOnly";
33
+
34
+ // ─── Bootstrap ──────────────────────────────────────────────────────────────
35
+
36
+ const config = loadConfig();
37
+ if (config.boards.length === 0) {
38
+ console.error(
39
+ "No boards found. Create `.tuiboard/config.yaml` with a `boards:` list," +
40
+ " or run from a directory containing markdown files with `- [ ]` tasks.",
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ const store = createTuiStore({ config });
46
+
47
+ if (store.state.boards.length === 0) {
48
+ console.error("All boards failed to load. Check paths in .tuiboard/config.yaml.");
49
+ process.exit(1);
50
+ }
51
+
52
+ process.on("SIGINT", () => {
53
+ store.dispose().finally(() => process.exit(0));
54
+ });
55
+ process.on("SIGTERM", () => {
56
+ store.dispose().finally(() => process.exit(0));
57
+ });
58
+
59
+ // ─── Responsive layout ──────────────────────────────────────────────────────
60
+ // Auto-hide optional zones when the terminal isn't wide enough to host them
61
+ // comfortably. Breakpoints from the design spec (§4.2):
62
+ // ≥ 150 col → all four zones
63
+ // 120–149 → hide timeline
64
+ // 100–119 → hide agents too
65
+ // < 100 → hide virtual too (board is non-hideable)
66
+ //
67
+ // User F1/F2/F3 toggles still work — they last until the next resize, at
68
+ // which point auto re-evaluates. Acceptable trade-off: resize events are
69
+ // rare, predictable layout > sticky overrides.
70
+ function applyResponsiveLayout(): void {
71
+ const width = process.stdout.columns ?? 200;
72
+ store.setZoneVisible("timeline", width >= 150);
73
+ store.setZoneVisible("agents", width >= 120);
74
+ store.setZoneVisible("virtual", width >= 100);
75
+ }
76
+ applyResponsiveLayout();
77
+ process.stdout.on("resize", applyResponsiveLayout);
78
+
79
+ const { view } = parseArgs(process.argv.slice(2));
80
+
81
+ // ─── App shell ──────────────────────────────────────────────────────────────
82
+
83
+ function rootViewFor(v: ViewKind | undefined, s: TuiStore) {
84
+ switch (v) {
85
+ case "board": return <BoardOnly store={s} />;
86
+ case "timeline": return <TimelineOnly store={s} />;
87
+ case "agents": return <AgentsOnly store={s} />;
88
+ default: return <Dashboard store={s} />;
89
+ }
90
+ }
91
+
92
+ function App() {
93
+ const virtualItems = createMemo(() =>
94
+ buildVirtualItems(store.state.boards.map((b) => b.board)),
95
+ );
96
+
97
+ useKeyboard((key) => handleKey(store, key, virtualItems().length));
98
+
99
+ return (
100
+ <box
101
+ style={{
102
+ flexDirection: "column",
103
+ width: "100%",
104
+ height: "100%",
105
+ backgroundColor: T.bg,
106
+ padding: 1,
107
+ }}
108
+ >
109
+ <TopBar store={store} />
110
+ <box style={{ height: 1 }} />
111
+ {/*
112
+ rootView and ModalLayer are siblings inside a flex-row so the
113
+ modal can sit beside the view as a fixed-width, full-height
114
+ side panel. ModalLayer renders only when ui.modal is set,
115
+ otherwise its <Show> resolves to nothing and the rootView gets
116
+ the whole row.
117
+ */}
118
+ <box style={{ flexDirection: "row", flexGrow: 1 }}>
119
+ <box style={{ flexDirection: "column", flexGrow: 1 }}>
120
+ {rootViewFor(view, store)}
121
+ </box>
122
+ <ModalLayer store={store} />
123
+ </box>
124
+ <BottomBar store={store} />
125
+ </box>
126
+ );
127
+ }
128
+
129
+ await render(() => <App />, { useMouse: true });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseArgs, type ViewKind } from "./args";
3
+
4
+ describe("parseArgs", () => {
5
+ it("returns view=undefined when no flag is present", () => {
6
+ expect(parseArgs([])).toEqual({ view: undefined });
7
+ expect(parseArgs(["bun", "src/app.tsx"])).toEqual({ view: undefined });
8
+ });
9
+
10
+ it("parses --view=board", () => {
11
+ expect(parseArgs(["--view=board"])).toEqual({ view: "board" });
12
+ });
13
+
14
+ it("parses --view=timeline", () => {
15
+ expect(parseArgs(["--view=timeline"])).toEqual({ view: "timeline" });
16
+ });
17
+
18
+ it("parses --view=agents", () => {
19
+ expect(parseArgs(["--view=agents"])).toEqual({ view: "agents" });
20
+ });
21
+
22
+ it("parses --view <value> with space separator", () => {
23
+ expect(parseArgs(["--view", "board"])).toEqual({ view: "board" });
24
+ });
25
+
26
+ it("returns view=undefined for unknown view value (with warning to stderr)", () => {
27
+ const result = parseArgs(["--view=garbage"]);
28
+ expect(result.view).toBeUndefined();
29
+ });
30
+
31
+ it("ignores other flags", () => {
32
+ expect(parseArgs(["--debug", "--view=board", "--something-else"])).toEqual({
33
+ view: "board",
34
+ });
35
+ });
36
+ });
37
+
38
+ // Type-level assertion: ensure ViewKind covers exactly the four expected values.
39
+ const _viewKinds: ViewKind[] = ["board", "timeline", "agents"];
40
+ void _viewKinds;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Minimal argv parser for tuiboard. Only handles `--view=X` and `--view X`
3
+ * because that's all this app uses. Anything fancier (subcommands, multi-
4
+ * value flags) would warrant a real CLI library — YAGNI here.
5
+ */
6
+
7
+ export type ViewKind = "board" | "timeline" | "agents";
8
+
9
+ const VALID_VIEWS: readonly ViewKind[] = ["board", "timeline", "agents"];
10
+
11
+ export interface ParsedArgs {
12
+ /** Undefined means: render the default Dashboard (all 4 zones). */
13
+ view?: ViewKind;
14
+ }
15
+
16
+ export function parseArgs(argv: readonly string[]): ParsedArgs {
17
+ let view: ViewKind | undefined;
18
+
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const arg = argv[i]!;
21
+ let candidate: string | undefined;
22
+
23
+ if (arg.startsWith("--view=")) {
24
+ candidate = arg.slice("--view=".length);
25
+ } else if (arg === "--view") {
26
+ candidate = argv[i + 1];
27
+ i++; // consume the value
28
+ }
29
+
30
+ if (candidate === undefined) continue;
31
+ if ((VALID_VIEWS as readonly string[]).includes(candidate)) {
32
+ view = candidate as ViewKind;
33
+ } else {
34
+ console.error(
35
+ `tuiboard: unknown --view value "${candidate}" — must be one of ${VALID_VIEWS.join(", ")}. Falling back to dashboard.`,
36
+ );
37
+ }
38
+ }
39
+
40
+ return { view };
41
+ }