@hienlh/ppm 0.1.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.
- package/.claude/agent-memory/tester/MEMORY.md +3 -0
- package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/.env.example +1 -0
- package/.github/workflows/release.yml +46 -0
- package/README.md +349 -0
- package/bun.lock +1217 -0
- package/components.json +21 -0
- package/docs/code-standards.md +574 -0
- package/docs/codebase-summary.md +294 -0
- package/docs/deployment-guide.md +631 -0
- package/docs/design-guidelines.md +661 -0
- package/docs/project-overview-pdr.md +142 -0
- package/docs/project-roadmap.md +400 -0
- package/docs/system-architecture.md +459 -0
- package/package.json +68 -0
- package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
- package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
- package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
- package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
- package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
- package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
- package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
- package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
- package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
- package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
- package/plans/260314-2009-ppm-implementation/plan.md +202 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
- package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
- package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
- package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
- package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
- package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
- package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
- package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
- package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
- package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
- package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
- package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
- package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
- package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
- package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
- package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
- package/ppm.example.yaml +14 -0
- package/repomix-output.xml +23745 -0
- package/scripts/build.ts +13 -0
- package/src/cli/commands/chat-cmd.ts +259 -0
- package/src/cli/commands/config-cmd.ts +121 -0
- package/src/cli/commands/git-cmd.ts +315 -0
- package/src/cli/commands/init.ts +57 -0
- package/src/cli/commands/open.ts +19 -0
- package/src/cli/commands/projects.ts +100 -0
- package/src/cli/commands/start.ts +3 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/utils/project-resolver.ts +27 -0
- package/src/index.ts +59 -0
- package/src/providers/claude-agent-sdk.ts +499 -0
- package/src/providers/claude-binary-finder.ts +256 -0
- package/src/providers/claude-code-cli.ts +413 -0
- package/src/providers/claude-process-registry.ts +106 -0
- package/src/providers/mock-provider.ts +171 -0
- package/src/providers/provider.interface.ts +10 -0
- package/src/providers/registry.ts +45 -0
- package/src/server/helpers/resolve-project.ts +22 -0
- package/src/server/index.ts +181 -0
- package/src/server/middleware/auth.ts +30 -0
- package/src/server/routes/chat.ts +153 -0
- package/src/server/routes/files.ts +168 -0
- package/src/server/routes/git.ts +261 -0
- package/src/server/routes/project-scoped.ts +27 -0
- package/src/server/routes/projects.ts +57 -0
- package/src/server/routes/static.ts +26 -0
- package/src/server/ws/chat.ts +130 -0
- package/src/server/ws/terminal.ts +89 -0
- package/src/services/chat.service.ts +110 -0
- package/src/services/claude-usage.service.ts +113 -0
- package/src/services/config.service.ts +90 -0
- package/src/services/file.service.ts +261 -0
- package/src/services/git-dirs.service.ts +112 -0
- package/src/services/git.service.ts +372 -0
- package/src/services/project.service.ts +107 -0
- package/src/services/slash-items.service.ts +184 -0
- package/src/services/terminal.service.ts +212 -0
- package/src/types/api.ts +37 -0
- package/src/types/chat.ts +92 -0
- package/src/types/config.ts +41 -0
- package/src/types/git.ts +50 -0
- package/src/types/project.ts +18 -0
- package/src/types/terminal.ts +20 -0
- package/src/web/app.tsx +168 -0
- package/src/web/components/auth/login-screen.tsx +88 -0
- package/src/web/components/chat/attachment-chips.tsx +55 -0
- package/src/web/components/chat/chat-placeholder.tsx +10 -0
- package/src/web/components/chat/chat-tab.tsx +301 -0
- package/src/web/components/chat/file-picker.tsx +126 -0
- package/src/web/components/chat/message-input.tsx +420 -0
- package/src/web/components/chat/message-list.tsx +838 -0
- package/src/web/components/chat/session-picker.tsx +139 -0
- package/src/web/components/chat/slash-command-picker.tsx +135 -0
- package/src/web/components/chat/usage-badge.tsx +186 -0
- package/src/web/components/editor/code-editor.tsx +329 -0
- package/src/web/components/editor/diff-viewer.tsx +276 -0
- package/src/web/components/editor/editor-placeholder.tsx +10 -0
- package/src/web/components/explorer/file-actions.tsx +191 -0
- package/src/web/components/explorer/file-tree.tsx +298 -0
- package/src/web/components/git/git-graph.tsx +727 -0
- package/src/web/components/git/git-placeholder.tsx +55 -0
- package/src/web/components/git/git-status-panel.tsx +850 -0
- package/src/web/components/layout/mobile-drawer.tsx +137 -0
- package/src/web/components/layout/mobile-nav.tsx +103 -0
- package/src/web/components/layout/sidebar.tsx +90 -0
- package/src/web/components/layout/tab-bar.tsx +152 -0
- package/src/web/components/layout/tab-content.tsx +85 -0
- package/src/web/components/projects/dir-suggest.tsx +152 -0
- package/src/web/components/projects/project-list.tsx +187 -0
- package/src/web/components/settings/settings-tab.tsx +57 -0
- package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
- package/src/web/components/terminal/terminal-tab.tsx +133 -0
- package/src/web/components/ui/button.tsx +64 -0
- package/src/web/components/ui/context-menu.tsx +250 -0
- package/src/web/components/ui/dialog.tsx +156 -0
- package/src/web/components/ui/dropdown-menu.tsx +257 -0
- package/src/web/components/ui/input.tsx +21 -0
- package/src/web/components/ui/scroll-area.tsx +56 -0
- package/src/web/components/ui/separator.tsx +26 -0
- package/src/web/components/ui/sonner.tsx +40 -0
- package/src/web/components/ui/tabs.tsx +91 -0
- package/src/web/components/ui/tooltip.tsx +57 -0
- package/src/web/hooks/use-chat.ts +420 -0
- package/src/web/hooks/use-terminal.ts +182 -0
- package/src/web/hooks/use-url-sync.ts +66 -0
- package/src/web/hooks/use-websocket.ts +48 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/api-client.ts +90 -0
- package/src/web/lib/file-support.ts +68 -0
- package/src/web/lib/utils.ts +6 -0
- package/src/web/lib/ws-client.ts +100 -0
- package/src/web/main.tsx +10 -0
- package/src/web/public/icon-192.svg +5 -0
- package/src/web/public/icon-512.svg +5 -0
- package/src/web/stores/file-store.ts +81 -0
- package/src/web/stores/project-store.ts +50 -0
- package/src/web/stores/settings-store.ts +65 -0
- package/src/web/stores/tab-store.ts +187 -0
- package/src/web/styles/globals.css +227 -0
- package/src/web/vite-env.d.ts +1 -0
- package/tests/integration/api/chat-routes.test.ts +95 -0
- package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
- package/tests/integration/ws/chat-websocket.test.ts +312 -0
- package/tests/test-setup.ts +5 -0
- package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
- package/tests/unit/providers/mock-provider.test.ts +143 -0
- package/tests/unit/services/chat-service.test.ts +100 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +62 -0
package/components.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/web/styles/globals.css",
|
|
9
|
+
"baseColor": "slate",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide"
|
|
21
|
+
}
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# PPM Code Standards & Conventions
|
|
2
|
+
|
|
3
|
+
## File Naming
|
|
4
|
+
|
|
5
|
+
| File Type | Convention | Example | Purpose |
|
|
6
|
+
|-----------|-----------|---------|---------|
|
|
7
|
+
| CLI commands | kebab-case | `start-cmd.ts`, `init.ts` | Descriptive command names |
|
|
8
|
+
| Services | kebab-case | `chat.service.ts`, `file.service.ts` | `{feature}.service.ts` pattern |
|
|
9
|
+
| Providers | kebab-case | `claude-agent-sdk.ts`, `mock-provider.ts` | `{name}-provider.ts` or `{name}.ts` |
|
|
10
|
+
| Routes | kebab-case | `chat.ts`, `project-scoped.ts` | Describe HTTP route group |
|
|
11
|
+
| WebSocket | kebab-case | `chat.ts`, `terminal.ts` | Match feature area |
|
|
12
|
+
| Components | PascalCase | `ChatTab.tsx`, `FileTree.tsx` | React convention |
|
|
13
|
+
| Hooks | camelCase with `use` prefix | `useChat.ts`, `useTerminal.ts` | React hook convention |
|
|
14
|
+
| Stores | kebab-case | `chat-store.ts`, `project-store.ts` | Zustand store files |
|
|
15
|
+
| Utilities | kebab-case | `utils.ts`, `file-support.ts` | Grouped by function |
|
|
16
|
+
| Types | kebab-case | `api.ts`, `chat.ts` | Group related types together |
|
|
17
|
+
| Tests | kebab-case with `.test.ts` | `chat.service.test.ts` | Match source file name |
|
|
18
|
+
|
|
19
|
+
## TypeScript Conventions
|
|
20
|
+
|
|
21
|
+
### Strict Mode
|
|
22
|
+
All files use TypeScript strict mode (`tsconfig.json` `"strict": true`).
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Required:
|
|
26
|
+
- Explicit return types on functions
|
|
27
|
+
- No `any` types (use `unknown` if necessary)
|
|
28
|
+
- No implicit `any` parameters
|
|
29
|
+
- Exhaustive type checking (switch, conditionals)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Path Aliases
|
|
33
|
+
Use `@/*` alias for web layer imports (configured in `tsconfig.json`):
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Good
|
|
37
|
+
import { useChat } from "@/hooks/use-chat";
|
|
38
|
+
import { chatStore } from "@/stores/chat-store";
|
|
39
|
+
|
|
40
|
+
// Avoid
|
|
41
|
+
import { useChat } from "../../hooks/use-chat";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Type Definitions
|
|
45
|
+
Place types near usage. Group related types in single files:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Good: src/types/chat.ts
|
|
49
|
+
export interface Session { id: string; title: string; }
|
|
50
|
+
export interface Message { id: string; content: string; role: "user" | "assistant"; }
|
|
51
|
+
export type ChatEvent = { type: "text"; content: string } | { type: "done" };
|
|
52
|
+
|
|
53
|
+
// Avoid: spread across separate files
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Enums & Unions
|
|
57
|
+
Prefer discriminated unions over enums for better tree-shaking:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Good: Discriminated union
|
|
61
|
+
type ChatEvent =
|
|
62
|
+
| { type: "text"; content: string }
|
|
63
|
+
| { type: "tool_use"; tool: string; input: unknown }
|
|
64
|
+
| { type: "done" };
|
|
65
|
+
|
|
66
|
+
// Avoid: Enum
|
|
67
|
+
enum MessageType {
|
|
68
|
+
TEXT,
|
|
69
|
+
TOOL_USE,
|
|
70
|
+
DONE,
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Async/Await
|
|
75
|
+
Always use `async`/`await` over Promise chains. Use async generators for streaming:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Good: Async generator for streaming
|
|
79
|
+
async *streamMessages(input: string) {
|
|
80
|
+
for await (const event of provider.sendMessage(input)) {
|
|
81
|
+
yield event;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Avoid: Promise chains
|
|
86
|
+
provider.sendMessage(input).then(...)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Error Handling
|
|
90
|
+
Use try-catch for async operations. Throw structured errors:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Good
|
|
94
|
+
try {
|
|
95
|
+
const file = await FileService.read(path);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
98
|
+
console.error(`Failed to read ${path}: ${message}`);
|
|
99
|
+
throw new Error(`FileService.read failed: ${message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Avoid: Silent failures
|
|
103
|
+
const file = await FileService.read(path).catch(() => null);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Component Patterns
|
|
107
|
+
|
|
108
|
+
### React Components
|
|
109
|
+
Use functional components with hooks. Keep components focused:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Good: Single responsibility
|
|
113
|
+
export function ChatTab() {
|
|
114
|
+
const { messages, sendMessage } = useChat();
|
|
115
|
+
return <div>/* Chat UI */</div>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Avoid: God component
|
|
119
|
+
export function ChatTab() {
|
|
120
|
+
// File management, git status, terminal, chat
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Zustand Stores
|
|
125
|
+
Define stores as singleton exports. Use selectors to subscribe to specific state:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Good: src/web/stores/chat-store.ts
|
|
129
|
+
export const chatStore = create<ChatState>((set) => ({
|
|
130
|
+
messages: [],
|
|
131
|
+
addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// Usage with selector (avoids full re-render)
|
|
135
|
+
const messages = chatStore((state) => state.messages);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Custom Hooks
|
|
139
|
+
Extract logic into hooks for reusability. Return stable references:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Good: useChat hook
|
|
143
|
+
export function useChat() {
|
|
144
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
145
|
+
|
|
146
|
+
const sendMessage = useCallback(async (text: string) => {
|
|
147
|
+
// Send logic
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
return { messages, sendMessage };
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Lazy-Loaded Tab Content
|
|
155
|
+
Use React.lazy() for code splitting:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Good
|
|
159
|
+
const ChatTab = lazy(() => import("./chat-tab").then(m => ({ default: m.ChatTab })));
|
|
160
|
+
|
|
161
|
+
// In component
|
|
162
|
+
<Suspense fallback={<Spinner />}>
|
|
163
|
+
<ChatTab />
|
|
164
|
+
</Suspense>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Service Patterns
|
|
168
|
+
|
|
169
|
+
### Singleton Services
|
|
170
|
+
Services are singletons exported as functions or instances:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Good: services/chat.service.ts
|
|
174
|
+
export async function createSession(projectPath: string): Promise<Session> {
|
|
175
|
+
// Shared logic across all callers
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Good: services/config.service.ts
|
|
179
|
+
export const ConfigService = {
|
|
180
|
+
load: () => YAML.parse(configFile),
|
|
181
|
+
save: (config) => YAML.stringify(config),
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Dependency Injection
|
|
186
|
+
Services should receive dependencies as parameters or imports:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Good: Pass dependencies explicitly
|
|
190
|
+
export async function streamChat(
|
|
191
|
+
session: Session,
|
|
192
|
+
message: string,
|
|
193
|
+
provider: AIProvider, // Dependency
|
|
194
|
+
) {
|
|
195
|
+
// Use provider
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Avoid: Implicit globals
|
|
199
|
+
import { globalProvider } from "./global"; // Hidden dependency
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Error Propagation
|
|
203
|
+
Services throw descriptive errors; routes catch and format:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Good: Service throws
|
|
207
|
+
export function validatePath(path: string) {
|
|
208
|
+
if (path.includes("..")) {
|
|
209
|
+
throw new Error(`Path traversal detected: ${path}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Good: Route catches and formats
|
|
214
|
+
try {
|
|
215
|
+
const content = await FileService.read(path);
|
|
216
|
+
res.json(ok(content));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const msg = error instanceof Error ? error.message : "Unknown";
|
|
219
|
+
res.json(err(msg));
|
|
220
|
+
res.status(400);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## API Conventions
|
|
225
|
+
|
|
226
|
+
### Response Envelope
|
|
227
|
+
All REST responses use the `ApiResponse<T>` envelope:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// Good
|
|
231
|
+
{ ok: true, data: { /* payload */ } }
|
|
232
|
+
{ ok: false, error: "descriptive error message" }
|
|
233
|
+
|
|
234
|
+
// Avoid: Inconsistent shapes
|
|
235
|
+
{ success: true, result: { } }
|
|
236
|
+
{ error: "error message" } // No shape contract
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Project-Scoped Routes
|
|
240
|
+
All project-specific endpoints use the pattern `/api/project/:name/*`:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
GET /api/projects # List all projects
|
|
244
|
+
POST /api/projects # Create project
|
|
245
|
+
DELETE /api/projects/:name # Delete project
|
|
246
|
+
GET /api/project/:name/chat/... # Chat (project-scoped)
|
|
247
|
+
GET /api/project/:name/git/... # Git (project-scoped)
|
|
248
|
+
GET /api/project/:name/files/... # Files (project-scoped)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### WebSocket Message Formats
|
|
252
|
+
Structure WebSocket messages as typed JSON objects:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// Client -> Server (chat)
|
|
256
|
+
{ type: "message"; content: string }
|
|
257
|
+
{ type: "cancel" }
|
|
258
|
+
{ type: "approval_response"; requestId: string; approved: boolean }
|
|
259
|
+
|
|
260
|
+
// Server -> Client (chat)
|
|
261
|
+
{ type: "text"; content: string }
|
|
262
|
+
{ type: "tool_use"; tool: string; input: unknown }
|
|
263
|
+
{ type: "approval_request"; requestId: string; tool: string; input: unknown }
|
|
264
|
+
{ type: "done"; sessionId: string }
|
|
265
|
+
{ type: "error"; message: string }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Status Codes
|
|
269
|
+
Use standard HTTP status codes:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Success
|
|
273
|
+
200 OK - GET successful, POST/PUT/DELETE with response body
|
|
274
|
+
201 Created - POST created resource
|
|
275
|
+
204 No Content - DELETE successful
|
|
276
|
+
|
|
277
|
+
// Client error
|
|
278
|
+
400 Bad Request - Invalid input, validation failure
|
|
279
|
+
401 Unauthorized - Missing/invalid auth token
|
|
280
|
+
403 Forbidden - Authenticated but not authorized (rare in PPM)
|
|
281
|
+
404 Not Found - Project/file/session not found
|
|
282
|
+
|
|
283
|
+
// Server error
|
|
284
|
+
500 Internal Error - Unexpected exception
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Import/Export Conventions
|
|
288
|
+
|
|
289
|
+
### Named Exports (Preferred)
|
|
290
|
+
Use named exports for better tree-shaking and clarity:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// Good: services/file.service.ts
|
|
294
|
+
export async function read(path: string): Promise<string> { }
|
|
295
|
+
export async function write(path: string, content: string): Promise<void> { }
|
|
296
|
+
|
|
297
|
+
// Usage
|
|
298
|
+
import { read, write } from "./services/file.service";
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Default Exports (React Components Only)
|
|
302
|
+
Use default exports for React components (enables lazy loading):
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Good: components/chat/chat-tab.tsx
|
|
306
|
+
export default function ChatTab() { }
|
|
307
|
+
|
|
308
|
+
// Usage
|
|
309
|
+
const ChatTab = lazy(() => import("./components/chat/chat-tab"));
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Wildcard Imports (Avoid)
|
|
313
|
+
Avoid wildcard imports except for types:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// Good: Explicit imports
|
|
317
|
+
import { send, receive } from "./ws-client";
|
|
318
|
+
|
|
319
|
+
// Good: Type wildcard (rare)
|
|
320
|
+
import type * as Types from "./types";
|
|
321
|
+
|
|
322
|
+
// Avoid: Implicit exports
|
|
323
|
+
import * as WsClient from "./ws-client";
|
|
324
|
+
WsClient.send(); // Unclear what's exported
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Error Handling Patterns
|
|
328
|
+
|
|
329
|
+
### Service Layer
|
|
330
|
+
Throw descriptive errors with context:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// Good
|
|
334
|
+
throw new Error(`GitService.commit failed: ${error.message}`);
|
|
335
|
+
throw new Error(`FileService: path traversal detected: ${path}`);
|
|
336
|
+
|
|
337
|
+
// Avoid: Generic errors
|
|
338
|
+
throw new Error("Failed");
|
|
339
|
+
throw error; // Re-throw loses context
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Route Layer
|
|
343
|
+
Catch, format, and return `ApiResponse` with error:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// Good
|
|
347
|
+
try {
|
|
348
|
+
const result = await service.doSomething();
|
|
349
|
+
return res.json(ok(result));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
352
|
+
res.status(400);
|
|
353
|
+
return res.json(err(message));
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Component Layer
|
|
358
|
+
Handle errors from API calls with user-friendly messages:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// Good
|
|
362
|
+
try {
|
|
363
|
+
const response = await api.post("/...");
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
setError(response.error);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
setState(response.data);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
setError("Network error. Please try again.");
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Testing Conventions
|
|
375
|
+
|
|
376
|
+
### Test File Location
|
|
377
|
+
Tests colocate with source files or in `tests/` directory:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
src/services/chat.service.ts
|
|
381
|
+
tests/unit/services/chat.service.test.ts ← Match path, add .test suffix
|
|
382
|
+
|
|
383
|
+
src/web/hooks/use-chat.ts
|
|
384
|
+
tests/unit/hooks/use-chat.test.ts
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Test Structure
|
|
388
|
+
Use AAA pattern (Arrange, Act, Assert):
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
describe("ChatService", () => {
|
|
392
|
+
it("should create session with unique ID", () => {
|
|
393
|
+
// Arrange
|
|
394
|
+
const projectPath = "/tmp/project";
|
|
395
|
+
|
|
396
|
+
// Act
|
|
397
|
+
const session = ChatService.createSession(projectPath);
|
|
398
|
+
const session2 = ChatService.createSession(projectPath);
|
|
399
|
+
|
|
400
|
+
// Assert
|
|
401
|
+
expect(session.id).not.toBe(session2.id);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Mocking
|
|
407
|
+
Mock external dependencies (providers, file system):
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// Good: Mock provider
|
|
411
|
+
const mockProvider = {
|
|
412
|
+
createSession: () => ({ id: "test-id" }),
|
|
413
|
+
sendMessage: async function*() { yield { type: "text", content: "response" }; },
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Avoid: Mock implementation details
|
|
417
|
+
jest.spyOn(fs, "readFile").mockResolvedValue("content");
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Security Conventions
|
|
421
|
+
|
|
422
|
+
### Path Traversal Protection
|
|
423
|
+
Always validate file paths before operations:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// Good: Validate before file access
|
|
427
|
+
export function validatePath(path: string) {
|
|
428
|
+
const normalized = Path.normalize(path);
|
|
429
|
+
if (normalized.startsWith("..")) {
|
|
430
|
+
throw new Error("Path traversal detected");
|
|
431
|
+
}
|
|
432
|
+
if (!normalized.startsWith(projectPath)) {
|
|
433
|
+
throw new Error("Access denied: outside project directory");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Token-Based Auth
|
|
439
|
+
Every API route requires token validation via middleware:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// Good: Middleware validates token
|
|
443
|
+
app.use("/api", authMiddleware);
|
|
444
|
+
|
|
445
|
+
// In middleware
|
|
446
|
+
const token = req.header("Authorization");
|
|
447
|
+
if (!token || token !== config.auth.token) {
|
|
448
|
+
res.status(401);
|
|
449
|
+
res.json(err("Unauthorized"));
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Input Validation
|
|
454
|
+
Validate all user input (file paths, command arguments, message content):
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// Good: Validate before processing
|
|
458
|
+
if (!path || typeof path !== "string") {
|
|
459
|
+
throw new Error("Invalid path");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (message.length > 10000) {
|
|
463
|
+
throw new Error("Message too long");
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Documentation Conventions
|
|
468
|
+
|
|
469
|
+
### Inline Comments
|
|
470
|
+
Use comments for **why**, not **what**. Let code speak for itself:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// Good: Explain intent
|
|
474
|
+
// Expand node lazily to avoid blocking on large directories
|
|
475
|
+
async function loadChildren(node: TreeNode) {
|
|
476
|
+
// ...
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Avoid: Obvious comments
|
|
480
|
+
// Set messages to empty array
|
|
481
|
+
const [messages, setMessages] = useState([]);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### JSDoc for Public APIs
|
|
485
|
+
Document exported functions with JSDoc:
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
/**
|
|
489
|
+
* Stream chat messages from AI provider.
|
|
490
|
+
*
|
|
491
|
+
* @param sessionId - Chat session ID
|
|
492
|
+
* @param message - User message text
|
|
493
|
+
* @param provider - AI provider (defaults to registry.default)
|
|
494
|
+
* @yields ChatEvent objects (text, tool_use, approval_request, done)
|
|
495
|
+
* @throws Error if session not found or provider fails
|
|
496
|
+
*/
|
|
497
|
+
export async *streamMessages(
|
|
498
|
+
sessionId: string,
|
|
499
|
+
message: string,
|
|
500
|
+
provider?: AIProvider,
|
|
501
|
+
) {
|
|
502
|
+
// ...
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Type Comments
|
|
507
|
+
Use type comments for complex types:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// File status with git tracking info
|
|
511
|
+
type FileStatus =
|
|
512
|
+
| { status: "modified" }
|
|
513
|
+
| { status: "untracked" }
|
|
514
|
+
| { status: "staged"; originalPath?: string };
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Performance Conventions
|
|
518
|
+
|
|
519
|
+
### Code Splitting
|
|
520
|
+
Use React.lazy() for routes and heavy components:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
// Good: Lazy-load terminal component
|
|
524
|
+
const TerminalTab = lazy(() => import("./terminal-tab"));
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Memoization
|
|
528
|
+
Memoize expensive computations and callbacks:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// Good: Memoize filter result
|
|
532
|
+
const filteredFiles = useMemo(
|
|
533
|
+
() => files.filter(f => f.name.includes(query)),
|
|
534
|
+
[files, query]
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// Good: Stable callback reference
|
|
538
|
+
const handleClick = useCallback(() => {
|
|
539
|
+
// ...
|
|
540
|
+
}, [dependencies]);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Bundle Analysis
|
|
544
|
+
Monitor bundle size growth:
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
# Check bundle stats
|
|
548
|
+
bun run build && ls -lh dist/web/assets/
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Git Conventions
|
|
552
|
+
|
|
553
|
+
### Commit Messages
|
|
554
|
+
Use conventional commit format:
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
feat: add file attachment support to chat
|
|
558
|
+
fix: resolve WebSocket reconnection issue
|
|
559
|
+
refactor: simplify GitService.status method
|
|
560
|
+
docs: update deployment guide
|
|
561
|
+
test: add chat-service unit tests
|
|
562
|
+
chore: upgrade TypeScript to 5.9.3
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Branch Names
|
|
566
|
+
Use descriptive kebab-case names:
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
feature/chat-file-attachments
|
|
570
|
+
fix/websocket-reconnect
|
|
571
|
+
refactor/service-layer-cleanup
|
|
572
|
+
docs/deployment-guide
|
|
573
|
+
```
|
|
574
|
+
|