prjct-cli 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CLAUDE.md +74 -211
  3. package/core/agentic/prompt-builder.ts +3 -7
  4. package/core/command-registry/optional-commands.ts +0 -20
  5. package/core/infrastructure/command-installer/command-installer.ts +8 -1
  6. package/core/infrastructure/command-installer/global-config.ts +31 -1
  7. package/core/infrastructure/command-installer/index.ts +1 -1
  8. package/core/infrastructure/setup.ts +3 -0
  9. package/package.json +3 -17
  10. package/templates/agentic/agents/uxui.md +210 -0
  11. package/templates/commands/bug.md +219 -41
  12. package/templates/commands/done.md +57 -258
  13. package/templates/commands/feature.md +368 -80
  14. package/templates/commands/now.md +72 -277
  15. package/templates/commands/ship.md +167 -246
  16. package/templates/commands/sync.md +62 -3
  17. package/templates/commands/test.md +160 -20
  18. package/templates/global/CLAUDE.md +40 -205
  19. package/templates/global/docs/agents.md +88 -0
  20. package/templates/global/docs/architecture.md +103 -0
  21. package/templates/global/docs/commands.md +98 -0
  22. package/templates/global/docs/validation.md +95 -0
  23. package/bin/dev.js +0 -216
  24. package/bin/serve.js +0 -361
  25. package/packages/web/README.md +0 -36
  26. package/packages/web/app/api/claude/sessions/route.ts +0 -44
  27. package/packages/web/app/api/claude/status/route.ts +0 -34
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
  29. package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
  30. package/packages/web/app/api/projects/[id]/route.ts +0 -29
  31. package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
  32. package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
  33. package/packages/web/app/api/projects/route.ts +0 -16
  34. package/packages/web/app/api/sessions/current/route.ts +0 -132
  35. package/packages/web/app/api/sessions/history/route.ts +0 -204
  36. package/packages/web/app/error.tsx +0 -34
  37. package/packages/web/app/favicon.ico +0 -0
  38. package/packages/web/app/globals.css +0 -198
  39. package/packages/web/app/layout.tsx +0 -53
  40. package/packages/web/app/loading.tsx +0 -7
  41. package/packages/web/app/not-found.tsx +0 -25
  42. package/packages/web/app/page.tsx +0 -12
  43. package/packages/web/app/project/[id]/code/layout.tsx +0 -18
  44. package/packages/web/app/project/[id]/code/page.tsx +0 -408
  45. package/packages/web/app/project/[id]/error.tsx +0 -41
  46. package/packages/web/app/project/[id]/loading.tsx +0 -9
  47. package/packages/web/app/project/[id]/not-found.tsx +0 -27
  48. package/packages/web/app/project/[id]/page.tsx +0 -384
  49. package/packages/web/app/project/[id]/reports/page.tsx +0 -59
  50. package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
  51. package/packages/web/app/sessions/page.tsx +0 -165
  52. package/packages/web/app/settings/page.tsx +0 -151
  53. package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
  54. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
  55. package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
  56. package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
  57. package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
  58. package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
  59. package/packages/web/components/ActivityTimeline/index.ts +0 -2
  60. package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
  61. package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
  62. package/packages/web/components/AgentsCard/index.ts +0 -2
  63. package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
  64. package/packages/web/components/AppSidebar/index.ts +0 -1
  65. package/packages/web/components/BackLink/BackLink.tsx +0 -18
  66. package/packages/web/components/BackLink/BackLink.types.ts +0 -5
  67. package/packages/web/components/BackLink/index.ts +0 -2
  68. package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
  69. package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
  70. package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
  71. package/packages/web/components/BentoCard/index.ts +0 -2
  72. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
  73. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
  74. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
  75. package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
  76. package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
  77. package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
  78. package/packages/web/components/BentoGrid/index.ts +0 -2
  79. package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
  80. package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
  81. package/packages/web/components/BlockersCard/index.ts +0 -2
  82. package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
  83. package/packages/web/components/CommandBar/index.ts +0 -1
  84. package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
  85. package/packages/web/components/CommandButton/index.ts +0 -1
  86. package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
  87. package/packages/web/components/ConnectionStatus/index.ts +0 -1
  88. package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
  89. package/packages/web/components/DashboardContent/index.ts +0 -1
  90. package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
  91. package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
  92. package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
  93. package/packages/web/components/DateGroup/index.ts +0 -2
  94. package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
  95. package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
  96. package/packages/web/components/EmptyState/index.ts +0 -2
  97. package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
  98. package/packages/web/components/EventRow/EventRow.tsx +0 -49
  99. package/packages/web/components/EventRow/EventRow.types.ts +0 -7
  100. package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
  101. package/packages/web/components/EventRow/index.ts +0 -2
  102. package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
  103. package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
  104. package/packages/web/components/ExpandButton/index.ts +0 -2
  105. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
  106. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
  107. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
  108. package/packages/web/components/HealthGradientBackground/index.ts +0 -2
  109. package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
  110. package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
  111. package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
  112. package/packages/web/components/HeroSection/hooks/index.ts +0 -2
  113. package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
  114. package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
  115. package/packages/web/components/HeroSection/index.ts +0 -2
  116. package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
  117. package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
  118. package/packages/web/components/IdeasCard/index.ts +0 -2
  119. package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
  120. package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
  121. package/packages/web/components/InsightMessage/index.ts +0 -2
  122. package/packages/web/components/Logo/Logo.tsx +0 -65
  123. package/packages/web/components/Logo/index.ts +0 -1
  124. package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
  125. package/packages/web/components/MarkdownContent/index.ts +0 -1
  126. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
  127. package/packages/web/components/MasonryGrid/index.ts +0 -1
  128. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
  129. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
  130. package/packages/web/components/MomentumWidget/index.ts +0 -2
  131. package/packages/web/components/NowCard/NowCard.tsx +0 -118
  132. package/packages/web/components/NowCard/NowCard.types.ts +0 -16
  133. package/packages/web/components/NowCard/index.ts +0 -2
  134. package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
  135. package/packages/web/components/PageHeader/index.ts +0 -1
  136. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
  137. package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
  138. package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
  139. package/packages/web/components/ProgressRing/index.ts +0 -2
  140. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
  141. package/packages/web/components/ProjectAvatar/index.ts +0 -1
  142. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
  143. package/packages/web/components/ProjectColorDot/index.ts +0 -1
  144. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
  145. package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
  146. package/packages/web/components/Providers/Providers.tsx +0 -48
  147. package/packages/web/components/Providers/index.ts +0 -1
  148. package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
  149. package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
  150. package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
  151. package/packages/web/components/QueueCard/index.ts +0 -2
  152. package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
  153. package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
  154. package/packages/web/components/RecoverCard/index.ts +0 -2
  155. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
  156. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
  157. package/packages/web/components/RoadmapCard/index.ts +0 -2
  158. package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
  159. package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
  160. package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
  161. package/packages/web/components/ShipsCard/index.ts +0 -2
  162. package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
  163. package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
  164. package/packages/web/components/SparklineChart/index.ts +0 -2
  165. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
  166. package/packages/web/components/StatsMasonry/index.ts +0 -1
  167. package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
  168. package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
  169. package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
  170. package/packages/web/components/StreakCard/index.ts +0 -2
  171. package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
  172. package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
  173. package/packages/web/components/TasksCounter/index.ts +0 -2
  174. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
  175. package/packages/web/components/TechStackBadges/index.ts +0 -1
  176. package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
  177. package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
  178. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
  179. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
  180. package/packages/web/components/TerminalDock/index.ts +0 -2
  181. package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
  182. package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
  183. package/packages/web/components/TerminalTabs/index.ts +0 -1
  184. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
  185. package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
  186. package/packages/web/components/VelocityBadge/index.ts +0 -2
  187. package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
  188. package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
  189. package/packages/web/components/VelocityCard/index.ts +0 -2
  190. package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
  191. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
  192. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
  193. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
  194. package/packages/web/components/WeeklyReports/index.ts +0 -4
  195. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
  196. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
  197. package/packages/web/components/WeeklySparkline/index.ts +0 -2
  198. package/packages/web/components/charts/SessionsChart.tsx +0 -175
  199. package/packages/web/components/ui/alert-dialog.tsx +0 -157
  200. package/packages/web/components/ui/badge.tsx +0 -46
  201. package/packages/web/components/ui/button.tsx +0 -60
  202. package/packages/web/components/ui/card.tsx +0 -92
  203. package/packages/web/components/ui/chart.tsx +0 -385
  204. package/packages/web/components/ui/dialog.tsx +0 -143
  205. package/packages/web/components/ui/drawer.tsx +0 -135
  206. package/packages/web/components/ui/dropdown-menu.tsx +0 -257
  207. package/packages/web/components/ui/input.tsx +0 -21
  208. package/packages/web/components/ui/scroll-area.tsx +0 -58
  209. package/packages/web/components/ui/select.tsx +0 -187
  210. package/packages/web/components/ui/sheet.tsx +0 -139
  211. package/packages/web/components/ui/tabs.tsx +0 -66
  212. package/packages/web/components/ui/tooltip.tsx +0 -61
  213. package/packages/web/components.json +0 -22
  214. package/packages/web/context/GlobalTerminalContext.tsx +0 -538
  215. package/packages/web/context/TerminalContext.tsx +0 -45
  216. package/packages/web/context/TerminalTabsContext.tsx +0 -181
  217. package/packages/web/eslint.config.mjs +0 -18
  218. package/packages/web/hooks/useClaudeTerminal.ts +0 -425
  219. package/packages/web/hooks/useProjectStats.ts +0 -93
  220. package/packages/web/hooks/useProjects.ts +0 -73
  221. package/packages/web/lib/actions/projects.ts +0 -15
  222. package/packages/web/lib/commands.ts +0 -81
  223. package/packages/web/lib/format.ts +0 -23
  224. package/packages/web/lib/generate-week-report.ts +0 -285
  225. package/packages/web/lib/parse-prjct-files.ts +0 -1123
  226. package/packages/web/lib/project-colors.ts +0 -58
  227. package/packages/web/lib/projects.ts +0 -506
  228. package/packages/web/lib/pty.ts +0 -101
  229. package/packages/web/lib/query-config.ts +0 -44
  230. package/packages/web/lib/services/index.ts +0 -9
  231. package/packages/web/lib/services/projects.server.ts +0 -66
  232. package/packages/web/lib/services/stats.server.ts +0 -562
  233. package/packages/web/lib/unified-loader.ts +0 -396
  234. package/packages/web/lib/utils.ts +0 -6
  235. package/packages/web/next-env.d.ts +0 -6
  236. package/packages/web/next.config.ts +0 -7
  237. package/packages/web/package.json +0 -57
  238. package/packages/web/postcss.config.mjs +0 -7
  239. package/packages/web/public/file.svg +0 -1
  240. package/packages/web/public/globe.svg +0 -1
  241. package/packages/web/public/next.svg +0 -1
  242. package/packages/web/public/vercel.svg +0 -1
  243. package/packages/web/public/window.svg +0 -1
  244. package/packages/web/server.ts +0 -312
  245. package/packages/web/tsconfig.json +0 -34
  246. package/templates/commands/serve.md +0 -121
