zyflow 0.6.4

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 (705) hide show
  1. package/.claude-flow/metrics/agent-metrics.json +1 -0
  2. package/.claude-flow/metrics/performance.json +87 -0
  3. package/.claude-flow/metrics/system-metrics.json +4370 -0
  4. package/.claude-flow/metrics/task-metrics.json +10 -0
  5. package/.claude-plugin/marketplace.json +18 -0
  6. package/.claude-plugin/plugin.json +17 -0
  7. package/.gitleaks.toml +69 -0
  8. package/.hive-mind/config/queens.json +59 -0
  9. package/.hive-mind/config/workers.json +72 -0
  10. package/.hive-mind/config.json +111 -0
  11. package/.hive-mind/hive.db +0 -0
  12. package/.hive-mind/hive.db-shm +0 -0
  13. package/.hive-mind/hive.db-wal +0 -0
  14. package/.leann/indexes/zyflow/documents.ids.txt +2078 -0
  15. package/.leann/indexes/zyflow/documents.index +0 -0
  16. package/.leann/indexes/zyflow/documents.leann.meta.json +25 -0
  17. package/.leann/indexes/zyflow/documents.leann.passages.idx +0 -0
  18. package/.leann/indexes/zyflow/documents.leann.passages.jsonl +2078 -0
  19. package/.mcp.json +41 -0
  20. package/.moai-backups/20260126_231508/.mcp.json +11 -0
  21. package/.moai-backups/20260126_231508/backup_metadata.json +34 -0
  22. package/.moai-backups/20260129_145438/.mcp.json +41 -0
  23. package/.moai-backups/20260129_145438/backup_metadata.json +53 -0
  24. package/.moai-backups/20260129_145504/.mcp.json +41 -0
  25. package/.moai-backups/20260129_145504/backup_metadata.json +20 -0
  26. package/.moai-backups/20260201_140004/.mcp.json +41 -0
  27. package/.moai-backups/20260201_140004/backup_metadata.json +51 -0
  28. package/.moai-backups/backup/.mcp.json +12 -0
  29. package/.moai-backups/settings-backup/settings.local.json +61 -0
  30. package/.pre-commit-config.yaml +74 -0
  31. package/.prettierignore +3 -0
  32. package/.prettierrc +7 -0
  33. package/.scannerwork/.sonar_lock +0 -0
  34. package/.scannerwork/report-task.txt +6 -0
  35. package/.serena/project.yml +105 -0
  36. package/.shadcn-admin-ref/.env.example +1 -0
  37. package/.shadcn-admin-ref/.prettierignore +18 -0
  38. package/.shadcn-admin-ref/.prettierrc +50 -0
  39. package/.shadcn-admin-ref/LICENSE +21 -0
  40. package/.shadcn-admin-ref/components.json +21 -0
  41. package/.shadcn-admin-ref/cz.yaml +7 -0
  42. package/.shadcn-admin-ref/eslint.config.js +59 -0
  43. package/.shadcn-admin-ref/index.html +80 -0
  44. package/.shadcn-admin-ref/knip.config.ts +8 -0
  45. package/.shadcn-admin-ref/netlify.toml +4 -0
  46. package/.shadcn-admin-ref/package.json +83 -0
  47. package/.shadcn-admin-ref/public/images/favicon.png +0 -0
  48. package/.shadcn-admin-ref/public/images/favicon.svg +4 -0
  49. package/.shadcn-admin-ref/public/images/favicon_light.png +0 -0
  50. package/.shadcn-admin-ref/public/images/favicon_light.svg +1 -0
  51. package/.shadcn-admin-ref/public/images/shadcn-admin.png +0 -0
  52. package/.shadcn-admin-ref/src/assets/brand-icons/icon-discord.tsx +28 -0
  53. package/.shadcn-admin-ref/src/assets/brand-icons/icon-docker.tsx +33 -0
  54. package/.shadcn-admin-ref/src/assets/brand-icons/icon-facebook.tsx +25 -0
  55. package/.shadcn-admin-ref/src/assets/brand-icons/icon-figma.tsx +27 -0
  56. package/.shadcn-admin-ref/src/assets/brand-icons/icon-github.tsx +25 -0
  57. package/.shadcn-admin-ref/src/assets/brand-icons/icon-gitlab.tsx +25 -0
  58. package/.shadcn-admin-ref/src/assets/brand-icons/icon-gmail.tsx +28 -0
  59. package/.shadcn-admin-ref/src/assets/brand-icons/icon-medium.tsx +30 -0
  60. package/.shadcn-admin-ref/src/assets/brand-icons/icon-notion.tsx +28 -0
  61. package/.shadcn-admin-ref/src/assets/brand-icons/icon-skype.tsx +26 -0
  62. package/.shadcn-admin-ref/src/assets/brand-icons/icon-slack.tsx +28 -0
  63. package/.shadcn-admin-ref/src/assets/brand-icons/icon-stripe.tsx +25 -0
  64. package/.shadcn-admin-ref/src/assets/brand-icons/icon-telegram.tsx +25 -0
  65. package/.shadcn-admin-ref/src/assets/brand-icons/icon-trello.tsx +27 -0
  66. package/.shadcn-admin-ref/src/assets/brand-icons/icon-whatsapp.tsx +26 -0
  67. package/.shadcn-admin-ref/src/assets/brand-icons/icon-zoom.tsx +26 -0
  68. package/.shadcn-admin-ref/src/assets/brand-icons/index.ts +16 -0
  69. package/.shadcn-admin-ref/src/assets/clerk-full-logo.tsx +41 -0
  70. package/.shadcn-admin-ref/src/assets/clerk-logo.tsx +23 -0
  71. package/.shadcn-admin-ref/src/assets/custom/icon-dir.tsx +110 -0
  72. package/.shadcn-admin-ref/src/assets/custom/icon-layout-compact.tsx +131 -0
  73. package/.shadcn-admin-ref/src/assets/custom/icon-layout-default.tsx +124 -0
  74. package/.shadcn-admin-ref/src/assets/custom/icon-layout-full.tsx +100 -0
  75. package/.shadcn-admin-ref/src/assets/custom/icon-sidebar-floating.tsx +82 -0
  76. package/.shadcn-admin-ref/src/assets/custom/icon-sidebar-inset.tsx +58 -0
  77. package/.shadcn-admin-ref/src/assets/custom/icon-sidebar-sidebar.tsx +53 -0
  78. package/.shadcn-admin-ref/src/assets/custom/icon-theme-dark.tsx +79 -0
  79. package/.shadcn-admin-ref/src/assets/custom/icon-theme-light.tsx +78 -0
  80. package/.shadcn-admin-ref/src/assets/custom/icon-theme-system.tsx +116 -0
  81. package/.shadcn-admin-ref/src/assets/logo.tsx +24 -0
  82. package/.shadcn-admin-ref/src/components/coming-soon.tsx +16 -0
  83. package/.shadcn-admin-ref/src/components/command-menu.tsx +91 -0
  84. package/.shadcn-admin-ref/src/components/config-drawer.tsx +354 -0
  85. package/.shadcn-admin-ref/src/components/confirm-dialog.tsx +67 -0
  86. package/.shadcn-admin-ref/src/components/data-table/bulk-actions.tsx +213 -0
  87. package/.shadcn-admin-ref/src/components/data-table/column-header.tsx +74 -0
  88. package/.shadcn-admin-ref/src/components/data-table/faceted-filter.tsx +146 -0
  89. package/.shadcn-admin-ref/src/components/data-table/index.ts +6 -0
  90. package/.shadcn-admin-ref/src/components/data-table/pagination.tsx +130 -0
  91. package/.shadcn-admin-ref/src/components/data-table/toolbar.tsx +85 -0
  92. package/.shadcn-admin-ref/src/components/data-table/view-options.tsx +56 -0
  93. package/.shadcn-admin-ref/src/components/date-picker.tsx +51 -0
  94. package/.shadcn-admin-ref/src/components/layout/app-sidebar.tsx +37 -0
  95. package/.shadcn-admin-ref/src/components/layout/app-title.tsx +64 -0
  96. package/.shadcn-admin-ref/src/components/layout/authenticated-layout.tsx +42 -0
  97. package/.shadcn-admin-ref/src/components/layout/data/sidebar-data.ts +205 -0
  98. package/.shadcn-admin-ref/src/components/layout/header.tsx +50 -0
  99. package/.shadcn-admin-ref/src/components/layout/main.tsx +27 -0
  100. package/.shadcn-admin-ref/src/components/layout/nav-group.tsx +185 -0
  101. package/.shadcn-admin-ref/src/components/layout/nav-user.tsx +124 -0
  102. package/.shadcn-admin-ref/src/components/layout/team-switcher.tsx +86 -0
  103. package/.shadcn-admin-ref/src/components/layout/top-nav.tsx +67 -0
  104. package/.shadcn-admin-ref/src/components/layout/types.ts +44 -0
  105. package/.shadcn-admin-ref/src/components/learn-more.tsx +44 -0
  106. package/.shadcn-admin-ref/src/components/long-text.tsx +84 -0
  107. package/.shadcn-admin-ref/src/components/navigation-progress.tsx +25 -0
  108. package/.shadcn-admin-ref/src/components/password-input.tsx +42 -0
  109. package/.shadcn-admin-ref/src/components/profile-dropdown.tsx +75 -0
  110. package/.shadcn-admin-ref/src/components/search.tsx +37 -0
  111. package/.shadcn-admin-ref/src/components/select-dropdown.tsx +62 -0
  112. package/.shadcn-admin-ref/src/components/sign-out-dialog.tsx +38 -0
  113. package/.shadcn-admin-ref/src/components/skip-to-main.tsx +10 -0
  114. package/.shadcn-admin-ref/src/components/theme-switch.tsx +58 -0
  115. package/.shadcn-admin-ref/src/components/ui/alert-dialog.tsx +154 -0
  116. package/.shadcn-admin-ref/src/components/ui/alert.tsx +65 -0
  117. package/.shadcn-admin-ref/src/components/ui/avatar.tsx +50 -0
  118. package/.shadcn-admin-ref/src/components/ui/badge.tsx +45 -0
  119. package/.shadcn-admin-ref/src/components/ui/button.tsx +58 -0
  120. package/.shadcn-admin-ref/src/components/ui/calendar.tsx +210 -0
  121. package/.shadcn-admin-ref/src/components/ui/card.tsx +91 -0
  122. package/.shadcn-admin-ref/src/components/ui/checkbox.tsx +29 -0
  123. package/.shadcn-admin-ref/src/components/ui/collapsible.tsx +31 -0
  124. package/.shadcn-admin-ref/src/components/ui/command.tsx +181 -0
  125. package/.shadcn-admin-ref/src/components/ui/dialog.tsx +142 -0
  126. package/.shadcn-admin-ref/src/components/ui/dropdown-menu.tsx +254 -0
  127. package/.shadcn-admin-ref/src/components/ui/form.tsx +164 -0
  128. package/.shadcn-admin-ref/src/components/ui/input-otp.tsx +74 -0
  129. package/.shadcn-admin-ref/src/components/ui/input.tsx +20 -0
  130. package/.shadcn-admin-ref/src/components/ui/label.tsx +23 -0
  131. package/.shadcn-admin-ref/src/components/ui/popover.tsx +45 -0
  132. package/.shadcn-admin-ref/src/components/ui/radio-group.tsx +42 -0
  133. package/.shadcn-admin-ref/src/components/ui/scroll-area.tsx +65 -0
  134. package/.shadcn-admin-ref/src/components/ui/select.tsx +182 -0
  135. package/.shadcn-admin-ref/src/components/ui/separator.tsx +25 -0
  136. package/.shadcn-admin-ref/src/components/ui/sheet.tsx +136 -0
  137. package/.shadcn-admin-ref/src/components/ui/sidebar.tsx +728 -0
  138. package/.shadcn-admin-ref/src/components/ui/skeleton.tsx +13 -0
  139. package/.shadcn-admin-ref/src/components/ui/sonner.tsx +21 -0
  140. package/.shadcn-admin-ref/src/components/ui/switch.tsx +28 -0
  141. package/.shadcn-admin-ref/src/components/ui/table.tsx +113 -0
  142. package/.shadcn-admin-ref/src/components/ui/tabs.tsx +63 -0
  143. package/.shadcn-admin-ref/src/components/ui/textarea.tsx +17 -0
  144. package/.shadcn-admin-ref/src/components/ui/tooltip.tsx +60 -0
  145. package/.shadcn-admin-ref/src/config/fonts.ts +19 -0
  146. package/.shadcn-admin-ref/src/context/direction-provider.tsx +61 -0
  147. package/.shadcn-admin-ref/src/context/font-provider.tsx +58 -0
  148. package/.shadcn-admin-ref/src/context/layout-provider.tsx +85 -0
  149. package/.shadcn-admin-ref/src/context/search-provider.tsx +46 -0
  150. package/.shadcn-admin-ref/src/context/theme-provider.tsx +110 -0
  151. package/.shadcn-admin-ref/src/features/apps/data/apps.tsx +110 -0
  152. package/.shadcn-admin-ref/src/features/apps/index.tsx +179 -0
  153. package/.shadcn-admin-ref/src/features/auth/auth-layout.tsx +19 -0
  154. package/.shadcn-admin-ref/src/features/auth/forgot-password/components/forgot-password-form.tsx +82 -0
  155. package/.shadcn-admin-ref/src/features/auth/forgot-password/index.tsx +44 -0
  156. package/.shadcn-admin-ref/src/features/auth/otp/components/otp-form.tsx +100 -0
  157. package/.shadcn-admin-ref/src/features/auth/otp/index.tsx +44 -0
  158. package/.shadcn-admin-ref/src/features/auth/sign-in/assets/dashboard-dark.png +0 -0
  159. package/.shadcn-admin-ref/src/features/auth/sign-in/assets/dashboard-light.png +0 -0
  160. package/.shadcn-admin-ref/src/features/auth/sign-in/components/user-auth-form.tsx +150 -0
  161. package/.shadcn-admin-ref/src/features/auth/sign-in/index.tsx +51 -0
  162. package/.shadcn-admin-ref/src/features/auth/sign-in/sign-in-2.tsx +69 -0
  163. package/.shadcn-admin-ref/src/features/auth/sign-up/components/sign-up-form.tsx +143 -0
  164. package/.shadcn-admin-ref/src/features/auth/sign-up/index.tsx +57 -0
  165. package/.shadcn-admin-ref/src/features/chats/components/new-chat.tsx +127 -0
  166. package/.shadcn-admin-ref/src/features/chats/data/chat-types.ts +4 -0
  167. package/.shadcn-admin-ref/src/features/chats/data/convo.json +309 -0
  168. package/.shadcn-admin-ref/src/features/chats/index.tsx +349 -0
  169. package/.shadcn-admin-ref/src/features/dashboard/components/analytics-chart.tsx +77 -0
  170. package/.shadcn-admin-ref/src/features/dashboard/components/analytics.tsx +189 -0
  171. package/.shadcn-admin-ref/src/features/dashboard/components/overview.tsx +82 -0
  172. package/.shadcn-admin-ref/src/features/dashboard/components/recent-sales.tsx +83 -0
  173. package/.shadcn-admin-ref/src/features/dashboard/index.tsx +220 -0
  174. package/.shadcn-admin-ref/src/features/errors/forbidden.tsx +25 -0
  175. package/.shadcn-admin-ref/src/features/errors/general-error.tsx +36 -0
  176. package/.shadcn-admin-ref/src/features/errors/maintenance-error.tsx +19 -0
  177. package/.shadcn-admin-ref/src/features/errors/not-found-error.tsx +25 -0
  178. package/.shadcn-admin-ref/src/features/errors/unauthorized-error.tsx +25 -0
  179. package/.shadcn-admin-ref/src/features/settings/account/account-form.tsx +173 -0
  180. package/.shadcn-admin-ref/src/features/settings/account/index.tsx +14 -0
  181. package/.shadcn-admin-ref/src/features/settings/appearance/appearance-form.tsx +162 -0
  182. package/.shadcn-admin-ref/src/features/settings/appearance/index.tsx +14 -0
  183. package/.shadcn-admin-ref/src/features/settings/components/content-section.tsx +22 -0
  184. package/.shadcn-admin-ref/src/features/settings/components/sidebar-nav.tsx +84 -0
  185. package/.shadcn-admin-ref/src/features/settings/display/display-form.tsx +121 -0
  186. package/.shadcn-admin-ref/src/features/settings/display/index.tsx +13 -0
  187. package/.shadcn-admin-ref/src/features/settings/index.tsx +74 -0
  188. package/.shadcn-admin-ref/src/features/settings/notifications/index.tsx +13 -0
  189. package/.shadcn-admin-ref/src/features/settings/notifications/notifications-form.tsx +220 -0
  190. package/.shadcn-admin-ref/src/features/settings/profile/index.tsx +13 -0
  191. package/.shadcn-admin-ref/src/features/settings/profile/profile-form.tsx +177 -0
  192. package/.shadcn-admin-ref/src/features/tasks/components/data-table-bulk-actions.tsx +193 -0
  193. package/.shadcn-admin-ref/src/features/tasks/components/data-table-row-actions.tsx +83 -0
  194. package/.shadcn-admin-ref/src/features/tasks/components/tasks-columns.tsx +123 -0
  195. package/.shadcn-admin-ref/src/features/tasks/components/tasks-dialogs.tsx +72 -0
  196. package/.shadcn-admin-ref/src/features/tasks/components/tasks-import-dialog.tsx +110 -0
  197. package/.shadcn-admin-ref/src/features/tasks/components/tasks-multi-delete-dialog.tsx +95 -0
  198. package/.shadcn-admin-ref/src/features/tasks/components/tasks-mutate-drawer.tsx +212 -0
  199. package/.shadcn-admin-ref/src/features/tasks/components/tasks-primary-buttons.tsx +21 -0
  200. package/.shadcn-admin-ref/src/features/tasks/components/tasks-provider.tsx +36 -0
  201. package/.shadcn-admin-ref/src/features/tasks/components/tasks-table.tsx +197 -0
  202. package/.shadcn-admin-ref/src/features/tasks/data/data.tsx +77 -0
  203. package/.shadcn-admin-ref/src/features/tasks/data/schema.ts +13 -0
  204. package/.shadcn-admin-ref/src/features/tasks/data/tasks.ts +29 -0
  205. package/.shadcn-admin-ref/src/features/tasks/index.tsx +41 -0
  206. package/.shadcn-admin-ref/src/features/users/components/data-table-bulk-actions.tsx +139 -0
  207. package/.shadcn-admin-ref/src/features/users/components/data-table-row-actions.tsx +63 -0
  208. package/.shadcn-admin-ref/src/features/users/components/users-action-dialog.tsx +326 -0
  209. package/.shadcn-admin-ref/src/features/users/components/users-columns.tsx +138 -0
  210. package/.shadcn-admin-ref/src/features/users/components/users-delete-dialog.tsx +81 -0
  211. package/.shadcn-admin-ref/src/features/users/components/users-dialogs.tsx +51 -0
  212. package/.shadcn-admin-ref/src/features/users/components/users-invite-dialog.tsx +150 -0
  213. package/.shadcn-admin-ref/src/features/users/components/users-multi-delete-dialog.tsx +95 -0
  214. package/.shadcn-admin-ref/src/features/users/components/users-primary-buttons.tsx +21 -0
  215. package/.shadcn-admin-ref/src/features/users/components/users-provider.tsx +36 -0
  216. package/.shadcn-admin-ref/src/features/users/components/users-table.tsx +194 -0
  217. package/.shadcn-admin-ref/src/features/users/data/data.ts +35 -0
  218. package/.shadcn-admin-ref/src/features/users/data/schema.ts +32 -0
  219. package/.shadcn-admin-ref/src/features/users/data/users.ts +33 -0
  220. package/.shadcn-admin-ref/src/features/users/index.tsx +47 -0
  221. package/.shadcn-admin-ref/src/hooks/use-dialog-state.tsx +18 -0
  222. package/.shadcn-admin-ref/src/hooks/use-mobile.tsx +19 -0
  223. package/.shadcn-admin-ref/src/hooks/use-table-url-state.ts +219 -0
  224. package/.shadcn-admin-ref/src/lib/cookies.ts +43 -0
  225. package/.shadcn-admin-ref/src/lib/handle-server-error.ts +24 -0
  226. package/.shadcn-admin-ref/src/lib/show-submitted-data.tsx +15 -0
  227. package/.shadcn-admin-ref/src/lib/utils.ts +60 -0
  228. package/.shadcn-admin-ref/src/main.tsx +107 -0
  229. package/.shadcn-admin-ref/src/routeTree.gen.ts +719 -0
  230. package/.shadcn-admin-ref/src/routes/(auth)/forgot-password.tsx +6 -0
  231. package/.shadcn-admin-ref/src/routes/(auth)/otp.tsx +6 -0
  232. package/.shadcn-admin-ref/src/routes/(auth)/sign-in-2.tsx +6 -0
  233. package/.shadcn-admin-ref/src/routes/(auth)/sign-in.tsx +12 -0
  234. package/.shadcn-admin-ref/src/routes/(auth)/sign-up.tsx +6 -0
  235. package/.shadcn-admin-ref/src/routes/(errors)/401.tsx +6 -0
  236. package/.shadcn-admin-ref/src/routes/(errors)/403.tsx +6 -0
  237. package/.shadcn-admin-ref/src/routes/(errors)/404.tsx +6 -0
  238. package/.shadcn-admin-ref/src/routes/(errors)/500.tsx +6 -0
  239. package/.shadcn-admin-ref/src/routes/(errors)/503.tsx +6 -0
  240. package/.shadcn-admin-ref/src/routes/__root.tsx +30 -0
  241. package/.shadcn-admin-ref/src/routes/_authenticated/apps/index.tsx +17 -0
  242. package/.shadcn-admin-ref/src/routes/_authenticated/chats/index.tsx +6 -0
  243. package/.shadcn-admin-ref/src/routes/_authenticated/errors/$error.tsx +45 -0
  244. package/.shadcn-admin-ref/src/routes/_authenticated/help-center/index.tsx +6 -0
  245. package/.shadcn-admin-ref/src/routes/_authenticated/index.tsx +6 -0
  246. package/.shadcn-admin-ref/src/routes/_authenticated/route.tsx +6 -0
  247. package/.shadcn-admin-ref/src/routes/_authenticated/settings/account.tsx +6 -0
  248. package/.shadcn-admin-ref/src/routes/_authenticated/settings/appearance.tsx +6 -0
  249. package/.shadcn-admin-ref/src/routes/_authenticated/settings/display.tsx +6 -0
  250. package/.shadcn-admin-ref/src/routes/_authenticated/settings/index.tsx +6 -0
  251. package/.shadcn-admin-ref/src/routes/_authenticated/settings/notifications.tsx +6 -0
  252. package/.shadcn-admin-ref/src/routes/_authenticated/settings/route.tsx +6 -0
  253. package/.shadcn-admin-ref/src/routes/_authenticated/tasks/index.tsx +23 -0
  254. package/.shadcn-admin-ref/src/routes/_authenticated/users/index.tsx +32 -0
  255. package/.shadcn-admin-ref/src/routes/clerk/(auth)/route.tsx +60 -0
  256. package/.shadcn-admin-ref/src/routes/clerk/(auth)/sign-in.tsx +14 -0
  257. package/.shadcn-admin-ref/src/routes/clerk/(auth)/sign-up.tsx +9 -0
  258. package/.shadcn-admin-ref/src/routes/clerk/_authenticated/route.tsx +6 -0
  259. package/.shadcn-admin-ref/src/routes/clerk/_authenticated/user-management.tsx +184 -0
  260. package/.shadcn-admin-ref/src/routes/clerk/route.tsx +135 -0
  261. package/.shadcn-admin-ref/src/stores/auth-store.ts +53 -0
  262. package/.shadcn-admin-ref/src/styles/index.css +87 -0
  263. package/.shadcn-admin-ref/src/styles/theme.css +102 -0
  264. package/.shadcn-admin-ref/src/tanstack-table.d.ts +10 -0
  265. package/.shadcn-admin-ref/src/vite-env.d.ts +1 -0
  266. package/.swarm/memory.db +0 -0
  267. package/.swarm/memory.db-shm +0 -0
  268. package/.swarm/memory.db-wal +0 -0
  269. package/.zyflow/cli-settings.json +30 -0
  270. package/.zyflow/db.sqlite +0 -0
  271. package/.zyflow/logs/add-gitdiagram-integration/1633-1765491505852.json +10 -0
  272. package/.zyflow/logs/add-gitdiagram-integration/1633-1765491622627.json +10 -0
  273. package/.zyflow/logs/add-gitdiagram-integration/1633-1765491794652.json +10 -0
  274. package/.zyflow/logs/add-gitdiagram-integration/1633-1765491890392.json +10 -0
  275. package/.zyflow/logs/add-gitdiagram-integration/1633-1765494002879.json +10 -0
  276. package/.zyflow/logs/add-gitdiagram-integration/1633-1765494183887.json +10 -0
  277. package/.zyflow/logs/add-gitdiagram-integration/1633-1765494342052.json +10 -0
  278. package/.zyflow/logs/add-gitdiagram-integration/1633-1765494387244.json +10 -0
  279. package/.zyflow/logs/add-gitdiagram-integration/1633-1765494387245.json +10 -0
  280. package/.zyflow/logs/add-gitdiagram-integration/1633-1765494606176.json +10 -0
  281. package/.zyflow/logs/add-gitdiagram-integration/1633-1765495967542.json +16 -0
  282. package/.zyflow/logs/add-gitdiagram-integration/1633-1765495967629.json +16 -0
  283. package/.zyflow/logs/add-gitdiagram-integration/1633-1765497861143.json +16 -0
  284. package/.zyflow/logs/add-gitdiagram-integration/1633-1765497861870.json +20 -0
  285. package/.zyflow/logs/add-gitdiagram-integration/1633-1765498021377.json +18 -0
  286. package/.zyflow/logs/add-gitdiagram-integration/1633-1765498021660.json +18 -0
  287. package/.zyflow/logs/add-gitdiagram-integration/1633-1765503255525.json +13 -0
  288. package/.zyflow/logs/add-gitdiagram-integration/1633-1765503256018.json +13 -0
  289. package/.zyflow/logs/add-gitdiagram-integration/1633-1765504009102.json +16 -0
  290. package/.zyflow/logs/add-gitdiagram-integration/1633-1765504492051.json +18 -0
  291. package/.zyflow/logs/add-gitdiagram-integration/1633-1765504946437.json +16 -0
  292. package/.zyflow/logs/add-gitdiagram-integration/1633-1765504946640.json +16 -0
  293. package/.zyflow/logs/add-gitdiagram-integration/1634-1765505950215.json +16 -0
  294. package/.zyflow/logs/add-gitdiagram-integration/1634-1765505950948.json +18 -0
  295. package/.zyflow/logs/add-gitdiagram-integration/1635-1765505971712.json +18 -0
  296. package/.zyflow/logs/add-gitdiagram-integration/1635-1765505971976.json +18 -0
  297. package/.zyflow/logs/add-gitdiagram-integration/1636-1765505986208.json +18 -0
  298. package/.zyflow/logs/add-gitdiagram-integration/1636-1765505986620.json +16 -0
  299. package/.zyflow/logs/integrate-claude-flow/3580-1765996816612.json +10 -0
  300. package/.zyflow/logs/integrate-claude-flow/3580-1766014825819.json +10 -0
  301. package/.zyflow/logs/integrate-claude-flow/3580-1766015183794.json +12 -0
  302. package/.zyflow/logs/integrate-claude-flow/3580-1766015474608.json +12 -0
  303. package/.zyflow/logs/integrate-claude-flow/3581-1766016502824.json +63 -0
  304. package/.zyflow/logs/integrate-claude-flow/3581-1766016576008.json +60 -0
  305. package/.zyflow/logs/integrate-claude-flow/3582-1766022737754.json +110 -0
  306. package/.zyflow/logs/integrate-claude-flow/3582-1766022809327.json +135 -0
  307. package/.zyflow/sessions.json +242 -0
  308. package/.zyflow/settings.json +6 -0
  309. package/.zyflow/tasks.db +0 -0
  310. package/.zyflow/tasks.db-shm +0 -0
  311. package/.zyflow/tasks.db-wal +0 -0
  312. package/.zyflow/zyflow.sqlite +0 -0
  313. package/Dockerfile +82 -0
  314. package/LICENSE +21 -0
  315. package/README.md +506 -0
  316. package/claude-flow +34 -0
  317. package/components.json +21 -0
  318. package/config/ports.ts +28 -0
  319. package/docker-compose.yml +52 -0
  320. package/eslint.config.js +34 -0
  321. package/index.html +19 -0
  322. package/logs/mcp-error.log +55 -0
  323. package/logs/mcp-out.log +0 -0
  324. package/logs/pm2-error.log +0 -0
  325. package/logs/pm2-out.log +265 -0
  326. package/logs/py-error.log +22 -0
  327. package/logs/py-out.log +0 -0
  328. package/logs/server-error.log +11000 -0
  329. package/logs/server-out.log +8117 -0
  330. package/logs/vite-error.log +404 -0
  331. package/logs/vite-out.log +311 -0
  332. package/mcp-server/agent-tools.ts +375 -0
  333. package/mcp-server/cli-models.ts +193 -0
  334. package/mcp-server/context.ts +110 -0
  335. package/mcp-server/diagram-tools.ts +341 -0
  336. package/mcp-server/index.ts +2014 -0
  337. package/mcp-server/integration-tools.ts +909 -0
  338. package/mcp-server/moai-spec-tools.ts +416 -0
  339. package/mcp-server/parser.ts +422 -0
  340. package/mcp-server/post-task-runner.ts +253 -0
  341. package/mcp-server/post-task-types.ts +426 -0
  342. package/mcp-server/quarantine-manager.ts +479 -0
  343. package/mcp-server/report-generator.ts +386 -0
  344. package/mcp-server/task-tools.ts +619 -0
  345. package/mcp-server/trigger-config.ts +288 -0
  346. package/mcp-server/trigger-router.ts +305 -0
  347. package/mcp-server/triggers/event-listener.ts +331 -0
  348. package/mcp-server/triggers/git-hooks.ts +283 -0
  349. package/mcp-server/triggers/scheduler.ts +289 -0
  350. package/mcp-server/types.ts +55 -0
  351. package/memory/claude-flow@alpha-data.json +5 -0
  352. package/nginx/zyflow.conf +144 -0
  353. package/openspec/config.yaml +78 -0
  354. package/openspec-backup.tar.gz +0 -0
  355. package/package.json +154 -0
  356. package/packages/gitdiagram-core/.claude-flow/metrics/agent-metrics.json +1 -0
  357. package/packages/gitdiagram-core/.claude-flow/metrics/performance.json +87 -0
  358. package/packages/gitdiagram-core/.claude-flow/metrics/task-metrics.json +10 -0
  359. package/packages/gitdiagram-core/package.json +41 -0
  360. package/packages/gitdiagram-core/src/file-tree.ts +272 -0
  361. package/packages/gitdiagram-core/src/generator.ts +283 -0
  362. package/packages/gitdiagram-core/src/index.ts +78 -0
  363. package/packages/gitdiagram-core/src/llm-adapter.ts +235 -0
  364. package/packages/gitdiagram-core/src/mermaid-utils.ts +304 -0
  365. package/packages/gitdiagram-core/src/prompts.ts +281 -0
  366. package/packages/zyflow-parser/package.json +34 -0
  367. package/packages/zyflow-parser/src/index.ts +26 -0
  368. package/packages/zyflow-parser/src/moai-parser.ts +603 -0
  369. package/packages/zyflow-parser/src/moai-types.ts +110 -0
  370. package/packages/zyflow-remote-plugin/.claude-flow/metrics/agent-metrics.json +1 -0
  371. package/packages/zyflow-remote-plugin/.claude-flow/metrics/performance.json +87 -0
  372. package/packages/zyflow-remote-plugin/.claude-flow/metrics/task-metrics.json +10 -0
  373. package/packages/zyflow-remote-plugin/package.json +31 -0
  374. package/packages/zyflow-remote-plugin/src/index.ts +71 -0
  375. package/packages/zyflow-remote-plugin/src/remote-config.ts +232 -0
  376. package/packages/zyflow-remote-plugin/src/router.ts +535 -0
  377. package/packages/zyflow-remote-plugin/src/ssh-config-parser.ts +123 -0
  378. package/packages/zyflow-remote-plugin/src/ssh-manager.ts +598 -0
  379. package/packages/zyflow-remote-plugin/src/types.ts +149 -0
  380. package/plugin/manifest.json +26 -0
  381. package/plugin/package.json +13 -0
  382. package/public/favicon.svg +4 -0
  383. package/server/adk/agents/error-analyzer.ts +223 -0
  384. package/server/adk/agents/fix-generator.ts +187 -0
  385. package/server/adk/agents/pr-agent.ts +264 -0
  386. package/server/adk/agents/validator.ts +187 -0
  387. package/server/adk/config.ts +43 -0
  388. package/server/adk/index.ts +69 -0
  389. package/server/adk/integration.ts +297 -0
  390. package/server/adk/orchestrator.ts +405 -0
  391. package/server/adk/tools/build-tools.ts +290 -0
  392. package/server/adk/tools/file-tools.ts +351 -0
  393. package/server/adk/tools/git-tools.ts +280 -0
  394. package/server/adk/tools/github-tools.ts +249 -0
  395. package/server/agents/agent-monitor.ts +416 -0
  396. package/server/agents/alert-integration.ts +312 -0
  397. package/server/agents/error-analyzer.ts +472 -0
  398. package/server/agents/error-detector.ts +442 -0
  399. package/server/agents/fix-generator.ts +421 -0
  400. package/server/agents/fix-validator.ts +428 -0
  401. package/server/agents/merge-policy.ts +362 -0
  402. package/server/agents/pr-workflow.ts +476 -0
  403. package/server/agents/prompts/error-analysis.ts +393 -0
  404. package/server/ai/gemini-client.ts +499 -0
  405. package/server/ai/index.ts +317 -0
  406. package/server/ai/types.ts +137 -0
  407. package/server/app.ts +3693 -0
  408. package/server/archive-manager.ts +604 -0
  409. package/server/backlog/index.ts +7 -0
  410. package/server/backlog/migration.ts +331 -0
  411. package/server/backlog/parser.ts +323 -0
  412. package/server/backlog/sync.ts +325 -0
  413. package/server/change-log.ts +868 -0
  414. package/server/claude-flow/index.ts +12 -0
  415. package/server/claude-flow/prompt-builder.ts +407 -0
  416. package/server/claude-flow/types.ts +33 -0
  417. package/server/cli-adapter/index.ts +11 -0
  418. package/server/cli-adapter/process-manager.ts +612 -0
  419. package/server/cli-adapter/profile-manager.ts +286 -0
  420. package/server/cli-adapter/routes.ts +561 -0
  421. package/server/cli-adapter/types.ts +226 -0
  422. package/server/config.d.ts +18 -0
  423. package/server/config.js +79 -0
  424. package/server/config.ts +262 -0
  425. package/server/flow-sync.ts +543 -0
  426. package/server/git/change-workflow.ts +446 -0
  427. package/server/git/commands.ts +370 -0
  428. package/server/git/github.ts +247 -0
  429. package/server/git/index.ts +1202 -0
  430. package/server/git/status.ts +322 -0
  431. package/server/index.ts +136 -0
  432. package/server/integrations/crypto.ts +142 -0
  433. package/server/integrations/db/client.ts +169 -0
  434. package/server/integrations/db/schema.ts +167 -0
  435. package/server/integrations/env-parser.ts +365 -0
  436. package/server/integrations/index.ts +101 -0
  437. package/server/integrations/keychain.ts +239 -0
  438. package/server/integrations/local/file-utils.ts +383 -0
  439. package/server/integrations/local/index.ts +64 -0
  440. package/server/integrations/local/resolver.ts +439 -0
  441. package/server/integrations/local/types.ts +122 -0
  442. package/server/integrations/routes.ts +1100 -0
  443. package/server/integrations/service-patterns.ts +771 -0
  444. package/server/integrations/services/accounts.ts +356 -0
  445. package/server/integrations/services/env-import.ts +279 -0
  446. package/server/integrations/services/projects.ts +552 -0
  447. package/server/integrations/services/system-import.ts +1110 -0
  448. package/server/migrations/ears-generator.ts +491 -0
  449. package/server/migrations/gherkin-generator.ts +605 -0
  450. package/server/migrations/index.ts +73 -0
  451. package/server/migrations/migrate-spec-format.ts +492 -0
  452. package/server/migrations/openspec-parser.ts +542 -0
  453. package/server/migrations/tag-generator.ts +474 -0
  454. package/server/moai-specs.ts +487 -0
  455. package/server/moai-watcher.ts +145 -0
  456. package/server/parser-debug.ts +37 -0
  457. package/server/parser-utils.ts +316 -0
  458. package/server/parser.d.ts +17 -0
  459. package/server/parser.js +221 -0
  460. package/server/parser.ts +342 -0
  461. package/server/remote-watcher.ts +367 -0
  462. package/server/replay-engine.ts +915 -0
  463. package/server/routes/alerts.ts +1028 -0
  464. package/server/routes/changes.ts +812 -0
  465. package/server/routes/docs.ts +898 -0
  466. package/server/routes/flow.ts +2814 -0
  467. package/server/routes/global-chat.ts +162 -0
  468. package/server/routes/leann.ts +327 -0
  469. package/server/routes/projects.ts +1282 -0
  470. package/server/routes/search.ts +266 -0
  471. package/server/routes/specs.ts +482 -0
  472. package/server/routes/webhooks.ts +579 -0
  473. package/server/server/parser.js +265 -0
  474. package/server/services/githubActionsPoller.ts +797 -0
  475. package/server/services/slackNotifier.ts +476 -0
  476. package/server/src/types/index.js +1 -0
  477. package/server/sync-tasks.ts +741 -0
  478. package/server/tasks/cli/commands.ts +269 -0
  479. package/server/tasks/cli/index.ts +152 -0
  480. package/server/tasks/core/search.ts +81 -0
  481. package/server/tasks/core/task.ts +307 -0
  482. package/server/tasks/db/client.ts +1008 -0
  483. package/server/tasks/db/schema.ts +572 -0
  484. package/server/tasks/index.ts +24 -0
  485. package/server/tasks.db +0 -0
  486. package/server/types/archive.ts +136 -0
  487. package/server/types/change-log.ts +643 -0
  488. package/server/types/spec.ts +188 -0
  489. package/server/unified-spec-scanner.ts +753 -0
  490. package/server/utils/crypto.ts +179 -0
  491. package/server/utils/webhook-verify.ts +216 -0
  492. package/server/watcher.ts +132 -0
  493. package/server/websocket.ts +99 -0
  494. package/server-output.log +6 -0
  495. package/sonar-project.properties +18 -0
  496. package/src/App.tsx +386 -0
  497. package/src/api/client.ts +346 -0
  498. package/src/api/error-interceptor.ts +366 -0
  499. package/src/api/errors.ts +123 -0
  500. package/src/api/flow.ts +233 -0
  501. package/src/api/offline-queue.ts +351 -0
  502. package/src/api/retry-logic.ts +233 -0
  503. package/src/components/OfflineModeBanner.tsx +159 -0
  504. package/src/components/SSEStatusIndicator.tsx +194 -0
  505. package/src/components/agent/AgentChat.tsx +243 -0
  506. package/src/components/agent/AgentPage.tsx +182 -0
  507. package/src/components/agent/AgentSidebar.tsx +231 -0
  508. package/src/components/agent/index.ts +7 -0
  509. package/src/components/alerts/AlertCenter.tsx +239 -0
  510. package/src/components/alerts/AlertDashboard.tsx +211 -0
  511. package/src/components/alerts/AlertDetail.tsx +474 -0
  512. package/src/components/alerts/AlertList.tsx +113 -0
  513. package/src/components/alerts/AlertSettings.tsx +336 -0
  514. package/src/components/alerts/index.ts +5 -0
  515. package/src/components/chat/ChatPanel.tsx +642 -0
  516. package/src/components/chat/index.ts +1 -0
  517. package/src/components/cli/AddCustomCLIDialog.tsx +210 -0
  518. package/src/components/cli/CLISelector.tsx +187 -0
  519. package/src/components/cli/index.ts +8 -0
  520. package/src/components/dashboard/ArchivedChangeList.tsx +102 -0
  521. package/src/components/dashboard/ArchivedChangeViewer.tsx +184 -0
  522. package/src/components/dashboard/ArchivedChangesPage.tsx +31 -0
  523. package/src/components/dashboard/ChangeList.tsx +86 -0
  524. package/src/components/dashboard/ThemeToggle.tsx +33 -0
  525. package/src/components/diagram/DiagramViewer.tsx +256 -0
  526. package/src/components/diagram/MermaidRenderer.tsx +163 -0
  527. package/src/components/diagram/ProjectDiagramTab.tsx +161 -0
  528. package/src/components/diagram/index.ts +13 -0
  529. package/src/components/errors/ErrorBoundary.tsx +276 -0
  530. package/src/components/errors/ErrorFallback.tsx +198 -0
  531. package/src/components/errors/ErrorToast.tsx +221 -0
  532. package/src/components/flow/BacklogView.tsx +1142 -0
  533. package/src/components/flow/ChangeDetail.tsx +475 -0
  534. package/src/components/flow/ChangeItem.tsx +230 -0
  535. package/src/components/flow/ChangeList.tsx +92 -0
  536. package/src/components/flow/ExecutionHistoryDialog.tsx +224 -0
  537. package/src/components/flow/FlowContent.tsx +212 -0
  538. package/src/components/flow/FlowPage.tsx +9 -0
  539. package/src/components/flow/PipelineBar.tsx +214 -0
  540. package/src/components/flow/ProjectDashboard.tsx +222 -0
  541. package/src/components/flow/SpecDetail.tsx +138 -0
  542. package/src/components/flow/SpecDetailTabs.tsx +176 -0
  543. package/src/components/flow/SpecItem.tsx +93 -0
  544. package/src/components/flow/SpecProgressBar.tsx +47 -0
  545. package/src/components/flow/StageContent.tsx +620 -0
  546. package/src/components/flow/StandaloneTasks.tsx +960 -0
  547. package/src/components/flow/TaskExecutionDialog.tsx +1204 -0
  548. package/src/components/flow/index.ts +9 -0
  549. package/src/components/flow/task-execution/AgentSlider.tsx +37 -0
  550. package/src/components/flow/task-execution/ConsensusSettings.tsx +129 -0
  551. package/src/components/flow/task-execution/ExecutionOutput.tsx +398 -0
  552. package/src/components/flow/task-execution/ModelSelector.tsx +134 -0
  553. package/src/components/flow/task-execution/ProviderSelector.tsx +137 -0
  554. package/src/components/flow/task-execution/RecommendationBanner.tsx +71 -0
  555. package/src/components/flow/task-execution/StatusBadge.tsx +43 -0
  556. package/src/components/flow/task-execution/StrategySelector.tsx +48 -0
  557. package/src/components/flow/task-execution/SwarmSummary.tsx +55 -0
  558. package/src/components/flow/task-execution/index.ts +14 -0
  559. package/src/components/flow/task-execution/types.ts +56 -0
  560. package/src/components/git/ChangeWorkflowDialog.tsx +582 -0
  561. package/src/components/git/ConflictResolutionDialog.tsx +398 -0
  562. package/src/components/git/GitBranchSelector.tsx +212 -0
  563. package/src/components/git/GitCommitDialog.tsx +254 -0
  564. package/src/components/git/GitStatusBadge.tsx +148 -0
  565. package/src/components/git/GitSyncButton.tsx +128 -0
  566. package/src/components/git/RemoteStatusBanner.tsx +143 -0
  567. package/src/components/git/index.ts +9 -0
  568. package/src/components/integrations/EnvImportDialog.tsx +524 -0
  569. package/src/components/integrations/EnvironmentDialog.tsx +227 -0
  570. package/src/components/integrations/IntegrationBadges.tsx +91 -0
  571. package/src/components/integrations/IntegrationsSettings.tsx +55 -0
  572. package/src/components/integrations/ProjectIntegrations.tsx +481 -0
  573. package/src/components/integrations/ServiceAccountDialog.tsx +422 -0
  574. package/src/components/integrations/ServiceAccountList.tsx +305 -0
  575. package/src/components/integrations/SystemImportDialog.tsx +436 -0
  576. package/src/components/integrations/TestAccountDialog.tsx +162 -0
  577. package/src/components/integrations/index.ts +6 -0
  578. package/src/components/layout/AppSidebar.tsx +284 -0
  579. package/src/components/layout/FlowSidebar.tsx +435 -0
  580. package/src/components/layout/GlobalCommandPalette.tsx +410 -0
  581. package/src/components/layout/MenuBar.tsx +227 -0
  582. package/src/components/layout/StatusBar.tsx +226 -0
  583. package/src/components/monitoring/ErrorDashboard.tsx +274 -0
  584. package/src/components/monitoring/ErrorDetailPanel.tsx +200 -0
  585. package/src/components/monitoring/ErrorFilters.tsx +219 -0
  586. package/src/components/monitoring/ErrorHistoryList.tsx +141 -0
  587. package/src/components/monitoring/ErrorStats.tsx +249 -0
  588. package/src/components/remote/RemoteFileBrowser.tsx +249 -0
  589. package/src/components/remote/RemoteServerDialog.tsx +234 -0
  590. package/src/components/remote/RemoteServerList.tsx +366 -0
  591. package/src/components/remote/index.ts +7 -0
  592. package/src/components/settings/CLISettings.tsx +522 -0
  593. package/src/components/settings/CustomCLIDialog.tsx +548 -0
  594. package/src/components/settings/IntegrationsSettings.tsx +51 -0
  595. package/src/components/settings/ProjectSettings.tsx +441 -0
  596. package/src/components/settings/ProjectsSettings.tsx +541 -0
  597. package/src/components/settings/SearchSettings.tsx +272 -0
  598. package/src/components/settings/SettingsPage.tsx +68 -0
  599. package/src/components/settings/index.ts +5 -0
  600. package/src/components/swarm/ExecutionPanel.tsx +284 -0
  601. package/src/components/swarm/LogViewer.tsx +196 -0
  602. package/src/components/swarm/ProgressIndicator.tsx +111 -0
  603. package/src/components/swarm/index.ts +3 -0
  604. package/src/components/tasks/ArchiveTable.tsx +203 -0
  605. package/src/components/tasks/KanbanBoard.tsx +264 -0
  606. package/src/components/tasks/TaskCard.tsx +138 -0
  607. package/src/components/tasks/TaskColumn.tsx +81 -0
  608. package/src/components/tasks/TaskDialog.tsx +274 -0
  609. package/src/components/tasks/index.ts +5 -0
  610. package/src/components/tasks/types.ts +43 -0
  611. package/src/components/ui/alert-dialog.tsx +154 -0
  612. package/src/components/ui/alert.tsx +65 -0
  613. package/src/components/ui/badge.tsx +45 -0
  614. package/src/components/ui/button.tsx +58 -0
  615. package/src/components/ui/card.tsx +91 -0
  616. package/src/components/ui/checkbox.tsx +29 -0
  617. package/src/components/ui/collapsible.tsx +31 -0
  618. package/src/components/ui/command.tsx +184 -0
  619. package/src/components/ui/confirm-dialog.tsx +55 -0
  620. package/src/components/ui/dialog.tsx +142 -0
  621. package/src/components/ui/dropdown-menu.tsx +254 -0
  622. package/src/components/ui/input.tsx +20 -0
  623. package/src/components/ui/label.tsx +22 -0
  624. package/src/components/ui/markdown.tsx +100 -0
  625. package/src/components/ui/progress.tsx +27 -0
  626. package/src/components/ui/resizable-sidebar.tsx +156 -0
  627. package/src/components/ui/resizable.tsx +54 -0
  628. package/src/components/ui/right-resizable-sidebar.tsx +158 -0
  629. package/src/components/ui/scroll-area.tsx +64 -0
  630. package/src/components/ui/select.tsx +185 -0
  631. package/src/components/ui/separator.tsx +25 -0
  632. package/src/components/ui/sheet.tsx +136 -0
  633. package/src/components/ui/sidebar.tsx +726 -0
  634. package/src/components/ui/skeleton.tsx +13 -0
  635. package/src/components/ui/slider.tsx +56 -0
  636. package/src/components/ui/switch.tsx +29 -0
  637. package/src/components/ui/table.tsx +113 -0
  638. package/src/components/ui/tabs.tsx +63 -0
  639. package/src/components/ui/textarea.tsx +17 -0
  640. package/src/components/ui/tooltip.tsx +60 -0
  641. package/src/config/api.ts +83 -0
  642. package/src/constants/error-codes.ts +255 -0
  643. package/src/constants/stages.ts +27 -0
  644. package/src/context/ErrorContext.tsx +185 -0
  645. package/src/context/theme-provider.tsx +63 -0
  646. package/src/hooks/use-mobile.tsx +19 -0
  647. package/src/hooks/useAI.ts +206 -0
  648. package/src/hooks/useAgentSession.ts +431 -0
  649. package/src/hooks/useAlerts.ts +935 -0
  650. package/src/hooks/useArchivedChanges.ts +39 -0
  651. package/src/hooks/useAsyncError.ts +45 -0
  652. package/src/hooks/useChangeGit.ts +727 -0
  653. package/src/hooks/useChanges.ts +20 -0
  654. package/src/hooks/useClaude.ts +130 -0
  655. package/src/hooks/useDocs.ts +182 -0
  656. package/src/hooks/useErrorDashboard.ts +243 -0
  657. package/src/hooks/useErrorHandler.ts +150 -0
  658. package/src/hooks/useExecutionHistory.ts +55 -0
  659. package/src/hooks/useFlowChanges.ts +850 -0
  660. package/src/hooks/useFlowItems.ts +205 -0
  661. package/src/hooks/useGit.ts +427 -0
  662. package/src/hooks/useHideCompletedSpecs.ts +15 -0
  663. package/src/hooks/useInstance.ts +40 -0
  664. package/src/hooks/useIntegrations.ts +737 -0
  665. package/src/hooks/useLeannStatus.ts +93 -0
  666. package/src/hooks/useNetworkStatus.ts +167 -0
  667. package/src/hooks/useProjects.ts +353 -0
  668. package/src/hooks/useRemoteServers.ts +383 -0
  669. package/src/hooks/useSSEConnection.ts +346 -0
  670. package/src/hooks/useSpecs.ts +39 -0
  671. package/src/hooks/useSwarm.ts +462 -0
  672. package/src/hooks/useTasks.ts +137 -0
  673. package/src/hooks/useURLSync.ts +122 -0
  674. package/src/hooks/useWebSocket.ts +262 -0
  675. package/src/lib/utils.ts +121 -0
  676. package/src/main.tsx +22 -0
  677. package/src/stores/errorStore.ts +301 -0
  678. package/src/stores/offlineStore.ts +266 -0
  679. package/src/stores/sseStore.ts +247 -0
  680. package/src/stores/useHideCompletedStore.ts +21 -0
  681. package/src/styles/index.css +87 -0
  682. package/src/styles/theme.css +102 -0
  683. package/src/types/ai.ts +191 -0
  684. package/src/types/errors.ts +253 -0
  685. package/src/types/flow.ts +382 -0
  686. package/src/types/index.ts +614 -0
  687. package/src/utils/error-logger.ts +399 -0
  688. package/src/utils/error-statistics.ts +305 -0
  689. package/src/utils/logger.ts +280 -0
  690. package/src/utils/task-routing.ts +795 -0
  691. package/src/vite-env.d.ts +1 -0
  692. package/test-results/.last-run.json +4 -0
  693. package/tmp/check-docker-final.ts +48 -0
  694. package/tmp/check-docker-tasks.ts +58 -0
  695. package/tmp/check-docker-tasks2.ts +48 -0
  696. package/tmp/check-docker-tasks3.ts +42 -0
  697. package/tmp/check-mobile-tasks.ts +57 -0
  698. package/tmp/check-zywiki-tasks.ts +49 -0
  699. package/tmp/sync-mobile.ts +11 -0
  700. package/tmp/sync-zywiki.ts +68 -0
  701. package/tmp/test-docker-parser.ts +15 -0
  702. package/tmp/test-mobile-parser.ts +28 -0
  703. package/tmp/test-parser.ts +27 -0
  704. package/tmp/test-unnumbered.ts +35 -0
  705. package/zyflow.db +0 -0
