@taskmark/ui 0.2.4 → 0.2.5

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 (202) hide show
  1. package/README.md +17 -6
  2. package/app/api/taskmark-reload/route.ts +103 -0
  3. package/app/board/page.tsx +6 -4
  4. package/app/layout.tsx +2 -0
  5. package/bin/lib/watch-board-markdown.mjs +65 -0
  6. package/bin/taskmark.js +190 -56
  7. package/components/board/app-bar.tsx +31 -34
  8. package/components/board/board-dev-reloader.tsx +49 -0
  9. package/components/board/epic-list.tsx +2 -4
  10. package/components/board/overall-work-items-list.tsx +2 -1
  11. package/components/board/static-board-app.tsx +0 -1
  12. package/components/board/story-list.tsx +2 -1
  13. package/components/board/task-list.tsx +2 -1
  14. package/components/board/work-item-sheet.tsx +15 -2
  15. package/components/board/work-items-list.tsx +2 -2
  16. package/components/setup/setup-wizard.tsx +2 -6
  17. package/dist/prod-next/BUILD_ID +1 -0
  18. package/dist/{standalone/.next → prod-next}/app-path-routes-manifest.json +3 -2
  19. package/dist/{standalone/.next → prod-next}/build-manifest.json +3 -3
  20. package/dist/prod-next/export-marker.json +6 -0
  21. package/dist/prod-next/images-manifest.json +68 -0
  22. package/dist/prod-next/next-minimal-server.js.nft.json +1 -0
  23. package/dist/prod-next/next-server.js.nft.json +1 -0
  24. package/dist/{standalone/.next → prod-next}/prerender-manifest.json +3 -3
  25. package/dist/{standalone/.next → prod-next}/react-loadable-manifest.json +2 -2
  26. package/dist/prod-next/required-server-files.js +337 -0
  27. package/dist/{standalone/.next → prod-next}/required-server-files.json +0 -7
  28. package/dist/{standalone/.next → prod-next}/routes-manifest.json +6 -0
  29. package/dist/prod-next/server/app/_global-error/page.js +32 -0
  30. package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -0
  31. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  32. package/dist/{standalone/.next → prod-next}/server/app/_global-error.html +1 -1
  33. package/dist/{standalone/.next → prod-next}/server/app/_global-error.rsc +1 -1
  34. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_full.segment.rsc +1 -1
  35. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  36. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  37. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_head.segment.rsc +1 -1
  38. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_index.segment.rsc +1 -1
  39. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  40. package/dist/prod-next/server/app/_not-found/page.js +2 -0
  41. package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -0
  42. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  43. package/dist/prod-next/server/app/_not-found.html +1 -0
  44. package/dist/prod-next/server/app/_not-found.rsc +21 -0
  45. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +21 -0
  46. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_index.segment.rsc +5 -4
  48. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -0
  52. package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -0
  53. package/dist/prod-next/server/app/api/taskmark-reload/route_client-reference-manifest.js +1 -0
  54. package/dist/{standalone/.next → prod-next}/server/app/board/page.js +14 -14
  55. package/dist/prod-next/server/app/board/page.js.nft.json +1 -0
  56. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -0
  57. package/dist/{standalone/.next → prod-next}/server/app/favicon.ico/route.js +1 -1
  58. package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -0
  59. package/dist/prod-next/server/app/page.js +2 -0
  60. package/dist/prod-next/server/app/page.js.nft.json +1 -0
  61. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -0
  62. package/dist/prod-next/server/app/setup/page.js +2 -0
  63. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -0
  64. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -0
  65. package/dist/{standalone/.next → prod-next}/server/app-paths-manifest.json +3 -2
  66. package/dist/{standalone/.next → prod-next}/server/chunks/319.js +1 -1
  67. package/dist/{standalone/.next → prod-next}/server/chunks/490.js +1 -1
  68. package/dist/prod-next/server/chunks/57.js +19 -0
  69. package/dist/prod-next/server/chunks/616.js +1 -0
  70. package/dist/prod-next/server/chunks/708.js +1 -0
  71. package/dist/prod-next/server/chunks/724.js +1 -0
  72. package/dist/{standalone/.next/server/chunks/445.js → prod-next/server/chunks/741.js} +2 -2
  73. package/dist/prod-next/server/functions-config-manifest.json +6 -0
  74. package/dist/prod-next/server/interception-route-rewrite-manifest.js +1 -0
  75. package/dist/{standalone/.next → prod-next}/server/middleware-build-manifest.js +1 -1
  76. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -0
  77. package/dist/prod-next/server/pages/404.html +1 -0
  78. package/dist/{standalone/.next → prod-next}/server/pages/500.html +1 -1
  79. package/dist/prod-next/server/server-reference-manifest.js +1 -0
  80. package/dist/prod-next/server/server-reference-manifest.json +1 -0
  81. package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +1 -0
  82. package/dist/{standalone/.next/static/chunks/781-f9f201ddc8c8591a.js → prod-next/static/chunks/613-6f794154d28bb75a.js} +1 -1
  83. package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +1 -0
  84. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +1 -0
  85. package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +1 -0
  86. package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +1 -0
  87. package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +1 -0
  88. package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +1 -0
  89. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +1 -0
  90. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +1 -0
  91. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +1 -0
  92. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +1 -0
  93. package/dist/{standalone/.next/static/chunks/webpack-b59eaf276b7fb38f.js → prod-next/static/chunks/webpack-bfe8992962004e2b.js} +1 -1
  94. package/dist/prod-next/static/wbcgYS3Jw87KqhnA9ttOL/_buildManifest.js +1 -0
  95. package/dist/public/taskmark-snapshot.json +1 -0
  96. package/lib/display-path.ts +6 -0
  97. package/lib/taskmark/dev-reload-token.ts +5 -0
  98. package/lib/taskmark/project-label.ts +3 -2
  99. package/lib/taskmark/validate.ts +4 -4
  100. package/next.config.ts +3 -8
  101. package/package.json +5 -4
  102. package/public/taskmark-snapshot.json +1 -1
  103. package/scripts/build-static.mjs +10 -1
  104. package/scripts/prepare-standalone.mjs +53 -20
  105. package/dist/standalone/.next/BUILD_ID +0 -1
  106. package/dist/standalone/.next/server/app/_global-error/page.js +0 -32
  107. package/dist/standalone/.next/server/app/_global-error/page.js.nft.json +0 -1
  108. package/dist/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +0 -1
  109. package/dist/standalone/.next/server/app/_not-found/page.js +0 -2
  110. package/dist/standalone/.next/server/app/_not-found/page.js.nft.json +0 -1
  111. package/dist/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  112. package/dist/standalone/.next/server/app/_not-found.html +0 -1
  113. package/dist/standalone/.next/server/app/_not-found.rsc +0 -20
  114. package/dist/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +0 -20
  115. package/dist/standalone/.next/server/app/board/page.js.nft.json +0 -1
  116. package/dist/standalone/.next/server/app/board/page_client-reference-manifest.js +0 -1
  117. package/dist/standalone/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  118. package/dist/standalone/.next/server/app/page.js +0 -2
  119. package/dist/standalone/.next/server/app/page.js.nft.json +0 -1
  120. package/dist/standalone/.next/server/app/page_client-reference-manifest.js +0 -1
  121. package/dist/standalone/.next/server/app/setup/page.js +0 -2
  122. package/dist/standalone/.next/server/app/setup/page.js.nft.json +0 -1
  123. package/dist/standalone/.next/server/app/setup/page_client-reference-manifest.js +0 -1
  124. package/dist/standalone/.next/server/chunks/164.js +0 -1
  125. package/dist/standalone/.next/server/chunks/57.js +0 -19
  126. package/dist/standalone/.next/server/chunks/570.js +0 -1
  127. package/dist/standalone/.next/server/chunks/666.js +0 -1
  128. package/dist/standalone/.next/server/functions-config-manifest.json +0 -4
  129. package/dist/standalone/.next/server/middleware-react-loadable-manifest.js +0 -1
  130. package/dist/standalone/.next/server/pages/404.html +0 -1
  131. package/dist/standalone/.next/server/server-reference-manifest.js +0 -1
  132. package/dist/standalone/.next/server/server-reference-manifest.json +0 -1
  133. package/dist/standalone/.next/static/chunks/211.a43663747d668728.js +0 -1
  134. package/dist/standalone/.next/static/chunks/app/_global-error/page-82d456ade9f47021.js +0 -1
  135. package/dist/standalone/.next/static/chunks/app/board/page-6db8dec814516c28.js +0 -1
  136. package/dist/standalone/.next/static/chunks/app/layout-3745e06d87691845.js +0 -1
  137. package/dist/standalone/.next/static/chunks/app/page-82d456ade9f47021.js +0 -1
  138. package/dist/standalone/.next/static/chunks/app/setup/page-8a54c6f26633bdca.js +0 -1
  139. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-82d456ade9f47021.js +0 -1
  140. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-82d456ade9f47021.js +0 -1
  141. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-82d456ade9f47021.js +0 -1
  142. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-82d456ade9f47021.js +0 -1
  143. package/dist/standalone/.next/static/jm3kxf8c6qGe0cYKCktaO/_buildManifest.js +0 -1
  144. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/processChild.js +0 -1
  145. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/threadChild.js +0 -1
  146. package/dist/standalone/node_modules/next/dist/server/lib/start-server.js +0 -494
  147. package/dist/standalone/node_modules/next/dist/server/next-server.js +0 -1429
  148. package/dist/standalone/node_modules/next/dist/server/next.js +0 -455
  149. package/dist/standalone/node_modules/next/dist/server/require-hook.js +0 -82
  150. package/dist/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +0 -36
  151. package/dist/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +0 -20
  152. package/dist/standalone/node_modules/styled-jsx/index.js +0 -1
  153. package/dist/standalone/node_modules/styled-jsx/package.json +0 -132
  154. package/dist/standalone/node_modules/styled-jsx/style.js +0 -1
  155. package/dist/standalone/package.json +0 -77
  156. package/dist/standalone/public/taskmark-snapshot.json +0 -1
  157. package/dist/standalone/server.js +0 -44
  158. /package/dist/{standalone/.next → prod-next}/package.json +0 -0
  159. /package/dist/{standalone/.next → prod-next}/server/app/_global-error.meta +0 -0
  160. /package/dist/{standalone/.next → prod-next}/server/app/_not-found.meta +0 -0
  161. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.body +0 -0
  162. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.meta +0 -0
  163. /package/dist/{standalone/.next → prod-next}/server/chunks/318.js +0 -0
  164. /package/dist/{standalone/.next → prod-next}/server/middleware-manifest.json +0 -0
  165. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.js +0 -0
  166. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.json +0 -0
  167. /package/dist/{standalone/.next → prod-next}/server/pages-manifest.json +0 -0
  168. /package/dist/{standalone/.next → prod-next}/server/prefetch-hints.json +0 -0
  169. /package/dist/{standalone/.next → prod-next}/server/webpack-runtime.js +0 -0
  170. /package/dist/{standalone/.next → prod-next}/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -0
  171. /package/dist/{standalone/.next → prod-next}/static/chunks/669-8103441c67b185da.js +0 -0
  172. /package/dist/{standalone/.next → prod-next}/static/chunks/701-df3ee5f1cf837b1a.js +0 -0
  173. /package/dist/{standalone/.next → prod-next}/static/chunks/794-25668765560cb197.js +0 -0
  174. /package/dist/{standalone/.next → prod-next}/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -0
  175. /package/dist/{standalone/.next → prod-next}/static/chunks/framework-b29f9083d3cda3b2.js +0 -0
  176. /package/dist/{standalone/.next → prod-next}/static/chunks/main-app-bfb36455d24b3e5a.js +0 -0
  177. /package/dist/{standalone/.next → prod-next}/static/chunks/main-bb33c46a9c25dee8.js +0 -0
  178. /package/dist/{standalone/.next → prod-next}/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -0
  179. /package/dist/{standalone/.next → prod-next}/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  180. /package/dist/{standalone/.next → prod-next}/static/css/2e6ec4c80894e3c0.css +0 -0
  181. /package/dist/{standalone/.next → prod-next}/static/css/c593627ceef17a4b.css +0 -0
  182. /package/dist/{standalone/.next → prod-next}/static/media/36966cca54120369-s.p.woff2 +0 -0
  183. /package/dist/{standalone/.next → prod-next}/static/media/982ceffe7b733b3b-s.p.woff2 +0 -0
  184. /package/dist/{standalone/.next → prod-next}/static/media/b7387a63dd068245-s.woff2 +0 -0
  185. /package/dist/{standalone/.next → prod-next}/static/media/b8bfb68ab9ab617c-s.woff2 +0 -0
  186. /package/dist/{standalone/.next → prod-next}/static/media/e1aab0933260df4d-s.woff2 +0 -0
  187. /package/dist/{standalone/.next/static/jm3kxf8c6qGe0cYKCktaO → prod-next/static/wbcgYS3Jw87KqhnA9ttOL}/_ssgManifest.js +0 -0
  188. /package/dist/{standalone/public → public}/apple-touch-icon.png +0 -0
  189. /package/dist/{standalone/public → public}/favicon-96x96.png +0 -0
  190. /package/dist/{standalone/public → public}/favicon.ico +0 -0
  191. /package/dist/{standalone/public → public}/favicon.svg +0 -0
  192. /package/dist/{standalone/public → public}/file.svg +0 -0
  193. /package/dist/{standalone/public → public}/globe.svg +0 -0
  194. /package/dist/{standalone/public → public}/next.svg +0 -0
  195. /package/dist/{standalone/public → public}/og/og-default.png +0 -0
  196. /package/dist/{standalone/public → public}/site.webmanifest +0 -0
  197. /package/dist/{standalone/public → public}/tm_dark.png +0 -0
  198. /package/dist/{standalone/public → public}/tm_light.png +0 -0
  199. /package/dist/{standalone/public → public}/vercel.svg +0 -0
  200. /package/dist/{standalone/public → public}/web-app-manifest-192x192.png +0 -0
  201. /package/dist/{standalone/public → public}/web-app-manifest-512x512.png +0 -0
  202. /package/dist/{standalone/public → public}/window.svg +0 -0