@@ -1,58 +0,0 @@
1
- /**
2
- * Project Colors - Consistent color generation based on projectId
3
- *
4
- * Uses a hash of the projectId to generate a consistent color
5
- * that matches between UI elements and browser tab titles.
6
- */
7
-
8
- // Color palette - visually distinct colors that work well in both
9
- // Tailwind classes and as emojis
10
- export const PROJECT_COLORS = [
11
- { name: 'red', emoji: '🔴', bg: 'bg-red-500', text: 'text-red-500' },
12
- { name: 'orange', emoji: '🟠', bg: 'bg-orange-500', text: 'text-orange-500' },
13
- { name: 'yellow', emoji: '🟡', bg: 'bg-yellow-500', text: 'text-yellow-500' },
14
- { name: 'green', emoji: '🟢', bg: 'bg-green-500', text: 'text-green-500' },
15
- { name: 'blue', emoji: '🔵', bg: 'bg-blue-500', text: 'text-blue-500' },
16
- { name: 'purple', emoji: '🟣', bg: 'bg-purple-500', text: 'text-purple-500' },
17
- { name: 'brown', emoji: '🟤', bg: 'bg-amber-700', text: 'text-amber-700' },
18
- ] as const
19
-
20
- export type ProjectColor = typeof PROJECT_COLORS[number]
21
-
22
- /**
23
- * Simple hash function for strings
24
- */
25
- function hashString(str: string): number {
26
- let hash = 0
27
- for (let i = 0; i < str.length; i++) {
28
- const char = str.charCodeAt(i)
29
- hash = ((hash << 5) - hash) + char
30
- hash = hash & hash // Convert to 32bit integer
31
- }
32
- return Math.abs(hash)
33
- }
34
-
35
- /**
36
- * Get a consistent color for a project based on its ID
37
- */
38
- export function getProjectColor(projectId: string): ProjectColor {
39
- const hash = hashString(projectId)
40
- const index = hash % PROJECT_COLORS.length
41
- return PROJECT_COLORS[index]
42
- }
43
-
44
- /**
45
- * Get just the emoji for a project (for use in titles)
46
- * Returns a neutral symbol instead of colored circles
47
- */
48
- export function getProjectEmoji(projectId: string): string {
49
- return '▸'
50
- }
51
-
52
- /**
53
- * Get the Tailwind background class for a project
54
- * Returns neutral color - projects no longer have color-coded backgrounds
55
- */
56
- export function getProjectBgClass(projectId: string): string {
57
- return 'bg-muted'
58
- }
@@ -1,506 +0,0 @@
1
- /**
2
- * Project utilities for prjct
3
- */
4
-
5
- import { promises as fs } from 'fs'
6
- import { join, dirname } from 'path'
7
- import { homedir } from 'os'
8
- import { exec } from 'child_process'
9
- import { promisify } from 'util'
10
- import { listSessions } from './pty'
11
-
12
- const execAsync = promisify(exec)
13
-
14
- export const GLOBAL_STORAGE = join(homedir(), '.prjct-cli', 'projects')
15
- export const TRASH_PATH = join(homedir(), '.prjct-cli', '.trash')
16
-
17
- // Cache for project paths (projectId -> real path)
18
- const projectPathCache = new Map<string, string>()
19
-
20
- /**
21
- * Scan common directories for .prjct/prjct.config.json files
22
- */
23
- export async function scanForProjects(): Promise<Map<string, string>> {
24
- const searchPaths = [
25
- join(homedir(), 'Apps'),
26
- join(homedir(), 'Projects'),
27
- join(homedir(), 'Documents'),
28
- join(homedir(), 'Development'),
29
- join(homedir(), 'Code'),
30
- join(homedir(), 'dev'),
31
- ]
32
-
33
- for (const searchPath of searchPaths) {
34
- try {
35
- const { stdout } = await execAsync(
36
- `find "${searchPath}" -maxdepth 4 -type f -name "prjct.config.json" -path "*/.prjct/*" 2>/dev/null`,
37
- { timeout: 5000 }
38
- )
39
-
40
- const configFiles = stdout.trim().split('\n').filter(Boolean)
41
-
42
- for (const configFile of configFiles) {
43
- try {
44
- const content = await fs.readFile(configFile, 'utf-8')
45
- const config = JSON.parse(content)
46
- if (config.projectId) {
47
- const projectPath = dirname(dirname(configFile))
48
- projectPathCache.set(config.projectId, projectPath)
49
- }
50
- } catch {
51
- // Skip invalid config files
52
- }
53
- }
54
- } catch {
55
- // Skip directories that don't exist or are not accessible
56
- }
57
- }
58
-
59
- return projectPathCache
60
- }
61
-
62
- /**
63
- * Extract project path from CLAUDE.md
64
- */
65
- export function extractProjectPath(claudeMd: string): string | null {
66
- const pathMatch = claudeMd.match(/(?:Path|Location|Directory):\s*`?([^\n`]+)`?/i)
67
- if (pathMatch) return pathMatch[1].trim()
68
-
69
- const infoMatch = claudeMd.match(/\*\*Path\*\*:\s*`?([^\n`]+)`?/i)
70
- if (infoMatch) return infoMatch[1].trim()
71
-
72
- return null
73
- }
74
-
75
- /**
76
- * Get all projects with rich metadata
77
- */
78
- export async function getProjects() {
79
- if (projectPathCache.size === 0) {
80
- await scanForProjects()
81
- }
82
-
83
- const projects = []
84
-
85
- try {
86
- const dirs = await fs.readdir(GLOBAL_STORAGE)
87
-
88
- for (const projectId of dirs) {
89
- // Skip hidden directories like .trash
90
- if (projectId.startsWith('.')) continue
91
-
92
- const storagePath = join(GLOBAL_STORAGE, projectId)
93
-
94
- // Try project.json first (source of truth)
95
- let name: string = projectId
96
- let repoPath: string | null = null
97
- let techStack: string[] = []
98
-
99
- try {
100
- const projectJsonPath = join(storagePath, 'project.json')
101
- const projectJson = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
102
- name = projectJson.name || projectId
103
- repoPath = projectJson.repoPath || null
104
- techStack = projectJson.techStack || []
105
- } catch {
106
- // Fallback to CLAUDE.md for name/repoPath only
107
- // techStack comes from project.json (populated by /p:sync)
108
- try {
109
- const claudeMd = await fs.readFile(join(storagePath, 'CLAUDE.md'), 'utf-8')
110
- const nameMatch = claudeMd.match(/# (.+) - Project Context/)
111
- if (nameMatch) name = nameMatch[1]
112
-
113
- const cachedPath = projectPathCache.get(projectId)
114
- repoPath = cachedPath || extractProjectPath(claudeMd)
115
- } catch {
116
- // Skip projects without valid config
117
- continue
118
- }
119
- }
120
-
121
- // Get current task
122
- let currentTask: string | null = null
123
- try {
124
- const nowContent = await fs.readFile(join(storagePath, 'core', 'now.md'), 'utf-8')
125
- // Skip headers like "# NOW", "# Current Task" and find the actual task content
126
- // Look for **bold text** (task description) or first non-header, non-metadata line
127
- const boldMatch = nowContent.match(/\*\*([^*]+)\*\*/)
128
- if (boldMatch && boldMatch[1].trim() && !boldMatch[1].includes(':')) {
129
- currentTask = boldMatch[1].trim()
130
- } else {
131
- // Find first content line that's not a header, metadata, or empty
132
- const lines = nowContent.split('\n')
133
- for (const line of lines) {
134
- const trimmed = line.trim()
135
- // Skip headers, empty lines, metadata lines (key: value), and "No active/current task" messages
136
- if (trimmed &&
137
- !trimmed.startsWith('#') &&
138
- !trimmed.toLowerCase().includes('no active task') &&
139
- !trimmed.toLowerCase().includes('no current task') &&
140
- !trimmed.match(/^(Feature|Started|Status|Agent):/i) &&
141
- !trimmed.startsWith('**') &&
142
- !trimmed.startsWith('-')) {
143
- currentTask = trimmed
144
- break
145
- }
146
- }
147
- }
148
- // Truncate if too long
149
- if (currentTask && currentTask.length > 60) {
150
- currentTask = currentTask.substring(0, 57) + '...'
151
- }
152
- } catch {}
153
-
154
- // Get session status and last activity
155
- let hasActiveSession = false
156
- let lastActivity: string | null = null
157
-
158
- // Check for real PTY sessions (actual Claude sessions in memory)
159
- try {
160
- const activeSessions = listSessions()
161
- hasActiveSession = activeSessions.some(s => s.projectDir === repoPath)
162
- } catch {}
163
-
164
- // Try current session for lastActivity only
165
- try {
166
- const sessionPath = join(storagePath, 'sessions', 'current.json')
167
- const sessionData = JSON.parse(await fs.readFile(sessionPath, 'utf-8'))
168
- lastActivity = sessionData.startedAt || sessionData.updatedAt
169
- } catch {}
170
-
171
- // If no session, get last modified time from key files
172
- if (!lastActivity) {
173
- const filesToCheck = [
174
- join(storagePath, 'core', 'now.md'),
175
- join(storagePath, 'core', 'next.md'),
176
- join(storagePath, 'planning', 'ideas.md'),
177
- join(storagePath, 'progress', 'shipped.md'),
178
- join(storagePath, 'memory', 'context.jsonl'),
179
- join(storagePath, 'CLAUDE.md')
180
- ]
181
-
182
- let latestMtime = 0
183
- for (const filePath of filesToCheck) {
184
- try {
185
- const stat = await fs.stat(filePath)
186
- if (stat.mtimeMs > latestMtime) {
187
- latestMtime = stat.mtimeMs
188
- }
189
- } catch {}
190
- }
191
-
192
- if (latestMtime > 0) {
193
- lastActivity = new Date(latestMtime).toISOString()
194
- }
195
- }
196
-
197
- // Count ideas, next tasks, and shipped items
198
- let ideasCount = 0
199
- let nextTasksCount = 0
200
- let shippedCount = 0
201
- try {
202
- const ideasContent = await fs.readFile(join(storagePath, 'planning', 'ideas.md'), 'utf-8')
203
- ideasCount = (ideasContent.match(/^- /gm) || []).length
204
- } catch {}
205
- try {
206
- const nextContent = await fs.readFile(join(storagePath, 'core', 'next.md'), 'utf-8')
207
- nextTasksCount = (nextContent.match(/^- /gm) || []).length
208
- } catch {}
209
- try {
210
- const shippedContent = await fs.readFile(join(storagePath, 'progress', 'shipped.md'), 'utf-8')
211
- // Count shipped items: either "- **Name**" or "### Name" format
212
- const bulletItems = (shippedContent.match(/^- \*\*/gm) || []).length
213
- const headingItems = (shippedContent.match(/^### /gm) || []).length
214
- shippedCount = bulletItems + headingItems
215
- } catch {}
216
-
217
- // Find favicon/icon in project repo
218
- let iconPath: string | null = null
219
- if (repoPath) {
220
- const iconPatterns = [
221
- 'public/favicon.ico',
222
- 'public/favicon.svg',
223
- 'public/icon.svg',
224
- 'public/icon.png',
225
- 'public/logo.svg',
226
- 'public/logo.png',
227
- 'app/favicon.ico',
228
- 'app/icon.svg',
229
- 'app/icon.png',
230
- 'favicon.ico',
231
- 'favicon.svg'
232
- ]
233
- for (const pattern of iconPatterns) {
234
- try {
235
- const fullPath = join(repoPath, pattern)
236
- await fs.access(fullPath)
237
- iconPath = fullPath
238
- break
239
- } catch {}
240
- }
241
- }
242
-
243
- projects.push({
244
- id: projectId,
245
- name,
246
- path: repoPath || storagePath,
247
- repoPath,
248
- storagePath,
249
- currentTask,
250
- hasActiveSession,
251
- lastActivity,
252
- ideasCount,
253
- nextTasksCount,
254
- shippedCount,
255
- techStack,
256
- iconPath
257
- })
258
- }
259
- } catch {
260
- // Storage directory doesn't exist
261
- }
262
-
263
- // Sort by lastActivity (most recent first), then by name
264
- projects.sort((a, b) => {
265
- if (a.lastActivity && b.lastActivity) {
266
- return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
267
- }
268
- if (a.lastActivity) return -1
269
- if (b.lastActivity) return 1
270
- return a.name.localeCompare(b.name)
271
- })
272
-
273
- return projects
274
- }
275
-
276
- /**
277
- * Get project by ID
278
- */
279
- export async function getProject(projectId: string) {
280
- const storagePath = join(GLOBAL_STORAGE, projectId)
281
-
282
- // 1. Try to read from project.json (source of truth for dashboard)
283
- let repoPath: string | null = null
284
- let name: string = projectId
285
- let version: string | null = null
286
- let stack: string | null = null
287
- let filesCount: string | null = null
288
- let commitsCount: string | null = null
289
- let techStack: string[] = []
290
-
291
- try {
292
- const projectJsonPath = join(storagePath, 'project.json')
293
- const projectJson = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
294
- repoPath = projectJson.repoPath || null
295
- name = projectJson.name || projectId
296
- version = projectJson.version || null
297
- stack = projectJson.stack || null
298
- filesCount = projectJson.fileCount ? String(projectJson.fileCount) : null
299
- commitsCount = projectJson.commitCount ? String(projectJson.commitCount) : null
300
- techStack = projectJson.techStack || []
301
- } catch {
302
- // project.json doesn't exist - fallback to scan
303
- if (projectPathCache.size === 0) {
304
- await scanForProjects()
305
- }
306
- repoPath = projectPathCache.get(projectId) || null
307
- }
308
-
309
- try {
310
- const claudeMd = await fs.readFile(join(storagePath, 'CLAUDE.md'), 'utf-8')
311
-
312
- // If still no repoPath, try extracting from CLAUDE.md
313
- if (!repoPath) {
314
- repoPath = extractProjectPath(claudeMd)
315
- }
316
-
317
- // If name is still projectId, try CLAUDE.md
318
- if (name === projectId) {
319
- const nameMatch = claudeMd.match(/# (.+) - Project Context/)
320
- if (nameMatch) name = nameMatch[1]
321
- }
322
-
323
- let currentSession = null
324
- try {
325
- const sessionPath = join(storagePath, 'sessions', 'current.json')
326
- const sessionData = await fs.readFile(sessionPath, 'utf-8')
327
- currentSession = JSON.parse(sessionData)
328
- } catch {}
329
-
330
- let currentTask = null
331
- try {
332
- const nowPath = join(storagePath, 'core', 'now.md')
333
- currentTask = await fs.readFile(nowPath, 'utf-8')
334
- } catch {}
335
-
336
- // Count shipped and queue items (DRY - same logic as getProjects)
337
- let shippedCount = 0
338
- let nextTasksCount = 0
339
- try {
340
- const shippedContent = await fs.readFile(join(storagePath, 'progress', 'shipped.md'), 'utf-8')
341
- const bulletItems = (shippedContent.match(/^- \*\*/gm) || []).length
342
- const headingItems = (shippedContent.match(/^### /gm) || []).length
343
- shippedCount = bulletItems + headingItems
344
- } catch {}
345
- try {
346
- const nextContent = await fs.readFile(join(storagePath, 'core', 'next.md'), 'utf-8')
347
- nextTasksCount = (nextContent.match(/^- /gm) || []).length
348
- } catch {}
349
-
350
- const total = shippedCount + nextTasksCount
351
- const completionRate = total > 0 ? Math.round((shippedCount / total) * 100) : 0
352
-
353
- // Fallback: Extract stats from claudeMd Quick Reference table if not from project.json
354
- if (!version) {
355
- const versionMatch = claudeMd.match(/\*\*Version\*\*\s*\|\s*([^\n|]+)/)
356
- if (versionMatch) version = versionMatch[1].trim()
357
- }
358
-
359
- if (!stack) {
360
- const stackMatch = claudeMd.match(/\*\*Stack\*\*\s*\|\s*([^\n|]+)/)
361
- if (stackMatch) stack = stackMatch[1].trim()
362
- }
363
-
364
- if (!filesCount) {
365
- const filesMatch = claudeMd.match(/\*\*Files\*\*\s*\|\s*([^\n|]+)/)
366
- if (filesMatch) filesCount = filesMatch[1].trim()
367
- }
368
-
369
- if (!commitsCount) {
370
- const commitsMatch = claudeMd.match(/\*\*Commits\*\*\s*\|\s*([^\n|]+)/)
371
- if (commitsMatch) commitsCount = commitsMatch[1].trim()
372
- }
373
-
374
- // Find favicon/icon in project repo
375
- let iconPath: string | null = null
376
- if (repoPath) {
377
- const iconPatterns = [
378
- 'public/favicon.ico',
379
- 'public/favicon.svg',
380
- 'public/icon.svg',
381
- 'public/icon.png',
382
- 'public/logo.svg',
383
- 'public/logo.png',
384
- 'app/favicon.ico',
385
- 'app/icon.svg',
386
- 'app/icon.png',
387
- 'favicon.ico',
388
- 'favicon.svg'
389
- ]
390
- for (const pattern of iconPatterns) {
391
- try {
392
- const fullPath = join(repoPath, pattern)
393
- await fs.access(fullPath)
394
- iconPath = fullPath
395
- break
396
- } catch {}
397
- }
398
- }
399
-
400
- return {
401
- id: projectId,
402
- name,
403
- path: storagePath, // Storage path (for prjct data)
404
- repoPath, // Real repository path (for terminal/Claude)
405
- storagePath,
406
- claudeMd,
407
- currentSession,
408
- currentTask,
409
- // Parsed stats
410
- version,
411
- stack,
412
- filesCount,
413
- commitsCount,
414
- techStack,
415
- iconPath,
416
- // Counts (DRY - same source as dashboard)
417
- shippedCount,
418
- nextTasksCount,
419
- completionRate
420
- }
421
- } catch {
422
- return null
423
- }
424
- }
425
-
426
- /**
427
- * Move project to trash (soft delete)
428
- */
429
- export async function moveToTrash(projectId: string) {
430
- const sourcePath = join(GLOBAL_STORAGE, projectId)
431
- const trashPath = join(TRASH_PATH, projectId)
432
-
433
- // Verify source exists
434
- try {
435
- await fs.access(sourcePath)
436
- } catch {
437
- throw new Error(`Project ${projectId} not found`)
438
- }
439
-
440
- // Create trash directory if it doesn't exist
441
- await fs.mkdir(TRASH_PATH, { recursive: true })
442
-
443
- // Move to trash
444
- await fs.rename(sourcePath, trashPath)
445
-
446
- // Write deletion metadata
447
- const deletedAt = new Date().toISOString()
448
- await fs.writeFile(
449
- join(trashPath, '.deleted'),
450
- JSON.stringify({ deletedAt, projectId })
451
- )
452
-
453
- return { trashedAt: deletedAt }
454
- }
455
-
456
- /**
457
- * Get project status
458
- */
459
- export async function getProjectStatus(projectId: string) {
460
- const projectPath = join(GLOBAL_STORAGE, projectId)
461
-
462
- let session = null
463
- let repoPath: string | null = null
464
- try {
465
- const sessionPath = join(projectPath, 'sessions', 'current.json')
466
- session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'))
467
- } catch {}
468
-
469
- // Get repoPath from project.json for PTY session check
470
- try {
471
- const projectJsonPath = join(projectPath, 'project.json')
472
- const projectJson = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
473
- repoPath = projectJson.repoPath || null
474
- } catch {}
475
-
476
- // Check for real PTY sessions
477
- let hasActiveSession = false
478
- if (repoPath) {
479
- try {
480
- const activeSessions = listSessions()
481
- hasActiveSession = activeSessions.some(s => s.projectDir === repoPath)
482
- } catch {}
483
- }
484
-
485
- let ideas: string[] = []
486
- try {
487
- const ideasPath = join(projectPath, 'planning', 'ideas.md')
488
- const content = await fs.readFile(ideasPath, 'utf-8')
489
- ideas = content.split('\n').filter(l => l.startsWith('- ')).slice(0, 5)
490
- } catch {}
491
-
492
- let nextTasks: string[] = []
493
- try {
494
- const nextPath = join(projectPath, 'core', 'next.md')
495
- const content = await fs.readFile(nextPath, 'utf-8')
496
- nextTasks = content.split('\n').filter(l => l.startsWith('- ')).slice(0, 5)
497
- } catch {}
498
-
499
- return {
500
- projectId,
501
- session,
502
- hasActiveSession,
503
- ideas,
504
- nextTasks
505
- }
506
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * PTY Manager - Handle Claude Code CLI sessions via pseudo-terminal
3
- */
4
-
5
- import * as pty from 'node-pty'
6
- import type { IPty } from 'node-pty'
7
-
8
- interface Session {
9
- pty: IPty
10
- projectDir: string
11
- createdAt: Date
12
- }
13
-
14
- const sessions = new Map<string, Session>()
15
-
16
- export function createClaudeSession(sessionId: string, projectDir: string): IPty {
17
- // Kill existing session if any
18
- const existing = sessions.get(sessionId)
19
- if (existing) {
20
- try {
21
- existing.pty.kill()
22
- } catch {
23
- // Ignore
24
- }
25
- sessions.delete(sessionId)
26
- }
27
-
28
- // Spawn claude CLI
29
- const shell = process.platform === 'win32' ? 'cmd.exe' : 'bash'
30
- const args = process.platform === 'win32' ? [] : ['-l']
31
-
32
- const ptyProcess = pty.spawn(shell, args, {
33
- name: 'xterm-256color',
34
- cols: 120,
35
- rows: 30,
36
- cwd: projectDir,
37
- env: {
38
- ...process.env,
39
- TERM: 'xterm-256color',
40
- COLORTERM: 'truecolor'
41
- }
42
- })
43
-
44
- // Store session
45
- sessions.set(sessionId, {
46
- pty: ptyProcess,
47
- projectDir,
48
- createdAt: new Date()
49
- })
50
-
51
- // Auto-start Claude Code CLI
52
- setTimeout(() => {
53
- ptyProcess.write('claude\r')
54
- }, 500)
55
-
56
- return ptyProcess
57
- }
58
-
59
- export function getSession(sessionId: string): IPty | null {
60
- const session = sessions.get(sessionId)
61
- return session?.pty || null
62
- }
63
-
64
- export function killSession(sessionId: string): boolean {
65
- const session = sessions.get(sessionId)
66
- if (session) {
67
- try {
68
- session.pty.kill()
69
- } catch {
70
- // Ignore
71
- }
72
- sessions.delete(sessionId)
73
- return true
74
- }
75
- return false
76
- }
77
-
78
- export function resizeSession(sessionId: string, cols: number, rows: number): boolean {
79
- const session = sessions.get(sessionId)
80
- if (session) {
81
- try {
82
- session.pty.resize(cols, rows)
83
- return true
84
- } catch {
85
- return false
86
- }
87
- }
88
- return false
89
- }
90
-
91
- export function listSessions(): { sessionId: string; projectDir: string; createdAt: Date }[] {
92
- const result: { sessionId: string; projectDir: string; createdAt: Date }[] = []
93
- sessions.forEach((session, sessionId) => {
94
- result.push({
95
- sessionId,
96
- projectDir: session.projectDir,
97
- createdAt: session.createdAt
98
- })
99
- })
100
- return result
101
- }
@@ -1,44 +0,0 @@
1
- import type { UseQueryOptions } from '@tanstack/react-query'
2
-
3
- // Refresh intervals in milliseconds
4
- export const REFRESH_INTERVALS = {
5
- realtime: 2000, // 2s - for active sessions, connection status
6
- fast: 5000, // 5s - for project status, current task
7
- normal: 10000, // 10s - for project list, stats
8
- slow: 30000, // 30s - for historical data
9
- } as const
10
-
11
- // Default query options for different data freshness needs
12
- export const queryPresets = {
13
- // For data that needs to feel "live" (sessions, connection status)
14
- realtime: {
15
- staleTime: 0,
16
- refetchInterval: REFRESH_INTERVALS.realtime,
17
- refetchOnWindowFocus: true,
18
- refetchOnReconnect: true,
19
- },
20
-
21
- // For frequently changing data (project status, tasks)
22
- fast: {
23
- staleTime: REFRESH_INTERVALS.fast / 2,
24
- refetchInterval: REFRESH_INTERVALS.fast,
25
- refetchOnWindowFocus: true,
26
- refetchOnReconnect: true,
27
- },
28
-
29
- // For moderately changing data (project list, stats)
30
- normal: {
31
- staleTime: REFRESH_INTERVALS.normal / 2,
32
- refetchInterval: REFRESH_INTERVALS.normal,
33
- refetchOnWindowFocus: true,
34
- refetchOnReconnect: true,
35
- },
36
-
37
- // For rarely changing data (settings, historical)
38
- slow: {
39
- staleTime: REFRESH_INTERVALS.slow / 2,
40
- refetchInterval: REFRESH_INTERVALS.slow,
41
- refetchOnWindowFocus: true,
42
- refetchOnReconnect: false,
43
- },
44
- } as const satisfies Record<string, Partial<UseQueryOptions>>