@taskmark/ui 0.2.4 → 0.2.6

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 (215) hide show
  1. package/README.md +46 -10
  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/app/setup/page.tsx +4 -1
  6. package/bin/lib/resolve-board.mjs +50 -1
  7. package/bin/lib/watch-board-markdown.mjs +65 -0
  8. package/bin/taskmark.js +310 -67
  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/overall-work-items-list.tsx +2 -1
  13. package/components/board/static-board-app.tsx +0 -1
  14. package/components/board/story-list.tsx +2 -1
  15. package/components/board/task-list.tsx +2 -1
  16. package/components/board/work-item-sheet.tsx +15 -2
  17. package/components/board/work-items-list.tsx +2 -2
  18. package/components/setup/setup-wizard.tsx +2 -6
  19. package/dist/prod-next/BUILD_ID +1 -0
  20. package/dist/{standalone/.next → prod-next}/app-path-routes-manifest.json +3 -2
  21. package/dist/{standalone/.next → prod-next}/build-manifest.json +3 -3
  22. package/dist/prod-next/export-marker.json +6 -0
  23. package/dist/prod-next/images-manifest.json +68 -0
  24. package/dist/prod-next/next-minimal-server.js.nft.json +1 -0
  25. package/dist/prod-next/next-server.js.nft.json +1 -0
  26. package/dist/{standalone/.next → prod-next}/prerender-manifest.json +27 -3
  27. package/dist/{standalone/.next → prod-next}/react-loadable-manifest.json +2 -2
  28. package/dist/prod-next/required-server-files.js +337 -0
  29. package/dist/{standalone/.next → prod-next}/required-server-files.json +0 -7
  30. package/dist/{standalone/.next → prod-next}/routes-manifest.json +6 -0
  31. package/dist/prod-next/server/app/_global-error/page.js +32 -0
  32. package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -0
  33. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  34. package/dist/{standalone/.next → prod-next}/server/app/_global-error.html +1 -1
  35. package/dist/{standalone/.next → prod-next}/server/app/_global-error.rsc +1 -1
  36. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_full.segment.rsc +1 -1
  37. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  38. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  39. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/dist/prod-next/server/app/_not-found/page.js +2 -0
  43. package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -0
  44. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  45. package/dist/prod-next/server/app/_not-found.html +1 -0
  46. package/dist/prod-next/server/app/_not-found.rsc +21 -0
  47. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +21 -0
  48. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_index.segment.rsc +5 -4
  50. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -0
  54. package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -0
  55. package/dist/prod-next/server/app/api/taskmark-reload/route_client-reference-manifest.js +1 -0
  56. package/dist/{standalone/.next → prod-next}/server/app/board/page.js +14 -14
  57. package/dist/prod-next/server/app/board/page.js.nft.json +1 -0
  58. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -0
  59. package/dist/{standalone/.next → prod-next}/server/app/favicon.ico/route.js +1 -1
  60. package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -0
  61. package/dist/prod-next/server/app/page.js +2 -0
  62. package/dist/prod-next/server/app/page.js.nft.json +1 -0
  63. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -0
  64. package/dist/prod-next/server/app/setup/page.js +2 -0
  65. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -0
  66. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -0
  67. package/dist/prod-next/server/app/setup.html +1 -0
  68. package/dist/prod-next/server/app/setup.meta +17 -0
  69. package/dist/{standalone/.next/server/app/_not-found.rsc → prod-next/server/app/setup.rsc} +17 -15
  70. package/dist/{standalone/.next/server/app/_not-found.segments → prod-next/server/app/setup.segments}/_full.segment.rsc +17 -15
  71. package/dist/prod-next/server/app/setup.segments/_head.segment.rsc +6 -0
  72. package/dist/prod-next/server/app/setup.segments/_index.segment.rsc +8 -0
  73. package/dist/prod-next/server/app/setup.segments/_tree.segment.rsc +5 -0
  74. package/dist/prod-next/server/app/setup.segments/setup/__PAGE__.segment.rsc +6 -0
  75. package/dist/prod-next/server/app/setup.segments/setup.segment.rsc +5 -0
  76. package/dist/{standalone/.next → prod-next}/server/app-paths-manifest.json +3 -2
  77. package/dist/{standalone/.next → prod-next}/server/chunks/319.js +1 -1
  78. package/dist/{standalone/.next → prod-next}/server/chunks/490.js +2 -2
  79. package/dist/prod-next/server/chunks/57.js +19 -0
  80. package/dist/prod-next/server/chunks/616.js +1 -0
  81. package/dist/prod-next/server/chunks/708.js +1 -0
  82. package/dist/prod-next/server/chunks/724.js +1 -0
  83. package/dist/{standalone/.next/server/chunks/445.js → prod-next/server/chunks/741.js} +2 -2
  84. package/dist/prod-next/server/functions-config-manifest.json +6 -0
  85. package/dist/prod-next/server/interception-route-rewrite-manifest.js +1 -0
  86. package/dist/{standalone/.next → prod-next}/server/middleware-build-manifest.js +1 -1
  87. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -0
  88. package/dist/prod-next/server/pages/404.html +1 -0
  89. package/dist/{standalone/.next → prod-next}/server/pages/500.html +1 -1
  90. package/dist/prod-next/server/server-reference-manifest.js +1 -0
  91. package/dist/prod-next/server/server-reference-manifest.json +1 -0
  92. package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +1 -0
  93. package/dist/{standalone/.next/static/chunks/781-f9f201ddc8c8591a.js → prod-next/static/chunks/613-6f794154d28bb75a.js} +1 -1
  94. package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +1 -0
  95. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +1 -0
  96. package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +1 -0
  97. package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +1 -0
  98. package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +1 -0
  99. package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +1 -0
  100. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +1 -0
  101. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +1 -0
  102. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +1 -0
  103. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +1 -0
  104. package/dist/{standalone/.next/static/chunks/webpack-b59eaf276b7fb38f.js → prod-next/static/chunks/webpack-bfe8992962004e2b.js} +1 -1
  105. package/dist/prod-next/static/sAWr5ATVuqk6Wqs18kAWs/_buildManifest.js +1 -0
  106. package/dist/public/taskmark-snapshot.json +1 -0
  107. package/lib/display-path.ts +6 -0
  108. package/lib/taskmark/autoconfig.ts +54 -3
  109. package/lib/taskmark/dev-reload-token.ts +5 -0
  110. package/lib/taskmark/discover.ts +2 -1
  111. package/lib/taskmark/project-label.ts +3 -2
  112. package/lib/taskmark/validate.ts +4 -4
  113. package/lib/taskmark/workspace-mode.ts +24 -0
  114. package/lib/taskmark/workspace.ts +5 -2
  115. package/next.config.ts +3 -8
  116. package/package.json +7 -4
  117. package/public/taskmark-snapshot.json +1 -1
  118. package/scripts/build-static.mjs +10 -1
  119. package/scripts/prepare-standalone.mjs +53 -20
  120. package/dist/standalone/.next/BUILD_ID +0 -1
  121. package/dist/standalone/.next/server/app/_global-error/page.js +0 -32
  122. package/dist/standalone/.next/server/app/_global-error/page.js.nft.json +0 -1
  123. package/dist/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +0 -1
  124. package/dist/standalone/.next/server/app/_not-found/page.js +0 -2
  125. package/dist/standalone/.next/server/app/_not-found/page.js.nft.json +0 -1
  126. package/dist/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  127. package/dist/standalone/.next/server/app/_not-found.html +0 -1
  128. package/dist/standalone/.next/server/app/board/page.js.nft.json +0 -1
  129. package/dist/standalone/.next/server/app/board/page_client-reference-manifest.js +0 -1
  130. package/dist/standalone/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  131. package/dist/standalone/.next/server/app/page.js +0 -2
  132. package/dist/standalone/.next/server/app/page.js.nft.json +0 -1
  133. package/dist/standalone/.next/server/app/page_client-reference-manifest.js +0 -1
  134. package/dist/standalone/.next/server/app/setup/page.js +0 -2
  135. package/dist/standalone/.next/server/app/setup/page.js.nft.json +0 -1
  136. package/dist/standalone/.next/server/app/setup/page_client-reference-manifest.js +0 -1
  137. package/dist/standalone/.next/server/chunks/164.js +0 -1
  138. package/dist/standalone/.next/server/chunks/57.js +0 -19
  139. package/dist/standalone/.next/server/chunks/570.js +0 -1
  140. package/dist/standalone/.next/server/chunks/666.js +0 -1
  141. package/dist/standalone/.next/server/functions-config-manifest.json +0 -4
  142. package/dist/standalone/.next/server/middleware-react-loadable-manifest.js +0 -1
  143. package/dist/standalone/.next/server/pages/404.html +0 -1
  144. package/dist/standalone/.next/server/server-reference-manifest.js +0 -1
  145. package/dist/standalone/.next/server/server-reference-manifest.json +0 -1
  146. package/dist/standalone/.next/static/chunks/211.a43663747d668728.js +0 -1
  147. package/dist/standalone/.next/static/chunks/app/_global-error/page-82d456ade9f47021.js +0 -1
  148. package/dist/standalone/.next/static/chunks/app/board/page-6db8dec814516c28.js +0 -1
  149. package/dist/standalone/.next/static/chunks/app/layout-3745e06d87691845.js +0 -1
  150. package/dist/standalone/.next/static/chunks/app/page-82d456ade9f47021.js +0 -1
  151. package/dist/standalone/.next/static/chunks/app/setup/page-8a54c6f26633bdca.js +0 -1
  152. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-82d456ade9f47021.js +0 -1
  153. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-82d456ade9f47021.js +0 -1
  154. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-82d456ade9f47021.js +0 -1
  155. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-82d456ade9f47021.js +0 -1
  156. package/dist/standalone/.next/static/jm3kxf8c6qGe0cYKCktaO/_buildManifest.js +0 -1
  157. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/processChild.js +0 -1
  158. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/threadChild.js +0 -1
  159. package/dist/standalone/node_modules/next/dist/server/lib/start-server.js +0 -494
  160. package/dist/standalone/node_modules/next/dist/server/next-server.js +0 -1429
  161. package/dist/standalone/node_modules/next/dist/server/next.js +0 -455
  162. package/dist/standalone/node_modules/next/dist/server/require-hook.js +0 -82
  163. package/dist/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +0 -36
  164. package/dist/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +0 -20
  165. package/dist/standalone/node_modules/styled-jsx/index.js +0 -1
  166. package/dist/standalone/node_modules/styled-jsx/package.json +0 -132
  167. package/dist/standalone/node_modules/styled-jsx/style.js +0 -1
  168. package/dist/standalone/package.json +0 -77
  169. package/dist/standalone/public/taskmark-snapshot.json +0 -1
  170. package/dist/standalone/server.js +0 -44
  171. /package/dist/{standalone/.next → prod-next}/package.json +0 -0
  172. /package/dist/{standalone/.next → prod-next}/server/app/_global-error.meta +0 -0
  173. /package/dist/{standalone/.next → prod-next}/server/app/_not-found.meta +0 -0
  174. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.body +0 -0
  175. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.meta +0 -0
  176. /package/dist/{standalone/.next → prod-next}/server/chunks/318.js +0 -0
  177. /package/dist/{standalone/.next → prod-next}/server/middleware-manifest.json +0 -0
  178. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.js +0 -0
  179. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.json +0 -0
  180. /package/dist/{standalone/.next → prod-next}/server/pages-manifest.json +0 -0
  181. /package/dist/{standalone/.next → prod-next}/server/prefetch-hints.json +0 -0
  182. /package/dist/{standalone/.next → prod-next}/server/webpack-runtime.js +0 -0
  183. /package/dist/{standalone/.next → prod-next}/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -0
  184. /package/dist/{standalone/.next → prod-next}/static/chunks/669-8103441c67b185da.js +0 -0
  185. /package/dist/{standalone/.next → prod-next}/static/chunks/701-df3ee5f1cf837b1a.js +0 -0
  186. /package/dist/{standalone/.next → prod-next}/static/chunks/794-25668765560cb197.js +0 -0
  187. /package/dist/{standalone/.next → prod-next}/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -0
  188. /package/dist/{standalone/.next → prod-next}/static/chunks/framework-b29f9083d3cda3b2.js +0 -0
  189. /package/dist/{standalone/.next → prod-next}/static/chunks/main-app-bfb36455d24b3e5a.js +0 -0
  190. /package/dist/{standalone/.next → prod-next}/static/chunks/main-bb33c46a9c25dee8.js +0 -0
  191. /package/dist/{standalone/.next → prod-next}/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -0
  192. /package/dist/{standalone/.next → prod-next}/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  193. /package/dist/{standalone/.next → prod-next}/static/css/2e6ec4c80894e3c0.css +0 -0
  194. /package/dist/{standalone/.next → prod-next}/static/css/c593627ceef17a4b.css +0 -0
  195. /package/dist/{standalone/.next → prod-next}/static/media/36966cca54120369-s.p.woff2 +0 -0
  196. /package/dist/{standalone/.next → prod-next}/static/media/982ceffe7b733b3b-s.p.woff2 +0 -0
  197. /package/dist/{standalone/.next → prod-next}/static/media/b7387a63dd068245-s.woff2 +0 -0
  198. /package/dist/{standalone/.next → prod-next}/static/media/b8bfb68ab9ab617c-s.woff2 +0 -0
  199. /package/dist/{standalone/.next → prod-next}/static/media/e1aab0933260df4d-s.woff2 +0 -0
  200. /package/dist/{standalone/.next/static/jm3kxf8c6qGe0cYKCktaO → prod-next/static/sAWr5ATVuqk6Wqs18kAWs}/_ssgManifest.js +0 -0
  201. /package/dist/{standalone/public → public}/apple-touch-icon.png +0 -0
  202. /package/dist/{standalone/public → public}/favicon-96x96.png +0 -0
  203. /package/dist/{standalone/public → public}/favicon.ico +0 -0
  204. /package/dist/{standalone/public → public}/favicon.svg +0 -0
  205. /package/dist/{standalone/public → public}/file.svg +0 -0
  206. /package/dist/{standalone/public → public}/globe.svg +0 -0
  207. /package/dist/{standalone/public → public}/next.svg +0 -0
  208. /package/dist/{standalone/public → public}/og/og-default.png +0 -0
  209. /package/dist/{standalone/public → public}/site.webmanifest +0 -0
  210. /package/dist/{standalone/public → public}/tm_dark.png +0 -0
  211. /package/dist/{standalone/public → public}/tm_light.png +0 -0
  212. /package/dist/{standalone/public → public}/vercel.svg +0 -0
  213. /package/dist/{standalone/public → public}/web-app-manifest-192x192.png +0 -0
  214. /package/dist/{standalone/public → public}/web-app-manifest-512x512.png +0 -0
  215. /package/dist/{standalone/public → public}/window.svg +0 -0
