@taskmark/ui 0.2.2 → 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 (209) hide show
  1. package/README.md +23 -7
  2. package/app/api/taskmark-reload/route.ts +103 -0
  3. package/app/board/page.tsx +6 -4
  4. package/app/globals.css +17 -1
  5. package/app/layout.tsx +2 -0
  6. package/bin/lib/static-preview.mjs +110 -0
  7. package/bin/lib/watch-board-markdown.mjs +65 -0
  8. package/bin/taskmark.js +254 -46
  9. package/components/board/app-bar.tsx +31 -34
  10. package/components/board/board-dev-reloader.tsx +49 -0
  11. package/components/board/epic-list.tsx +2 -4
  12. package/components/board/markdown-content.tsx +1 -2
  13. package/components/board/overall-work-items-list.tsx +2 -1
  14. package/components/board/static-board-app.tsx +0 -1
  15. package/components/board/story-list.tsx +2 -1
  16. package/components/board/task-list.tsx +2 -1
  17. package/components/board/work-item-sheet.tsx +15 -2
  18. package/components/board/work-items-list.tsx +2 -2
  19. package/components/setup/setup-wizard.tsx +2 -6
  20. package/dist/prod-next/BUILD_ID +1 -0
  21. package/dist/{standalone/.next → prod-next}/app-path-routes-manifest.json +3 -2
  22. package/dist/{standalone/.next → prod-next}/build-manifest.json +3 -3
  23. package/dist/prod-next/export-marker.json +6 -0
  24. package/dist/prod-next/images-manifest.json +68 -0
  25. package/dist/prod-next/next-minimal-server.js.nft.json +1 -0
  26. package/dist/prod-next/next-server.js.nft.json +1 -0
  27. package/dist/{standalone/.next → prod-next}/prerender-manifest.json +3 -3
  28. package/dist/{standalone/.next → prod-next}/react-loadable-manifest.json +2 -2
  29. package/dist/prod-next/required-server-files.js +337 -0
  30. package/dist/{standalone/.next → prod-next}/required-server-files.json +0 -7
  31. package/dist/{standalone/.next → prod-next}/routes-manifest.json +6 -0
  32. package/dist/prod-next/server/app/_global-error/page.js +32 -0
  33. package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -0
  34. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  35. package/dist/{standalone/.next → prod-next}/server/app/_global-error.html +1 -1
  36. package/dist/{standalone/.next → prod-next}/server/app/_global-error.rsc +1 -1
  37. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/prod-next/server/app/_not-found/page.js +2 -0
  44. package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -0
  45. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  46. package/dist/prod-next/server/app/_not-found.html +1 -0
  47. package/dist/prod-next/server/app/_not-found.rsc +21 -0
  48. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +21 -0
  49. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +8 -0
  51. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  54. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -0
  55. package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -0
  56. package/dist/prod-next/server/app/api/taskmark-reload/route_client-reference-manifest.js +1 -0
  57. package/dist/prod-next/server/app/board/page.js +50 -0
  58. package/dist/prod-next/server/app/board/page.js.nft.json +1 -0
  59. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -0
  60. package/dist/{standalone/.next → prod-next}/server/app/favicon.ico/route.js +1 -1
  61. package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -0
  62. package/dist/prod-next/server/app/page.js +2 -0
  63. package/dist/prod-next/server/app/page.js.nft.json +1 -0
  64. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -0
  65. package/dist/prod-next/server/app/setup/page.js +2 -0
  66. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -0
  67. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -0
  68. package/dist/{standalone/.next → prod-next}/server/app-paths-manifest.json +3 -2
  69. package/dist/{standalone/.next → prod-next}/server/chunks/319.js +1 -1
  70. package/dist/{standalone/.next → prod-next}/server/chunks/490.js +1 -1
  71. package/dist/prod-next/server/chunks/57.js +19 -0
  72. package/dist/prod-next/server/chunks/616.js +1 -0
  73. package/dist/prod-next/server/chunks/708.js +1 -0
  74. package/dist/prod-next/server/chunks/724.js +1 -0
  75. package/dist/{standalone/.next/server/chunks/445.js → prod-next/server/chunks/741.js} +2 -2
  76. package/dist/prod-next/server/functions-config-manifest.json +6 -0
  77. package/dist/prod-next/server/interception-route-rewrite-manifest.js +1 -0
  78. package/dist/{standalone/.next → prod-next}/server/middleware-build-manifest.js +1 -1
  79. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -0
  80. package/dist/prod-next/server/pages/404.html +1 -0
  81. package/dist/{standalone/.next → prod-next}/server/pages/500.html +1 -1
  82. package/dist/prod-next/server/server-reference-manifest.js +1 -0
  83. package/dist/prod-next/server/server-reference-manifest.json +1 -0
  84. package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +1 -0
  85. package/dist/{standalone/.next/static/chunks/781-f9f201ddc8c8591a.js → prod-next/static/chunks/613-6f794154d28bb75a.js} +1 -1
  86. package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +1 -0
  87. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +1 -0
  88. package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +1 -0
  89. package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +1 -0
  90. package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +1 -0
  91. package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +1 -0
  92. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +1 -0
  93. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +1 -0
  94. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +1 -0
  95. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +1 -0
  96. package/dist/prod-next/static/chunks/webpack-bfe8992962004e2b.js +1 -0
  97. package/dist/prod-next/static/css/2e6ec4c80894e3c0.css +1 -0
  98. package/dist/prod-next/static/wbcgYS3Jw87KqhnA9ttOL/_buildManifest.js +1 -0
  99. package/dist/public/taskmark-snapshot.json +1 -0
  100. package/lib/display-path.ts +6 -0
  101. package/lib/taskmark/dev-reload-token.ts +5 -0
  102. package/lib/taskmark/project-label.ts +3 -2
  103. package/lib/taskmark/validate.ts +4 -4
  104. package/next.config.ts +3 -8
  105. package/package.json +6 -4
  106. package/public/taskmark-snapshot.json +1 -1
  107. package/scripts/build-static.mjs +10 -1
  108. package/scripts/prepare-standalone.mjs +53 -20
  109. package/dist/standalone/.next/BUILD_ID +0 -1
  110. package/dist/standalone/.next/server/app/_global-error/page.js +0 -32
  111. package/dist/standalone/.next/server/app/_global-error/page.js.nft.json +0 -1
  112. package/dist/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +0 -1
  113. package/dist/standalone/.next/server/app/_not-found/page.js +0 -2
  114. package/dist/standalone/.next/server/app/_not-found/page.js.nft.json +0 -1
  115. package/dist/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  116. package/dist/standalone/.next/server/app/_not-found.html +0 -1
  117. package/dist/standalone/.next/server/app/_not-found.rsc +0 -20
  118. package/dist/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +0 -20
  119. package/dist/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +0 -7
  120. package/dist/standalone/.next/server/app/board/page.js +0 -50
  121. package/dist/standalone/.next/server/app/board/page.js.nft.json +0 -1
  122. package/dist/standalone/.next/server/app/board/page_client-reference-manifest.js +0 -1
  123. package/dist/standalone/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  124. package/dist/standalone/.next/server/app/page.js +0 -2
  125. package/dist/standalone/.next/server/app/page.js.nft.json +0 -1
  126. package/dist/standalone/.next/server/app/page_client-reference-manifest.js +0 -1
  127. package/dist/standalone/.next/server/app/setup/page.js +0 -2
  128. package/dist/standalone/.next/server/app/setup/page.js.nft.json +0 -1
  129. package/dist/standalone/.next/server/app/setup/page_client-reference-manifest.js +0 -1
  130. package/dist/standalone/.next/server/chunks/57.js +0 -19
  131. package/dist/standalone/.next/server/chunks/570.js +0 -1
  132. package/dist/standalone/.next/server/chunks/600.js +0 -1
  133. package/dist/standalone/.next/server/chunks/666.js +0 -1
  134. package/dist/standalone/.next/server/functions-config-manifest.json +0 -4
  135. package/dist/standalone/.next/server/middleware-react-loadable-manifest.js +0 -1
  136. package/dist/standalone/.next/server/pages/404.html +0 -1
  137. package/dist/standalone/.next/server/server-reference-manifest.js +0 -1
  138. package/dist/standalone/.next/server/server-reference-manifest.json +0 -1
  139. package/dist/standalone/.next/static/O83kNzYUmJlkfw7SS-SsU/_buildManifest.js +0 -1
  140. package/dist/standalone/.next/static/chunks/826.92750d8ed3a55429.js +0 -1
  141. package/dist/standalone/.next/static/chunks/app/_global-error/page-82d456ade9f47021.js +0 -1
  142. package/dist/standalone/.next/static/chunks/app/board/page-f609a854d76933c2.js +0 -1
  143. package/dist/standalone/.next/static/chunks/app/layout-3745e06d87691845.js +0 -1
  144. package/dist/standalone/.next/static/chunks/app/page-82d456ade9f47021.js +0 -1
  145. package/dist/standalone/.next/static/chunks/app/setup/page-acaffda2083bf4d0.js +0 -1
  146. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-82d456ade9f47021.js +0 -1
  147. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-82d456ade9f47021.js +0 -1
  148. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-82d456ade9f47021.js +0 -1
  149. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-82d456ade9f47021.js +0 -1
  150. package/dist/standalone/.next/static/chunks/webpack-f181688592aeb1f6.js +0 -1
  151. package/dist/standalone/.next/static/css/7af268deac5c5edb.css +0 -1
  152. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/processChild.js +0 -1
  153. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/threadChild.js +0 -1
  154. package/dist/standalone/node_modules/next/dist/server/lib/start-server.js +0 -494
  155. package/dist/standalone/node_modules/next/dist/server/next-server.js +0 -1429
  156. package/dist/standalone/node_modules/next/dist/server/next.js +0 -455
  157. package/dist/standalone/node_modules/next/dist/server/require-hook.js +0 -82
  158. package/dist/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +0 -36
  159. package/dist/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +0 -20
  160. package/dist/standalone/node_modules/styled-jsx/index.js +0 -1
  161. package/dist/standalone/node_modules/styled-jsx/package.json +0 -132
  162. package/dist/standalone/node_modules/styled-jsx/style.js +0 -1
  163. package/dist/standalone/package.json +0 -76
  164. package/dist/standalone/public/taskmark-snapshot.json +0 -1
  165. package/dist/standalone/server.js +0 -44
  166. /package/dist/{standalone/.next → prod-next}/package.json +0 -0
  167. /package/dist/{standalone/.next → prod-next}/server/app/_global-error.meta +0 -0
  168. /package/dist/{standalone/.next → prod-next}/server/app/_not-found.meta +0 -0
  169. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.body +0 -0
  170. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.meta +0 -0
  171. /package/dist/{standalone/.next → prod-next}/server/chunks/318.js +0 -0
  172. /package/dist/{standalone/.next → prod-next}/server/middleware-manifest.json +0 -0
  173. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.js +0 -0
  174. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.json +0 -0
  175. /package/dist/{standalone/.next → prod-next}/server/pages-manifest.json +0 -0
  176. /package/dist/{standalone/.next → prod-next}/server/prefetch-hints.json +0 -0
  177. /package/dist/{standalone/.next → prod-next}/server/webpack-runtime.js +0 -0
  178. /package/dist/{standalone/.next → prod-next}/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -0
  179. /package/dist/{standalone/.next → prod-next}/static/chunks/669-8103441c67b185da.js +0 -0
  180. /package/dist/{standalone/.next → prod-next}/static/chunks/701-df3ee5f1cf837b1a.js +0 -0
  181. /package/dist/{standalone/.next → prod-next}/static/chunks/794-25668765560cb197.js +0 -0
  182. /package/dist/{standalone/.next → prod-next}/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -0
  183. /package/dist/{standalone/.next → prod-next}/static/chunks/framework-b29f9083d3cda3b2.js +0 -0
  184. /package/dist/{standalone/.next → prod-next}/static/chunks/main-app-bfb36455d24b3e5a.js +0 -0
  185. /package/dist/{standalone/.next → prod-next}/static/chunks/main-bb33c46a9c25dee8.js +0 -0
  186. /package/dist/{standalone/.next → prod-next}/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -0
  187. /package/dist/{standalone/.next → prod-next}/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  188. /package/dist/{standalone/.next → prod-next}/static/css/c593627ceef17a4b.css +0 -0
  189. /package/dist/{standalone/.next → prod-next}/static/media/36966cca54120369-s.p.woff2 +0 -0
  190. /package/dist/{standalone/.next → prod-next}/static/media/982ceffe7b733b3b-s.p.woff2 +0 -0
  191. /package/dist/{standalone/.next → prod-next}/static/media/b7387a63dd068245-s.woff2 +0 -0
  192. /package/dist/{standalone/.next → prod-next}/static/media/b8bfb68ab9ab617c-s.woff2 +0 -0
  193. /package/dist/{standalone/.next → prod-next}/static/media/e1aab0933260df4d-s.woff2 +0 -0
  194. /package/dist/{standalone/.next/static/O83kNzYUmJlkfw7SS-SsU → prod-next/static/wbcgYS3Jw87KqhnA9ttOL}/_ssgManifest.js +0 -0
  195. /package/dist/{standalone/public → public}/apple-touch-icon.png +0 -0
  196. /package/dist/{standalone/public → public}/favicon-96x96.png +0 -0
  197. /package/dist/{standalone/public → public}/favicon.ico +0 -0
  198. /package/dist/{standalone/public → public}/favicon.svg +0 -0
  199. /package/dist/{standalone/public → public}/file.svg +0 -0
  200. /package/dist/{standalone/public → public}/globe.svg +0 -0
  201. /package/dist/{standalone/public → public}/next.svg +0 -0
  202. /package/dist/{standalone/public → public}/og/og-default.png +0 -0
  203. /package/dist/{standalone/public → public}/site.webmanifest +0 -0
  204. /package/dist/{standalone/public → public}/tm_dark.png +0 -0
  205. /package/dist/{standalone/public → public}/tm_light.png +0 -0
  206. /package/dist/{standalone/public → public}/vercel.svg +0 -0
  207. /package/dist/{standalone/public → public}/web-app-manifest-192x192.png +0 -0
  208. /package/dist/{standalone/public → public}/web-app-manifest-512x512.png +0 -0
  209. /package/dist/{standalone/public → public}/window.svg +0 -0
