@polderlabs/bizar-dash 3.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.
Files changed (59) hide show
  1. package/dist/assets/index-B5X9g8B4.css +1 -0
  2. package/dist/assets/index-LqQuSp9d.js +388 -0
  3. package/dist/assets/index-LqQuSp9d.js.map +1 -0
  4. package/dist/index.html +18 -0
  5. package/package.json +67 -0
  6. package/src/cli.mjs +228 -0
  7. package/src/server/agents-store.mjs +190 -0
  8. package/src/server/api.mjs +913 -0
  9. package/src/server/browser.mjs +40 -0
  10. package/src/server/diagnostics-store.mjs +138 -0
  11. package/src/server/mods-loader.mjs +361 -0
  12. package/src/server/projects-store.mjs +198 -0
  13. package/src/server/providers-store.mjs +183 -0
  14. package/src/server/schedules-runner.mjs +150 -0
  15. package/src/server/schedules-store.mjs +233 -0
  16. package/src/server/search-store.mjs +120 -0
  17. package/src/server/server.mjs +388 -0
  18. package/src/server/state.mjs +357 -0
  19. package/src/server/tailscale-store.mjs +113 -0
  20. package/src/server/tasks-store.mjs +275 -0
  21. package/src/server/tui.mjs +844 -0
  22. package/src/server/watcher.mjs +81 -0
  23. package/src/web/App.tsx +316 -0
  24. package/src/web/components/Button.tsx +55 -0
  25. package/src/web/components/Card.tsx +40 -0
  26. package/src/web/components/EmptyState.tsx +30 -0
  27. package/src/web/components/Modal.tsx +137 -0
  28. package/src/web/components/SearchModal.tsx +185 -0
  29. package/src/web/components/Spinner.tsx +19 -0
  30. package/src/web/components/StatusBadge.tsx +25 -0
  31. package/src/web/components/Tag.tsx +28 -0
  32. package/src/web/components/Toast.tsx +142 -0
  33. package/src/web/components/Topbar.tsx +203 -0
  34. package/src/web/index.html +17 -0
  35. package/src/web/lib/api.ts +71 -0
  36. package/src/web/lib/markdown.tsx +59 -0
  37. package/src/web/lib/types.ts +388 -0
  38. package/src/web/lib/utils.ts +79 -0
  39. package/src/web/lib/ws.ts +132 -0
  40. package/src/web/main.tsx +12 -0
  41. package/src/web/styles/main.css +3148 -0
  42. package/src/web/views/Agents.tsx +406 -0
  43. package/src/web/views/Chat.tsx +527 -0
  44. package/src/web/views/Config.tsx +683 -0
  45. package/src/web/views/Mods.tsx +350 -0
  46. package/src/web/views/Overview.tsx +350 -0
  47. package/src/web/views/Plans.tsx +667 -0
  48. package/src/web/views/Schedules.tsx +299 -0
  49. package/src/web/views/Settings.tsx +571 -0
  50. package/src/web/views/Tasks.tsx +761 -0
  51. package/templates/mod/FORMAT.md +76 -0
  52. package/templates/mod/hello-mod/README.md +19 -0
  53. package/templates/mod/hello-mod/agents/greeter.md +8 -0
  54. package/templates/mod/hello-mod/commands/hello.md +6 -0
  55. package/templates/mod/hello-mod/mod.json +20 -0
  56. package/templates/mod/hello-mod/routes/ping.mjs +9 -0
  57. package/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  58. package/tsconfig.json +23 -0
  59. package/vite.config.ts +24 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * src/server/tasks-store.mjs
