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.
- package/.claude-flow/metrics/agent-metrics.json +1 -0
- package/.claude-flow/metrics/performance.json +87 -0
- package/.claude-flow/metrics/system-metrics.json +4370 -0
- package/.claude-flow/metrics/task-metrics.json +10 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +17 -0
- package/.gitleaks.toml +69 -0
- package/.hive-mind/config/queens.json +59 -0
- package/.hive-mind/config/workers.json +72 -0
- package/.hive-mind/config.json +111 -0
- package/.hive-mind/hive.db +0 -0
- package/.hive-mind/hive.db-shm +0 -0
- package/.hive-mind/hive.db-wal +0 -0
- package/.leann/indexes/zyflow/documents.ids.txt +2078 -0
- package/.leann/indexes/zyflow/documents.index +0 -0
- package/.leann/indexes/zyflow/documents.leann.meta.json +25 -0
- package/.leann/indexes/zyflow/documents.leann.passages.idx +0 -0
- package/.leann/indexes/zyflow/documents.leann.passages.jsonl +2078 -0
- package/.mcp.json +41 -0
- package/.moai-backups/20260126_231508/.mcp.json +11 -0
- package/.moai-backups/20260126_231508/backup_metadata.json +34 -0
- package/.moai-backups/20260129_145438/.mcp.json +41 -0
- package/.moai-backups/20260129_145438/backup_metadata.json +53 -0
- package/.moai-backups/20260129_145504/.mcp.json +41 -0
- package/.moai-backups/20260129_145504/backup_metadata.json +20 -0
- package/.moai-backups/20260201_140004/.mcp.json +41 -0
- package/.moai-backups/20260201_140004/backup_metadata.json +51 -0
- package/.moai-backups/backup/.mcp.json +12 -0
- package/.moai-backups/settings-backup/settings.local.json +61 -0
- package/.pre-commit-config.yaml +74 -0
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/.scannerwork/.sonar_lock +0 -0
- package/.scannerwork/report-task.txt +6 -0
- package/.serena/project.yml +105 -0
- package/.shadcn-admin-ref/.env.example +1 -0
- package/.shadcn-admin-ref/.prettierignore +18 -0
- package/.shadcn-admin-ref/.prettierrc +50 -0
- package/.shadcn-admin-ref/LICENSE +21 -0
- package/.shadcn-admin-ref/components.json +21 -0
- package/.shadcn-admin-ref/cz.yaml +7 -0
- package/.shadcn-admin-ref/eslint.config.js +59 -0
- package/.shadcn-admin-ref/index.html +80 -0
- package/.shadcn-admin-ref/knip.config.ts +8 -0
- package/.shadcn-admin-ref/netlify.toml +4 -0
- package/.shadcn-admin-ref/package.json +83 -0
- package/.shadcn-admin-ref/public/images/favicon.png +0 -0
- package/.shadcn-admin-ref/public/images/favicon.svg +4 -0
- package/.shadcn-admin-ref/public/images/favicon_light.png +0 -0
- package/.shadcn-admin-ref/public/images/favicon_light.svg +1 -0
- package/.shadcn-admin-ref/public/images/shadcn-admin.png +0 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-discord.tsx +28 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-docker.tsx +33 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-facebook.tsx +25 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-figma.tsx +27 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-github.tsx +25 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-gitlab.tsx +25 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-gmail.tsx +28 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-medium.tsx +30 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-notion.tsx +28 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-skype.tsx +26 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-slack.tsx +28 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-stripe.tsx +25 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-telegram.tsx +25 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-trello.tsx +27 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-whatsapp.tsx +26 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/icon-zoom.tsx +26 -0
- package/.shadcn-admin-ref/src/assets/brand-icons/index.ts +16 -0
- package/.shadcn-admin-ref/src/assets/clerk-full-logo.tsx +41 -0
- package/.shadcn-admin-ref/src/assets/clerk-logo.tsx +23 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-dir.tsx +110 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-layout-compact.tsx +131 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-layout-default.tsx +124 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-layout-full.tsx +100 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-sidebar-floating.tsx +82 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-sidebar-inset.tsx +58 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-sidebar-sidebar.tsx +53 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-theme-dark.tsx +79 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-theme-light.tsx +78 -0
- package/.shadcn-admin-ref/src/assets/custom/icon-theme-system.tsx +116 -0
- package/.shadcn-admin-ref/src/assets/logo.tsx +24 -0
- package/.shadcn-admin-ref/src/components/coming-soon.tsx +16 -0
- package/.shadcn-admin-ref/src/components/command-menu.tsx +91 -0
- package/.shadcn-admin-ref/src/components/config-drawer.tsx +354 -0
- package/.shadcn-admin-ref/src/components/confirm-dialog.tsx +67 -0
- package/.shadcn-admin-ref/src/components/data-table/bulk-actions.tsx +213 -0
- package/.shadcn-admin-ref/src/components/data-table/column-header.tsx +74 -0
- package/.shadcn-admin-ref/src/components/data-table/faceted-filter.tsx +146 -0
- package/.shadcn-admin-ref/src/components/data-table/index.ts +6 -0
- package/.shadcn-admin-ref/src/components/data-table/pagination.tsx +130 -0
- package/.shadcn-admin-ref/src/components/data-table/toolbar.tsx +85 -0
- package/.shadcn-admin-ref/src/components/data-table/view-options.tsx +56 -0
- package/.shadcn-admin-ref/src/components/date-picker.tsx +51 -0
- package/.shadcn-admin-ref/src/components/layout/app-sidebar.tsx +37 -0
- package/.shadcn-admin-ref/src/components/layout/app-title.tsx +64 -0
- package/.shadcn-admin-ref/src/components/layout/authenticated-layout.tsx +42 -0
- package/.shadcn-admin-ref/src/components/layout/data/sidebar-data.ts +205 -0
- package/.shadcn-admin-ref/src/components/layout/header.tsx +50 -0
- package/.shadcn-admin-ref/src/components/layout/main.tsx +27 -0
- package/.shadcn-admin-ref/src/components/layout/nav-group.tsx +185 -0
- package/.shadcn-admin-ref/src/components/layout/nav-user.tsx +124 -0
- package/.shadcn-admin-ref/src/components/layout/team-switcher.tsx +86 -0
- package/.shadcn-admin-ref/src/components/layout/top-nav.tsx +67 -0
- package/.shadcn-admin-ref/src/components/layout/types.ts +44 -0
- package/.shadcn-admin-ref/src/components/learn-more.tsx +44 -0
- package/.shadcn-admin-ref/src/components/long-text.tsx +84 -0
- package/.shadcn-admin-ref/src/components/navigation-progress.tsx +25 -0
- package/.shadcn-admin-ref/src/components/password-input.tsx +42 -0
- package/.shadcn-admin-ref/src/components/profile-dropdown.tsx +75 -0
- package/.shadcn-admin-ref/src/components/search.tsx +37 -0
- package/.shadcn-admin-ref/src/components/select-dropdown.tsx +62 -0
- package/.shadcn-admin-ref/src/components/sign-out-dialog.tsx +38 -0
- package/.shadcn-admin-ref/src/components/skip-to-main.tsx +10 -0
- package/.shadcn-admin-ref/src/components/theme-switch.tsx +58 -0
- package/.shadcn-admin-ref/src/components/ui/alert-dialog.tsx +154 -0
- package/.shadcn-admin-ref/src/components/ui/alert.tsx +65 -0
- package/.shadcn-admin-ref/src/components/ui/avatar.tsx +50 -0
- package/.shadcn-admin-ref/src/components/ui/badge.tsx +45 -0
- package/.shadcn-admin-ref/src/components/ui/button.tsx +58 -0
- package/.shadcn-admin-ref/src/components/ui/calendar.tsx +210 -0
- package/.shadcn-admin-ref/src/components/ui/card.tsx +91 -0
- package/.shadcn-admin-ref/src/components/ui/checkbox.tsx +29 -0
- package/.shadcn-admin-ref/src/components/ui/collapsible.tsx +31 -0
- package/.shadcn-admin-ref/src/components/ui/command.tsx +181 -0
- package/.shadcn-admin-ref/src/components/ui/dialog.tsx +142 -0
- package/.shadcn-admin-ref/src/components/ui/dropdown-menu.tsx +254 -0
- package/.shadcn-admin-ref/src/components/ui/form.tsx +164 -0
- package/.shadcn-admin-ref/src/components/ui/input-otp.tsx +74 -0
- package/.shadcn-admin-ref/src/components/ui/input.tsx +20 -0
- package/.shadcn-admin-ref/src/components/ui/label.tsx +23 -0
- package/.shadcn-admin-ref/src/components/ui/popover.tsx +45 -0
- package/.shadcn-admin-ref/src/components/ui/radio-group.tsx +42 -0
- package/.shadcn-admin-ref/src/components/ui/scroll-area.tsx +65 -0
- package/.shadcn-admin-ref/src/components/ui/select.tsx +182 -0
- package/.shadcn-admin-ref/src/components/ui/separator.tsx +25 -0
- package/.shadcn-admin-ref/src/components/ui/sheet.tsx +136 -0
- package/.shadcn-admin-ref/src/components/ui/sidebar.tsx +728 -0
- package/.shadcn-admin-ref/src/components/ui/skeleton.tsx +13 -0
- package/.shadcn-admin-ref/src/components/ui/sonner.tsx +21 -0
- package/.shadcn-admin-ref/src/components/ui/switch.tsx +28 -0
- package/.shadcn-admin-ref/src/components/ui/table.tsx +113 -0
- package/.shadcn-admin-ref/src/components/ui/tabs.tsx +63 -0
- package/.shadcn-admin-ref/src/components/ui/textarea.tsx +17 -0
- package/.shadcn-admin-ref/src/components/ui/tooltip.tsx +60 -0
- package/.shadcn-admin-ref/src/config/fonts.ts +19 -0
- package/.shadcn-admin-ref/src/context/direction-provider.tsx +61 -0
- package/.shadcn-admin-ref/src/context/font-provider.tsx +58 -0
- package/.shadcn-admin-ref/src/context/layout-provider.tsx +85 -0
- package/.shadcn-admin-ref/src/context/search-provider.tsx +46 -0
- package/.shadcn-admin-ref/src/context/theme-provider.tsx +110 -0
- package/.shadcn-admin-ref/src/features/apps/data/apps.tsx +110 -0
- package/.shadcn-admin-ref/src/features/apps/index.tsx +179 -0
- package/.shadcn-admin-ref/src/features/auth/auth-layout.tsx +19 -0
- package/.shadcn-admin-ref/src/features/auth/forgot-password/components/forgot-password-form.tsx +82 -0
- package/.shadcn-admin-ref/src/features/auth/forgot-password/index.tsx +44 -0
- package/.shadcn-admin-ref/src/features/auth/otp/components/otp-form.tsx +100 -0
- package/.shadcn-admin-ref/src/features/auth/otp/index.tsx +44 -0
- package/.shadcn-admin-ref/src/features/auth/sign-in/assets/dashboard-dark.png +0 -0
- package/.shadcn-admin-ref/src/features/auth/sign-in/assets/dashboard-light.png +0 -0
- package/.shadcn-admin-ref/src/features/auth/sign-in/components/user-auth-form.tsx +150 -0
- package/.shadcn-admin-ref/src/features/auth/sign-in/index.tsx +51 -0
- package/.shadcn-admin-ref/src/features/auth/sign-in/sign-in-2.tsx +69 -0
- package/.shadcn-admin-ref/src/features/auth/sign-up/components/sign-up-form.tsx +143 -0
- package/.shadcn-admin-ref/src/features/auth/sign-up/index.tsx +57 -0
- package/.shadcn-admin-ref/src/features/chats/components/new-chat.tsx +127 -0
- package/.shadcn-admin-ref/src/features/chats/data/chat-types.ts +4 -0
- package/.shadcn-admin-ref/src/features/chats/data/convo.json +309 -0
- package/.shadcn-admin-ref/src/features/chats/index.tsx +349 -0
- package/.shadcn-admin-ref/src/features/dashboard/components/analytics-chart.tsx +77 -0
- package/.shadcn-admin-ref/src/features/dashboard/components/analytics.tsx +189 -0
- package/.shadcn-admin-ref/src/features/dashboard/components/overview.tsx +82 -0
- package/.shadcn-admin-ref/src/features/dashboard/components/recent-sales.tsx +83 -0
- package/.shadcn-admin-ref/src/features/dashboard/index.tsx +220 -0
- package/.shadcn-admin-ref/src/features/errors/forbidden.tsx +25 -0
- package/.shadcn-admin-ref/src/features/errors/general-error.tsx +36 -0
- package/.shadcn-admin-ref/src/features/errors/maintenance-error.tsx +19 -0
- package/.shadcn-admin-ref/src/features/errors/not-found-error.tsx +25 -0
- package/.shadcn-admin-ref/src/features/errors/unauthorized-error.tsx +25 -0
- package/.shadcn-admin-ref/src/features/settings/account/account-form.tsx +173 -0
- package/.shadcn-admin-ref/src/features/settings/account/index.tsx +14 -0
- package/.shadcn-admin-ref/src/features/settings/appearance/appearance-form.tsx +162 -0
- package/.shadcn-admin-ref/src/features/settings/appearance/index.tsx +14 -0
- package/.shadcn-admin-ref/src/features/settings/components/content-section.tsx +22 -0
- package/.shadcn-admin-ref/src/features/settings/components/sidebar-nav.tsx +84 -0
- package/.shadcn-admin-ref/src/features/settings/display/display-form.tsx +121 -0
- package/.shadcn-admin-ref/src/features/settings/display/index.tsx +13 -0
- package/.shadcn-admin-ref/src/features/settings/index.tsx +74 -0
- package/.shadcn-admin-ref/src/features/settings/notifications/index.tsx +13 -0
- package/.shadcn-admin-ref/src/features/settings/notifications/notifications-form.tsx +220 -0
- package/.shadcn-admin-ref/src/features/settings/profile/index.tsx +13 -0
- package/.shadcn-admin-ref/src/features/settings/profile/profile-form.tsx +177 -0
- package/.shadcn-admin-ref/src/features/tasks/components/data-table-bulk-actions.tsx +193 -0
- package/.shadcn-admin-ref/src/features/tasks/components/data-table-row-actions.tsx +83 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-columns.tsx +123 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-dialogs.tsx +72 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-import-dialog.tsx +110 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-multi-delete-dialog.tsx +95 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-mutate-drawer.tsx +212 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-primary-buttons.tsx +21 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-provider.tsx +36 -0
- package/.shadcn-admin-ref/src/features/tasks/components/tasks-table.tsx +197 -0
- package/.shadcn-admin-ref/src/features/tasks/data/data.tsx +77 -0
- package/.shadcn-admin-ref/src/features/tasks/data/schema.ts +13 -0
- package/.shadcn-admin-ref/src/features/tasks/data/tasks.ts +29 -0
- package/.shadcn-admin-ref/src/features/tasks/index.tsx +41 -0
- package/.shadcn-admin-ref/src/features/users/components/data-table-bulk-actions.tsx +139 -0
- package/.shadcn-admin-ref/src/features/users/components/data-table-row-actions.tsx +63 -0
- package/.shadcn-admin-ref/src/features/users/components/users-action-dialog.tsx +326 -0
- package/.shadcn-admin-ref/src/features/users/components/users-columns.tsx +138 -0
- package/.shadcn-admin-ref/src/features/users/components/users-delete-dialog.tsx +81 -0
- package/.shadcn-admin-ref/src/features/users/components/users-dialogs.tsx +51 -0
- package/.shadcn-admin-ref/src/features/users/components/users-invite-dialog.tsx +150 -0
- package/.shadcn-admin-ref/src/features/users/components/users-multi-delete-dialog.tsx +95 -0
- package/.shadcn-admin-ref/src/features/users/components/users-primary-buttons.tsx +21 -0
- package/.shadcn-admin-ref/src/features/users/components/users-provider.tsx +36 -0
- package/.shadcn-admin-ref/src/features/users/components/users-table.tsx +194 -0
- package/.shadcn-admin-ref/src/features/users/data/data.ts +35 -0
- package/.shadcn-admin-ref/src/features/users/data/schema.ts +32 -0
- package/.shadcn-admin-ref/src/features/users/data/users.ts +33 -0
- package/.shadcn-admin-ref/src/features/users/index.tsx +47 -0
- package/.shadcn-admin-ref/src/hooks/use-dialog-state.tsx +18 -0
- package/.shadcn-admin-ref/src/hooks/use-mobile.tsx +19 -0
- package/.shadcn-admin-ref/src/hooks/use-table-url-state.ts +219 -0
- package/.shadcn-admin-ref/src/lib/cookies.ts +43 -0
- package/.shadcn-admin-ref/src/lib/handle-server-error.ts +24 -0
- package/.shadcn-admin-ref/src/lib/show-submitted-data.tsx +15 -0
- package/.shadcn-admin-ref/src/lib/utils.ts +60 -0
- package/.shadcn-admin-ref/src/main.tsx +107 -0
- package/.shadcn-admin-ref/src/routeTree.gen.ts +719 -0
- package/.shadcn-admin-ref/src/routes/(auth)/forgot-password.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(auth)/otp.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(auth)/sign-in-2.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(auth)/sign-in.tsx +12 -0
- package/.shadcn-admin-ref/src/routes/(auth)/sign-up.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(errors)/401.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(errors)/403.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(errors)/404.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(errors)/500.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/(errors)/503.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/__root.tsx +30 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/apps/index.tsx +17 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/chats/index.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/errors/$error.tsx +45 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/help-center/index.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/index.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/route.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/settings/account.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/settings/appearance.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/settings/display.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/settings/index.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/settings/notifications.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/settings/route.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/tasks/index.tsx +23 -0
- package/.shadcn-admin-ref/src/routes/_authenticated/users/index.tsx +32 -0
- package/.shadcn-admin-ref/src/routes/clerk/(auth)/route.tsx +60 -0
- package/.shadcn-admin-ref/src/routes/clerk/(auth)/sign-in.tsx +14 -0
- package/.shadcn-admin-ref/src/routes/clerk/(auth)/sign-up.tsx +9 -0
- package/.shadcn-admin-ref/src/routes/clerk/_authenticated/route.tsx +6 -0
- package/.shadcn-admin-ref/src/routes/clerk/_authenticated/user-management.tsx +184 -0
- package/.shadcn-admin-ref/src/routes/clerk/route.tsx +135 -0
- package/.shadcn-admin-ref/src/stores/auth-store.ts +53 -0
- package/.shadcn-admin-ref/src/styles/index.css +87 -0
- package/.shadcn-admin-ref/src/styles/theme.css +102 -0
- package/.shadcn-admin-ref/src/tanstack-table.d.ts +10 -0
- package/.shadcn-admin-ref/src/vite-env.d.ts +1 -0
- package/.swarm/memory.db +0 -0
- package/.swarm/memory.db-shm +0 -0
- package/.swarm/memory.db-wal +0 -0
- package/.zyflow/cli-settings.json +30 -0
- package/.zyflow/db.sqlite +0 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765491505852.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765491622627.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765491794652.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765491890392.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765494002879.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765494183887.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765494342052.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765494387244.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765494387245.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765494606176.json +10 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765495967542.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765495967629.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765497861143.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765497861870.json +20 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765498021377.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765498021660.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765503255525.json +13 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765503256018.json +13 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765504009102.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765504492051.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765504946437.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1633-1765504946640.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1634-1765505950215.json +16 -0
- package/.zyflow/logs/add-gitdiagram-integration/1634-1765505950948.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1635-1765505971712.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1635-1765505971976.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1636-1765505986208.json +18 -0
- package/.zyflow/logs/add-gitdiagram-integration/1636-1765505986620.json +16 -0
- package/.zyflow/logs/integrate-claude-flow/3580-1765996816612.json +10 -0
- package/.zyflow/logs/integrate-claude-flow/3580-1766014825819.json +10 -0
- package/.zyflow/logs/integrate-claude-flow/3580-1766015183794.json +12 -0
- package/.zyflow/logs/integrate-claude-flow/3580-1766015474608.json +12 -0
- package/.zyflow/logs/integrate-claude-flow/3581-1766016502824.json +63 -0
- package/.zyflow/logs/integrate-claude-flow/3581-1766016576008.json +60 -0
- package/.zyflow/logs/integrate-claude-flow/3582-1766022737754.json +110 -0
- package/.zyflow/logs/integrate-claude-flow/3582-1766022809327.json +135 -0
- package/.zyflow/sessions.json +242 -0
- package/.zyflow/settings.json +6 -0
- package/.zyflow/tasks.db +0 -0
- package/.zyflow/tasks.db-shm +0 -0
- package/.zyflow/tasks.db-wal +0 -0
- package/.zyflow/zyflow.sqlite +0 -0
- package/Dockerfile +82 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/claude-flow +34 -0
- package/components.json +21 -0
- package/config/ports.ts +28 -0
- package/docker-compose.yml +52 -0
- package/eslint.config.js +34 -0
- package/index.html +19 -0
- package/logs/mcp-error.log +55 -0
- package/logs/mcp-out.log +0 -0
- package/logs/pm2-error.log +0 -0
- package/logs/pm2-out.log +265 -0
- package/logs/py-error.log +22 -0
- package/logs/py-out.log +0 -0
- package/logs/server-error.log +11000 -0
- package/logs/server-out.log +8117 -0
- package/logs/vite-error.log +404 -0
- package/logs/vite-out.log +311 -0
- package/mcp-server/agent-tools.ts +375 -0
- package/mcp-server/cli-models.ts +193 -0
- package/mcp-server/context.ts +110 -0
- package/mcp-server/diagram-tools.ts +341 -0
- package/mcp-server/index.ts +2014 -0
- package/mcp-server/integration-tools.ts +909 -0
- package/mcp-server/moai-spec-tools.ts +416 -0
- package/mcp-server/parser.ts +422 -0
- package/mcp-server/post-task-runner.ts +253 -0
- package/mcp-server/post-task-types.ts +426 -0
- package/mcp-server/quarantine-manager.ts +479 -0
- package/mcp-server/report-generator.ts +386 -0
- package/mcp-server/task-tools.ts +619 -0
- package/mcp-server/trigger-config.ts +288 -0
- package/mcp-server/trigger-router.ts +305 -0
- package/mcp-server/triggers/event-listener.ts +331 -0
- package/mcp-server/triggers/git-hooks.ts +283 -0
- package/mcp-server/triggers/scheduler.ts +289 -0
- package/mcp-server/types.ts +55 -0
- package/memory/claude-flow@alpha-data.json +5 -0
- package/nginx/zyflow.conf +144 -0
- package/openspec/config.yaml +78 -0
- package/openspec-backup.tar.gz +0 -0
- package/package.json +154 -0
- package/packages/gitdiagram-core/.claude-flow/metrics/agent-metrics.json +1 -0
- package/packages/gitdiagram-core/.claude-flow/metrics/performance.json +87 -0
- package/packages/gitdiagram-core/.claude-flow/metrics/task-metrics.json +10 -0
- package/packages/gitdiagram-core/package.json +41 -0
- package/packages/gitdiagram-core/src/file-tree.ts +272 -0
- package/packages/gitdiagram-core/src/generator.ts +283 -0
- package/packages/gitdiagram-core/src/index.ts +78 -0
- package/packages/gitdiagram-core/src/llm-adapter.ts +235 -0
- package/packages/gitdiagram-core/src/mermaid-utils.ts +304 -0
- package/packages/gitdiagram-core/src/prompts.ts +281 -0
- package/packages/zyflow-parser/package.json +34 -0
- package/packages/zyflow-parser/src/index.ts +26 -0
- package/packages/zyflow-parser/src/moai-parser.ts +603 -0
- package/packages/zyflow-parser/src/moai-types.ts +110 -0
- package/packages/zyflow-remote-plugin/.claude-flow/metrics/agent-metrics.json +1 -0
- package/packages/zyflow-remote-plugin/.claude-flow/metrics/performance.json +87 -0
- package/packages/zyflow-remote-plugin/.claude-flow/metrics/task-metrics.json +10 -0
- package/packages/zyflow-remote-plugin/package.json +31 -0
- package/packages/zyflow-remote-plugin/src/index.ts +71 -0
- package/packages/zyflow-remote-plugin/src/remote-config.ts +232 -0
- package/packages/zyflow-remote-plugin/src/router.ts +535 -0
- package/packages/zyflow-remote-plugin/src/ssh-config-parser.ts +123 -0
- package/packages/zyflow-remote-plugin/src/ssh-manager.ts +598 -0
- package/packages/zyflow-remote-plugin/src/types.ts +149 -0
- package/plugin/manifest.json +26 -0
- package/plugin/package.json +13 -0
- package/public/favicon.svg +4 -0
- package/server/adk/agents/error-analyzer.ts +223 -0
- package/server/adk/agents/fix-generator.ts +187 -0
- package/server/adk/agents/pr-agent.ts +264 -0
- package/server/adk/agents/validator.ts +187 -0
- package/server/adk/config.ts +43 -0
- package/server/adk/index.ts +69 -0
- package/server/adk/integration.ts +297 -0
- package/server/adk/orchestrator.ts +405 -0
- package/server/adk/tools/build-tools.ts +290 -0
- package/server/adk/tools/file-tools.ts +351 -0
- package/server/adk/tools/git-tools.ts +280 -0
- package/server/adk/tools/github-tools.ts +249 -0
- package/server/agents/agent-monitor.ts +416 -0
- package/server/agents/alert-integration.ts +312 -0
- package/server/agents/error-analyzer.ts +472 -0
- package/server/agents/error-detector.ts +442 -0
- package/server/agents/fix-generator.ts +421 -0
- package/server/agents/fix-validator.ts +428 -0
- package/server/agents/merge-policy.ts +362 -0
- package/server/agents/pr-workflow.ts +476 -0
- package/server/agents/prompts/error-analysis.ts +393 -0
- package/server/ai/gemini-client.ts +499 -0
- package/server/ai/index.ts +317 -0
- package/server/ai/types.ts +137 -0
- package/server/app.ts +3693 -0
- package/server/archive-manager.ts +604 -0
- package/server/backlog/index.ts +7 -0
- package/server/backlog/migration.ts +331 -0
- package/server/backlog/parser.ts +323 -0
- package/server/backlog/sync.ts +325 -0
- package/server/change-log.ts +868 -0
- package/server/claude-flow/index.ts +12 -0
- package/server/claude-flow/prompt-builder.ts +407 -0
- package/server/claude-flow/types.ts +33 -0
- package/server/cli-adapter/index.ts +11 -0
- package/server/cli-adapter/process-manager.ts +612 -0
- package/server/cli-adapter/profile-manager.ts +286 -0
- package/server/cli-adapter/routes.ts +561 -0
- package/server/cli-adapter/types.ts +226 -0
- package/server/config.d.ts +18 -0
- package/server/config.js +79 -0
- package/server/config.ts +262 -0
- package/server/flow-sync.ts +543 -0
- package/server/git/change-workflow.ts +446 -0
- package/server/git/commands.ts +370 -0
- package/server/git/github.ts +247 -0
- package/server/git/index.ts +1202 -0
- package/server/git/status.ts +322 -0
- package/server/index.ts +136 -0
- package/server/integrations/crypto.ts +142 -0
- package/server/integrations/db/client.ts +169 -0
- package/server/integrations/db/schema.ts +167 -0
- package/server/integrations/env-parser.ts +365 -0
- package/server/integrations/index.ts +101 -0
- package/server/integrations/keychain.ts +239 -0
- package/server/integrations/local/file-utils.ts +383 -0
- package/server/integrations/local/index.ts +64 -0
- package/server/integrations/local/resolver.ts +439 -0
- package/server/integrations/local/types.ts +122 -0
- package/server/integrations/routes.ts +1100 -0
- package/server/integrations/service-patterns.ts +771 -0
- package/server/integrations/services/accounts.ts +356 -0
- package/server/integrations/services/env-import.ts +279 -0
- package/server/integrations/services/projects.ts +552 -0
- package/server/integrations/services/system-import.ts +1110 -0
- package/server/migrations/ears-generator.ts +491 -0
- package/server/migrations/gherkin-generator.ts +605 -0
- package/server/migrations/index.ts +73 -0
- package/server/migrations/migrate-spec-format.ts +492 -0
- package/server/migrations/openspec-parser.ts +542 -0
- package/server/migrations/tag-generator.ts +474 -0
- package/server/moai-specs.ts +487 -0
- package/server/moai-watcher.ts +145 -0
- package/server/parser-debug.ts +37 -0
- package/server/parser-utils.ts +316 -0
- package/server/parser.d.ts +17 -0
- package/server/parser.js +221 -0
- package/server/parser.ts +342 -0
- package/server/remote-watcher.ts +367 -0
- package/server/replay-engine.ts +915 -0
- package/server/routes/alerts.ts +1028 -0
- package/server/routes/changes.ts +812 -0
- package/server/routes/docs.ts +898 -0
- package/server/routes/flow.ts +2814 -0
- package/server/routes/global-chat.ts +162 -0
- package/server/routes/leann.ts +327 -0
- package/server/routes/projects.ts +1282 -0
- package/server/routes/search.ts +266 -0
- package/server/routes/specs.ts +482 -0
- package/server/routes/webhooks.ts +579 -0
- package/server/server/parser.js +265 -0
- package/server/services/githubActionsPoller.ts +797 -0
- package/server/services/slackNotifier.ts +476 -0
- package/server/src/types/index.js +1 -0
- package/server/sync-tasks.ts +741 -0
- package/server/tasks/cli/commands.ts +269 -0
- package/server/tasks/cli/index.ts +152 -0
- package/server/tasks/core/search.ts +81 -0
- package/server/tasks/core/task.ts +307 -0
- package/server/tasks/db/client.ts +1008 -0
- package/server/tasks/db/schema.ts +572 -0
- package/server/tasks/index.ts +24 -0
- package/server/tasks.db +0 -0
- package/server/types/archive.ts +136 -0
- package/server/types/change-log.ts +643 -0
- package/server/types/spec.ts +188 -0
- package/server/unified-spec-scanner.ts +753 -0
- package/server/utils/crypto.ts +179 -0
- package/server/utils/webhook-verify.ts +216 -0
- package/server/watcher.ts +132 -0
- package/server/websocket.ts +99 -0
- package/server-output.log +6 -0
- package/sonar-project.properties +18 -0
- package/src/App.tsx +386 -0
- package/src/api/client.ts +346 -0
- package/src/api/error-interceptor.ts +366 -0
- package/src/api/errors.ts +123 -0
- package/src/api/flow.ts +233 -0
- package/src/api/offline-queue.ts +351 -0
- package/src/api/retry-logic.ts +233 -0
- package/src/components/OfflineModeBanner.tsx +159 -0
- package/src/components/SSEStatusIndicator.tsx +194 -0
- package/src/components/agent/AgentChat.tsx +243 -0
- package/src/components/agent/AgentPage.tsx +182 -0
- package/src/components/agent/AgentSidebar.tsx +231 -0
- package/src/components/agent/index.ts +7 -0
- package/src/components/alerts/AlertCenter.tsx +239 -0
- package/src/components/alerts/AlertDashboard.tsx +211 -0
- package/src/components/alerts/AlertDetail.tsx +474 -0
- package/src/components/alerts/AlertList.tsx +113 -0
- package/src/components/alerts/AlertSettings.tsx +336 -0
- package/src/components/alerts/index.ts +5 -0
- package/src/components/chat/ChatPanel.tsx +642 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/cli/AddCustomCLIDialog.tsx +210 -0
- package/src/components/cli/CLISelector.tsx +187 -0
- package/src/components/cli/index.ts +8 -0
- package/src/components/dashboard/ArchivedChangeList.tsx +102 -0
- package/src/components/dashboard/ArchivedChangeViewer.tsx +184 -0
- package/src/components/dashboard/ArchivedChangesPage.tsx +31 -0
- package/src/components/dashboard/ChangeList.tsx +86 -0
- package/src/components/dashboard/ThemeToggle.tsx +33 -0
- package/src/components/diagram/DiagramViewer.tsx +256 -0
- package/src/components/diagram/MermaidRenderer.tsx +163 -0
- package/src/components/diagram/ProjectDiagramTab.tsx +161 -0
- package/src/components/diagram/index.ts +13 -0
- package/src/components/errors/ErrorBoundary.tsx +276 -0
- package/src/components/errors/ErrorFallback.tsx +198 -0
- package/src/components/errors/ErrorToast.tsx +221 -0
- package/src/components/flow/BacklogView.tsx +1142 -0
- package/src/components/flow/ChangeDetail.tsx +475 -0
- package/src/components/flow/ChangeItem.tsx +230 -0
- package/src/components/flow/ChangeList.tsx +92 -0
- package/src/components/flow/ExecutionHistoryDialog.tsx +224 -0
- package/src/components/flow/FlowContent.tsx +212 -0
- package/src/components/flow/FlowPage.tsx +9 -0
- package/src/components/flow/PipelineBar.tsx +214 -0
- package/src/components/flow/ProjectDashboard.tsx +222 -0
- package/src/components/flow/SpecDetail.tsx +138 -0
- package/src/components/flow/SpecDetailTabs.tsx +176 -0
- package/src/components/flow/SpecItem.tsx +93 -0
- package/src/components/flow/SpecProgressBar.tsx +47 -0
- package/src/components/flow/StageContent.tsx +620 -0
- package/src/components/flow/StandaloneTasks.tsx +960 -0
- package/src/components/flow/TaskExecutionDialog.tsx +1204 -0
- package/src/components/flow/index.ts +9 -0
- package/src/components/flow/task-execution/AgentSlider.tsx +37 -0
- package/src/components/flow/task-execution/ConsensusSettings.tsx +129 -0
- package/src/components/flow/task-execution/ExecutionOutput.tsx +398 -0
- package/src/components/flow/task-execution/ModelSelector.tsx +134 -0
- package/src/components/flow/task-execution/ProviderSelector.tsx +137 -0
- package/src/components/flow/task-execution/RecommendationBanner.tsx +71 -0
- package/src/components/flow/task-execution/StatusBadge.tsx +43 -0
- package/src/components/flow/task-execution/StrategySelector.tsx +48 -0
- package/src/components/flow/task-execution/SwarmSummary.tsx +55 -0
- package/src/components/flow/task-execution/index.ts +14 -0
- package/src/components/flow/task-execution/types.ts +56 -0
- package/src/components/git/ChangeWorkflowDialog.tsx +582 -0
- package/src/components/git/ConflictResolutionDialog.tsx +398 -0
- package/src/components/git/GitBranchSelector.tsx +212 -0
- package/src/components/git/GitCommitDialog.tsx +254 -0
- package/src/components/git/GitStatusBadge.tsx +148 -0
- package/src/components/git/GitSyncButton.tsx +128 -0
- package/src/components/git/RemoteStatusBanner.tsx +143 -0
- package/src/components/git/index.ts +9 -0
- package/src/components/integrations/EnvImportDialog.tsx +524 -0
- package/src/components/integrations/EnvironmentDialog.tsx +227 -0
- package/src/components/integrations/IntegrationBadges.tsx +91 -0
- package/src/components/integrations/IntegrationsSettings.tsx +55 -0
- package/src/components/integrations/ProjectIntegrations.tsx +481 -0
- package/src/components/integrations/ServiceAccountDialog.tsx +422 -0
- package/src/components/integrations/ServiceAccountList.tsx +305 -0
- package/src/components/integrations/SystemImportDialog.tsx +436 -0
- package/src/components/integrations/TestAccountDialog.tsx +162 -0
- package/src/components/integrations/index.ts +6 -0
- package/src/components/layout/AppSidebar.tsx +284 -0
- package/src/components/layout/FlowSidebar.tsx +435 -0
- package/src/components/layout/GlobalCommandPalette.tsx +410 -0
- package/src/components/layout/MenuBar.tsx +227 -0
- package/src/components/layout/StatusBar.tsx +226 -0
- package/src/components/monitoring/ErrorDashboard.tsx +274 -0
- package/src/components/monitoring/ErrorDetailPanel.tsx +200 -0
- package/src/components/monitoring/ErrorFilters.tsx +219 -0
- package/src/components/monitoring/ErrorHistoryList.tsx +141 -0
- package/src/components/monitoring/ErrorStats.tsx +249 -0
- package/src/components/remote/RemoteFileBrowser.tsx +249 -0
- package/src/components/remote/RemoteServerDialog.tsx +234 -0
- package/src/components/remote/RemoteServerList.tsx +366 -0
- package/src/components/remote/index.ts +7 -0
- package/src/components/settings/CLISettings.tsx +522 -0
- package/src/components/settings/CustomCLIDialog.tsx +548 -0
- package/src/components/settings/IntegrationsSettings.tsx +51 -0
- package/src/components/settings/ProjectSettings.tsx +441 -0
- package/src/components/settings/ProjectsSettings.tsx +541 -0
- package/src/components/settings/SearchSettings.tsx +272 -0
- package/src/components/settings/SettingsPage.tsx +68 -0
- package/src/components/settings/index.ts +5 -0
- package/src/components/swarm/ExecutionPanel.tsx +284 -0
- package/src/components/swarm/LogViewer.tsx +196 -0
- package/src/components/swarm/ProgressIndicator.tsx +111 -0
- package/src/components/swarm/index.ts +3 -0
- package/src/components/tasks/ArchiveTable.tsx +203 -0
- package/src/components/tasks/KanbanBoard.tsx +264 -0
- package/src/components/tasks/TaskCard.tsx +138 -0
- package/src/components/tasks/TaskColumn.tsx +81 -0
- package/src/components/tasks/TaskDialog.tsx +274 -0
- package/src/components/tasks/index.ts +5 -0
- package/src/components/tasks/types.ts +43 -0
- package/src/components/ui/alert-dialog.tsx +154 -0
- package/src/components/ui/alert.tsx +65 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +91 -0
- package/src/components/ui/checkbox.tsx +29 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/confirm-dialog.tsx +55 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/markdown.tsx +100 -0
- package/src/components/ui/progress.tsx +27 -0
- package/src/components/ui/resizable-sidebar.tsx +156 -0
- package/src/components/ui/resizable.tsx +54 -0
- package/src/components/ui/right-resizable-sidebar.tsx +158 -0
- package/src/components/ui/scroll-area.tsx +64 -0
- package/src/components/ui/select.tsx +185 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +136 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +56 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +63 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/tooltip.tsx +60 -0
- package/src/config/api.ts +83 -0
- package/src/constants/error-codes.ts +255 -0
- package/src/constants/stages.ts +27 -0
- package/src/context/ErrorContext.tsx +185 -0
- package/src/context/theme-provider.tsx +63 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/useAI.ts +206 -0
- package/src/hooks/useAgentSession.ts +431 -0
- package/src/hooks/useAlerts.ts +935 -0
- package/src/hooks/useArchivedChanges.ts +39 -0
- package/src/hooks/useAsyncError.ts +45 -0
- package/src/hooks/useChangeGit.ts +727 -0
- package/src/hooks/useChanges.ts +20 -0
- package/src/hooks/useClaude.ts +130 -0
- package/src/hooks/useDocs.ts +182 -0
- package/src/hooks/useErrorDashboard.ts +243 -0
- package/src/hooks/useErrorHandler.ts +150 -0
- package/src/hooks/useExecutionHistory.ts +55 -0
- package/src/hooks/useFlowChanges.ts +850 -0
- package/src/hooks/useFlowItems.ts +205 -0
- package/src/hooks/useGit.ts +427 -0
- package/src/hooks/useHideCompletedSpecs.ts +15 -0
- package/src/hooks/useInstance.ts +40 -0
- package/src/hooks/useIntegrations.ts +737 -0
- package/src/hooks/useLeannStatus.ts +93 -0
- package/src/hooks/useNetworkStatus.ts +167 -0
- package/src/hooks/useProjects.ts +353 -0
- package/src/hooks/useRemoteServers.ts +383 -0
- package/src/hooks/useSSEConnection.ts +346 -0
- package/src/hooks/useSpecs.ts +39 -0
- package/src/hooks/useSwarm.ts +462 -0
- package/src/hooks/useTasks.ts +137 -0
- package/src/hooks/useURLSync.ts +122 -0
- package/src/hooks/useWebSocket.ts +262 -0
- package/src/lib/utils.ts +121 -0
- package/src/main.tsx +22 -0
- package/src/stores/errorStore.ts +301 -0
- package/src/stores/offlineStore.ts +266 -0
- package/src/stores/sseStore.ts +247 -0
- package/src/stores/useHideCompletedStore.ts +21 -0
- package/src/styles/index.css +87 -0
- package/src/styles/theme.css +102 -0
- package/src/types/ai.ts +191 -0
- package/src/types/errors.ts +253 -0
- package/src/types/flow.ts +382 -0
- package/src/types/index.ts +614 -0
- package/src/utils/error-logger.ts +399 -0
- package/src/utils/error-statistics.ts +305 -0
- package/src/utils/logger.ts +280 -0
- package/src/utils/task-routing.ts +795 -0
- package/src/vite-env.d.ts +1 -0
- package/test-results/.last-run.json +4 -0
- package/tmp/check-docker-final.ts +48 -0
- package/tmp/check-docker-tasks.ts +58 -0
- package/tmp/check-docker-tasks2.ts +48 -0
- package/tmp/check-docker-tasks3.ts +42 -0
- package/tmp/check-mobile-tasks.ts +57 -0
- package/tmp/check-zywiki-tasks.ts +49 -0
- package/tmp/sync-mobile.ts +11 -0
- package/tmp/sync-zywiki.ts +68 -0
- package/tmp/test-docker-parser.ts +15 -0
- package/tmp/test-mobile-parser.ts +28 -0
- package/tmp/test-parser.ts +27 -0
- package/tmp/test-unnumbered.ts +35 -0
- 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
|
+
})
|