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.
- package/.tuiboard/config.example.yaml +32 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/bin/tuiboard.ts +28 -0
- package/package.json +62 -0
- package/src/app.tsx +129 -0
- package/src/cli/args.test.ts +40 -0
- package/src/cli/args.ts +41 -0
- package/src/config/loader.ts +169 -0
- package/src/input/handleKey.ts +733 -0
- package/src/io/watcher.ts +85 -0
- package/src/io/writer.ts +92 -0
- package/src/parser/markdown.ts +351 -0
- package/src/parser/serialize.ts +97 -0
- package/src/scripts/agents-check.ts +24 -0
- package/src/scripts/parse-check.ts +124 -0
- package/src/scripts/roundtrip-check.ts +79 -0
- package/src/store/agents.test.ts +181 -0
- package/src/store/agents.ts +435 -0
- package/src/store/index.test.ts +110 -0
- package/src/store/index.ts +972 -0
- package/src/store/parsers.ts +243 -0
- package/src/store/timeline.test.ts +279 -0
- package/src/store/timeline.ts +279 -0
- package/src/store/virtual-panel.ts +0 -0
- package/src/types.ts +116 -0
- package/src/ui/AgentRow.tsx +79 -0
- package/src/ui/AgentsBar.tsx +102 -0
- package/src/ui/BoardView.tsx +333 -0
- package/src/ui/Chrome.tsx +122 -0
- package/src/ui/Modal.tsx +613 -0
- package/src/ui/TaskRow.tsx +240 -0
- package/src/ui/TimelineView.tsx +643 -0
- package/src/ui/VirtualPanel.tsx +237 -0
- package/src/ui/board-scroll.test.ts +63 -0
- package/src/ui/board-scroll.ts +49 -0
- package/src/ui/glyphs.ts +129 -0
- package/src/views/AgentsOnly.tsx +103 -0
- package/src/views/BoardOnly.tsx +35 -0
- package/src/views/Dashboard.tsx +106 -0
- package/src/views/TimelineOnly.tsx +12 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order (first hit wins):
|
|
5
|
+
* 1. $TUIBOARD_CONFIG — explicit path to a config file.
|
|
6
|
+
* 2. Project-local — `.tuiboard/config.(yaml|yml)` in cwd, walking up.
|
|
7
|
+
* 3. Global — `~/.config/tuiboard/config.(yaml|yml)` or `~/.tuiboard/…`.
|
|
8
|
+
* 4. Fallback — scan cwd for `.md` files containing tasks.
|
|
9
|
+
*
|
|
10
|
+
* The global step is what lets `tuiboard` run from ANY directory and still
|
|
11
|
+
* show your boards: drop one config in your home dir (with absolute board
|
|
12
|
+
* paths) and it's found regardless of cwd. A project-local config still wins
|
|
13
|
+
* when you're inside a project that has its own.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
19
|
+
import * as YAML from "js-yaml";
|
|
20
|
+
|
|
21
|
+
export interface BoardConfig {
|
|
22
|
+
/** Path to the .md file, absolute or relative to the config directory. */
|
|
23
|
+
path: string;
|
|
24
|
+
/** Display name. Defaults to filename without extension. */
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Config {
|
|
29
|
+
/** Directory containing `.tuiboard/config.yaml`, or cwd if no config found. */
|
|
30
|
+
root: string;
|
|
31
|
+
/** True if a config file was actually loaded. */
|
|
32
|
+
loaded: boolean;
|
|
33
|
+
boards: BoardConfig[];
|
|
34
|
+
assignees: string[];
|
|
35
|
+
doneColumn: string;
|
|
36
|
+
archiveColumn: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_CONFIG: Omit<Config, "root" | "loaded" | "boards"> = {
|
|
40
|
+
assignees: [],
|
|
41
|
+
doneColumn: "Done",
|
|
42
|
+
archiveColumn: "Archive",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Columns that exist in the markdown model but are never rendered in the
|
|
47
|
+
* board view: the Done column (completed-work log) and the Archive column.
|
|
48
|
+
* Their tasks stay in the file; the board just doesn't show them.
|
|
49
|
+
*/
|
|
50
|
+
export function isHiddenColumn(config: Config, columnName: string): boolean {
|
|
51
|
+
return columnName === config.doneColumn || columnName === config.archiveColumn;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LoadConfigOptions {
|
|
55
|
+
/** Starting directory for upward search. Defaults to process.cwd(). */
|
|
56
|
+
startDir?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function loadConfig({ startDir }: LoadConfigOptions = {}): Config {
|
|
60
|
+
const start = resolve(startDir ?? process.cwd());
|
|
61
|
+
|
|
62
|
+
const found =
|
|
63
|
+
findEnvConfigFile() ?? // 1. $TUIBOARD_CONFIG
|
|
64
|
+
findConfigFile(start) ?? // 2. project-local, walking up from cwd
|
|
65
|
+
findGlobalConfigFile(); // 3. ~/.config/tuiboard or ~/.tuiboard
|
|
66
|
+
|
|
67
|
+
if (found) {
|
|
68
|
+
const raw = readFileSync(found.path, "utf-8");
|
|
69
|
+
const data = (YAML.load(raw) ?? {}) as Partial<RawConfig>;
|
|
70
|
+
return normalize(data, found.dir, true);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Fallback: scan cwd for .md files containing tasks.
|
|
74
|
+
return normalize({ boards: scanFallbackBoards(start) }, start, false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
interface RawConfig {
|
|
80
|
+
boards: Array<string | BoardConfig>;
|
|
81
|
+
assignees: string[];
|
|
82
|
+
done_column: string;
|
|
83
|
+
archive_column: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface FoundConfig {
|
|
87
|
+
path: string;
|
|
88
|
+
dir: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findConfigFile(start: string): FoundConfig | undefined {
|
|
92
|
+
let dir = start;
|
|
93
|
+
// Hard guard against infinite loops on weird FS.
|
|
94
|
+
for (let i = 0; i < 64; i++) {
|
|
95
|
+
const candidate = join(dir, ".tuiboard", "config.yaml");
|
|
96
|
+
if (existsSync(candidate)) return { path: candidate, dir };
|
|
97
|
+
const altCandidate = join(dir, ".tuiboard", "config.yml");
|
|
98
|
+
if (existsSync(altCandidate)) return { path: altCandidate, dir };
|
|
99
|
+
const parent = dirname(dir);
|
|
100
|
+
if (parent === dir) return undefined;
|
|
101
|
+
dir = parent;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Explicit config path via $TUIBOARD_CONFIG (points at the file itself). */
|
|
107
|
+
function findEnvConfigFile(): FoundConfig | undefined {
|
|
108
|
+
const envPath = process.env.TUIBOARD_CONFIG;
|
|
109
|
+
if (!envPath) return undefined;
|
|
110
|
+
const abs = resolve(envPath);
|
|
111
|
+
return existsSync(abs) ? { path: abs, dir: dirname(abs) } : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** User-global config in the home dir — found regardless of cwd. */
|
|
115
|
+
function findGlobalConfigFile(): FoundConfig | undefined {
|
|
116
|
+
const home = homedir();
|
|
117
|
+
const candidates = [
|
|
118
|
+
join(home, ".config", "tuiboard", "config.yaml"),
|
|
119
|
+
join(home, ".config", "tuiboard", "config.yml"),
|
|
120
|
+
join(home, ".tuiboard", "config.yaml"),
|
|
121
|
+
join(home, ".tuiboard", "config.yml"),
|
|
122
|
+
];
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
if (existsSync(candidate)) return { path: candidate, dir: dirname(candidate) };
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalize(raw: Partial<RawConfig>, root: string, loaded: boolean): Config {
|
|
130
|
+
const boards: BoardConfig[] = (raw.boards ?? []).map((entry) => {
|
|
131
|
+
if (typeof entry === "string") return { path: entry };
|
|
132
|
+
return entry;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Resolve paths relative to config root.
|
|
136
|
+
for (const b of boards) {
|
|
137
|
+
if (!isAbsolute(b.path)) b.path = resolve(root, b.path);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
root,
|
|
142
|
+
loaded,
|
|
143
|
+
boards,
|
|
144
|
+
assignees: raw.assignees ?? DEFAULT_CONFIG.assignees,
|
|
145
|
+
doneColumn: raw.done_column ?? DEFAULT_CONFIG.doneColumn,
|
|
146
|
+
archiveColumn: raw.archive_column ?? DEFAULT_CONFIG.archiveColumn,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function scanFallbackBoards(dir: string): BoardConfig[] {
|
|
151
|
+
try {
|
|
152
|
+
const entries = readdirSync(dir);
|
|
153
|
+
return entries
|
|
154
|
+
.filter((f) => f.endsWith(".md"))
|
|
155
|
+
.map((f) => join(dir, f))
|
|
156
|
+
.filter((p) => {
|
|
157
|
+
try {
|
|
158
|
+
if (!statSync(p).isFile()) return false;
|
|
159
|
+
const head = readFileSync(p, "utf-8").slice(0, 4096);
|
|
160
|
+
return /^- \[[ xX]\] /m.test(head);
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.map((path) => ({ path }));
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|