3
+ *
4
+ * v3.0.0 — Per-project tasks with extended fields:
5
+ * assignee, parent, dependencies, timeSpent, recurring, attachments,
6
+ * comments, activity
7
+ *
8
+ * Storage: per-project `~/.config/opencode/projects/<id>/tasks.json`
9
+ *
10
+ * Backward compat: if no active project is set, fall back to the legacy
11
+ * global location (`~/.config/bizar/tasks.json`).
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import { randomBytes } from 'node:crypto';
17
+ import { projectsStore } from './projects-store.mjs';
18
+
19
+ const HOME = homedir();
20
+ const LEGACY_FILE = join(HOME, '.config', 'bizar', 'tasks.json');
21
+
22
+ let _busy = false;
23
+ const _waiters = [];
24
+
25
+ function acquire() {
26
+ return new Promise((resolve) => {
27
+ if (!_busy) {
28
+ _busy = true;
29
+ resolve();
30
+ } else {
31
+ _waiters.push({ resolve });
32
+ }
33
+ });
34
+ }
35
+
36
+ function release() {
37
+ if (_waiters.length > 0) {
38
+ _waiters.shift().resolve();
39
+ } else {
40
+ _busy = false;
41
+ }
42
+ }
43
+
44
+ function safeReadJSON(file, fallback = null) {
45
+ try {
46
+ if (!existsSync(file)) return fallback;
47
+ const text = readFileSync(file, 'utf8');
48
+ if (!text.trim()) return fallback;
49
+ return JSON.parse(text);
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ }
54
+
55
+ function genId() {
56
+ return 'tsk_' + randomBytes(6).toString('hex').slice(0, 10);
57
+ }
58
+
59
+ function genShortId() {
60
+ return randomBytes(4).toString('hex').slice(0, 8);
61
+ }
62
+
63
+ function resolveStorageFile(projectId) {
64
+ if (projectId) {
65
+ const dir = projectsStore.ensureProjectDir(projectId);
66
+ return join(dir, 'tasks.json');
67
+ }
68
+ // legacy fallback
69
+ return LEGACY_FILE;
70
+ }
71
+
72
+ function loadStore(file) {
73
+ const raw = safeReadJSON(file, null);
74
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.tasks)) {
75
+ return { version: 2, tasks: [] };
76
+ }
77
+ return raw;
78
+ }
79
+
80
+ function saveStore(file, store) {
81
+ mkdirSync(dirname(file), { recursive: true });
82
+ writeFileSync(file, JSON.stringify(store, null, 2) + '\n', 'utf8');
83
+ }
84
+
85
+ function appendActivity(task, type, data) {
86
+ task.activity = task.activity || [];
87
+ task.activity.push({
88
+ id: 'act_' + genShortId(),
89
+ type,
90
+ ts: new Date().toISOString(),
91
+ data: data || null,
92
+ });
93
+ if (task.activity.length > 100) task.activity = task.activity.slice(-100);
94
+ }
95
+
96
+ export const tasksStore = {
97
+ /** Load tasks for a project (or legacy). */
98
+ loadTasks(projectId) {
99
+ const file = resolveStorageFile(projectId);
100
+ const store = loadStore(file);
101
+ return store.tasks
102
+ .slice()
103
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
104
+ },
105
+
106
+ /** Save all tasks. */
107
+ saveTasks(projectId, tasks) {
108
+ const file = resolveStorageFile(projectId);
109
+ saveStore(file, { version: 2, tasks });
110
+ },
111
+
112
+ /** Create a task. */
113
+ async create(projectId, input) {
114
+ if (!input || typeof input !== 'object') {
115
+ throw new Error('task input required');
116
+ }
117
+ if (!input.title || typeof input.title !== 'string' || input.title.length > 200) {
118
+ throw new Error('title required (1-200 chars)');
119
+ }
120
+ await acquire();
121
+ try {
122
+ const file = resolveStorageFile(projectId);
123
+ const store = loadStore(file);
124
+ const now = new Date().toISOString();
125
+ const task = {
126
+ id: genId(),
127
+ title: input.title,
128
+ description: input.description || '',
129
+ status: ['queued', 'doing', 'done'].includes(input.status) ? input.status : 'queued',
130
+ tags: Array.isArray(input.tags) ? input.tags : [],
131
+ priority: ['low', 'normal', 'high'].includes(input.priority) ? input.priority : 'normal',
132
+ assignee: input.assignee || null,
133
+ parent: input.parent || null,
134
+ dependencies: Array.isArray(input.dependencies) ? input.dependencies : [],
135
+ timeSpent: 0,
136
+ recurring: input.recurring || null,
137
+ attachments: Array.isArray(input.attachments) ? input.attachments : [],
138
+ comments: [],
139
+ activity: [],
140
+ createdAt: now,
141
+ updatedAt: now,
142
+ completedAt: null,
143
+ };
144
+ appendActivity(task, 'created', { priority: task.priority });
145
+ store.tasks.push(task);
146
+ saveStore(file, store);
147
+ return task;
148
+ } finally {
149
+ release();
150
+ }
151
+ },
152
+
153
+ /** Update a task. */
154
+ async update(projectId, id, patch) {
155
+ await acquire();
156
+ try {
157
+ const file = resolveStorageFile(projectId);
158
+ const store = loadStore(file);
159
+ const idx = store.tasks.findIndex((t) => t.id === id);
160
+ if (idx === -1) return null;
161
+ const task = { ...store.tasks[idx] };
162
+
163
+ if (typeof patch.title === 'string') task.title = patch.title.slice(0, 200);
164
+ if (typeof patch.description === 'string') task.description = patch.description;
165
+ if (['queued', 'doing', 'done'].includes(patch.status)) {
166
+ if (patch.status !== task.status) {
167
+ appendActivity(task, 'status', { from: task.status, to: patch.status });
168
+ }
169
+ task.status = patch.status;
170
+ }
171
+ if (Array.isArray(patch.tags)) task.tags = patch.tags;
172
+ if (['low', 'normal', 'high'].includes(patch.priority)) {
173
+ task.priority = patch.priority;
174
+ }
175
+ if (typeof patch.assignee === 'string' || patch.assignee === null) {
176
+ task.assignee = patch.assignee;
177
+ }
178
+ if (typeof patch.parent === 'string' || patch.parent === null) {
179
+ task.parent = patch.parent;
180
+ }
181
+ if (Array.isArray(patch.dependencies)) task.dependencies = patch.dependencies;
182
+ if (typeof patch.timeSpent === 'number' && Number.isFinite(patch.timeSpent)) {
183
+ task.timeSpent = Math.max(0, patch.timeSpent);
184
+ }
185
+ if (patch.recurring === null || typeof patch.recurring === 'object') {
186
+ task.recurring = patch.recurring;
187
+ }
188
+ if (Array.isArray(patch.attachments)) task.attachments = patch.attachments;
189
+
190
+ task.updatedAt = new Date().toISOString();
191
+ if (task.status === 'done' && !task.completedAt) {
192
+ task.completedAt = task.updatedAt;
193
+ appendActivity(task, 'completed', null);
194
+ } else if (task.status !== 'done') {
195
+ task.completedAt = null;
196
+ }
197
+
198
+ store.tasks[idx] = task;
199
+ saveStore(file, store);
200
+ return task;
201
+ } finally {
202
+ release();
203
+ }
204
+ },
205
+
206
+ async delete(projectId, id) {
207
+ await acquire();
208
+ try {
209
+ const file = resolveStorageFile(projectId);
210
+ const store = loadStore(file);
211
+ const before = store.tasks.length;
212
+ store.tasks = store.tasks.filter((t) => t.id !== id);
213
+ if (store.tasks.length === before) return false;
214
+ saveStore(file, store);
215
+ return true;
216
+ } finally {
217
+ release();
218
+ }
219
+ },
220
+
221
+ async move(projectId, id, newStatus) {
222
+ return this.update(projectId, id, { status: newStatus });
223
+ },
224
+
225
+ /** Add a comment to a task. */
226
+ async addComment(projectId, id, text) {
227
+ if (!text || typeof text !== 'string') throw new Error('comment text required');
228
+ await acquire();
229
+ try {
230
+ const file = resolveStorageFile(projectId);
231
+ const store = loadStore(file);
232
+ const task = store.tasks.find((t) => t.id === id);
233
+ if (!task) return null;
234
+ task.comments = task.comments || [];
235
+ const comment = {
236
+ id: 'cmt_' + genShortId(),
237
+ text: text.slice(0, 4000),
238
+ createdAt: new Date().toISOString(),
239
+ };
240
+ task.comments.push(comment);
241
+ appendActivity(task, 'comment', { commentId: comment.id });
242
+ task.updatedAt = new Date().toISOString();
243
+ saveStore(file, store);
244
+ return task;
245
+ } finally {
246
+ release();
247
+ }
248
+ },
249
+
250
+ /** Toggle the time-tracking timer. Returns the task. */
251
+ async toggleTimer(projectId, id) {
252
+ await acquire();
253
+ try {
254
+ const file = resolveStorageFile(projectId);
255
+ const store = loadStore(file);
256
+ const task = store.tasks.find((t) => t.id === id);
257
+ if (!task) return null;
258
+ const now = Date.now();
259
+ if (task._timerStart) {
260
+ const elapsed = Math.floor((now - task._timerStart) / 1000);
261
+ task.timeSpent = (task.timeSpent || 0) + elapsed;
262
+ appendActivity(task, 'timer-stop', { elapsed });
263
+ delete task._timerStart;
264
+ } else {
265
+ task._timerStart = now;
266
+ appendActivity(task, 'timer-start', null);
267
+ }
268
+ task.updatedAt = new Date().toISOString();
269
+ saveStore(file, store);
270
+ return task;
271
+ } finally {
272
+ release();
273
+ }
274
+ },
275
+ };