prjct-cli 0.18.2 → 0.19.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 (243) hide show
  1. package/CHANGELOG.md +40 -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/commands/done.md +57 -258
  11. package/templates/commands/now.md +72 -277
  12. package/templates/commands/ship.md +55 -261
  13. package/templates/commands/test.md +328 -21
  14. package/templates/global/CLAUDE.md +40 -205
  15. package/templates/global/docs/agents.md +88 -0
  16. package/templates/global/docs/architecture.md +103 -0
  17. package/templates/global/docs/commands.md +98 -0
  18. package/templates/global/docs/validation.md +95 -0
  19. package/templates/mcp-config.json +36 -0
  20. package/bin/dev.js +0 -216
  21. package/bin/serve.js +0 -361
  22. package/packages/web/README.md +0 -36
  23. package/packages/web/app/api/claude/sessions/route.ts +0 -44
  24. package/packages/web/app/api/claude/status/route.ts +0 -34
  25. package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
  26. package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
  27. package/packages/web/app/api/projects/[id]/route.ts +0 -29
  28. package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
  29. package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
  30. package/packages/web/app/api/projects/route.ts +0 -16
  31. package/packages/web/app/api/sessions/current/route.ts +0 -132
  32. package/packages/web/app/api/sessions/history/route.ts +0 -204
  33. package/packages/web/app/error.tsx +0 -34
  34. package/packages/web/app/favicon.ico +0 -0
  35. package/packages/web/app/globals.css +0 -198
  36. package/packages/web/app/layout.tsx +0 -53
  37. package/packages/web/app/loading.tsx +0 -7
  38. package/packages/web/app/not-found.tsx +0 -25
  39. package/packages/web/app/page.tsx +0 -12
  40. package/packages/web/app/project/[id]/code/layout.tsx +0 -18
  41. package/packages/web/app/project/[id]/code/page.tsx +0 -408
  42. package/packages/web/app/project/[id]/error.tsx +0 -41
  43. package/packages/web/app/project/[id]/loading.tsx +0 -9
  44. package/packages/web/app/project/[id]/not-found.tsx +0 -27
  45. package/packages/web/app/project/[id]/page.tsx +0 -384
  46. package/packages/web/app/project/[id]/reports/page.tsx +0 -59
  47. package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
  48. package/packages/web/app/sessions/page.tsx +0 -165
  49. package/packages/web/app/settings/page.tsx +0 -151
  50. package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
  51. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
  52. package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
  53. package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
  54. package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
  55. package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
  56. package/packages/web/components/ActivityTimeline/index.ts +0 -2
  57. package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
  58. package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
  59. package/packages/web/components/AgentsCard/index.ts +0 -2
  60. package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
  61. package/packages/web/components/AppSidebar/index.ts +0 -1
  62. package/packages/web/components/BackLink/BackLink.tsx +0 -18
  63. package/packages/web/components/BackLink/BackLink.types.ts +0 -5
  64. package/packages/web/components/BackLink/index.ts +0 -2
  65. package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
  66. package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
  67. package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
  68. package/packages/web/components/BentoCard/index.ts +0 -2
  69. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
  70. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
  71. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
  72. package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
  73. package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
  74. package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
  75. package/packages/web/components/BentoGrid/index.ts +0 -2
  76. package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
  77. package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
  78. package/packages/web/components/BlockersCard/index.ts +0 -2
  79. package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
  80. package/packages/web/components/CommandBar/index.ts +0 -1
  81. package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
  82. package/packages/web/components/CommandButton/index.ts +0 -1
  83. package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
  84. package/packages/web/components/ConnectionStatus/index.ts +0 -1
  85. package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
  86. package/packages/web/components/DashboardContent/index.ts +0 -1
  87. package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
  88. package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
  89. package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
  90. package/packages/web/components/DateGroup/index.ts +0 -2
  91. package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
  92. package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
  93. package/packages/web/components/EmptyState/index.ts +0 -2
  94. package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
  95. package/packages/web/components/EventRow/EventRow.tsx +0 -49
  96. package/packages/web/components/EventRow/EventRow.types.ts +0 -7
  97. package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
  98. package/packages/web/components/EventRow/index.ts +0 -2
  99. package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
  100. package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
  101. package/packages/web/components/ExpandButton/index.ts +0 -2
  102. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
  103. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
  104. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
  105. package/packages/web/components/HealthGradientBackground/index.ts +0 -2
  106. package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
  107. package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
  108. package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
  109. package/packages/web/components/HeroSection/hooks/index.ts +0 -2
  110. package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
  111. package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
  112. package/packages/web/components/HeroSection/index.ts +0 -2
  113. package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
  114. package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
  115. package/packages/web/components/IdeasCard/index.ts +0 -2
  116. package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
  117. package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
  118. package/packages/web/components/InsightMessage/index.ts +0 -2
  119. package/packages/web/components/Logo/Logo.tsx +0 -65
  120. package/packages/web/components/Logo/index.ts +0 -1
  121. package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
  122. package/packages/web/components/MarkdownContent/index.ts +0 -1
  123. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
  124. package/packages/web/components/MasonryGrid/index.ts +0 -1
  125. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
  126. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
  127. package/packages/web/components/MomentumWidget/index.ts +0 -2
  128. package/packages/web/components/NowCard/NowCard.tsx +0 -118
  129. package/packages/web/components/NowCard/NowCard.types.ts +0 -16
  130. package/packages/web/components/NowCard/index.ts +0 -2
  131. package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
  132. package/packages/web/components/PageHeader/index.ts +0 -1
  133. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
  134. package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
  135. package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
  136. package/packages/web/components/ProgressRing/index.ts +0 -2
  137. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
  138. package/packages/web/components/ProjectAvatar/index.ts +0 -1
  139. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
  140. package/packages/web/components/ProjectColorDot/index.ts +0 -1
  141. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
  142. package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
  143. package/packages/web/components/Providers/Providers.tsx +0 -48
  144. package/packages/web/components/Providers/index.ts +0 -1
  145. package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
  146. package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
  147. package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
  148. package/packages/web/components/QueueCard/index.ts +0 -2
  149. package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
  150. package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
  151. package/packages/web/components/RecoverCard/index.ts +0 -2
  152. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
  153. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
  154. package/packages/web/components/RoadmapCard/index.ts +0 -2
  155. package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
  156. package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
  157. package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
  158. package/packages/web/components/ShipsCard/index.ts +0 -2
  159. package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
  160. package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
  161. package/packages/web/components/SparklineChart/index.ts +0 -2
  162. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
  163. package/packages/web/components/StatsMasonry/index.ts +0 -1
  164. package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
  165. package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
  166. package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
  167. package/packages/web/components/StreakCard/index.ts +0 -2
  168. package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
  169. package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
  170. package/packages/web/components/TasksCounter/index.ts +0 -2
  171. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
  172. package/packages/web/components/TechStackBadges/index.ts +0 -1
  173. package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
  174. package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
  175. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
  176. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
  177. package/packages/web/components/TerminalDock/index.ts +0 -2
  178. package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
  179. package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
  180. package/packages/web/components/TerminalTabs/index.ts +0 -1
  181. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
  182. package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
  183. package/packages/web/components/VelocityBadge/index.ts +0 -2
  184. package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
  185. package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
  186. package/packages/web/components/VelocityCard/index.ts +0 -2
  187. package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
  188. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
  189. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
  190. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
  191. package/packages/web/components/WeeklyReports/index.ts +0 -4
  192. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
  193. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
  194. package/packages/web/components/WeeklySparkline/index.ts +0 -2
  195. package/packages/web/components/charts/SessionsChart.tsx +0 -175
  196. package/packages/web/components/ui/alert-dialog.tsx +0 -157
  197. package/packages/web/components/ui/badge.tsx +0 -46
  198. package/packages/web/components/ui/button.tsx +0 -60
  199. package/packages/web/components/ui/card.tsx +0 -92
  200. package/packages/web/components/ui/chart.tsx +0 -385
  201. package/packages/web/components/ui/dialog.tsx +0 -143
  202. package/packages/web/components/ui/drawer.tsx +0 -135
  203. package/packages/web/components/ui/dropdown-menu.tsx +0 -257
  204. package/packages/web/components/ui/input.tsx +0 -21
  205. package/packages/web/components/ui/scroll-area.tsx +0 -58
  206. package/packages/web/components/ui/select.tsx +0 -187
  207. package/packages/web/components/ui/sheet.tsx +0 -139
  208. package/packages/web/components/ui/tabs.tsx +0 -66
  209. package/packages/web/components/ui/tooltip.tsx +0 -61
  210. package/packages/web/components.json +0 -22
  211. package/packages/web/context/GlobalTerminalContext.tsx +0 -538
  212. package/packages/web/context/TerminalContext.tsx +0 -45
  213. package/packages/web/context/TerminalTabsContext.tsx +0 -181
  214. package/packages/web/eslint.config.mjs +0 -18
  215. package/packages/web/hooks/useClaudeTerminal.ts +0 -425
  216. package/packages/web/hooks/useProjectStats.ts +0 -93
  217. package/packages/web/hooks/useProjects.ts +0 -73
  218. package/packages/web/lib/actions/projects.ts +0 -15
  219. package/packages/web/lib/commands.ts +0 -81
  220. package/packages/web/lib/format.ts +0 -23
  221. package/packages/web/lib/generate-week-report.ts +0 -285
  222. package/packages/web/lib/parse-prjct-files.ts +0 -1123
  223. package/packages/web/lib/project-colors.ts +0 -58
  224. package/packages/web/lib/projects.ts +0 -506
  225. package/packages/web/lib/pty.ts +0 -101
  226. package/packages/web/lib/query-config.ts +0 -44
  227. package/packages/web/lib/services/index.ts +0 -9
  228. package/packages/web/lib/services/projects.server.ts +0 -66
  229. package/packages/web/lib/services/stats.server.ts +0 -562
  230. package/packages/web/lib/unified-loader.ts +0 -396
  231. package/packages/web/lib/utils.ts +0 -6
  232. package/packages/web/next-env.d.ts +0 -6
  233. package/packages/web/next.config.ts +0 -7
  234. package/packages/web/package.json +0 -57
  235. package/packages/web/postcss.config.mjs +0 -7
  236. package/packages/web/public/file.svg +0 -1
  237. package/packages/web/public/globe.svg +0 -1
  238. package/packages/web/public/next.svg +0 -1
  239. package/packages/web/public/vercel.svg +0 -1
  240. package/packages/web/public/window.svg +0 -1
  241. package/packages/web/server.ts +0 -312
  242. package/packages/web/tsconfig.json +0 -34
  243. 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>>