prjct-cli 0.10.14 → 0.11.1

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 (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. package/templates/commands/undo.md +152 -0
@@ -0,0 +1,204 @@
1
+ // src/schemas.ts
2
+ import { z } from "zod";
3
+ var SessionMetricsSchema = z.object({
4
+ filesChanged: z.number().default(0),
5
+ linesAdded: z.number().default(0),
6
+ linesRemoved: z.number().default(0),
7
+ commits: z.number().default(0),
8
+ snapshots: z.array(z.string()).default([])
9
+ });
10
+ var TimelineEventSchema = z.object({
11
+ type: z.enum(["start", "pause", "resume", "complete", "snapshot"]),
12
+ at: z.string().datetime(),
13
+ data: z.record(z.unknown()).optional()
14
+ });
15
+ var SessionSchema = z.object({
16
+ id: z.string().regex(/^sess_[a-z0-9]{8}$/),
17
+ projectId: z.string(),
18
+ task: z.string().min(1),
19
+ status: z.enum(["active", "paused", "completed"]),
20
+ startedAt: z.string().datetime(),
21
+ pausedAt: z.string().datetime().nullable(),
22
+ completedAt: z.string().datetime().nullable(),
23
+ duration: z.number().min(0),
24
+ metrics: SessionMetricsSchema,
25
+ timeline: z.array(TimelineEventSchema)
26
+ });
27
+ var TaskSchema = z.object({
28
+ id: z.string(),
29
+ title: z.string().min(1),
30
+ description: z.string().optional(),
31
+ status: z.enum(["pending", "in_progress", "completed", "blocked"]),
32
+ priority: z.enum(["low", "medium", "high", "critical"]),
33
+ createdAt: z.string().datetime(),
34
+ completedAt: z.string().datetime().optional(),
35
+ duration: z.number().optional(),
36
+ tags: z.array(z.string()).optional()
37
+ });
38
+ var IdeaSchema = z.object({
39
+ id: z.string(),
40
+ content: z.string().min(1),
41
+ capturedAt: z.string().datetime(),
42
+ source: z.string().optional(),
43
+ promoted: z.boolean().optional(),
44
+ promotedTo: z.string().optional()
45
+ });
46
+ var FeatureSchema = z.object({
47
+ id: z.string(),
48
+ title: z.string().min(1),
49
+ description: z.string().optional(),
50
+ status: z.enum(["planned", "in_progress", "shipped", "cancelled"]),
51
+ priority: z.number().min(1),
52
+ createdAt: z.string().datetime(),
53
+ shippedAt: z.string().datetime().optional(),
54
+ tasks: z.array(TaskSchema).optional(),
55
+ version: z.string().optional()
56
+ });
57
+ var ProjectConfigSchema = z.object({
58
+ projectId: z.string(),
59
+ name: z.string().optional(),
60
+ plugins: z.array(z.string()).optional()
61
+ }).passthrough();
62
+ var WSInputMessageSchema = z.object({
63
+ type: z.literal("input"),
64
+ payload: z.object({
65
+ data: z.string()
66
+ }),
67
+ timestamp: z.string().datetime()
68
+ });
69
+ var WSResizeMessageSchema = z.object({
70
+ type: z.literal("resize"),
71
+ payload: z.object({
72
+ cols: z.number().min(1),
73
+ rows: z.number().min(1)
74
+ }),
75
+ timestamp: z.string().datetime()
76
+ });
77
+ var WSMessageSchema = z.discriminatedUnion("type", [
78
+ WSInputMessageSchema,
79
+ WSResizeMessageSchema
80
+ ]);
81
+ var CreateSessionRequestSchema = z.object({
82
+ task: z.string().min(1).max(200),
83
+ projectId: z.string()
84
+ });
85
+ var CreateTaskRequestSchema = z.object({
86
+ title: z.string().min(1).max(200),
87
+ description: z.string().max(1e3).optional(),
88
+ priority: z.enum(["low", "medium", "high", "critical"]).default("medium")
89
+ });
90
+ var CaptureIdeaRequestSchema = z.object({
91
+ content: z.string().min(1).max(500),
92
+ source: z.string().optional()
93
+ });
94
+
95
+ // src/utils.ts
96
+ function generateSessionId() {
97
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
98
+ let id = "sess_";
99
+ for (let i = 0; i < 8; i++) {
100
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
101
+ }
102
+ return id;
103
+ }
104
+ function generateId(prefix = "id") {
105
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
106
+ let id = `${prefix}_`;
107
+ for (let i = 0; i < 8; i++) {
108
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
109
+ }
110
+ return id;
111
+ }
112
+ function formatDuration(seconds) {
113
+ if (seconds < 60) return `${seconds}s`;
114
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
115
+ const hours = Math.floor(seconds / 3600);
116
+ const minutes = Math.round(seconds % 3600 / 60);
117
+ if (minutes === 0) return `${hours}h`;
118
+ return `${hours}h ${minutes}m`;
119
+ }
120
+ function parseDuration(duration) {
121
+ const match = duration.match(/(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/);
122
+ if (!match) return 0;
123
+ const hours = parseInt(match[1] || "0", 10);
124
+ const minutes = parseInt(match[2] || "0", 10);
125
+ const secs = parseInt(match[3] || "0", 10);
126
+ return hours * 3600 + minutes * 60 + secs;
127
+ }
128
+ function getRelativeTime(date) {
129
+ const now = /* @__PURE__ */ new Date();
130
+ const then = typeof date === "string" ? new Date(date) : date;
131
+ const diffMs = now.getTime() - then.getTime();
132
+ const diffSecs = Math.floor(diffMs / 1e3);
133
+ const diffMins = Math.floor(diffSecs / 60);
134
+ const diffHours = Math.floor(diffMins / 60);
135
+ const diffDays = Math.floor(diffHours / 24);
136
+ if (diffSecs < 60) return "just now";
137
+ if (diffMins < 60) return `${diffMins}m ago`;
138
+ if (diffHours < 24) return `${diffHours}h ago`;
139
+ if (diffDays === 1) return "yesterday";
140
+ if (diffDays < 7) return `${diffDays}d ago`;
141
+ return then.toLocaleDateString();
142
+ }
143
+ function getTimestamp() {
144
+ return (/* @__PURE__ */ new Date()).toISOString();
145
+ }
146
+ function getDate() {
147
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
148
+ }
149
+ function getYearMonth() {
150
+ const now = /* @__PURE__ */ new Date();
151
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
152
+ }
153
+ function safeJsonParse(json, fallback) {
154
+ try {
155
+ return JSON.parse(json);
156
+ } catch {
157
+ return fallback;
158
+ }
159
+ }
160
+ function truncate(str, maxLength) {
161
+ if (str.length <= maxLength) return str;
162
+ return str.slice(0, maxLength - 3) + "...";
163
+ }
164
+ function slugify(str) {
165
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
166
+ }
167
+ function deepClone(obj) {
168
+ return JSON.parse(JSON.stringify(obj));
169
+ }
170
+ function isNode() {
171
+ return typeof process !== "undefined" && process.versions?.node != null;
172
+ }
173
+ function isBrowser() {
174
+ return typeof window !== "undefined";
175
+ }
176
+ export {
177
+ CaptureIdeaRequestSchema,
178
+ CreateSessionRequestSchema,
179
+ CreateTaskRequestSchema,
180
+ FeatureSchema,
181
+ IdeaSchema,
182
+ ProjectConfigSchema,
183
+ SessionMetricsSchema,
184
+ SessionSchema,
185
+ TaskSchema,
186
+ TimelineEventSchema,
187
+ WSInputMessageSchema,
188
+ WSMessageSchema,
189
+ WSResizeMessageSchema,
190
+ deepClone,
191
+ formatDuration,
192
+ generateId,
193
+ generateSessionId,
194
+ getDate,
195
+ getRelativeTime,
196
+ getTimestamp,
197
+ getYearMonth,
198
+ isBrowser,
199
+ isNode,
200
+ parseDuration,
201
+ safeJsonParse,
202
+ slugify,
203
+ truncate
204
+ };
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@prjct/shared",
3
+ "version": "0.1.0",
4
+ "description": "Shared types and utilities for prjct",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format esm --dts",
17
+ "dev": "tsup src/index.ts --format esm --dts --watch",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src"
20
+ },
21
+ "dependencies": {
22
+ "zod": "^3.23.8"
23
+ },
24
+ "devDependencies": {
25
+ "esbuild": "^0.25.0",
26
+ "tsup": "^8.0.2",
27
+ "typescript": "^5.4.5"
28
+ }
29
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @prjct/shared - Shared Types and Utilities
3
+ *
4
+ * Types and schemas shared between CLI, server, and web.
5
+ */
6
+
7
+ export * from './types'
8
+ export * from './schemas'
9
+ export * from './utils'
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Zod Schemas for validation
3
+ */
4
+
5
+ import { z } from 'zod'
6
+
7
+ // Session Schemas
8
+ export const SessionMetricsSchema = z.object({
9
+ filesChanged: z.number().default(0),
10
+ linesAdded: z.number().default(0),
11
+ linesRemoved: z.number().default(0),
12
+ commits: z.number().default(0),
13
+ snapshots: z.array(z.string()).default([])
14
+ })
15
+
16
+ export const TimelineEventSchema = z.object({
17
+ type: z.enum(['start', 'pause', 'resume', 'complete', 'snapshot']),
18
+ at: z.string().datetime(),
19
+ data: z.record(z.unknown()).optional()
20
+ })
21
+
22
+ export const SessionSchema = z.object({
23
+ id: z.string().regex(/^sess_[a-z0-9]{8}$/),
24
+ projectId: z.string(),
25
+ task: z.string().min(1),
26
+ status: z.enum(['active', 'paused', 'completed']),
27
+ startedAt: z.string().datetime(),
28
+ pausedAt: z.string().datetime().nullable(),
29
+ completedAt: z.string().datetime().nullable(),
30
+ duration: z.number().min(0),
31
+ metrics: SessionMetricsSchema,
32
+ timeline: z.array(TimelineEventSchema)
33
+ })
34
+
35
+ // Task Schemas
36
+ export const TaskSchema = z.object({
37
+ id: z.string(),
38
+ title: z.string().min(1),
39
+ description: z.string().optional(),
40
+ status: z.enum(['pending', 'in_progress', 'completed', 'blocked']),
41
+ priority: z.enum(['low', 'medium', 'high', 'critical']),
42
+ createdAt: z.string().datetime(),
43
+ completedAt: z.string().datetime().optional(),
44
+ duration: z.number().optional(),
45
+ tags: z.array(z.string()).optional()
46
+ })
47
+
48
+ // Idea Schemas
49
+ export const IdeaSchema = z.object({
50
+ id: z.string(),
51
+ content: z.string().min(1),
52
+ capturedAt: z.string().datetime(),
53
+ source: z.string().optional(),
54
+ promoted: z.boolean().optional(),
55
+ promotedTo: z.string().optional()
56
+ })
57
+
58
+ // Feature Schemas
59
+ export const FeatureSchema = z.object({
60
+ id: z.string(),
61
+ title: z.string().min(1),
62
+ description: z.string().optional(),
63
+ status: z.enum(['planned', 'in_progress', 'shipped', 'cancelled']),
64
+ priority: z.number().min(1),
65
+ createdAt: z.string().datetime(),
66
+ shippedAt: z.string().datetime().optional(),
67
+ tasks: z.array(TaskSchema).optional(),
68
+ version: z.string().optional()
69
+ })
70
+
71
+ // Project Config Schema
72
+ export const ProjectConfigSchema = z.object({
73
+ projectId: z.string(),
74
+ name: z.string().optional(),
75
+ plugins: z.array(z.string()).optional()
76
+ }).passthrough()
77
+
78
+ // WebSocket Message Schemas
79
+ export const WSInputMessageSchema = z.object({
80
+ type: z.literal('input'),
81
+ payload: z.object({
82
+ data: z.string()
83
+ }),
84
+ timestamp: z.string().datetime()
85
+ })
86
+
87
+ export const WSResizeMessageSchema = z.object({
88
+ type: z.literal('resize'),
89
+ payload: z.object({
90
+ cols: z.number().min(1),
91
+ rows: z.number().min(1)
92
+ }),
93
+ timestamp: z.string().datetime()
94
+ })
95
+
96
+ export const WSMessageSchema = z.discriminatedUnion('type', [
97
+ WSInputMessageSchema,
98
+ WSResizeMessageSchema
99
+ ])
100
+
101
+ // API Request Schemas
102
+ export const CreateSessionRequestSchema = z.object({
103
+ task: z.string().min(1).max(200),
104
+ projectId: z.string()
105
+ })
106
+
107
+ export const CreateTaskRequestSchema = z.object({
108
+ title: z.string().min(1).max(200),
109
+ description: z.string().max(1000).optional(),
110
+ priority: z.enum(['low', 'medium', 'high', 'critical']).default('medium')
111
+ })
112
+
113
+ export const CaptureIdeaRequestSchema = z.object({
114
+ content: z.string().min(1).max(500),
115
+ source: z.string().optional()
116
+ })
117
+
118
+ // Inferred Types
119
+ export type SessionInput = z.infer<typeof SessionSchema>
120
+ export type TaskInput = z.infer<typeof TaskSchema>
121
+ export type IdeaInput = z.infer<typeof IdeaSchema>
122
+ export type FeatureInput = z.infer<typeof FeatureSchema>
123
+ export type ProjectConfigInput = z.infer<typeof ProjectConfigSchema>
124
+ export type WSMessageInput = z.infer<typeof WSMessageSchema>
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Core Types for prjct
3
+ */
4
+
5
+ // Session Types
6
+ export interface Session {
7
+ id: string
8
+ projectId: string
9
+ task: string
10
+ status: 'active' | 'paused' | 'completed'
11
+ startedAt: string
12
+ pausedAt: string | null
13
+ completedAt: string | null
14
+ duration: number
15
+ metrics: SessionMetrics
16
+ timeline: TimelineEvent[]
17
+ }
18
+
19
+ export interface SessionMetrics {
20
+ filesChanged: number
21
+ linesAdded: number
22
+ linesRemoved: number
23
+ commits: number
24
+ snapshots: string[]
25
+ }
26
+
27
+ export interface TimelineEvent {
28
+ type: 'start' | 'pause' | 'resume' | 'complete' | 'snapshot'
29
+ at: string
30
+ data?: Record<string, unknown>
31
+ }
32
+
33
+ // Snapshot Types
34
+ export interface Snapshot {
35
+ hash: string
36
+ shortHash: string
37
+ message: string
38
+ timestamp: string
39
+ files: string[]
40
+ }
41
+
42
+ // Task Types
43
+ export interface Task {
44
+ id: string
45
+ title: string
46
+ description?: string
47
+ status: 'pending' | 'in_progress' | 'completed' | 'blocked'
48
+ priority: 'low' | 'medium' | 'high' | 'critical'
49
+ createdAt: string
50
+ completedAt?: string
51
+ duration?: number
52
+ tags?: string[]
53
+ }
54
+
55
+ // Idea Types
56
+ export interface Idea {
57
+ id: string
58
+ content: string
59
+ capturedAt: string
60
+ source?: string
61
+ promoted?: boolean
62
+ promotedTo?: string
63
+ }
64
+
65
+ // Feature Types
66
+ export interface Feature {
67
+ id: string
68
+ title: string
69
+ description?: string
70
+ status: 'planned' | 'in_progress' | 'shipped' | 'cancelled'
71
+ priority: number
72
+ createdAt: string
73
+ shippedAt?: string
74
+ tasks?: Task[]
75
+ version?: string
76
+ }
77
+
78
+ // Project Types
79
+ export interface Project {
80
+ id: string
81
+ name: string
82
+ path: string
83
+ createdAt: string
84
+ lastActiveAt: string
85
+ config: ProjectConfig
86
+ }
87
+
88
+ export interface ProjectConfig {
89
+ projectId: string
90
+ name?: string
91
+ plugins?: string[]
92
+ [key: string]: unknown
93
+ }
94
+
95
+ // Metrics Types
96
+ export interface DailyMetrics {
97
+ date: string
98
+ sessions: number
99
+ duration: number
100
+ commits: number
101
+ filesChanged: number
102
+ linesAdded: number
103
+ linesRemoved: number
104
+ }
105
+
106
+ export interface WeeklyMetrics {
107
+ weekStart: string
108
+ weekEnd: string
109
+ totalSessions: number
110
+ totalDuration: number
111
+ averageDuration: number
112
+ tasksCompleted: number
113
+ featuresShipped: number
114
+ productivityScore: number
115
+ streak: number
116
+ byDay: Record<string, DailyMetrics>
117
+ }
118
+
119
+ // WebSocket Message Types
120
+ export interface WSMessage {
121
+ type: string
122
+ payload?: unknown
123
+ timestamp: string
124
+ }
125
+
126
+ export interface WSInputMessage extends WSMessage {
127
+ type: 'input'
128
+ payload: {
129
+ data: string
130
+ }
131
+ }
132
+
133
+ export interface WSOutputMessage extends WSMessage {
134
+ type: 'output'
135
+ payload: {
136
+ data: string
137
+ }
138
+ }
139
+
140
+ export interface WSResizeMessage extends WSMessage {
141
+ type: 'resize'
142
+ payload: {
143
+ cols: number
144
+ rows: number
145
+ }
146
+ }
147
+
148
+ export interface WSStatusMessage extends WSMessage {
149
+ type: 'status'
150
+ payload: {
151
+ status: 'connected' | 'disconnected' | 'error'
152
+ message?: string
153
+ }
154
+ }
155
+
156
+ // API Response Types
157
+ export interface ApiResponse<T = unknown> {
158
+ success: boolean
159
+ data?: T
160
+ error?: string
161
+ timestamp: string
162
+ }
163
+
164
+ // Event Types (for event bus)
165
+ export type EventType =
166
+ | 'session.started'
167
+ | 'session.paused'
168
+ | 'session.resumed'
169
+ | 'session.completed'
170
+ | 'task.created'
171
+ | 'task.completed'
172
+ | 'feature.added'
173
+ | 'feature.shipped'
174
+ | 'idea.captured'
175
+ | 'snapshot.created'
176
+ | 'snapshot.restored'
177
+ | 'git.commit'
178
+ | 'git.push'
179
+ | 'project.init'
180
+ | 'project.sync'
181
+
182
+ export interface EventPayload {
183
+ type: EventType
184
+ timestamp: string
185
+ projectId: string
186
+ data: Record<string, unknown>
187
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Shared Utilities
3
+ */
4
+
5
+ /**
6
+ * Generate a unique session ID
7
+ */
8
+ export function generateSessionId(): string {
9
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
10
+ let id = 'sess_'
11
+ for (let i = 0; i < 8; i++) {
12
+ id += chars.charAt(Math.floor(Math.random() * chars.length))
13
+ }
14
+ return id
15
+ }
16
+
17
+ /**
18
+ * Generate a unique ID with prefix
19
+ */
20
+ export function generateId(prefix: string = 'id'): string {
21
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
22
+ let id = `${prefix}_`
23
+ for (let i = 0; i < 8; i++) {
24
+ id += chars.charAt(Math.floor(Math.random() * chars.length))
25
+ }
26
+ return id
27
+ }
28
+
29
+ /**
30
+ * Format duration in seconds to human readable
31
+ */
32
+ export function formatDuration(seconds: number): string {
33
+ if (seconds < 60) return `${seconds}s`
34
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`
35
+
36
+ const hours = Math.floor(seconds / 3600)
37
+ const minutes = Math.round((seconds % 3600) / 60)
38
+
39
+ if (minutes === 0) return `${hours}h`
40
+ return `${hours}h ${minutes}m`
41
+ }
42
+
43
+ /**
44
+ * Parse duration string to seconds
45
+ */
46
+ export function parseDuration(duration: string): number {
47
+ const match = duration.match(/(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/)
48
+ if (!match) return 0
49
+
50
+ const hours = parseInt(match[1] || '0', 10)
51
+ const minutes = parseInt(match[2] || '0', 10)
52
+ const secs = parseInt(match[3] || '0', 10)
53
+
54
+ return hours * 3600 + minutes * 60 + secs
55
+ }
56
+
57
+ /**
58
+ * Get relative time string
59
+ */
60
+ export function getRelativeTime(date: Date | string): string {
61
+ const now = new Date()
62
+ const then = typeof date === 'string' ? new Date(date) : date
63
+ const diffMs = now.getTime() - then.getTime()
64
+ const diffSecs = Math.floor(diffMs / 1000)
65
+ const diffMins = Math.floor(diffSecs / 60)
66
+ const diffHours = Math.floor(diffMins / 60)
67
+ const diffDays = Math.floor(diffHours / 24)
68
+
69
+ if (diffSecs < 60) return 'just now'
70
+ if (diffMins < 60) return `${diffMins}m ago`
71
+ if (diffHours < 24) return `${diffHours}h ago`
72
+ if (diffDays === 1) return 'yesterday'
73
+ if (diffDays < 7) return `${diffDays}d ago`
74
+
75
+ return then.toLocaleDateString()
76
+ }
77
+
78
+ /**
79
+ * Get ISO timestamp
80
+ */
81
+ export function getTimestamp(): string {
82
+ return new Date().toISOString()
83
+ }
84
+
85
+ /**
86
+ * Get date in YYYY-MM-DD format
87
+ */
88
+ export function getDate(): string {
89
+ return new Date().toISOString().split('T')[0]
90
+ }
91
+
92
+ /**
93
+ * Get year-month in YYYY-MM format
94
+ */
95
+ export function getYearMonth(): string {
96
+ const now = new Date()
97
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
98
+ }
99
+
100
+ /**
101
+ * Safely parse JSON
102
+ */
103
+ export function safeJsonParse<T>(json: string, fallback: T): T {
104
+ try {
105
+ return JSON.parse(json) as T
106
+ } catch {
107
+ return fallback
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Truncate string with ellipsis
113
+ */
114
+ export function truncate(str: string, maxLength: number): string {
115
+ if (str.length <= maxLength) return str
116
+ return str.slice(0, maxLength - 3) + '...'
117
+ }
118
+
119
+ /**
120
+ * Slugify string
121
+ */
122
+ export function slugify(str: string): string {
123
+ return str
124
+ .toLowerCase()
125
+ .replace(/[^a-z0-9]+/g, '-')
126
+ .replace(/^-|-$/g, '')
127
+ }
128
+
129
+ /**
130
+ * Deep clone object
131
+ */
132
+ export function deepClone<T>(obj: T): T {
133
+ return JSON.parse(JSON.stringify(obj))
134
+ }
135
+
136
+ /**
137
+ * Check if running in Node.js
138
+ */
139
+ export function isNode(): boolean {
140
+ return typeof process !== 'undefined' && process.versions?.node != null
141
+ }
142
+
143
+ /**
144
+ * Check if running in browser
145
+ */
146
+ export function isBrowser(): boolean {
147
+ return typeof window !== 'undefined'
148
+ }