package/bin/taskmark.js CHANGED
@@ -3,46 +3,65 @@ 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:
20
+ taskmark [options] smart default: local board if found, else setup
21
+ taskmark open [options]
17
22
  taskmark serve [options]
23
+ taskmark dev [options]
18
24
  taskmark build [options]
19
25
  taskmark preview [options]
20
26
 
21
27
  Commands:
22
- serve Start the prebuilt local UI server (default port ${DEFAULT_PORT})
28
+ (default) Bind to a board in/near cwd when one exists; otherwise open
29
+ the standalone setup / project picker (same as global use)
30
+ open Always start in workspace mode (setup / project picker)
31
+ serve Start the prebuilt UI bound to one resolved board (port ${DEFAULT_PORT})
32
+ dev Next.js development server with board markdown live reload
23
33
  build Production static HTML export for Vercel / static hosting
24
34
  preview Serve an existing static export (default: <board>/out)
25
35
 
26
36
  Options:
27
37
  --port, -p <n> Listen port (default ${DEFAULT_PORT}; env PORT / TASKMARK_PORT)
28
38
  --board <path> Board or product root (sets TASKMARK_BOARD)
39
+ --workspace, -w Force multi-project / setup mode (skip local board binding)
29
40
  --out <dir> Static output directory for build/preview (default: <board>/out)