package/README.md CHANGED
@@ -12,6 +12,15 @@ npx taskmark serve
12
12
 
13
13
  Opens [http://localhost:8275](http://localhost:8275) bound to **this** board.
14
14
 
15
+ ### Development (live board reload)
16
+
17
+ ```bash
18
+ npx taskmark dev
19
+ # or: npm run dev / yarn dev
20
+ ```
21
+
22
+ Runs Next.js in development on port **8275** and refreshes the UI when board `.md` files change.
23
+
15
24
  Without a local install (once published):
16
25
 
17
26
  ```bash
@@ -49,16 +58,17 @@ You do **not** need to clone `taskmark-frontend`.
49
58
  "scripts": {
50
59
  "start": "taskmark serve --no-open",
51
60
  "serve": "taskmark serve",
61
+ "dev": "taskmark dev",
52
62
  "build": "taskmark build --board . --out out",
53
63
  "preview": "taskmark preview"
54
64
  },
55
65
  "dependencies": {
56
- "@taskmark/ui": "^0.2.4"
66
+ "@taskmark/ui": "^0.2.5"
57
67
  }
58
68
  }
59
69
  ```
60
70
 
61
- Then `npm install && npm run serve` (or `npm start`). For **Vercel**, use `npm run build` (static HTML in `out/`) — see the board-ui-stub `vercel.json`.
71
+ Then `npm install && npm run serve` (or `npm start`). Use `npm run dev` while editing board markdown. For **Vercel**, use `npm run build` (static HTML in `out/`) — see the board-ui-stub `vercel.json`.
62
72
 
63
73
  ### Static production build
64
74
 
@@ -78,14 +88,15 @@ Favicon, webmanifest, and Open Graph assets ship in `public/` and are included i
78
88
  ```bash
79
89
  npm install
80
90
  npm run build # next standalone + dist/ for the CLI
81
- npm run serve # local bin against cwd/env board
82
- npm run dev # Next dev server on :3000 (setup wizard unless auto-bound)
91
+ npm run serve # production standalone against cwd/env board
92
+ npm run dev # taskmark dev (Next + markdown live reload) on :8275
83
93
  ```
84
94
 
85
- Zero-config for `npm run dev`: `TASKMARK_BOARD` / `TASKMARK_MASTER` / cwd (see above). Cookie multi-master setup still works when unbound.
95
+ Zero-config for `npm run dev` / `serve`: `TASKMARK_BOARD` / `TASKMARK_MASTER` / cwd (see above). Cookie multi-master setup still works when unbound.
86
96
 
87
97
  ## Stack
88
98
 
89
- - Next.js standalone server packaged for `taskmark serve`
99
+ - Next.js production server packaged for `taskmark serve` (`next start` + slim `dist/prod-next`)
100
+ - `taskmark dev` for live board markdown reload
90
101
  - Tailwind CSS v4
91
102
  - Local markdown boards only (no hosted sync)
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ import { cwdCandidates } from "@/lib/taskmark/autoconfig"
5
+ import { resolveBoardAtPath } from "@/lib/taskmark/discover"
6
+
7
+ export const dynamic = "force-dynamic"
8
+ export const runtime = "nodejs"
9
+
10
+ function resolveBoardRoot(): string | null {
11
+ const env = process.env.TASKMARK_BOARD?.trim()
12
+ if (env) {
13
+ const board = resolveBoardAtPath(path.resolve(env))
14
+ if (board) return board
15
+ }
16
+ for (const candidate of cwdCandidates()) {
17
+ const board = resolveBoardAtPath(candidate)
18
+ if (board) return board
19
+ }
20
+ return null
21
+ }
22
+
23
+ function walkMarkdownFiles(root: string, out: string[] = []): string[] {
24
+ let entries: fs.Dirent[]
25
+ try {
26
+ entries = fs.readdirSync(root, { withFileTypes: true })
27
+ } catch {
28
+ return out
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "out") {
32
+ continue
33
+ }
34
+ const full = path.join(root, entry.name)
35
+ if (entry.isDirectory()) {
36
+ walkMarkdownFiles(full, out)
37
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
38
+ out.push(full)
39
+ }
40
+ }
41
+ return out
42
+ }
43
+
44
+ function latestMarkdownMtime(boardRoot: string): number {
45
+ let latest = 0
46
+ for (const file of walkMarkdownFiles(boardRoot)) {
47
+ try {
48
+ const mtime = fs.statSync(file).mtimeMs
49
+ if (mtime > latest) latest = mtime
50
+ } catch {
51
+ // ignore transient deletes
52
+ }
53
+ }
54
+ return latest
55
+ }
56
+
57
+ /**
58
+ * SSE stream that emits `reload` when any board `.md` mtime changes.
59
+ * Used only by `taskmark dev` / Next development mode.
60
+ */
61
+ export async function GET() {
62
+ if (process.env.NODE_ENV !== "development") {
63
+ return new Response("Not available", { status: 404 })
64
+ }
65
+
66
+ const boardRoot = resolveBoardRoot()
67
+ if (!boardRoot) {
68
+ return new Response("No board", { status: 404 })
69
+ }
70
+
71
+ const encoder = new TextEncoder()
72
+ let last = latestMarkdownMtime(boardRoot)
73
+ let interval: ReturnType<typeof setInterval> | null = null
74
+
75
+ const stream = new ReadableStream({
76
+ start(controller) {
77
+ const send = (event: string, data: string) => {
78
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${data}\n\n`))
79
+ }
80
+ send("hello", String(Date.now()))
81
+ interval = setInterval(() => {
82
+ const next = latestMarkdownMtime(boardRoot)
83
+ if (next > last) {
84
+ last = next
85
+ send("reload", String(Date.now()))
86
+ } else {
87
+ send("ping", String(Date.now()))
88
+ }
89
+ }, 750)
90
+ },
91
+ cancel() {
92
+ if (interval) clearInterval(interval)
93
+ },
94
+ })
95
+
96
+ return new Response(stream, {
97
+ headers: {
98
+ "Content-Type": "text/event-stream; charset=utf-8",
99
+ "Cache-Control": "no-cache, no-transform",
100
+ Connection: "keep-alive",
101
+ },
102
+ })
103
+ }
@@ -23,6 +23,7 @@ import {
23
23
  parseListViewMode,
24
24
  type ListViewMode,
25
25
  } from "@/lib/taskmark/list-view-mode"
26
+ import { DEV_RELOAD_TOKEN } from "@/lib/taskmark/dev-reload-token"
26
27
  import { loadSnapshotFromPublic } from "@/lib/taskmark/load-snapshot-server"
27
28
  import { parseEpicsForProject } from "@/lib/taskmark/parse-epics"
28
29
  import {
@@ -38,6 +39,11 @@ import {
38
39
  import { isStaticRuntime } from "@/lib/taskmark/static-mode"
39
40
  import { loadWorkspace } from "@/lib/taskmark/workspace"
40
41
 
42
+ export const dynamic = "force-dynamic"
43
+
44
+ // Keep this import so `taskmark dev` can touch the token module and refresh RSC.
45
+ void DEV_RELOAD_TOKEN
46
+
41
47
  type BoardPageProps = {
42
48
  searchParams: Promise<{
43
49
  view?: string | string[]
@@ -143,7 +149,6 @@ export default async function BoardPage({ searchParams }: BoardPageProps) {
143
149
  <AppBar
144
150
  projects={workspace.projects}
145
151
  activeProjectId={activeProject.id}
146
- masterCount={workspace.masters.length}
147
152
  autoconfig={workspace.autoconfig}
148
153
  />
149
154
 
@@ -177,9 +182,6 @@ export default async function BoardPage({ searchParams }: BoardPageProps) {
177
182
  ? ` · story ${selectedStoryId} not in list`
178
183
  : ""}
179
184
  </p>
180
- <p className="mt-1 font-mono text-xs text-muted-foreground">
181
- {activeProject.boardPath}
182
- </p>
183
185
  </div>
184
186
  <ListViewSwitcher
185
187
  activeView={activeView}
package/app/layout.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Metadata, Viewport } from "next"
2
2
  import { Archivo_Black, Space_Grotesk } from "next/font/google"
3
+ import { BoardDevReloader } from "@/components/board/board-dev-reloader"
3
4
  import { Toaster } from "@/components/ui/sonner"
4
5
  import { SITE } from "@/lib/site"
5
6
  import "./globals.css"
@@ -80,6 +81,7 @@ export default function RootLayout({
80
81
  className={`${archivoBlack.variable} ${spaceGrotesk.variable} font-sans antialiased`}
81
82
  >
82
83
  <Toaster />
84
+ <BoardDevReloader />
83
85
  {children}
84
86
  </body>
85
87
  </html>
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ const IGNORE_DIRS = new Set([
5
+ "node_modules",
6
+ ".git",
7
+ "out",
8
+ ".next",
9
+ "dist",
10
+ ".taskmark-ui-build",
11
+ ])
12
+
13
+ function shouldIgnoreDir(name) {
14
+ return IGNORE_DIRS.has(name) || name.startsWith(".")
15
+ }
16
+
17
+ function latestMarkdownMtime(boardRoot) {
18
+ let latest = 0
19
+ const walk = (dir) => {
20
+ let entries
21
+ try {
22
+ entries = fs.readdirSync(dir, { withFileTypes: true })
23
+ } catch {
24
+ return
25
+ }
26
+ for (const entry of entries) {
27
+ const full = path.join(dir, entry.name)
28
+ if (entry.isDirectory()) {
29
+ if (!shouldIgnoreDir(entry.name)) walk(full)
30
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
31
+ try {
32
+ latest = Math.max(latest, fs.statSync(full).mtimeMs)
33
+ } catch {
34
+ // ignore races
35
+ }
36
+ }
37
+ }
38
+ }
39
+ walk(boardRoot)
40
+ return latest
41
+ }
42
+
43
+ /**
44
+ * Poll a board directory for `.md` mtime changes.
45
+ * Uses polling (not recursive fs.watch) to avoid EMFILE on large boards.
46
+ * Returns a stop function.
47
+ */
48
+ export function watchBoardMarkdown(boardRoot, onChange) {
49
+ let stopped = false
50
+ let lastStamp = latestMarkdownMtime(boardRoot)
51
+
52
+ const interval = setInterval(() => {
53
+ if (stopped) return
54
+ const next = latestMarkdownMtime(boardRoot)
55
+ if (next > lastStamp) {
56
+ lastStamp = next
57
+ onChange()
58
+ }
59
+ }, 750)
60
+
61
+ return () => {
62
+ stopped = true
63
+ clearInterval(interval)
64
+ }
65
+ }
package/bin/taskmark.js CHANGED
@@ -3,23 +3,28 @@ import { spawn } from "node:child_process"
3
3
  import fs from "node:fs"
4
4
  import http from "node:http"
5
5
  import path from "node:path"
6
+ import { createRequire } from "node:module"
6
7
  import { fileURLToPath } from "node:url"
7
8
 
8
9
  import { resolveServeBoard } from "./lib/resolve-board.mjs"
9
10
  import { startStaticServer } from "./lib/static-preview.mjs"
11
+ import { watchBoardMarkdown } from "./lib/watch-board-markdown.mjs"
10
12
 
11
13
  const DEFAULT_PORT = 8275
12
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
15
  const packageRoot = path.resolve(__dirname, "..")
16
+ const requireFromPackage = createRequire(path.join(packageRoot, "package.json"))
14
17
 
15
18
  function printHelp() {
16
19
  console.log(`Usage:
17
20
  taskmark serve [options]
21
+ taskmark dev [options]
18
22
  taskmark build [options]
19
23
  taskmark preview [options]
20
24
 
21
25
  Commands:
22
26
  serve Start the prebuilt local UI server (default port ${DEFAULT_PORT})
27
+ dev Next.js development server with board markdown live reload
23
28
  build Production static HTML export for Vercel / static hosting
24
29
  preview Serve an existing static export (default: <board>/out)
25
30
 
@@ -27,7 +32,7 @@ Options:
27
32
  --port, -p <n> Listen port (default ${DEFAULT_PORT}; env PORT / TASKMARK_PORT)
28
33
  --board <path> Board or product root (sets TASKMARK_BOARD)
29
34
  --out <dir> Static output directory for build/preview (default: <board>/out)
30
- --no-open Do not open a browser (serve / preview)
35
+ --no-open Do not open a browser (serve / preview / dev)
31
36
  --help, -h Show help
32
37
 
33
38
  Board resolution (same as the UI):
@@ -38,6 +43,7 @@ Board resolution (same as the UI):
38
43
 
39
44
  Examples:
40
45
  npm i @taskmark/ui --save && npx taskmark serve
46
+ npx taskmark dev --board .
41
47
  npx taskmark build --board .
42
48
  npm run preview
43
49
  npx taskmark preview --port 9000
@@ -97,15 +103,73 @@ function resolvePort(raw) {
97
103
  return n
98
104
  }
99
105
 
100
- function findStandaloneServer() {
101
- const candidates = [
102
- path.join(packageRoot, "dist", "standalone", "server.js"),
103
- path.join(packageRoot, ".next", "standalone", "server.js"),
104
- ]
105
- for (const candidate of candidates) {
106
- if (fs.existsSync(candidate)) return candidate
106
+ function resolveNextBin() {
107
+ try {
108
+ return requireFromPackage.resolve("next/dist/bin/next")
109
+ } catch {
110
+ const fallback = path.join(
111
+ packageRoot,
112
+ "node_modules",
113
+ "next",
114
+ "dist",
115
+ "bin",
116
+ "next"
117
+ )
118
+ if (fs.existsSync(fallback)) return fallback
119
+ throw new Error(
120
+ "next binary not found. Reinstall @taskmark/ui (next is required for serve/dev)."
121
+ )
107
122
  }
108
- return null
123
+ }
124
+
125
+ /**
126
+ * Ensure packageRoot/.next points at a usable production build.
127
+ * Published packages ship dist/prod-next (npm cannot pack nested node_modules
128
+ * used by Next standalone).
129
+ */
130
+ function ensureProdNext() {
131
+ const nextDir = path.join(packageRoot, ".next")
132
+ const shipped = path.join(packageRoot, "dist", "prod-next")
133
+ const hasLocalServer = fs.existsSync(path.join(nextDir, "server"))
134
+ const hasShippedServer = fs.existsSync(path.join(shipped, "server"))
135
+
136
+ if (hasLocalServer) return nextDir
137
+ if (!hasShippedServer) {
138
+ throw new Error(`Prebuilt UI not found.
139
+
140
+ Expected ${shipped} (or a local .next/server from npm run build).
141
+ Reinstall @taskmark/ui or rebuild the package.`)
142
+ }
143
+
144
+ if (fs.existsSync(nextDir)) {
145
+ const st = fs.lstatSync(nextDir)
146
+ if (st.isSymbolicLink() || st.isDirectory()) {
147
+ fs.rmSync(nextDir, { recursive: true, force: true })
148
+ }
149
+ }
150
+
151
+ try {
152
+ const type = process.platform === "win32" ? "junction" : "dir"
153
+ fs.symlinkSync(shipped, nextDir, type)
154
+ } catch {
155
+ fs.cpSync(shipped, nextDir, { recursive: true })
156
+ }
157
+ return nextDir
158
+ }
159
+
160
+ function touchDevReloadToken() {
161
+ const tokenFile = path.join(
162
+ packageRoot,
163
+ "lib",
164
+ "taskmark",
165
+ "dev-reload-token.ts"
166
+ )
167
+ const stamp = String(Date.now())
168
+ fs.writeFileSync(
169
+ tokenFile,
170
+ `/**\n * Touched by \`taskmark dev\` when board markdown changes so Next Fast Refresh\n * re-runs server components that import this module.\n */\nexport const DEV_RELOAD_TOKEN = "${stamp}"\n`,
171
+ "utf8"
172
+ )
109
173
  }
110
174
 
111
175
  function openBrowser(url) {
@@ -155,8 +219,8 @@ function waitForServer(port, timeoutMs = 60_000) {
155
219
  })
156
220
  }
157
221
 
158
- async function serve(args) {
159
- const resolved = resolveServeBoard(args.board)
222
+ function resolveBoardOrExit(boardArg, command) {
223
+ const resolved = resolveServeBoard(boardArg)
160
224
  if (!resolved) {
161
225
  console.error(`No Taskmark board found.
162
226
 
@@ -166,50 +230,126 @@ Looked for:
166
230
  - cwd layouts: nested ./taskmark/ or dedicated *-taskmark root
167
231
 
168
232
  Fix: cd into a product repo (with ./taskmark/) or a board root, or set TASKMARK_BOARD.
169
- Then re-run: npx taskmark serve
170
- (or: npx -p @taskmark/ui taskmark serve)`)
233
+ Then re-run: npx taskmark ${command}
234
+ (or: npx -p @taskmark/ui taskmark ${command})`)
171
235
  process.exit(1)
172
236
  }
237
+ return resolved
238
+ }
173
239
 
174
- const serverJs = findStandaloneServer()
175
- if (!serverJs) {
176
- console.error(`Prebuilt server not found.
240
+ function attachChildLifecycle(child, onShutdown) {
241
+ const shutdown = (signal) => {
242
+ onShutdown?.()
243
+ if (!child.killed) child.kill(signal)
244
+ }
245
+ process.on("SIGINT", () => shutdown("SIGINT"))
246
+ process.on("SIGTERM", () => shutdown("SIGTERM"))
247
+ child.on("exit", (code, signal) => {
248
+ onShutdown?.()
249
+ if (signal) process.exit(1)
250
+ process.exit(code ?? 0)
251
+ })
252
+ return shutdown
253
+ }
177
254
 
178
- From the taskmark package source, run:
179
- npm run build
255
+ async function serve(args) {
256
+ const resolved = resolveBoardOrExit(args.board, "serve")
257
+ const port = resolvePort(args.port)
258
+ const url = `http://localhost:${port}`
259
+ const nextBin = resolveNextBin()
180
260
 
181
- Or install a published build that includes dist/standalone.`)
261
+ try {
262
+ ensureProdNext()
263
+ } catch (err) {
264
+ console.error(String(err?.message || err))
182
265
  process.exit(1)
183
266
  }
184
267
 
268
+ console.log(`Taskmark UI`)
269
+ console.log(` board: ${resolved.boardPath} (${resolved.source})`)
270
+ console.log(` listen: ${url}`)
271
+
272
+ const child = spawn(
273
+ process.execPath,
274
+ [nextBin, "start", "-H", "0.0.0.0", "-p", String(port)],
275
+ {
276
+ cwd: packageRoot,
277
+ env: {
278
+ ...process.env,
279
+ PORT: String(port),
280
+ HOSTNAME: "0.0.0.0",
281
+ TASKMARK_BOARD: resolved.boardPath,
282
+ NODE_ENV: "production",
283
+ },
284
+ stdio: "inherit",
285
+ }
286
+ )
287
+
288
+ const shutdown = attachChildLifecycle(child)
289
+
290
+ const hosted =
291
+ Boolean(process.env.VERCEL) ||
292
+ Boolean(process.env.NOW_REGION) ||
293
+ process.env.CI === "true"
294
+ const shouldOpen = args.open && !hosted
295
+
296
+ try {
297
+ await waitForServer(port)
298
+ if (shouldOpen) {
299
+ openBrowser(url)
300
+ } else {
301
+ console.log(`Ready: ${url}`)
302
+ }
303
+ } catch (err) {
304
+ console.error(String(err?.message || err))
305
+ shutdown("SIGTERM")
306
+ process.exit(1)
307
+ }
308
+ }
309
+
310
+ async function dev(args) {
311
+ const resolved = resolveBoardOrExit(args.board, "dev")
185
312
  const port = resolvePort(args.port)
186
313
  const url = `http://localhost:${port}`
187
- const standaloneDir = path.dirname(serverJs)
314
+ const nextBin = resolveNextBin()
188
315
 
189
- console.log(`Taskmark UI`)
316
+ console.log(`Taskmark UI (dev)`)
190
317
  console.log(` board: ${resolved.boardPath} (${resolved.source})`)
191
318
  console.log(` listen: ${url}`)
319
+ console.log(` watch: **/*.md under board`)
192
320
 
193
- const child = spawn(process.execPath, [serverJs], {
194
- cwd: standaloneDir,
195
- env: {
196
- ...process.env,
197
- PORT: String(port),
198
- HOSTNAME: "0.0.0.0",
199
- TASKMARK_BOARD: resolved.boardPath,
200
- },
201
- stdio: "inherit",
321
+ const child = spawn(
322
+ process.execPath,
323
+ [nextBin, "dev", "--webpack", "-H", "0.0.0.0", "-p", String(port)],
324
+ {
325
+ cwd: packageRoot,
326
+ env: {
327
+ ...process.env,
328
+ PORT: String(port),
329
+ TASKMARK_BOARD: resolved.boardPath,
330
+ TASKMARK_CWD: resolved.boardPath,
331
+ NODE_ENV: "development",
332
+ },
333
+ stdio: "inherit",
334
+ }
335
+ )
336
+
337
+ let debounce = null
338
+ const stopWatch = watchBoardMarkdown(resolved.boardPath, () => {
339
+ if (debounce) clearTimeout(debounce)
340
+ debounce = setTimeout(() => {
341
+ try {
342
+ touchDevReloadToken()
343
+ console.log(`[taskmark] board markdown changed — refreshing`)
344
+ } catch (err) {
345
+ console.warn(`[taskmark] reload touch failed: ${err?.message || err}`)
346
+ }
347
+ }, 200)
202
348
  })
203
349
 
204
- const shutdown = (signal) => {
205
- if (!child.killed) child.kill(signal)
206
- }
207
- process.on("SIGINT", () => shutdown("SIGINT"))
208
- process.on("SIGTERM", () => shutdown("SIGTERM"))
209
-
210
- child.on("exit", (code, signal) => {
211
- if (signal) process.exit(1)
212
- process.exit(code ?? 0)
350
+ const shutdown = attachChildLifecycle(child, () => {
351
+ stopWatch()
352
+ if (debounce) clearTimeout(debounce)
213
353
  })
214
354
 
215
355
  const hosted =
@@ -219,7 +359,7 @@ Or install a published build that includes dist/standalone.`)
219
359
  const shouldOpen = args.open && !hosted
220
360
 
221
361
  try {
222
- await waitForServer(port)
362
+ await waitForServer(port, 120_000)
223
363
  if (shouldOpen) {
224
364
  openBrowser(url)
225
365
  } else {
@@ -241,19 +381,7 @@ async function preview(args) {
241
381
  }
242
382
 
243
383
  const boardArg = resolveCallerPath(args.board)
244
- const resolved = resolveServeBoard(boardArg)
245
- if (!resolved) {
246
- console.error(`No Taskmark board found.
247
-
248
- Looked for:
249
- - --board / TASKMARK_BOARD
250
- - TASKMARK_MASTER
251
- - cwd layouts: nested ./taskmark/ or dedicated *-taskmark root
252
-
253
- Fix: cd into a product repo (with ./taskmark/) or a board root, or set TASKMARK_BOARD.
254
- Then re-run: npx taskmark preview`)
255
- process.exit(1)
256
- }
384
+ const resolved = resolveBoardOrExit(boardArg, "preview")
257
385
 
258
386
  const outDir = args.out
259
387
  ? resolveCallerPath(args.out)
@@ -308,6 +436,15 @@ async function main() {
308
436
  }
309
437
  return
310
438
  }
439
+ if (args.command === "dev") {
440
+ try {
441
+ await dev(args)
442
+ } catch (err) {
443
+ console.error(String(err?.message || err))
444
+ process.exit(1)
445
+ }
446
+ return
447
+ }
311
448
  if (args.command === "preview") {
312
449
  try {
313
450
  await preview(args)
@@ -319,14 +456,11 @@ async function main() {
319
456
  }
320
457
  if (args.command === "build") {
321
458
  const buildScript = path.join(packageRoot, "scripts", "build-static.mjs")
322
- // Resolve relative paths against the caller's cwd (npm/yarn script dir),
323
- // not packageRoot — the child runs with cwd = @taskmark/ui.
324
459
  const callerCwd = process.env.INIT_CWD || process.cwd()
325
460
  const childArgs = [buildScript]
326
461
  if (args.board) {
327
462
  childArgs.push("--board", path.resolve(callerCwd, args.board))
328
463
  } else {
329
- // Prefer the invoke directory as the board when no --board flag.
330
464
  childArgs.push("--board", callerCwd)
331
465
  }
332
466
  if (args.out) {