package/bin/taskmark.js CHANGED
@@ -3,28 +3,36 @@ 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"
10
+ import { startStaticServer } from "./lib/static-preview.mjs"
11
+ import { watchBoardMarkdown } from "./lib/watch-board-markdown.mjs"
9
12
 
10
13
  const DEFAULT_PORT = 8275
11
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
15
  const packageRoot = path.resolve(__dirname, "..")
16
+ const requireFromPackage = createRequire(path.join(packageRoot, "package.json"))
13
17
 
14
18
  function printHelp() {
15
19
  console.log(`Usage:
16
20
  taskmark serve [options]
21
+ taskmark dev [options]
17
22
  taskmark build [options]
23
+ taskmark preview [options]
18
24
 
19
25
  Commands:
20
26
  serve Start the prebuilt local UI server (default port ${DEFAULT_PORT})
27
+ dev Next.js development server with board markdown live reload
21
28
  build Production static HTML export for Vercel / static hosting
29
+ preview Serve an existing static export (default: <board>/out)
22
30
 
23
31
  Options:
24
- --port, -p <n> Listen port for serve (default ${DEFAULT_PORT}; env PORT / TASKMARK_PORT)
32
+ --port, -p <n> Listen port (default ${DEFAULT_PORT}; env PORT / TASKMARK_PORT)
25
33
  --board <path> Board or product root (sets TASKMARK_BOARD)
26
- --out <dir> Static output directory for build (default: <board>/out)
27
- --no-open Do not open a browser (serve)
34
+ --out <dir> Static output directory for build/preview (default: <board>/out)
35
+ --no-open Do not open a browser (serve / preview / dev)
28
36
  --help, -h Show help
29
37
 
30
38
  Board resolution (same as the UI):
@@ -35,9 +43,11 @@ Board resolution (same as the UI):
35
43
 
36
44
  Examples:
37
45
  npm i @taskmark/ui --save && npx taskmark serve
46
+ npx taskmark dev --board .
38
47
  npx taskmark build --board .
48
+ npm run preview
49
+ npx taskmark preview --port 9000
39
50
  npx -p @taskmark/ui taskmark serve
40
- npx taskmark serve --port 9000
41
51
  TASKMARK_BOARD=/path/to/my-app/taskmark npx taskmark serve
42
52
  `)
43
53
  }