30
- --no-open Do not open a browser (serve / preview)
41
+ --no-open Do not open a browser (default / open / serve / preview / dev)
31
42
  --help, -h Show help
32
43
 
33
- Board resolution (same as the UI):
44
+ Modes:
45
+ Bound One board from --board / env / cwd / sibling *-taskmark; no project picker.
46
+ Workspace Setup wizard + app-bar project picker (cookies). Used when no local
47
+ board is found, or with open / --workspace.
48
+
49
+ Board resolution for bound mode (same as the UI):
34
50
  1. --board / TASKMARK_BOARD
35
51
  2. TASKMARK_MASTER
36
52
  3. TASKMARK_CWD → npm INIT_CWD → process.cwd()
37
53
  (product root with ./taskmark/, or dedicated *-taskmark root)
54
+ 4. Sibling <parent>-taskmark next to a product repo (multi-git)
38
55
 
39
56
  Examples:
57
+ npx @taskmark/ui # global: setup / pick projects
58
+ cd my-app-taskmark && npx taskmark # local board in this folder
40
59
  npm i @taskmark/ui --save && npx taskmark serve
60
+ npx taskmark open # force setup even inside a board folder
61
+ npx taskmark dev --board .
41
62
  npx taskmark build --board .
42
63
  npm run preview
