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