@@ -93,15 +103,73 @@ function resolvePort(raw) {
93
103
  return n
94
104
  }
95
105
 
96
- function findStandaloneServer() {
97
- const candidates = [
98
- path.join(packageRoot, "dist", "standalone", "server.js"),
99
- path.join(packageRoot, ".next", "standalone", "server.js"),
100
- ]
101
- for (const candidate of candidates) {
102
- 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
+ )
122
+ }
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 })
103
156
  }
104
- return null
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
+ )
105
173
  }
106
174
 
107
175
  function openBrowser(url) {
@@ -151,8 +219,8 @@ function waitForServer(port, timeoutMs = 60_000) {
151
219
  })
152
220
  }
153
221
 
154
- async function serve(args) {
155
- const resolved = resolveServeBoard(args.board)
222
+ function resolveBoardOrExit(boardArg, command) {
223
+ const resolved = resolveServeBoard(boardArg)
156
224
  if (!resolved) {
157
225
  console.error(`No Taskmark board found.
158
226
 
@@ -162,50 +230,126 @@ Looked for:
162
230
  - cwd layouts: nested ./taskmark/ or dedicated *-taskmark root
163
231
 
164
232
  Fix: cd into a product repo (with ./taskmark/) or a board root, or set TASKMARK_BOARD.
165
- Then re-run: npx taskmark serve
166
- (or: npx -p @taskmark/ui taskmark serve)`)
233
+ Then re-run: npx taskmark ${command}
234
+ (or: npx -p @taskmark/ui taskmark ${command})`)
235
+ process.exit(1)
236
+ }
237
+ return resolved
238
+ }
239
+
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
+ }
254
+
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()
260
+
261
+ try {
262
+ ensureProdNext()
263
+ } catch (err) {
264
+ console.error(String(err?.message || err))
167
265
  process.exit(1)
168
266
  }
169
267
 
170
- const serverJs = findStandaloneServer()
171
- if (!serverJs) {
172
- console.error(`Prebuilt server not found.
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
+ )
173
287
 
174
- From the taskmark package source, run:
175
- npm run build
288
+ const shutdown = attachChildLifecycle(child)
176
289
 
177
- Or install a published build that includes dist/standalone.`)
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")
178
306
  process.exit(1)
179
307
  }
308
+ }
180
309
 
310
+ async function dev(args) {
311
+ const resolved = resolveBoardOrExit(args.board, "dev")
181
312
  const port = resolvePort(args.port)
182
313
  const url = `http://localhost:${port}`
183
- const standaloneDir = path.dirname(serverJs)
314
+ const nextBin = resolveNextBin()
184
315
 
185
- console.log(`Taskmark UI`)
316
+ console.log(`Taskmark UI (dev)`)
186
317
  console.log(` board: ${resolved.boardPath} (${resolved.source})`)
187
318
  console.log(` listen: ${url}`)
319
+ console.log(` watch: **/*.md under board`)
188
320
 
189
- const child = spawn(process.execPath, [serverJs], {
190
- cwd: standaloneDir,
191
- env: {
192
- ...process.env,
193
- PORT: String(port),
194
- HOSTNAME: "0.0.0.0",
195
- TASKMARK_BOARD: resolved.boardPath,
196
- },
197
- stdio: "inherit",
198
- })
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
+ )
199
336
 
200
- const shutdown = (signal) => {
201
- if (!child.killed) child.kill(signal)
202
- }
203
- process.on("SIGINT", () => shutdown("SIGINT"))
204
- process.on("SIGTERM", () => shutdown("SIGTERM"))
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)
348
+ })
205
349
 