43
- npx taskmark preview --port 9000
44
- npx -p @taskmark/ui taskmark serve
45
- TASKMARK_BOARD=/path/to/my-app/taskmark npx taskmark serve
64
+ TASKMARK_BOARD=/path/to/board npx taskmark serve
46
65
  `)
47
66
  }
48
67
 
@@ -53,20 +72,24 @@ function parseArgs(argv) {
53
72
  board: null,
54
73
  out: null,
55
74
  open: true,
75
+ workspace: false,
56
76
  help: false,
57
77
  }
58
78
  const rest = [...argv]
59
- if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") {
60
- args.help = true
79
+ if (rest.length === 0) {
80
+ args.command = "auto"
61
81
  return args
62
82
  }
63
- args.command = rest.shift()
83
+ // Bare `taskmark` with only flags → smart default (local board or setup).
84
+ args.command = rest[0].startsWith("-") ? "auto" : rest.shift()
64
85
  while (rest.length) {
65
86
  const token = rest.shift()
66
87
  if (token === "--help" || token === "-h") {
67
88
  args.help = true
68
89
  } else if (token === "--no-open") {
69
90
  args.open = false
91
+ } else if (token === "--workspace" || token === "-w") {
92
+ args.workspace = true
70
93
  } else if (token === "--port" || token === "-p") {
71
94
  args.port = rest.shift()
72
95
  } else if (token === "--board") {
@@ -97,15 +120,73 @@ function resolvePort(raw) {
97
120
  return n
98
121
  }
99
122
 
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
123
+ function resolveNextBin() {
124
+ try {
125
+ return requireFromPackage.resolve("next/dist/bin/next")
126
+ } catch {
127
+ const fallback = path.join(
128
+ packageRoot,
129
+ "node_modules",
130
+ "next",
131
+ "dist",
132
+ "bin",
133
+ "next"
134
+ )
135
+ if (fs.existsSync(fallback)) return fallback
136
+ throw new Error(
137
+ "next binary not found. Reinstall @taskmark/ui (next is required for serve/dev)."
138
+ )
107
139
  }
108
- return null
140
+ }
141
+
142
+ /**
143
+ * Ensure packageRoot/.next points at a usable production build.
144
+ * Published packages ship dist/prod-next (npm cannot pack nested node_modules
145
+ * used by Next standalone).
146
+ */
147
+ function ensureProdNext() {
148
+ const nextDir = path.join(packageRoot, ".next")
149
+ const shipped = path.join(packageRoot, "dist", "prod-next")
150
+ const hasLocalServer = fs.existsSync(path.join(nextDir, "server"))
151
+ const hasShippedServer = fs.existsSync(path.join(shipped, "server"))
152
+
153
+ if (hasLocalServer) return nextDir
154
+ if (!hasShippedServer) {
155
+ throw new Error(`Prebuilt UI not found.
156
+
157
+ Expected ${shipped} (or a local .next/server from npm run build).
158
+ Reinstall @taskmark/ui or rebuild the package.`)
159
+ }
160
+
161
+ if (fs.existsSync(nextDir)) {
162
+ const st = fs.lstatSync(nextDir)
163
+ if (st.isSymbolicLink() || st.isDirectory()) {
164
+ fs.rmSync(nextDir, { recursive: true, force: true })
165
+ }
166
+ }
167
+
168
+ try {
169
+ const type = process.platform === "win32" ? "junction" : "dir"
170
+ fs.symlinkSync(shipped, nextDir, type)
171
+ } catch {
172
+ fs.cpSync(shipped, nextDir, { recursive: true })
173
+ }
174
+ return nextDir
175
+ }
176
+
177
+ function touchDevReloadToken() {
178
+ const tokenFile = path.join(
179
+ packageRoot,
180
+ "lib",
181
+ "taskmark",
182
+ "dev-reload-token.ts"
183
+ )
184
+ const stamp = String(Date.now())
185
+ fs.writeFileSync(
186
+ tokenFile,
187
+ `/**\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`,
188
+ "utf8"
189
+ )
109
190
  }
110
191
 
111
192
  function openBrowser(url) {
@@ -155,8 +236,48 @@ function waitForServer(port, timeoutMs = 60_000) {
155
236
  })
156
237
  }
157
238
 
158
- async function serve(args) {
159
- const resolved = resolveServeBoard(args.board)
239
+ function callerCwd() {
240
+ return process.env.INIT_CWD || process.cwd()
241
+ }
242
+
243
+ function isWorkspaceRun(args) {
244
+ return args.command === "open" || args.workspace === true
245
+ }
246
+
247
+ /**
248
+ * Decide bound vs workspace for the default (`auto`) command.
249
+ * Returns a serve-compatible args object with workspace forced when unbound.
250
+ */
251
+ function resolveAutoArgs(args) {
252
+ if (args.workspace) {
253
+ return { ...args, command: "open", workspace: true }
254
+ }
255
+ if (args.board) {
256
+ return { ...args, command: "serve", workspace: false }
257
+ }
258
+ const resolved = resolveServeBoard(null)
259
+ if (resolved) {
260
+ return { ...args, command: "serve", workspace: false }
261
+ }
262
+ return { ...args, command: "open", workspace: true }
263
+ }
264
+
265
+ /**
266
+ * Workspace mode must out-rank an inherited board bind, so blank those vars for
267
+ * the child (the UI reads empty as unset) instead of only adding the flag.
268
+ */
269
+ function workspaceEnv() {
270
+ return {
271
+ TASKMARK_WORKSPACE: "1",
272
+ TASKMARK_WORKSPACE_ROOT: callerCwd(),
273
+ TASKMARK_BOARD: "",
274
+ TASKMARK_MASTER: "",
275
+ TASKMARK_CWD: "",
276
+ }
277
+ }
278
+
279
+ function resolveBoardOrExit(boardArg, command) {
280
+ const resolved = resolveServeBoard(boardArg)
160
281
  if (!resolved) {
161
282
  console.error(`No Taskmark board found.
162
283
 
@@ -164,52 +285,157 @@ Looked for:
164
285
  - --board / TASKMARK_BOARD
165
286
  - TASKMARK_MASTER
166
287
  - cwd layouts: nested ./taskmark/ or dedicated *-taskmark root
288
+ - sibling <parent>-taskmark beside a product repo
167
289
 
168
290
  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)`)
291
+ Then re-run: npx taskmark ${command}
292
+ (or: npx -p @taskmark/ui taskmark ${command})
293
+
294
+ To pick a project in the browser instead: npx taskmark`)
171
295
  process.exit(1)
172
296
  }
297
+ return resolved
298
+ }
173
299
 
174
- const serverJs = findStandaloneServer()
175
- if (!serverJs) {
176
- console.error(`Prebuilt server not found.
177
-
178
- From the taskmark package source, run:
179
- npm run build
180
-
181
- Or install a published build that includes dist/standalone.`)
182
- process.exit(1)
300
+ function attachChildLifecycle(child, onShutdown) {
301
+ const shutdown = (signal) => {
302
+ onShutdown?.()
303
+ if (!child.killed) child.kill(signal)
183
304
  }
305
+ process.on("SIGINT", () => shutdown("SIGINT"))
306
+ process.on("SIGTERM", () => shutdown("SIGTERM"))
307
+ child.on("exit", (code, signal) => {
308
+ onShutdown?.()
309
+ if (signal) process.exit(1)
310
+ process.exit(code ?? 0)
311
+ })
312
+ return shutdown
313
+ }
184
314
 
315
+ async function serve(args) {
316
+ const workspace = isWorkspaceRun(args)
317
+ const resolved = workspace ? null : resolveBoardOrExit(args.board, "serve")
185
318
  const port = resolvePort(args.port)
186
319
  const url = `http://localhost:${port}`
187
- const standaloneDir = path.dirname(serverJs)
320
+ const nextBin = resolveNextBin()
321
+
322
+ try {
323
+ ensureProdNext()
324
+ } catch (err) {
325
+ console.error(String(err?.message || err))
326
+ process.exit(1)
327
+ }
188
328
 
189
329
  console.log(`Taskmark UI`)
190
- console.log(` board: ${resolved.boardPath} (${resolved.source})`)
330
+ if (workspace) {
331
+ console.log(` mode: workspace (choose projects in the browser)`)
332
+ console.log(` from: ${callerCwd()}`)
333
+ } else {
334
+ console.log(` board: ${resolved.boardPath} (${resolved.source})`)
335
+ }
191
336
  console.log(` listen: ${url}`)
192
337
 
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",
202
- })
338
+ const child = spawn(
339
+ process.execPath,
340
+ [nextBin, "start", "-H", "0.0.0.0", "-p", String(port)],
341
+ {
342
+ cwd: packageRoot,
343
+ env: {
344
+ ...process.env,
345
+ PORT: String(port),
346
+ HOSTNAME: "0.0.0.0",
347
+ ...(workspace
348
+ ? workspaceEnv()
349
+ : { TASKMARK_BOARD: resolved.boardPath }),
350
+ NODE_ENV: "production",
351
+ },
352
+ stdio: "inherit",
353
+ }
354
+ )
203
355
 