@@ -0,0 +1,1282 @@
1
+ /**
2
+ * Projects Router
3
+ *
4
+ * 프로젝트 관리 API 라우터
5
+ */
6
+
7
+ import { Router } from 'express'
8
+ import { readdir, readFile, access } from 'fs/promises'
9
+ import type { Dirent } from 'fs'
10
+ import { join, basename } from 'path'
11
+ import { syncChangeTasksForProject, syncRemoteChangeTasksForProject } from '../sync-tasks.js'
12
+ import { exec } from 'child_process'
13
+ import { promisify } from 'util'
14
+ import {
15
+ loadConfig,
16
+ addProject,
17
+ removeProject,
18
+ setActiveProject,
19
+ getActiveProject,
20
+ updateProjectPath,
21
+ updateProjectName,
22
+ reorderProjects,
23
+ } from '../config.js'
24
+ import { initDb } from '../tasks/index.js'
25
+ import { getSqlite } from '../tasks/db/client.js'
26
+ import { parseTasksFile } from '../parser.js'
27
+ import { startTasksWatcher, stopTasksWatcher } from '../watcher.js'
28
+ import { startRemoteWatcher, stopRemoteWatcher } from '../remote-watcher.js'
29
+ import { scanMoaiSpecs, countMoaiTags, scanRemoteMoaiSpecs, countRemoteMoaiTags } from '../moai-specs.js'
30
+
31
+ // Remote plugin is optional - only load if installed
32
+ let remotePlugin: {
33
+ getRemoteServerById: (id: string) => Promise<unknown>
34
+ listDirectory: (server: unknown, path: string) => Promise<{ entries: Array<{ type: string; name: string; modifiedAt?: string }> }>
35
+ executeCommand: (server: unknown, cmd: string, opts?: { cwd?: string }) => Promise<{ stdout: string }>
36
+ readRemoteFile: (server: unknown, path: string) => Promise<string>
37
+ } | null = null
38
+
39
+ async function getRemotePlugin() {
40
+ if (remotePlugin) return remotePlugin
41
+ try {
42
+ const mod = await import('@zyflow/remote-plugin')
43
+ remotePlugin = mod
44
+ return remotePlugin
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ const execAsync = promisify(exec)
51
+
52
+ export const projectsRouter = Router()
53
+
54
+ // POST /browse - Open native folder picker dialog
55
+ projectsRouter.post('/browse', async (_req, res) => {
56
+ try {
57
+ // macOS: Use AppleScript to open folder picker (simplified version)
58
+ const script = `osascript -e 'POSIX path of (choose folder with prompt "OpenSpec 프로젝트 폴더를 선택하세요")'`
59
+
60
+ const { stdout } = await execAsync(script, { timeout: 120000 }) // 2분 타임아웃
61
+ const selectedPath = stdout.trim().replace(/\/$/, '') // Remove trailing slash
62
+
63
+ if (!selectedPath) {
64
+ return res.json({ success: true, data: { path: null, cancelled: true } })
65
+ }
66
+
67
+ res.json({ success: true, data: { path: selectedPath, cancelled: false } })
68
+ } catch (error) {
69
+ const errorMessage = (error as Error).message || ''
70
+ // User cancelled the dialog (error code -128)
71
+ if (
72
+ errorMessage.includes('-128') ||
73
+ errorMessage.includes('User canceled') ||
74
+ errorMessage.includes('취소')
75
+ ) {
76
+ return res.json({ success: true, data: { path: null, cancelled: true } })
77
+ }
78
+ console.error('Error opening folder picker:', error)
79
+ res.status(500).json({ success: false, error: 'Failed to open folder picker' })
80
+ }
81
+ })
82
+
83
+ // GET / - List all registered projects
84
+ projectsRouter.get('/', async (_req, res) => {
85
+ try {
86
+ const config = await loadConfig()
87
+ res.json({
88
+ success: true,
89
+ data: {
90
+ projects: config.projects,
91
+ activeProjectId: config.activeProjectId,
92
+ },
93
+ })
94
+ } catch (error) {
95
+ console.error('Error listing projects:', error)
96
+ res.status(500).json({ success: false, error: 'Failed to list projects' })
97
+ }
98
+ })
99
+
100
+ // POST / - Add a new project
101
+ projectsRouter.post('/', async (req, res) => {
102
+ try {
103
+ const { path: projectPath } = req.body
104
+
105
+ if (!projectPath) {
106
+ return res.status(400).json({ success: false, error: 'Path is required' })
107
+ }
108
+
109
+ // Check for spec directories - at least one must exist
110
+ const openspecPath = join(projectPath, 'openspec')
111
+ const moaiSpecsPath = join(projectPath, '.moai', 'specs')
112
+
113
+ const hasOpenspec = await access(openspecPath).then(
114
+ () => true,
115
+ () => false
116
+ )
117
+ const hasMoaiSpecs = await access(moaiSpecsPath).then(
118
+ () => true,
119
+ () => false
120
+ )
121
+
122
+ // Require at least one spec format to exist
123
+ if (!hasOpenspec && !hasMoaiSpecs) {
124
+ return res.status(400).json({
125
+ success: false,
126
+ error: 'Project must contain either MoAI SPEC (.moai/specs/) or OpenSpec (openspec/) directory',
127
+ })
128
+ }
129
+
130
+ const name = basename(projectPath)
131
+ const project = await addProject(name, projectPath)
132
+
133
+ res.json({ success: true, data: { project } })
134
+ } catch (error) {
135
+ console.error('Error adding project:', error)
136
+ res.status(500).json({ success: false, error: 'Failed to add project' })
137
+ }
138
+ })
139
+
140
+ // PUT /reorder - Reorder projects
141
+ // NOTE: This must be defined BEFORE /:id routes to avoid :id matching "reorder"
142
+ projectsRouter.put('/reorder', async (req, res) => {
143
+ try {
144
+ const { projectIds } = req.body
145
+
146
+ if (!projectIds || !Array.isArray(projectIds)) {
147
+ return res.status(400).json({ success: false, error: 'projectIds array is required' })
148
+ }
149
+
150
+ const projects = await reorderProjects(projectIds)
151
+ res.json({ success: true, data: { projects } })
152
+ } catch (error) {
153
+ console.error('Error reordering projects:', error)
154
+ res.status(500).json({
155
+ success: false,
156
+ error: error instanceof Error ? error.message : 'Failed to reorder projects',
157
+ })
158
+ }
159
+ })
160
+
161
+ // DELETE /:id - Remove a project
162
+ projectsRouter.delete('/:id', async (req, res) => {
163
+ try {
164
+ const projectId = req.params.id
165
+ await removeProject(projectId)
166
+ res.json({ success: true })
167
+ } catch (error) {
168
+ console.error('Error removing project:', error)
169
+ res.status(500).json({ success: false, error: 'Failed to remove project' })
170
+ }
171
+ })
172
+
173
+ // PUT /:id/activate - Set active project
174
+ projectsRouter.put('/:id/activate', async (req, res) => {
175
+ console.log('[Activate-Optimized] Handler called for project:', req.params.id)
176
+ try {
177
+ await setActiveProject(req.params.id)
178
+ const project = await getActiveProject()
179
+ console.log('[Activate-Optimized] Responding immediately, sync will run in background')
180
+
181
+ // 백그라운드에서 동기화 실행 (Fire-and-forget) - 사용자 응답 대기 시간 제거
182
+ if (project) {
183
+ (async () => {
184
+ try {
185
+ if (project.remote) {
186
+ await syncRemoteProjectChanges(project)
187
+ } else {
188
+ await syncLocalProjectChanges(project)
189
+ }
190
+ } catch (syncError) {
191
+ console.error('Error syncing project changes in background:', syncError)
192
+ }
193
+ })()
194
+ }
195
+
196
+ res.json({ success: true, data: { project } })
197
+ } catch (error) {
198
+ console.error('Error activating project:', error)
199
+ res.status(500).json({ success: false, error: 'Failed to activate project' })
200
+ }
201
+ })
202
+
203
+ async function syncLocalProjectChanges(project: { id: string; name: string; path: string }) {
204
+ initDb(project.path)
205
+ const config = await loadConfig()
206
+ const specConfig = config.specConfig || { defaultSpecFormat: 'moai', enableOpenSpecScanning: false }
207
+
208
+ const sqlite = getSqlite()
209
+ const now = Date.now()
210
+ const activeChangeIds: string[] = []
211
+
212
+ // 1. Scan MoAI SPECs FIRST and add to activeChangeIds
213
+ try {
214
+ const moaiSpecs = await scanMoaiSpecs(project.path)
215
+ const moaiSpecIds = moaiSpecs
216
+ .filter(spec => spec.status !== 'archived')
217
+ .map(spec => spec.id)
218
+ activeChangeIds.push(...moaiSpecIds)
219
+
220
+ if (moaiSpecIds.length > 0) {
221
+ console.log(`[Project] Found ${moaiSpecIds.length} MoAI SPECs: ${moaiSpecIds.join(', ')}`)
222
+
223
+ // Map MoAI SPEC status to changes table status
224
+ const mapSpecStatus = (specStatus: string): 'active' | 'completed' | 'archived' => {
225
+ switch (specStatus) {
226
+ case 'complete':
227
+ case 'completed':
228
+ return 'completed'
229
+ case 'archived':
230
+ return 'archived'
231
+ default:
232
+ return 'active'
233
+ }
234
+ }
235
+
236
+ // Upsert MoAI SPECs into changes table with actual status
237
+ const upsertMoaiStmt = sqlite.prepare(`
238
+ INSERT INTO changes (id, project_id, title, spec_path, status, current_stage, progress, created_at, updated_at)
239
+ VALUES (?, ?, ?, ?, ?, 'spec', ?, ?, ?)
240
+ ON CONFLICT(id, project_id) DO UPDATE SET
241
+ title = excluded.title,
242
+ spec_path = excluded.spec_path,
243
+ status = excluded.status,
244
+ progress = excluded.progress,
245
+ updated_at = excluded.updated_at
246
+ `)
247
+
248
+ for (const spec of moaiSpecs) {
249
+ if (spec.status === 'archived') continue
250
+ const progress = spec.tagCount > 0 ? Math.round((spec.completedTags / spec.tagCount) * 100) : 0
251
+ const specPath = `.moai/specs/${spec.id}/spec.md`
252
+ const dbStatus = mapSpecStatus(spec.status)
253
+ upsertMoaiStmt.run(spec.id, project.id, spec.title, specPath, dbStatus, progress, now, now)
254
+ }
255
+ }
256
+ } catch (err) {
257
+ console.warn('[Project] Failed to scan MoAI SPECs:', err)
258
+ }
259
+
260
+ // 2. Only scan OpenSpec if enabled in config
261
+ if (specConfig.enableOpenSpecScanning) {
262
+ const openspecDir = join(project.path, 'openspec', 'changes')
263
+ let entries: Dirent[] = []
264
+ try {
265
+ entries = await readdir(openspecDir, { withFileTypes: true })
266
+ } catch {
267
+ entries = []
268
+ }
269
+
270
+ const changeEntries = entries.filter((entry) => entry.isDirectory() && entry.name !== 'archive')
271
+ // Append OpenSpec changes to activeChangeIds (don't overwrite MoAI SPECs!)
272
+ activeChangeIds.push(...changeEntries.map((e) => e.name))
273
+
274
+ const changeDataPromises = changeEntries.map(async (entry) => {
275
+ const changeId = entry.name
276
+ const changeDir = join(openspecDir, changeId)
277
+ const specPath = `openspec/changes/${changeId}/proposal.md`
278
+ let title = changeId
279
+
280
+ try {
281
+ const proposalPath = join(changeDir, 'proposal.md')
282
+ const proposalContent = await readFile(proposalPath, 'utf-8')
283
+ const titleMatch = proposalContent.match(/^#\s+(?:Change:\s+)?(.+)$/m)
284
+ if (titleMatch) {
285
+ title = titleMatch[1].trim()
286
+ }
287
+ } catch {
288
+ // proposal.md not found
289
+ }
290
+
291
+ return { changeId, title, specPath }
292
+ })
293
+
294
+ const changeDataList = await Promise.all(changeDataPromises)
295
+
296
+ const upsertStmt = sqlite.prepare(`
297
+ INSERT INTO changes (id, project_id, title, spec_path, status, current_stage, progress, created_at, updated_at)
298
+ VALUES (?, ?, ?, ?, 'active', 'spec', 0, ?, ?)
299
+ ON CONFLICT(id, project_id) DO UPDATE SET
300
+ title = excluded.title,
301
+ spec_path = excluded.spec_path,
302
+ status = 'active',
303
+ updated_at = excluded.updated_at
304
+ `)
305
+
306
+ for (const { changeId, title, specPath } of changeDataList) {
307
+ upsertStmt.run(changeId, project.id, title, specPath, now, now)
308
+ }
309
+
310
+ // Tasks 동기화 (병렬)
311
+ await Promise.all(changeDataList.map(({ changeId }) =>
312
+ syncChangeTasksForProject(changeId, project.path, project.id).catch(err => console.error(`Failed to sync task ${changeId}:`, err))
313
+ ))
314
+
315
+ if (activeChangeIds.length > 0) {
316
+ const placeholders = activeChangeIds.map(() => '?').join(',')
317
+ sqlite
318
+ .prepare(
319
+ `
320
+ UPDATE changes SET status = 'archived', archived_at = COALESCE(archived_at, ?), updated_at = ?
321
+ WHERE project_id = ? AND status = 'active' AND id NOT IN (${placeholders})
322
+ `
323
+ )
324
+ .run(now, now, project.id, ...activeChangeIds)
325
+ } else {
326
+ sqlite
327
+ .prepare(
328
+ `
329
+ UPDATE changes SET status = 'archived', archived_at = COALESCE(archived_at, ?), updated_at = ?
330
+ WHERE project_id = ? AND status = 'active'
331
+ `
332
+ )
333
+ .run(now, now, project.id)
334
+ }
335
+ } else {
336
+ // OpenSpec scanning disabled - ensure changes are archived if they exist
337
+ if (activeChangeIds.length > 0) {
338
+ const placeholders = activeChangeIds.map(() => '?').join(',')
339
+ sqlite
340
+ .prepare(
341
+ `
342
+ UPDATE changes SET status = 'archived', archived_at = COALESCE(archived_at, ?), updated_at = ?
343
+ WHERE project_id = ? AND status = 'active' AND id NOT IN (${placeholders})
344
+ `
345
+ )
346
+ .run(now, now, project.id, ...activeChangeIds)
347
+ } else {
348
+ sqlite
349
+ .prepare(
350
+ `
351
+ UPDATE changes SET status = 'archived', archived_at = COALESCE(archived_at, ?), updated_at = ?
352
+ WHERE project_id = ? AND status = 'active'
353
+ `
354
+ )
355
+ .run(now, now, project.id)
356
+ }
357
+ }
358
+
359
+ // Get MoAI SPEC tag stats (SPECs already scanned above)
360
+ let moaiTagsTotal = 0
361
+ let moaiTagsCompleted = 0
362
+
363
+ try {
364
+ const tagStats = await countMoaiTags(project.path)
365
+ moaiTagsTotal = tagStats.total
366
+ moaiTagsCompleted = tagStats.completed
367
+ } catch {
368
+ // Tag counting failed, continue
369
+ }
370
+
371
+ console.log(
372
+ `[Project] Activated local "${project.name}" (${activeChangeIds.length} active items, tags: ${moaiTagsCompleted}/${moaiTagsTotal})`
373
+ )
374
+
375
+ // Stop any remote watcher and start local watcher
376
+ stopRemoteWatcher(project.id)
377
+ startTasksWatcher(project.path, project.id)
378
+ }
379
+
380
+ async function syncRemoteProjectChanges(project: {
381
+ id: string
382
+ name: string
383
+ path: string
384
+ remote?: { type: string; serverId: string; host: string; user: string }
385
+ }) {
386
+ if (!project.remote) return
387
+
388
+ const plugin = await getRemotePlugin()
389
+ if (!plugin) {
390
+ console.warn('[Sync Remote] Remote plugin not installed, skipping remote sync')
391
+ return
392
+ }
393
+
394
+ const server = await plugin.getRemoteServerById(project.remote.serverId)
395
+ if (!server) {
396
+ console.error(`[Sync Remote] Server not found: ${project.remote.serverId}`)
397
+ return
398
+ }
399
+
400
+ initDb(project.path)
401
+
402
+ const sqlite = getSqlite()
403
+ const now = Date.now()
404
+ const activeChangeIds: string[] = []
405
+
406
+ // 1. Scan remote MoAI SPECs FIRST and add to activeChangeIds
407
+ try {
408
+ const moaiSpecs = await scanRemoteMoaiSpecs(project.path, server, plugin)
409
+ const moaiSpecIds = moaiSpecs
410
+ .filter(spec => spec.status !== 'archived')
411
+ .map(spec => spec.id)
412
+ activeChangeIds.push(...moaiSpecIds)
413
+
414
+ if (moaiSpecIds.length > 0) {
415
+ console.log(`[Sync Remote] Found ${moaiSpecIds.length} MoAI SPECs: ${moaiSpecIds.join(', ')}`)
416
+
417
+ // Map MoAI SPEC status to changes table status
418
+ // MoAI: draft, active, complete, archived
419
+ // Changes: active, completed, archived
420
+ const mapSpecStatus = (specStatus: string): 'active' | 'completed' | 'archived' => {
421
+ switch (specStatus) {
422
+ case 'complete':
423
+ case 'completed':
424
+ return 'completed'
425
+ case 'archived':
426
+ return 'archived'
427
+ default:
428
+ return 'active' // draft, active, and any other status maps to active
429
+ }
430
+ }
431
+
432
+ // Upsert MoAI SPECs into changes table with actual status
433
+ const upsertMoaiStmt = sqlite.prepare(`
434
+ INSERT INTO changes (id, project_id, title, spec_path, status, current_stage, progress, created_at, updated_at)
435
+ VALUES (?, ?, ?, ?, ?, 'spec', ?, ?, ?)
436
+ ON CONFLICT(id, project_id) DO UPDATE SET
437
+ title = excluded.title,
438
+ spec_path = excluded.spec_path,
439
+ status = excluded.status,
440
+ progress = excluded.progress,
441
+ updated_at = excluded.updated_at
442
+ `)
443
+
444
+ for (const spec of moaiSpecs) {
445
+ if (spec.status === 'archived') continue
446
+ const progress = spec.tagCount > 0 ? Math.round((spec.completedTags / spec.tagCount) * 100) : 0
447
+ const specPath = `.moai/specs/${spec.id}/spec.md`
448
+ const dbStatus = mapSpecStatus(spec.status)
449
+ upsertMoaiStmt.run(spec.id, project.id, spec.title, specPath, dbStatus, progress, now, now)
450
+ }
451
+ }
452
+ } catch (err) {
453
+ console.warn('[Sync Remote] Failed to scan remote MoAI SPECs:', err)
454
+ }
455
+
456
+ // 2. Scan OpenSpec changes
457
+ const openspecDir = `${project.path}/openspec/changes`
458
+ let listing
459
+ try {
460
+ listing = await plugin.listDirectory(server, openspecDir)
461
+ } catch (err) {
462
+ console.warn(`[Sync Remote] Cannot list ${openspecDir}:`, err)
463
+ // Continue even if openspec/changes doesn't exist - we may have MoAI SPECs
464
+ listing = { entries: [] }
465
+ }
466
+
467
+ const { readRemoteFile, executeCommand } = plugin
468
+
469
+ // DB에서 현재 저장된 상태 조회 (Incremental Sync를 위해)
470
+ const existingChanges = sqlite
471
+ .prepare('SELECT id, updated_at FROM changes WHERE project_id = ?')
472
+ .all(project.id) as { id: string; updated_at: number }[]
473
+
474
+ const existingMap = new Map(existingChanges.map(c => [c.id, c.updated_at]))
475
+ const fileMtimes = new Map<string, number>()
476
+
477
+ // 원격 파일 변경 시간 일괄 조회 (최적화)
478
+ // Linux start -c "%Y", macOS/BSD stat -f "%m" 호환성 이슈가 있으므로
479
+ // 가장 호환성 높은 perl이나 python, 혹은 ls --full-time 등을 고려해야 하나
480
+ // 일단 Linux 가정 stat -c "%Y" 시도 후 실패 시 Full Scan
481
+ try {
482
+ // openspecDir is absolute path usually.
483
+ // Find all proposal.md files and print "dirName/proposal.md mtimeSeconds"
484
+ // Note: We need the changeId (parent dir name).
485
+ // Output format: path mtime
486
+ const cmd = `find "${openspecDir}" -maxdepth 2 -name "proposal.md" -exec stat -c "%n %Y" {} + 2>/dev/null`
487
+ const { stdout } = await executeCommand(server, cmd)
488
+ if (stdout) {
489
+ for (const line of stdout.split('\n')) {
490
+ const parts = line.trim().split(' ')
491
+ if (parts.length >= 2) {
492
+ const mtime = parseInt(parts.pop() || '0', 10) * 1000 // ms 단위로 변환
493
+ const path = parts.join(' ') // path may contain spaces
494
+ // Extract changeId from path: .../changes/<changeId>/proposal.md
495
+ const match = path.match(/changes\/([^/]+)\/proposal\.md$/)
496
+ if (match) {
497
+ fileMtimes.set(match[1], mtime)
498
+ }
499
+ }
500
+ }
501
+ }
502
+ } catch (e) {
503
+ console.warn('[Sync Remote] Bulk stat failed, falling back to full sync', e)
504
+ }
505
+
506
+ // 병렬 처리로 변경
507
+ const changePromises = listing.entries.map(async (entry) => {
508
+ if (entry.type !== 'directory' && entry.type !== 'd' && entry.type !== 'Directory') return null
509
+ if (entry.name === 'archive') return null
510
+
511
+ const changeId = entry.name
512
+ activeChangeIds.push(changeId)
513
+
514
+ // Tasks 동기화는 원격 프로젝트에서 너무 느리므로 비활성화
515
+ // 원격 프로젝트는 Watcher가 없으므로 Tasks는 수동 새로고침 또는 all-data API를 통해 조회
516
+ // await syncRemoteChangeTasksForProject(changeId, project.path, server, project.id).catch(e => console.error(`Remote task sync failed for ${changeId}`, e))
517
+
518
+ // Incremental Sync Check
519
+ const lastModified = fileMtimes.get(changeId)
520
+ const storedUpdated = existingMap.get(changeId)
521
+
522
+ // DB에 있고, 원격 파일 시간이 확인되었으며, DB 시간보다 이전이거나 같으면 스킵
523
+ // 단, storedUpdated가 null이 아니고 lastModified가 존재할 때만
524
+ let skipRead = false
525
+ if (storedUpdated && lastModified && lastModified <= storedUpdated) {
526
+ skipRead = true
527
+ }
528
+
529
+ let title = changeId
530
+ const specPath = `openspec/changes/${changeId}/proposal.md`
531
+
532
+ if (skipRead) {
533
+ // 내용 변경 없음 - DB의 기존 title 유지 (DB 쿼리 필요하지만 위에서 이미 가져옴 - title은 안가져왔네..)
534
+ // title 업데이트를 건너뛰려면 upsert 쿼리를 수정하거나,
535
+ // 여기서 그냥 'SAME' 마커를 리턴하고 DB 업데이트 로직에서 처리
536
+ return { changeId, title: null, specPath, status: 'skipped' }
537
+ }
538
+
539
+ try {
540
+ const proposalPath = `${openspecDir}/${changeId}/proposal.md`
541
+ const proposalContent = await readRemoteFile(server, proposalPath)
542
+ const titleMatch = proposalContent.match(/^#\s+(?:Change:\s+)?(.+)$/m)
543
+ if (titleMatch) {
544
+ title = titleMatch[1].trim()
545
+ }
546
+ } catch {
547
+ // proposal.md not found
548
+ }
549
+
550
+ return { changeId, title, specPath, status: 'updated' }
551
+ })
552
+
553
+ // 모든 변경사항 처리 대기 (병렬)
554
+ const results = await Promise.all(changePromises)
555
+
556
+ // DB 트랜잭션 (순차 처리)
557
+ const upsertStmt = sqlite.prepare(`
558
+ INSERT INTO changes (id, project_id, title, spec_path, status, current_stage, progress, created_at, updated_at)
559
+ VALUES (?, ?, ?, ?, 'active', 'spec', 0, ?, ?)
560
+ ON CONFLICT(id, project_id) DO UPDATE SET
561
+ title = excluded.title,
562
+ spec_path = excluded.spec_path,
563
+ status = 'active',
564
+ updated_at = excluded.updated_at
565
+ `)
566
+
567
+ // 유효한 결과만 DB 반영
568
+ for (const item of results) {
569
+ if (!item) continue
570
+
571
+ if (item.status === 'skipped') {
572
+ // 변경 없음 -> UpdatedAt만 갱신하거나, 그냥 둠 (Active 상태 유지를 위해 status='active' 업데이트 필요할 수 있음)
573
+ // 여기서는 activeChangeIds에 포함되어 있으므로 나중에 Archive 처리되지 않음.
574
+ // 단, 명시적으로 updated_at을 갱신하지 않으면 '최신 동기화' 시점을 알 수 없으므로
575
+ // 필요하다면 status='active'만 업데이트하는 쿼리 실행
576
+ // 성능을 위해 생략 가능하나, 안전을 위해 status만 active로 재설정
577
+ sqlite.prepare("UPDATE changes SET status = 'active' WHERE id = ? AND project_id = ?").run(item.changeId, project.id)
578
+ } else {
579
+ upsertStmt.run(item.changeId, project.id, item.title, item.specPath, now, now)
580
+ }
581
+ }
582
+
583
+ // Add OpenSpec change IDs to activeChangeIds (MoAI SPEC IDs are already in the array)
584
+ // Note: Don't clear the array - MoAI SPEC IDs were added earlier
585
+ results.forEach(r => { if(r && !activeChangeIds.includes(r.changeId)) activeChangeIds.push(r.changeId) })
586
+
587
+ if (activeChangeIds.length > 0) {
588
+ const placeholders = activeChangeIds.map(() => '?').join(',')
589
+ sqlite
590
+ .prepare(
591
+ `
592
+ UPDATE changes SET status = 'archived', archived_at = COALESCE(archived_at, ?), updated_at = ?
593
+ WHERE project_id = ? AND status = 'active' AND id NOT IN (${placeholders})
594
+ `
595
+ )
596
+ .run(now, now, project.id, ...activeChangeIds)
597
+ } else {
598
+ sqlite
599
+ .prepare(
600
+ `
601
+ UPDATE changes SET status = 'archived', archived_at = COALESCE(archived_at, ?), updated_at = ?
602
+ WHERE project_id = ? AND status = 'active'
603
+ `
604
+ )
605
+ .run(now, now, project.id)
606
+ }
607
+
608
+ console.log(
609
+ `[Project] Activated remote "${project.name}" via SSH (${activeChangeIds.length} changes)`
610
+ )
611
+
612
+ // Stop local watcher and start remote watcher for task file changes
613
+ stopTasksWatcher()
614
+ startRemoteWatcher(project, project.remote.serverId)
615
+ }
616
+
617
+ // PUT /:id/path - Update project path
618
+ projectsRouter.put('/:id/path', async (req, res) => {
619
+ try {
620
+ const projectId = req.params.id
621
+ const { path: newPath } = req.body
622
+
623
+ if (!newPath) {
624
+ return res.status(400).json({ success: false, error: 'Path is required' })
625
+ }
626
+
627
+ // Check for spec directories in new path - at least one must exist
628
+ const openspecPath = join(newPath, 'openspec')
629
+ const moaiSpecsPath = join(newPath, '.moai', 'specs')
630
+
631
+ const hasOpenspec = await access(openspecPath).then(
632
+ () => true,
633
+ () => false
634
+ )
635
+ const hasMoaiSpecs = await access(moaiSpecsPath).then(
636
+ () => true,
637
+ () => false
638
+ )
639
+
640
+ // Require at least one spec format to exist
641
+ if (!hasOpenspec && !hasMoaiSpecs) {
642
+ return res.status(400).json({
643
+ success: false,
644
+ error: 'Project must contain either MoAI SPEC (.moai/specs/) or OpenSpec (openspec/) directory',
645
+ })
646
+ }
647
+
648
+ const project = await updateProjectPath(projectId, newPath)
649
+
650
+ res.json({ success: true, data: { project } })
651
+ } catch (error) {
652
+ console.error('Error updating project path:', error)
653
+ res.status(500).json({
654
+ success: false,
655
+ error: error instanceof Error ? error.message : 'Failed to update project path',
656
+ })
657
+ }
658
+ })
659
+
660
+ // GET /:id/changes - Get changes for a project
661
+ projectsRouter.get('/:id/changes', async (req, res) => {
662
+ try {
663
+ const projectId = req.params.id
664
+ const sqlite = getSqlite()
665
+
666
+ const changes = sqlite
667
+ .prepare(
668
+ `
669
+ SELECT id, title, status, current_stage, progress, spec_path, created_at, updated_at
670
+ FROM changes
671
+ WHERE project_id = ?
672
+ ORDER BY updated_at DESC
673
+ `
674
+ )
675
+ .all(projectId)
676
+
677
+ res.json({ success: true, changes })
678
+ } catch (error) {
679
+ console.error('Error fetching project changes:', error)
680
+ res.status(500).json({
681
+ success: false,
682
+ error: error instanceof Error ? error.message : 'Failed to fetch changes',
683
+ })
684
+ }
685
+ })
686
+
687
+ // PUT /:id/name - Update project name
688
+ projectsRouter.put('/:id/name', async (req, res) => {
689
+ try {
690
+ const projectId = req.params.id
691
+ const { name: newName } = req.body
692
+
693
+ if (!newName || typeof newName !== 'string') {
694
+ return res.status(400).json({ success: false, error: 'Name is required' })
695
+ }
696
+
697
+ const trimmedName = newName.trim()
698
+ if (trimmedName.length === 0) {
699
+ return res.status(400).json({ success: false, error: 'Name cannot be empty' })
700
+ }
701
+
702
+ const project = await updateProjectName(projectId, trimmedName)
703
+ res.json({ success: true, data: { project } })
704
+ } catch (error) {
705
+ console.error('Error updating project name:', error)
706
+ res.status(500).json({
707
+ success: false,
708
+ error: error instanceof Error ? error.message : 'Failed to update project name',
709
+ })
710
+ }
711
+ })
712
+
713
+ // ==================== ALL PROJECTS DATA ====================
714
+
715
+ // Helper to parse affected specs from proposal content
716
+ function parseAffectedSpecs(proposalContent: string): string[] {
717
+ const specs: string[] = []
718
+
719
+ // Find ### Affected Specs section
720
+ const affectedSpecsMatch = proposalContent.match(
721
+ /###\s*Affected Specs\s*\n([\s\S]*?)(?=\n###|\n##|$)/i
722
+ )
723
+ if (!affectedSpecsMatch) return specs
724
+
725
+ const section = affectedSpecsMatch[1]
726
+ // Match patterns like: - **NEW**: `spec-name` or - **MODIFIED**: `spec-name`
727
+ const specMatches = section.matchAll(/`([^`]+)`/g)
728
+ for (const match of specMatches) {
729
+ specs.push(match[1])
730
+ }
731
+
732
+ return specs
733
+ }
734
+
735
+ // Helper to get changes for a specific project path (includes both OpenSpec and MoAI SPECs)
736
+ async function getChangesForProject(projectPath: string) {
737
+ const changes: Array<{
738
+ id: string
739
+ title: string
740
+ progress: number
741
+ totalTasks: number
742
+ completedTasks: number
743
+ relatedSpecs?: string[]
744
+ updatedAt: string | null
745
+ type?: 'openspec' | 'spec'
746
+ status?: 'active' | 'completed' | 'archived'
747
+ }> = []
748
+
749
+ // Get archived change IDs from DB to filter them out
750
+ const archivedChangeIds = new Set<string>()
751
+ try {
752
+ const projectId = projectPath.toLowerCase().replace(/[^a-z0-9]/g, '-')
753
+ const sqlite = getSqlite()
754
+ const archivedRows = sqlite
755
+ .prepare(
756
+ `
757
+ SELECT id FROM changes WHERE project_id = ? AND status = 'archived'
758
+ `
759
+ )
760
+ .all(projectId) as { id: string }[]
761
+ for (const row of archivedRows) {
762
+ archivedChangeIds.add(row.id)
763
+ }
764
+ } catch {
765
+ // DB not initialized yet, proceed without filtering
766
+ }
767
+
768
+ // 1. Scan MoAI SPECs from .moai/specs/
769
+ try {
770
+ const moaiSpecs = await scanMoaiSpecs(projectPath)
771
+ for (const spec of moaiSpecs) {
772
+ // Skip archived SPECs
773
+ if (archivedChangeIds.has(spec.id) || spec.status === 'archived') continue
774
+
775
+ const progress = spec.tagCount > 0 ? Math.round((spec.completedTags / spec.tagCount) * 100) : 0
776
+
777
+ // Get updatedAt from git or file system
778
+ let updatedAt: string | null = null
779
+ try {
780
+ const gitResult = await execAsync(`git log -1 --format="%aI" -- ".moai/specs/${spec.id}"`, {
781
+ cwd: projectPath,
782
+ })
783
+ if (gitResult.stdout.trim()) {
784
+ updatedAt = gitResult.stdout.trim()
785
+ }
786
+ } catch {
787
+ // Fallback to current time
788
+ updatedAt = new Date().toISOString()
789
+ }
790
+
791
+ // Map MoAI status to change status: complete -> completed, others -> active
792
+ const changeStatus = spec.status === 'complete' ? 'completed' : 'active'
793
+
794
+ changes.push({
795
+ id: spec.id,
796
+ title: spec.title,
797
+ progress,
798
+ totalTasks: spec.tagCount,
799
+ completedTasks: spec.completedTags,
800
+ updatedAt,
801
+ type: 'spec',
802
+ status: changeStatus,
803
+ })
804
+ }
805
+ } catch {
806
+ // .moai/specs/ scan failed, continue with OpenSpec
807
+ }
808
+
809
+ // 2. Scan OpenSpec changes from openspec/changes/ (only if enabled)
810
+ const config = await loadConfig()
811
+ const enableOpenSpec = config.specConfig?.enableOpenSpecScanning ?? true
812
+
813
+ if (!enableOpenSpec) {
814
+ return changes
815
+ }
816
+
817
+ const openspecDir = join(projectPath, 'openspec', 'changes')
818
+
819
+ let entries
820
+ try {
821
+ entries = await readdir(openspecDir, { withFileTypes: true })
822
+ } catch {
823
+ // openspec/changes/ does not exist
824
+ return changes
825
+ }
826
+
827
+ // Filter valid entries first
828
+ const validEntries = entries.filter(
829
+ (entry) => entry.isDirectory() && entry.name !== 'archive' && !archivedChangeIds.has(entry.name)
830
+ )
831
+
832
+ // Process all changes in parallel for better performance
833
+ const changePromises = validEntries.map(async (entry) => {
834
+ const changeId = entry.name
835
+ const changeDir = join(openspecDir, changeId)
836
+
837
+ // Read proposal and tasks files in parallel
838
+ const [proposalResult, tasksResult, gitResult] = await Promise.allSettled([
839
+ // Read proposal.md
840
+ readFile(join(changeDir, 'proposal.md'), 'utf-8'),
841
+ // Read tasks.md
842
+ readFile(join(changeDir, 'tasks.md'), 'utf-8'),
843
+ // Get git log
844
+ execAsync(`git log -1 --format="%aI" -- "openspec/changes/${changeId}"`, {
845
+ cwd: projectPath,
846
+ }),
847
+ ])
848
+
849
+ // Parse proposal
850
+ let title = changeId
851
+ let relatedSpecs: string[] = []
852
+ if (proposalResult.status === 'fulfilled') {
853
+ const titleMatch = proposalResult.value.match(/^#\s+(?:Change:\s+)?(.+)$/m)
854
+ if (titleMatch) title = titleMatch[1].trim()
855
+ relatedSpecs = parseAffectedSpecs(proposalResult.value)
856
+ }
857
+
858
+ // Parse tasks
859
+ let totalTasks = 0
860
+ let completedTasks = 0
861
+ if (tasksResult.status === 'fulfilled') {
862
+ const parsed = parseTasksFile(changeId, tasksResult.value)
863
+ for (const group of parsed.groups) {
864
+ totalTasks += group.tasks.length
865
+ completedTasks += group.tasks.filter((t) => t.completed).length
866
+ }
867
+ }
868
+
869
+ // Get updatedAt
870
+ let updatedAt: string | null = null
871
+ if (gitResult.status === 'fulfilled' && gitResult.value.stdout.trim()) {
872
+ updatedAt = gitResult.value.stdout.trim()
873
+ } else {
874
+ try {
875
+ const stat = await import('fs/promises').then((fs) => fs.stat(join(changeDir, 'tasks.md')))
876
+ updatedAt = stat.mtime.toISOString()
877
+ } catch {
878
+ updatedAt = new Date().toISOString()
879
+ }
880
+ }
881
+
882
+ const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
883
+ return { id: changeId, title, progress, totalTasks, completedTasks, relatedSpecs, updatedAt, type: 'openspec' as const }
884
+ })
885
+
886
+ const results = await Promise.all(changePromises)
887
+ changes.push(...results)
888
+
889
+ return changes
890
+ }
891
+
892
+ // Helper to get specs for a specific project path
893
+ async function getSpecsForProject(projectPath: string) {
894
+ const specsDir = join(projectPath, 'openspec', 'specs')
895
+ const specs = []
896
+
897
+ let entries
898
+ try {
899
+ entries = await readdir(specsDir, { withFileTypes: true })
900
+ } catch {
901
+ return []
902
+ }
903
+
904
+ for (const entry of entries) {
905
+ if (!entry.isDirectory()) continue
906
+
907
+ const specId = entry.name
908
+ const specDir = join(specsDir, specId)
909
+
910
+ let title = specId
911
+ let requirementsCount = 0
912
+ try {
913
+ const specPath = join(specDir, 'spec.md')
914
+ const specContent = await readFile(specPath, 'utf-8')
915
+ const titleMatch = specContent.match(/^#\s+(.+)$/m)
916
+ if (titleMatch) {
917
+ title = titleMatch[1].trim()
918
+ }
919
+ const reqMatches = specContent.match(/^###\s+Requirement:/gm)
920
+ requirementsCount = reqMatches ? reqMatches.length : 0
921
+ } catch {
922
+ // spec.md not found
923
+ }
924
+
925
+ specs.push({ id: specId, title, requirementsCount })
926
+ }
927
+
928
+ return specs
929
+ }
930
+
931
+ // Helper to get changes for a remote project via SSH
932
+ async function getChangesForRemoteProject(
933
+ projectPath: string,
934
+ serverId: string,
935
+ projectId?: string
936
+ ) {
937
+ const plugin = await getRemotePlugin()
938
+ if (!plugin) return []
939
+
940
+ const server = await plugin.getRemoteServerById(serverId)
941
+ if (!server) return []
942
+
943
+ const { readRemoteFile, listDirectory, executeCommand } = plugin
944
+
945
+ const changes: Array<{
946
+ id: string
947
+ title: string
948
+ progress: number
949
+ totalTasks: number
950
+ completedTasks: number
951
+ relatedSpecs?: string[]
952
+ updatedAt: string | null
953
+ type?: 'openspec' | 'spec'
954
+ status?: 'active' | 'completed' | 'archived'
955
+ }> = []
956
+
957
+ // Get archived change IDs from DB to filter them out
958
+ const archivedChangeIds = new Set<string>()
959
+ try {
960
+ // projectId가 전달되면 사용, 아니면 경로에서 생성
961
+ const dbProjectId = projectId || projectPath.toLowerCase().replace(/[^a-z0-9]/g, '-')
962
+ const sqlite = getSqlite()
963
+ const archivedRows = sqlite
964
+ .prepare(
965
+ `
966
+ SELECT id FROM changes WHERE project_id = ? AND status = 'archived'
967
+ `
968
+ )
969
+ .all(dbProjectId) as { id: string }[]
970
+ for (const row of archivedRows) {
971
+ archivedChangeIds.add(row.id)
972
+ }
973
+ } catch {
974
+ // DB not initialized yet
975
+ }
976
+
977
+ // 1. Scan remote MoAI SPECs from .moai/specs/
978
+ try {
979
+ const moaiSpecs = await scanRemoteMoaiSpecs(projectPath, server, plugin)
980
+ for (const spec of moaiSpecs) {
981
+ // Skip archived SPECs
982
+ if (archivedChangeIds.has(spec.id) || spec.status === 'archived') continue
983
+
984
+ const progress = spec.tagCount > 0
985
+ ? Math.round((spec.completedTags / spec.tagCount) * 100)
986
+ : 0
987
+
988
+ // Map MoAI status to change status: complete -> completed, others -> active
989
+ const changeStatus = spec.status === 'complete' ? 'completed' : 'active'
990
+
991
+ changes.push({
992
+ id: spec.id,
993
+ title: spec.title,
994
+ progress,
995
+ totalTasks: spec.tagCount,
996
+ completedTasks: spec.completedTags,
997
+ updatedAt: new Date().toISOString(),
998
+ type: 'spec',
999
+ status: changeStatus,
1000
+ })
1001
+ }
1002
+ } catch {
1003
+ // .moai/specs/ scan failed, continue with OpenSpec
1004
+ }
1005
+
1006
+ // 2. Scan OpenSpec changes from openspec/changes/
1007
+ const openspecDir = `${projectPath}/openspec/changes`
1008
+ let listing
1009
+ try {
1010
+ listing = await listDirectory(server, openspecDir)
1011
+ } catch {
1012
+ // openspec/changes/ might not exist, return what we have (MoAI SPECs)
1013
+ return changes
1014
+ }
1015
+
1016
+ // 디버깅 로그 추가
1017
+ console.log(`[Remote] Listing for ${openspecDir}:`, listing.entries.map(e => ({ name: e.name, type: e.type })))
1018
+
1019
+ // 디렉토리만 필터링 (archive 제외) - 타입 체크 완화
1020
+ const validEntries = listing.entries.filter(
1021
+ (entry) =>
1022
+ (entry.type === 'directory' || entry.type === 'd' || entry.type === 'Directory') &&
1023
+ entry.name !== 'archive' &&
1024
+ !archivedChangeIds.has(entry.name)
1025
+ )
1026
+
1027
+ // 병렬로 각 OpenSpec change 처리
1028
+ const openspecChanges = await Promise.all(
1029
+ validEntries.map(async (entry) => {
1030
+ const changeId = entry.name
1031
+ const changeDir = `${openspecDir}/${changeId}`
1032
+
1033
+ // 원격에서 proposal.md, tasks.md 읽기 및 git log 실행
1034
+ const [proposalResult, tasksResult, gitResult] = await Promise.allSettled([
1035
+ readRemoteFile(server, `${changeDir}/proposal.md`),
1036
+ readRemoteFile(server, `${changeDir}/tasks.md`),
1037
+ executeCommand(server, `git log -1 --format="%aI" -- "openspec/changes/${changeId}"`, {
1038
+ cwd: projectPath,
1039
+ }),
1040
+ ])
1041
+
1042
+ // Parse proposal
1043
+ let title = changeId
1044
+ let relatedSpecs: string[] = []
1045
+ if (proposalResult.status === 'fulfilled') {
1046
+ const titleMatch = proposalResult.value.match(/^#\s+(?:Change:\s+)?(.+)$/m)
1047
+ if (titleMatch) title = titleMatch[1].trim()
1048
+ relatedSpecs = parseAffectedSpecs(proposalResult.value)
1049
+ }
1050
+
1051
+ // Parse tasks
1052
+ let totalTasks = 0
1053
+ let completedTasks = 0
1054
+ if (tasksResult.status === 'fulfilled') {
1055
+ const parsed = parseTasksFile(changeId, tasksResult.value)
1056
+ for (const group of parsed.groups) {
1057
+ totalTasks += group.tasks.length
1058
+ completedTasks += group.tasks.filter((t) => t.completed).length
1059
+ }
1060
+ }
1061
+
1062
+ const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
1063
+ const status = progress === 100 ? 'completed' : 'active'
1064
+
1065
+ // Get updatedAt from git log or file modifiedAt
1066
+ let updatedAt: string | null = null
1067
+ if (gitResult.status === 'fulfilled') {
1068
+ const stdout = gitResult.value.stdout.trim()
1069
+ if (stdout) {
1070
+ updatedAt = stdout
1071
+ } else {
1072
+ // git log 결과가 비어있으면 modifiedAt 사용
1073
+ updatedAt = entry.modifiedAt || new Date().toISOString()
1074
+ }
1075
+ } else {
1076
+ // git log 실패 시 modifiedAt 사용
1077
+ updatedAt = entry.modifiedAt || new Date().toISOString()
1078
+ }
1079
+
1080
+ return { id: changeId, title, progress, totalTasks, completedTasks, relatedSpecs, updatedAt, type: 'openspec' as const, status }
1081
+ })
1082
+ )
1083
+
1084
+ // Combine MoAI SPECs with OpenSpec changes (avoid duplicates)
1085
+ const existingIds = new Set(changes.map(c => c.id))
1086
+ for (const change of openspecChanges) {
1087
+ if (!existingIds.has(change.id)) {
1088
+ changes.push(change)
1089
+ }
1090
+ }
1091
+
1092
+ return changes
1093
+ }
1094
+
1095
+ // Helper to get specs for a remote project via SSH
1096
+ async function getSpecsForRemoteProject(projectPath: string, serverId: string) {
1097
+ const plugin = await getRemotePlugin()
1098
+ if (!plugin) return []
1099
+
1100
+ const server = await plugin.getRemoteServerById(serverId)
1101
+ if (!server) return []
1102
+
1103
+ const { readRemoteFile, listDirectory } = plugin
1104
+ const specsDir = `${projectPath}/openspec/specs`
1105
+ const specs = []
1106
+
1107
+ let listing
1108
+ try {
1109
+ listing = await listDirectory(server, specsDir)
1110
+ } catch {
1111
+ return []
1112
+ }
1113
+
1114
+ for (const entry of listing.entries) {
1115
+ if (entry.type !== 'directory' && entry.type !== 'd' && entry.type !== 'Directory') continue
1116
+
1117
+ const specId = entry.name
1118
+ let title = specId
1119
+ let requirementsCount = 0
1120
+
1121
+ try {
1122
+ const specPath = `${specsDir}/${specId}/spec.md`
1123
+ const specContent = await readRemoteFile(server, specPath)
1124
+ const titleMatch = specContent.match(/^#\s+(.+)$/m)
1125
+ if (titleMatch) {
1126
+ title = titleMatch[1].trim()
1127
+ }
1128
+ const reqMatches = specContent.match(/^###\s+Requirement:/gm)
1129
+ requirementsCount = reqMatches ? reqMatches.length : 0
1130
+ } catch {
1131
+ // spec.md not found
1132
+ }
1133
+
1134
+ specs.push({ id: specId, title, requirementsCount })
1135
+ }
1136
+
1137
+ return specs
1138
+ }
1139
+
1140
+ // 원격 프로젝트 데이터 캐시 (30초 TTL)
1141
+ const remoteDataCache = new Map<string, { data: { changes: unknown[], specs: unknown[] }, timestamp: number }>()
1142
+ const CACHE_TTL = 30000 // 30 seconds
1143
+
1144
+ // NOTE: /all-data route must be defined BEFORE /:id route for correct Express routing priority
1145
+ // More specific routes (static paths) must come before pattern routes (:/id)
1146
+
1147
+ // GET /all-data - Get all projects with their changes and specs
1148
+ projectsRouter.get('/all-data', async (_req, res) => {
1149
+ try {
1150
+ const config = await loadConfig()
1151
+
1152
+ const projectsData = await Promise.all(
1153
+ config.projects.map(async (project) => {
1154
+ let changes: unknown[] = []
1155
+ let specs: unknown[] = []
1156
+
1157
+ try {
1158
+ if (project.remote) {
1159
+ // 원격 프로젝트: 캐시 확인 후 SSH 조회
1160
+ const cached = remoteDataCache.get(project.id)
1161
+ const now = Date.now()
1162
+
1163
+ if (cached && (now - cached.timestamp) < CACHE_TTL) {
1164
+ // 캐시 유효 - 캐시된 데이터 사용
1165
+ changes = cached.data.changes
1166
+ specs = cached.data.specs
1167
+ } else {
1168
+ // 캐시 만료 또는 없음 - SSH로 조회
1169
+ const [remoteChanges, remoteSpecs] = await Promise.all([
1170
+ getChangesForRemoteProject(project.path, project.remote.serverId, project.id),
1171
+ getSpecsForRemoteProject(project.path, project.remote.serverId)
1172
+ ])
1173
+ changes = remoteChanges
1174
+ specs = remoteSpecs
1175
+
1176
+ // 캐시 업데이트
1177
+ remoteDataCache.set(project.id, {
1178
+ data: { changes, specs },
1179
+ timestamp: now
1180
+ })
1181
+ }
1182
+ } else {
1183
+ // 로컬 프로젝트: 파일시스템에서 조회
1184
+ const [localChanges, localSpecs] = await Promise.all([
1185
+ getChangesForProject(project.path),
1186
+ getSpecsForProject(project.path)
1187
+ ])
1188
+ changes = localChanges
1189
+ specs = localSpecs
1190
+ }
1191
+ } catch (err) {
1192
+ console.error(`Error loading data for project ${project.name}:`, err)
1193
+ }
1194
+
1195
+ return {
1196
+ ...project,
1197
+ changes,
1198
+ specs,
1199
+ }
1200
+ })
1201
+ )
1202
+
1203
+ res.json({
1204
+ success: true,
1205
+ data: {
1206
+ projects: projectsData,
1207
+ activeProjectId: config.activeProjectId,
1208
+ },
1209
+ })
1210
+ } catch (error) {
1211
+ console.error('Error getting all projects data:', error)
1212
+ res.status(500).json({ success: false, error: 'Failed to get projects data' })
1213
+ }
1214
+ })
1215
+
1216
+ // GET /:id - Get project info with MoAI SPEC stats (defined AFTER /all-data for routing priority)
1217
+ projectsRouter.get('/:id', async (req, res) => {
1218
+ try {
1219
+ const projectId = req.params.id
1220
+ const config = await loadConfig()
1221
+ const project = config.projects.find((p) => p.id === projectId)
1222
+
1223
+ if (!project) {
1224
+ return res.status(404).json({ success: false, error: 'Project not found' })
1225
+ }
1226
+
1227
+ // Get MoAI SPEC stats
1228
+ const stats = {
1229
+ openspecChangeCount: 0,
1230
+ moaiSpecCount: 0,
1231
+ moaiTagsTotal: 0,
1232
+ moaiTagsCompleted: 0,
1233
+ }
1234
+
1235
+ try {
1236
+ // Count OpenSpec changes from DB
1237
+ initDb()
1238
+ const sqlite = getSqlite()
1239
+ const changeCountResult = sqlite
1240
+ .prepare('SELECT COUNT(*) as count FROM changes WHERE project_id = ? AND status = "active"')
1241
+ .get(projectId) as { count: number } | undefined
1242
+ stats.openspecChangeCount = changeCountResult?.count || 0
1243
+
1244
+ // Scan MoAI SPECs (local or remote)
1245
+ let moaiSpecs: Awaited<ReturnType<typeof scanMoaiSpecs>> = []
1246
+ let tagStats = { total: 0, completed: 0 }
1247
+
1248
+ if (project.remote) {
1249
+ // Remote SSH project
1250
+ const plugin = await getRemotePlugin()
1251
+ if (plugin) {
1252
+ const server = await plugin.getRemoteServerById(project.remote.serverId)
1253
+ if (server) {
1254
+ moaiSpecs = await scanRemoteMoaiSpecs(project.path, server, plugin)
1255
+ tagStats = await countRemoteMoaiTags(project.path, server, plugin)
1256
+ }
1257
+ }
1258
+ } else {
1259
+ // Local project
1260
+ moaiSpecs = await scanMoaiSpecs(project.path)
1261
+ tagStats = await countMoaiTags(project.path)
1262
+ }
1263
+
1264
+ stats.moaiSpecCount = moaiSpecs.length
1265
+ stats.moaiTagsTotal = tagStats.total
1266
+ stats.moaiTagsCompleted = tagStats.completed
1267
+ } catch (err) {
1268
+ console.warn(`Failed to calculate stats for project ${projectId}:`, err)
1269
+ }
1270
+
1271
+ res.json({
1272
+ success: true,
1273
+ data: {
1274
+ project,
1275
+ stats,
1276
+ },
1277
+ })
1278
+ } catch (error) {
1279
+ console.error('Error getting project:', error)
1280
+ res.status(500).json({ success: false, error: 'Failed to get project' })
1281
+ }
1282
+ })