206
- child.on("exit", (code, signal) => {
207
- if (signal) process.exit(1)
208
- process.exit(code ?? 0)
350
+ const shutdown = attachChildLifecycle(child, () => {
351
+ stopWatch()
352
+ if (debounce) clearTimeout(debounce)
209
353
  })
210
354
 
211
355
  const hosted =
@@ -215,7 +359,7 @@ Or install a published build that includes dist/standalone.`)
215
359
  const shouldOpen = args.open && !hosted
216
360
 
217
361
  try {
218
- await waitForServer(port)
362
+ await waitForServer(port, 120_000)
219
363
  if (shouldOpen) {
220
364
  openBrowser(url)
221
365
  } else {
@@ -228,6 +372,55 @@ Or install a published build that includes dist/standalone.`)
228
372
  }
229
373
  }
230
374
 
375
+ async function preview(args) {
376
+ const callerCwd = process.env.INIT_CWD || process.cwd()
377
+
378
+ function resolveCallerPath(p) {
379
+ if (!p) return null
380
+ return path.isAbsolute(p) ? path.resolve(p) : path.resolve(callerCwd, p)
381
+ }
382
+
383
+ const boardArg = resolveCallerPath(args.board)
384
+ const resolved = resolveBoardOrExit(boardArg, "preview")
385
+
386
+ const outDir = args.out
387
+ ? resolveCallerPath(args.out)
388
+ : path.join(resolved.boardPath, "out")
389
+
390
+ if (!outDir || !fs.existsSync(outDir) || !fs.statSync(outDir).isDirectory()) {
391
+ console.error(`Static export not found: ${outDir || "(missing)"}
392
+
393
+ Run a production build first:
394
+ npx taskmark build --board .
395
+ # or: npm run build
396
+
397
+ Then: npm run preview`)
398
+ process.exit(1)
399
+ }
400
+
401
+ const port = resolvePort(args.port)
402
+ const url = `http://localhost:${port}`
403
+
404
+ console.log(`Taskmark static preview`)
405
+ console.log(` board: ${resolved.boardPath} (${resolved.source})`)
406
+ console.log(` out: ${outDir}`)
407
+ console.log(` listen: ${url}`)
408
+
409
+ await startStaticServer({ root: outDir, port })
410
+
411
+ const hosted =
412
+ Boolean(process.env.VERCEL) ||
413
+ Boolean(process.env.NOW_REGION) ||
414
+ process.env.CI === "true"
415
+ const shouldOpen = args.open && !hosted
416
+
417
+ if (shouldOpen) {
418
+ openBrowser(url)
419
+ } else {
420
+ console.log(`Ready: ${url}`)
421
+ }
422
+ }
423
+
231
424
  async function main() {
232
425
  const args = parseArgs(process.argv.slice(2))
233
426
  if (args.help || !args.command) {
@@ -243,16 +436,31 @@ async function main() {
243
436
  }
244
437
  return
245
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
+ }
448
+ if (args.command === "preview") {
449
+ try {
450
+ await preview(args)
451
+ } catch (err) {
452
+ console.error(String(err?.message || err))
453
+ process.exit(1)
454
+ }
455
+ return
456
+ }
246
457
  if (args.command === "build") {
247
458
  const buildScript = path.join(packageRoot, "scripts", "build-static.mjs")
248
- // Resolve relative paths against the caller's cwd (npm/yarn script dir),
249
- // not packageRoot — the child runs with cwd = @taskmark/ui.
250
459
  const callerCwd = process.env.INIT_CWD || process.cwd()
251
460
  const childArgs = [buildScript]
252
461
  if (args.board) {
253
462
  childArgs.push("--board", path.resolve(callerCwd, args.board))
254
463
  } else {
255
- // Prefer the invoke directory as the board when no --board flag.
256
464
  childArgs.push("--board", callerCwd)
257
465
  }
258
466
  if (args.out) {
@@ -9,15 +9,13 @@ import type { DiscoveredProject } from "@/lib/taskmark/types"
9
9
  type AppBarProps = {
10
10
  projects: DiscoveredProject[]
11
11
  activeProjectId: string
12
- masterCount: number
13
- /** When true, workspace is env/cwd-bound — hide multi-master add. */
12
+ /** When true, workspace is env/cwd-bound — hide project switcher and add. */
14
13
  autoconfig?: boolean
15
14
  }
16
15
 
17
16
  export function AppBar({
18
17
  projects,
19
18
  activeProjectId,
20
- masterCount,
21
19
  autoconfig = false,
22
20
  }: AppBarProps) {
23
21
  return (
@@ -49,42 +47,41 @@ export function AppBar({
49
47
  </span>
50
48
  </p>
51
49
  <p className="truncate text-xs text-muted-foreground transition-transform duration-200 group-hover:translate-y-1">
52
- {projects.length} project{projects.length === 1 ? "" : "s"}
53
- {masterCount > 0
54
- ? ` · ${masterCount} workspace${masterCount === 1 ? "" : "s"}`
55
- : ""}
50
+ Product memory for agent work
56
51
  </p>
57
52
  </div>
58
53
  </div>
59
54
 
60
- <form
61
- action={selectActiveProject}
62
- className="flex min-w-0 flex-wrap items-center gap-2"
63
- >
64
- <label htmlFor="projectId" className="sr-only">
65
- Active project
66
- </label>
67
- <select
68
- id="projectId"
69
- name="projectId"
70
- defaultValue={activeProjectId}
71
- onChange={(e) => {
72
- e.currentTarget.form?.requestSubmit()
73
- }}
74
- className="max-w-[20rem] rounded border-2 border-border bg-input px-3 py-1.5 text-sm shadow-sm outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
55
+ {!autoconfig ? (
56
+ <form
57
+ action={selectActiveProject}
58
+ className="flex min-w-0 flex-wrap items-center gap-2"
75
59
  >
76
- {projects.map((project) => (
77
- <option key={project.id} value={project.id}>
78
- {projectOptionLabel(project, projects)}
79
- </option>
80
- ))}
81
- </select>
82
- <noscript>
83
- <Button type="submit" size="sm" variant="outline">
84
- Switch project
85
- </Button>
86
- </noscript>
87
- </form>
60
+ <label htmlFor="projectId" className="sr-only">
61
+ Active project
62
+ </label>
63
+ <select
64
+ id="projectId"
65
+ name="projectId"
66
+ defaultValue={activeProjectId}
67
+ onChange={(e) => {
68
+ e.currentTarget.form?.requestSubmit()
69
+ }}
70
+ className="max-w-[20rem] rounded border-2 border-border bg-input px-3 py-1.5 text-sm shadow-sm outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
71
+ >
72
+ {projects.map((project) => (
73
+ <option key={project.id} value={project.id}>
74
+ {projectOptionLabel(project, projects)}
75
+ </option>
76
+ ))}
77
+ </select>
78
+ <noscript>
79
+ <Button type="submit" size="sm" variant="outline">
80
+ Switch project
81
+ </Button>
82
+ </noscript>
83
+ </form>
84
+ ) : null}
88
85
 
89
86
  {!autoconfig ? (
90
87
  <form action={openAddProject}>
@@ -0,0 +1,49 @@
1
+ "use client"
2
+
3
+ import { useRouter } from "next/navigation"
4
+ import { useEffect, useRef } from "react"
5
+
6
+ /**
7
+ * In development, poll the board-reload SSE endpoint and soft-refresh RSC
8
+ * trees when markdown on disk changes.
9
+ */
10
+ export function BoardDevReloader() {
11
+ const router = useRouter()
12
+ const lastAt = useRef(0)
13
+
14
+ useEffect(() => {
15
+ if (process.env.NODE_ENV !== "development") return
16
+
17
+ let closed = false
18
+ let source: EventSource | null = null
19
+ let retryTimer: ReturnType<typeof setTimeout> | null = null
20
+
21
+ const connect = () => {
22
+ if (closed) return
23
+ source = new EventSource("/api/taskmark-reload")
24
+ source.addEventListener("reload", (event) => {
25
+ const data = String((event as MessageEvent).data || "")
26
+ const at = Number.parseInt(data, 10)
27
+ if (Number.isFinite(at) && at <= lastAt.current) return
28
+ lastAt.current = Number.isFinite(at) ? at : Date.now()
29
+ router.refresh()
30
+ })
31
+ source.onerror = () => {
32
+ source?.close()
33
+ source = null
34
+ if (!closed) {
35
+ retryTimer = setTimeout(connect, 1500)
36
+ }
37
+ }
38
+ }
39
+
40
+ connect()
41
+ return () => {
42
+ closed = true
43
+ if (retryTimer) clearTimeout(retryTimer)
44
+ source?.close()
45
+ }
46
+ }, [router])
47
+
48
+ return null
49
+ }
@@ -33,6 +33,7 @@ import { usePaginatedRows } from "@/hooks/use-paginated-rows"
33
33
  import { usePersistedHideCompleted } from "@/hooks/use-persisted-hide-completed"
34
34
  import { useTableSort } from "@/hooks/use-table-sort"
35
35
  import { cn } from "@/lib/utils"
36
+ import { displayFileName } from "@/lib/display-path"
36
37
  import type { EpicSummary, ProjectEpicList } from "@/lib/taskmark/epic-types"
37
38
  import { isGeneralEpic } from "@/lib/taskmark/general-epic"
38
39
  import {
@@ -145,9 +146,6 @@ function ProjectEpicCard({
145
146
  <Card>
146
147
  <CardHeader>
147
148
  <CardTitle className="font-head text-xl">{project.name}</CardTitle>
148
- <CardDescription className="font-mono text-xs">
149
- {project.boardPath}
150
- </CardDescription>
151
149
  {hasSourceRows ? (
152
150
  <CardAction>
153
151
  <TimeframeFilter
@@ -173,7 +171,7 @@ function ProjectEpicCard({
173
171
  <ul className="mt-2 flex flex-col gap-1 font-mono text-xs text-muted-foreground">
174
172
  {errors.map((err) => (
175
173
  <li key={err.filePath}>
176
- {err.filePath}: {err.message}
174
+ {displayFileName(err.filePath)}: {err.message}
177
175
  </li>
178
176
  ))}
179
177
  </ul>
@@ -13,8 +13,7 @@ type MarkdownContentProps = {
13
13
  }
14
14
 
15
15
  const proseClassName = cn(
16
- "text-sm leading-relaxed text-foreground",
17
- "[&>:first-child]:mt-0 [&>:last-child]:mb-0",
16
+ "tm-md text-sm leading-relaxed text-foreground",
18
17
  "[&_p]:my-2",
19
18
  "[&_h1]:mt-3 [&_h1]:mb-2 [&_h1]:font-head [&_h1]:text-lg [&_h1]:font-semibold",
20
19
  "[&_h2]:mt-3 [&_h2]:mb-2 [&_h2]:font-head [&_h2]:text-base [&_h2]:font-semibold",
@@ -36,6 +36,7 @@ import { usePaginatedRows } from "@/hooks/use-paginated-rows"
36
36
  import { usePersistedHideCompleted } from "@/hooks/use-persisted-hide-completed"
37
37
  import { useTableSort } from "@/hooks/use-table-sort"
38
38
  import { cn } from "@/lib/utils"
39
+ import { displayFileName } from "@/lib/display-path"
39
40
  import type { EpicWorkItemsList } from "@/lib/taskmark/flat-work-item-types"
40
41
  import {
41
42
  DEFAULT_TIMEFRAME_FILTER,
@@ -155,7 +156,7 @@ export function OverallWorkItemsList({
155
156
  <ul className="mt-2 flex flex-col gap-1 font-mono text-xs text-muted-foreground">
156
157
  {errors.map((err) => (
157
158
  <li key={`${err.filePath}:${err.message}`}>
158
- {err.filePath}: {err.message}
159
+ {displayFileName(err.filePath)}: {err.message}
159
160
  </li>
160
161
  ))}
161
162
  </ul>
@@ -82,7 +82,6 @@ export function StaticBoardApp({ snapshot }: { snapshot: BoardSnapshot }) {
82
82
  <AppBar
83
83
  projects={snapshot.projects}
84
84
  activeProjectId={activeProject.id}
85
- masterCount={1}
86
85
  autoconfig={true}
87
86
  />
88
87
 
@@ -34,6 +34,7 @@ import { usePaginatedRows } from "@/hooks/use-paginated-rows"
34
34
  import { usePersistedHideCompleted } from "@/hooks/use-persisted-hide-completed"
35
35
  import { useTableSort } from "@/hooks/use-table-sort"
36
36
  import { cn } from "@/lib/utils"
37
+ import { displayFileName } from "@/lib/display-path"
37
38
  import type { EpicStoryList } from "@/lib/taskmark/story-types"
38
39
  import {
39
40
  DEFAULT_TIMEFRAME_FILTER,
@@ -158,7 +159,7 @@ export function StoryList({
158
159
  <ul className="mt-2 flex flex-col gap-1 font-mono text-xs text-muted-foreground">
159
160
  {errors.map((err) => (
160
161
  <li key={`${err.filePath}:${err.message}`}>
161
- {err.filePath}: {err.message}
162
+ {displayFileName(err.filePath)}: {err.message}
162
163
  </li>
163
164
  ))}
164
165
  </ul>
@@ -33,6 +33,7 @@ import { usePaginatedRows } from "@/hooks/use-paginated-rows"
33
33
  import { usePersistedHideCompleted } from "@/hooks/use-persisted-hide-completed"
34
34
  import { useTableSort } from "@/hooks/use-table-sort"
35
35
  import type { StoryItemList } from "@/lib/taskmark/item-types"
36
+ import { displayFileName } from "@/lib/display-path"
36
37
  import {
37
38
  DEFAULT_TIMEFRAME_FILTER,
38
39
  filterListRows,
@@ -154,7 +155,7 @@ export function TaskList({
154
155
  <ul className="mt-2 flex flex-col gap-1 font-mono text-xs text-muted-foreground">
155
156
  {errors.map((err) => (
156
157
  <li key={`${err.filePath}:${err.message}`}>
157
- {err.filePath}: {err.message}
158
+ {displayFileName(err.filePath)}: {err.message}
158
159
  </li>
159
160
  ))}
160
161
  </ul>
@@ -39,6 +39,7 @@ import {
39
39
  loadWorkItemDetailFromSnapshot,
40
40
  resolveWorkItemByIdFromSnapshot,
41
41
  } from "@/lib/taskmark/snapshot-client"
42
+ import { displayFileName } from "@/lib/display-path"
42
43
 
43
44
  const isStatic =
44
45
  process.env.NEXT_PUBLIC_TASKMARK_STATIC === "1" ||
@@ -574,7 +575,7 @@ function WorkItemSheetProviderInner({
574
575
  <p className="mt-1 text-muted-foreground">{state.message}</p>
575
576
  {state.filePath ? (
576
577
  <p className="mt-2 break-all font-mono text-xs text-muted-foreground">
577
- {state.filePath}
578
+ {displayFileName(state.filePath)}
578
579
  </p>
579
580
  ) : null}
580
581
  </div>
@@ -598,13 +599,25 @@ function WorkItemSheetProviderInner({
598
599
  )
599
600
  }
600
601
 
602
+ /** Context while `useSearchParams` Suspense is pending — children must not render bare. */
603
+ const SUSPENSE_FALLBACK_SHEET: WorkItemSheetContextValue = {
604
+ openDetail: () => {},
605
+ openDetailById: () => {},
606
+ }
607
+
601
608
  export function WorkItemSheetProvider({
602
609
  children,
603
610
  }: {
604
611
  children: React.ReactNode
605
612
  }) {
606
613
  return (
607
- <React.Suspense fallback={children}>
614
+ <React.Suspense
615
+ fallback={
616
+ <WorkItemSheetContext.Provider value={SUSPENSE_FALLBACK_SHEET}>
617
+ {children}
618
+ </WorkItemSheetContext.Provider>
619
+ }
620
+ >
608
621
  <WorkItemSheetProviderInner>{children}</WorkItemSheetProviderInner>
609
622
  </React.Suspense>
610
623
  )