204
- const shutdown = (signal) => {
205
- if (!child.killed) child.kill(signal)
356
+ const shutdown = attachChildLifecycle(child)
357
+
358
+ const hosted =
359
+ Boolean(process.env.VERCEL) ||
360
+ Boolean(process.env.NOW_REGION) ||
361
+ process.env.CI === "true"
362
+ const shouldOpen = args.open && !hosted
363
+
364
+ try {
365
+ await waitForServer(port)
366
+ if (shouldOpen) {
367
+ openBrowser(url)
368
+ } else {
369
+ console.log(`Ready: ${url}`)
370
+ }
371
+ } catch (err) {
372
+ console.error(String(err?.message || err))
373
+ shutdown("SIGTERM")
374
+ process.exit(1)
206
375
  }
207
- process.on("SIGINT", () => shutdown("SIGINT"))
208
- process.on("SIGTERM", () => shutdown("SIGTERM"))
376
+ }
209
377
 
210
- child.on("exit", (code, signal) => {
211
- if (signal) process.exit(1)
212
- process.exit(code ?? 0)
378
+ async function dev(args) {
379
+ const workspace = isWorkspaceRun(args)
380
+ const resolved = workspace ? null : resolveBoardOrExit(args.board, "dev")
381
+ const port = resolvePort(args.port)
382
+ const url = `http://localhost:${port}`
383
+ const nextBin = resolveNextBin()
384
+
385
+ console.log(`Taskmark UI (dev)`)
386
+ if (workspace) {
387
+ console.log(` mode: workspace (choose projects in the browser)`)
388
+ console.log(` from: ${callerCwd()}`)
389
+ } else {
390
+ console.log(` board: ${resolved.boardPath} (${resolved.source})`)
391
+ }
392
+ console.log(` listen: ${url}`)
393
+ console.log(
394
+ workspace
395
+ ? ` watch: off (no single board bound)`
396
+ : ` watch: **/*.md under board`
397
+ )
398
+
399
+ const child = spawn(
400
+ process.execPath,
401
+ [nextBin, "dev", "--webpack", "-H", "0.0.0.0", "-p", String(port)],
402
+ {
403
+ cwd: packageRoot,
404
+ env: {
405
+ ...process.env,
406
+ PORT: String(port),
407
+ ...(workspace
408
+ ? workspaceEnv()
409
+ : {
410
+ TASKMARK_BOARD: resolved.boardPath,
411
+ TASKMARK_CWD: resolved.boardPath,
412
+ }),
413
+ NODE_ENV: "development",
414
+ },
415
+ stdio: "inherit",
416
+ }
417
+ )
418
+
419
+ let debounce = null
420
+ const stopWatch = workspace
421
+ ? () => {}
422
+ : watchBoardMarkdown(resolved.boardPath, () => {
423
+ if (debounce) clearTimeout(debounce)
424
+ debounce = setTimeout(() => {
425
+ try {
426
+ touchDevReloadToken()
427
+ console.log(`[taskmark] board markdown changed — refreshing`)
428
+ } catch (err) {
429
+ console.warn(
430
+ `[taskmark] reload touch failed: ${err?.message || err}`
431
+ )
432
+ }
433
+ }, 200)
434
+ })
435
+
436
+ const shutdown = attachChildLifecycle(child, () => {
437
+ stopWatch()
438
+ if (debounce) clearTimeout(debounce)
213
439
  })
214
440
 
215
441
  const hosted =
@@ -219,7 +445,7 @@ Or install a published build that includes dist/standalone.`)
219
445
  const shouldOpen = args.open && !hosted
220
446
 
221
447
  try {
222
- await waitForServer(port)
448
+ await waitForServer(port, 120_000)
223
449
  if (shouldOpen) {
224
450
  openBrowser(url)
225
451
  } else {
@@ -241,19 +467,7 @@ async function preview(args) {
241
467
  }
242
468
 
243
469
  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
- }
470
+ const resolved = resolveBoardOrExit(boardArg, "preview")
257
471
 
258
472
  const outDir = args.out
259
473
  ? resolveCallerPath(args.out)
@@ -299,7 +513,30 @@ async function main() {
299
513
  printHelp()
300
514
  process.exit(args.help || !args.command ? 0 : 1)
301
515
  }
302
- if (args.command === "serve") {
516
+ const workspaceCommands = new Set(["auto", "open", "serve", "dev"])
517
+ if (args.workspace && !workspaceCommands.has(args.command)) {
518
+ console.error(`--workspace applies to the default command, open, serve, and dev.
519
+
520
+ Use: npx taskmark (or: npx taskmark open)`)
521
+ process.exit(1)
522
+ }
523
+ if ((isWorkspaceRun(args) || (args.command === "auto" && args.workspace)) && args.board) {
524
+ console.error(`--board and workspace mode are mutually exclusive.
525
+
526
+ Bind one board: npx taskmark serve --board ${args.board}
527
+ Pick projects in UI: npx taskmark open`)
528
+ process.exit(1)
529
+ }
530
+ if (args.command === "auto") {
531
+ try {
532
+ await serve(resolveAutoArgs(args))
533
+ } catch (err) {
534
+ console.error(String(err?.message || err))
535
+ process.exit(1)
536
+ }
537
+ return
538
+ }
539
+ if (args.command === "open" || args.command === "serve") {
303
540
  try {
304
541
  await serve(args)
305
542
  } catch (err) {
@@ -308,6 +545,15 @@ async function main() {
308
545
  }
309
546
  return
310
547
  }
548
+ if (args.command === "dev") {
549
+ try {
550
+ await dev(args)
551
+ } catch (err) {
552
+ console.error(String(err?.message || err))
553
+ process.exit(1)
554
+ }
555
+ return
556
+ }
311
557
  if (args.command === "preview") {
312
558
  try {
313
559
  await preview(args)
@@ -319,14 +565,11 @@ async function main() {
319
565
  }
320
566
  if (args.command === "build") {
321
567
  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
568
  const callerCwd = process.env.INIT_CWD || process.cwd()
325
569
  const childArgs = [buildScript]
326
570
  if (args.board) {
327
571
  childArgs.push("--board", path.resolve(callerCwd, args.board))
328
572
  } else {
329
- // Prefer the invoke directory as the board when no --board flag.
330
573
  childArgs.push("--board", callerCwd)
331
574
  }
332
575
  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>