@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.
Files changed (159) hide show
  1. package/.claude/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/.env.example +1 -0
  4. package/.github/workflows/release.yml +46 -0
  5. package/README.md +349 -0
  6. package/bun.lock +1217 -0
  7. package/components.json +21 -0
  8. package/docs/code-standards.md +574 -0
  9. package/docs/codebase-summary.md +294 -0
  10. package/docs/deployment-guide.md +631 -0
  11. package/docs/design-guidelines.md +661 -0
  12. package/docs/project-overview-pdr.md +142 -0
  13. package/docs/project-roadmap.md +400 -0
  14. package/docs/system-architecture.md +459 -0
  15. package/package.json +68 -0
  16. package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
  17. package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
  18. package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
  19. package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
  20. package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
  21. package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
  22. package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
  23. package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
  24. package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
  25. package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
  26. package/plans/260314-2009-ppm-implementation/plan.md +202 -0
  27. package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
  28. package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
  29. package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
  30. package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
  31. package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
  32. package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
  33. package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
  34. package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
  35. package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
  36. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
  37. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
  38. package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
  39. package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
  40. package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
  41. package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
  42. package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
  43. package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
  44. package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
  45. package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
  46. package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
  47. package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
  48. package/ppm.example.yaml +14 -0
  49. package/repomix-output.xml +23745 -0
  50. package/scripts/build.ts +13 -0
  51. package/src/cli/commands/chat-cmd.ts +259 -0
  52. package/src/cli/commands/config-cmd.ts +121 -0
  53. package/src/cli/commands/git-cmd.ts +315 -0
  54. package/src/cli/commands/init.ts +57 -0
  55. package/src/cli/commands/open.ts +19 -0
  56. package/src/cli/commands/projects.ts +100 -0
  57. package/src/cli/commands/start.ts +3 -0
  58. package/src/cli/commands/stop.ts +33 -0
  59. package/src/cli/utils/project-resolver.ts +27 -0
  60. package/src/index.ts +59 -0
  61. package/src/providers/claude-agent-sdk.ts +499 -0
  62. package/src/providers/claude-binary-finder.ts +256 -0
  63. package/src/providers/claude-code-cli.ts +413 -0
  64. package/src/providers/claude-process-registry.ts +106 -0
  65. package/src/providers/mock-provider.ts +171 -0
  66. package/src/providers/provider.interface.ts +10 -0
  67. package/src/providers/registry.ts +45 -0
  68. package/src/server/helpers/resolve-project.ts +22 -0
  69. package/src/server/index.ts +181 -0
  70. package/src/server/middleware/auth.ts +30 -0
  71. package/src/server/routes/chat.ts +153 -0
  72. package/src/server/routes/files.ts +168 -0
  73. package/src/server/routes/git.ts +261 -0
  74. package/src/server/routes/project-scoped.ts +27 -0
  75. package/src/server/routes/projects.ts +57 -0
  76. package/src/server/routes/static.ts +26 -0
  77. package/src/server/ws/chat.ts +130 -0
  78. package/src/server/ws/terminal.ts +89 -0
  79. package/src/services/chat.service.ts +110 -0
  80. package/src/services/claude-usage.service.ts +113 -0
  81. package/src/services/config.service.ts +90 -0
  82. package/src/services/file.service.ts +261 -0
  83. package/src/services/git-dirs.service.ts +112 -0
  84. package/src/services/git.service.ts +372 -0
  85. package/src/services/project.service.ts +107 -0
  86. package/src/services/slash-items.service.ts +184 -0
  87. package/src/services/terminal.service.ts +212 -0
  88. package/src/types/api.ts +37 -0
  89. package/src/types/chat.ts +92 -0
  90. package/src/types/config.ts +41 -0
  91. package/src/types/git.ts +50 -0
  92. package/src/types/project.ts +18 -0
  93. package/src/types/terminal.ts +20 -0
  94. package/src/web/app.tsx +168 -0
  95. package/src/web/components/auth/login-screen.tsx +88 -0
  96. package/src/web/components/chat/attachment-chips.tsx +55 -0
  97. package/src/web/components/chat/chat-placeholder.tsx +10 -0
  98. package/src/web/components/chat/chat-tab.tsx +301 -0
  99. package/src/web/components/chat/file-picker.tsx +126 -0
  100. package/src/web/components/chat/message-input.tsx +420 -0
  101. package/src/web/components/chat/message-list.tsx +838 -0
  102. package/src/web/components/chat/session-picker.tsx +139 -0
  103. package/src/web/components/chat/slash-command-picker.tsx +135 -0
  104. package/src/web/components/chat/usage-badge.tsx +186 -0
  105. package/src/web/components/editor/code-editor.tsx +329 -0
  106. package/src/web/components/editor/diff-viewer.tsx +276 -0
  107. package/src/web/components/editor/editor-placeholder.tsx +10 -0
  108. package/src/web/components/explorer/file-actions.tsx +191 -0
  109. package/src/web/components/explorer/file-tree.tsx +298 -0
  110. package/src/web/components/git/git-graph.tsx +727 -0
  111. package/src/web/components/git/git-placeholder.tsx +55 -0
  112. package/src/web/components/git/git-status-panel.tsx +850 -0
  113. package/src/web/components/layout/mobile-drawer.tsx +137 -0
  114. package/src/web/components/layout/mobile-nav.tsx +103 -0
  115. package/src/web/components/layout/sidebar.tsx +90 -0
  116. package/src/web/components/layout/tab-bar.tsx +152 -0
  117. package/src/web/components/layout/tab-content.tsx +85 -0
  118. package/src/web/components/projects/dir-suggest.tsx +152 -0
  119. package/src/web/components/projects/project-list.tsx +187 -0
  120. package/src/web/components/settings/settings-tab.tsx +57 -0
  121. package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
  122. package/src/web/components/terminal/terminal-tab.tsx +133 -0
  123. package/src/web/components/ui/button.tsx +64 -0
  124. package/src/web/components/ui/context-menu.tsx +250 -0
  125. package/src/web/components/ui/dialog.tsx +156 -0
  126. package/src/web/components/ui/dropdown-menu.tsx +257 -0
  127. package/src/web/components/ui/input.tsx +21 -0
  128. package/src/web/components/ui/scroll-area.tsx +56 -0
  129. package/src/web/components/ui/separator.tsx +26 -0
  130. package/src/web/components/ui/sonner.tsx +40 -0
  131. package/src/web/components/ui/tabs.tsx +91 -0
  132. package/src/web/components/ui/tooltip.tsx +57 -0
  133. package/src/web/hooks/use-chat.ts +420 -0
  134. package/src/web/hooks/use-terminal.ts +182 -0
  135. package/src/web/hooks/use-url-sync.ts +66 -0
  136. package/src/web/hooks/use-websocket.ts +48 -0
  137. package/src/web/index.html +16 -0
  138. package/src/web/lib/api-client.ts +90 -0
  139. package/src/web/lib/file-support.ts +68 -0
  140. package/src/web/lib/utils.ts +6 -0
  141. package/src/web/lib/ws-client.ts +100 -0
  142. package/src/web/main.tsx +10 -0
  143. package/src/web/public/icon-192.svg +5 -0
  144. package/src/web/public/icon-512.svg +5 -0
  145. package/src/web/stores/file-store.ts +81 -0
  146. package/src/web/stores/project-store.ts +50 -0
  147. package/src/web/stores/settings-store.ts +65 -0
  148. package/src/web/stores/tab-store.ts +187 -0
  149. package/src/web/styles/globals.css +227 -0
  150. package/src/web/vite-env.d.ts +1 -0
  151. package/tests/integration/api/chat-routes.test.ts +95 -0
  152. package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
  153. package/tests/integration/ws/chat-websocket.test.ts +312 -0
  154. package/tests/test-setup.ts +5 -0
  155. package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
  156. package/tests/unit/providers/mock-provider.test.ts +143 -0
  157. package/tests/unit/services/chat-service.test.ts +100 -0
  158. package/tsconfig.json +32 -0
  159. package/vite.config.ts +62 -0
@@ -0,0 +1,143 @@
1
+ # Phase 8: CLI Commands
2
+
3
+ **Owner:** backend-dev
4
+ **Priority:** Medium
5
+ **Depends on:** Phase 2, Phase 6, Phase 7
6
+ **Effort:** Medium
7
+
8
+ ## Overview
9
+
10
+ Implement remaining CLI commands that call Service Layer directly. All commands share project resolution logic (CWD auto-detect + `-p` flag).
11
+
12
+ ## Files
13
+ ```
14
+ src/cli/commands/projects.ts
15
+ src/cli/commands/config.ts
16
+ src/cli/commands/git.ts
17
+ src/cli/commands/chat.ts
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ ### ppm projects
23
+ ```bash
24
+ ppm projects list # Table: name, path, branch, status
25
+ ppm projects add <path> [--name <n>] # Add project to config
26
+ ppm projects remove <name-or-path> # Remove from config
27
+ ```
28
+
29
+ ### ppm config
30
+ ```bash
31
+ ppm config get <key> # e.g., ppm config get port
32
+ ppm config set <key> <value> # e.g., ppm config set port 9090
33
+ ```
34
+
35
+ ### ppm git
36
+ All git commands accept `-p <project>` flag. Default: CWD auto-detect.
37
+
38
+ ```bash
39
+ ppm git status [-p proj] # Show status (like git status --short)
40
+ ppm git log [-p proj] [-n 20] # Show recent commits
41
+ ppm git diff [-p proj] [ref1] [ref2] # Show diff
42
+ ppm git stage [-p proj] <files...> # Stage files (or "." for all)
43
+ ppm git unstage [-p proj] <files...> # Unstage files
44
+ ppm git commit [-p proj] -m "msg" # Commit staged changes
45
+ ppm git push [-p proj] # Push to remote
46
+ ppm git pull [-p proj] # Pull from remote
47
+ ppm git branch create [-p proj] <name> [--from <ref>]
48
+ ppm git branch checkout [-p proj] <name>
49
+ ppm git branch delete [-p proj] <name> [--force]
50
+ ppm git branch merge [-p proj] <source>
51
+ ```
52
+
53
+ ### ppm chat
54
+ ```bash
55
+ ppm chat list [-p proj] # List sessions (table: id, provider, title, date)
56
+ ppm chat create [-p proj] [--provider claude] # Create session, print session ID
57
+ ppm chat send [-p proj] <session-id> "message" # Send message, stream response to stdout
58
+ ppm chat resume [-p proj] <session-id> # Interactive mode (stdin/stdout)
59
+ ppm chat delete [-p proj] <session-id> # Delete session
60
+ ```
61
+
62
+ `ppm chat send` streams response to stdout as it arrives. Useful for AI-to-AI orchestration:
63
+ ```bash
64
+ # AI agent sends a task to PPM chat
65
+ RESPONSE=$(ppm chat send -p myapp abc123 "Fix the bug in auth.ts")
66
+ ```
67
+
68
+ `ppm chat resume` enters interactive mode:
69
+ ```
70
+ You: Fix the auth bug
71
+ Claude: I'll read the file...
72
+ [Tool: Read auth.ts] Allow? (y/n): y
73
+ Claude: Found the issue...
74
+ You:
75
+ ```
76
+
77
+ ## Implementation Pattern
78
+
79
+ All commands follow same pattern:
80
+ ```typescript
81
+ // commands/git.ts
82
+ import { Command } from 'commander';
83
+
84
+ export function registerGitCommands(program: Command) {
85
+ const git = program.command('git').description('Git operations');
86
+
87
+ git.command('status')
88
+ .option('-p, --project <name>', 'Project name')
89
+ .action(async (options) => {
90
+ const project = resolveProject(options);
91
+ const gitService = new GitService();
92
+ const status = await gitService.status(project.path);
93
+ // Pretty print to terminal
94
+ printGitStatus(status);
95
+ });
96
+
97
+ // ... more subcommands
98
+ }
99
+ ```
100
+
101
+ ## Output Formatting
102
+
103
+ - Use colors (via `chalk` or Bun built-in ANSI) for terminal output
104
+ - Tables for list commands (projects list, chat list)
105
+ - Git status: colored M/A/D indicators like git
106
+ - Streaming output for chat send
107
+
108
+ ## Success Criteria
109
+
110
+ **Project Resolution:**
111
+ - [ ] `-p myproject` flag resolves project by name
112
+ - [ ] No `-p` flag + CWD inside registered project → auto-detects
113
+ - [ ] No `-p` flag + CWD not in any project → clear error: "Not in a registered project. Use -p <name>"
114
+
115
+ **ppm projects:**
116
+ - [ ] `ppm projects list` → formatted table with columns: Name, Path, Branch, Status
117
+ - [ ] `ppm projects add /path/to/repo --name myrepo` → adds project, confirms with message
118
+ - [ ] `ppm projects add` with duplicate name → error message
119
+ - [ ] `ppm projects remove myrepo` → removes, confirms with message
120
+
121
+ **ppm config:**
122
+ - [ ] `ppm config get port` → prints current port value
123
+ - [ ] `ppm config set port 9090` → updates config file, confirms
124
+ - [ ] `ppm config get nonexistent` → error message
125
+
126
+ **ppm git:**
127
+ - [ ] `ppm git status` → colored output matching git status --short format (M=yellow, A=green, D=red)
128
+ - [ ] `ppm git log -n 5` → shows last 5 commits with hash, message, author, date
129
+ - [ ] `ppm git stage .` → stages all files, prints count
130
+ - [ ] `ppm git commit -m "test"` → creates commit, prints hash
131
+ - [ ] `ppm git commit` with nothing staged → "Nothing to commit" error
132
+ - [ ] `ppm git push` → pushes to remote, prints result
133
+ - [ ] `ppm git branch create feature-x` → creates branch, confirms
134
+ - [ ] `ppm git branch checkout feature-x` → switches branch, confirms
135
+ - [ ] `ppm git branch delete feature-x` → deletes branch, confirms
136
+
137
+ **ppm chat:**
138
+ - [ ] `ppm chat list` → table with columns: ID, Provider, Title, Date
139
+ - [ ] `ppm chat create` → creates session, prints session ID
140
+ - [ ] `ppm chat send <id> "fix the bug"` → streams response to stdout in real-time
141
+ - [ ] `ppm chat resume <id>` → interactive mode with `You:` / `Claude:` prompts
142
+ - [ ] Tool approval in interactive mode: `[Tool: Bash] Allow? (y/n):` prompt
143
+ - [ ] `ppm chat delete <id>` → deletes session, confirms
@@ -0,0 +1,209 @@
1
+ # Phase 9: PWA + Build + Deploy
2
+
3
+ **Owner:** Lead
4
+ **Priority:** Medium
5
+ **Depends on:** All previous phases
6
+ **Effort:** Medium
7
+
8
+ ## Overview
9
+
10
+ PWA configuration, production build pipeline, single binary compilation, CI/CD for cross-platform releases.
11
+
12
+ ## PWA Setup
13
+
14
+ ### vite-plugin-pwa Config
15
+ ```typescript
16
+ // vite.config.ts
17
+ import { VitePWA } from 'vite-plugin-pwa';
18
+
19
+ export default defineConfig({
20
+ plugins: [
21
+ react(),
22
+ VitePWA({
23
+ registerType: 'autoUpdate',
24
+ manifest: {
25
+ name: 'PPM - Personal Project Manager',
26
+ short_name: 'PPM',
27
+ description: 'Mobile-first web IDE for managing code projects',
28
+ theme_color: '#0f172a',
29
+ background_color: '#0f172a',
30
+ display: 'standalone',
31
+ orientation: 'any',
32
+ icons: [
33
+ { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
34
+ { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
35
+ ],
36
+ },
37
+ workbox: {
38
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
39
+ // Cache UI shell for offline. API calls always need network.
40
+ runtimeCaching: [
41
+ {
42
+ urlPattern: /^https?:\/\/.*\/api\//,
43
+ handler: 'NetworkOnly', // API = always online
44
+ },
45
+ ],
46
+ },
47
+ }),
48
+ ],
49
+ });
50
+ ```
51
+
52
+ ### PWA Features
53
+ - Install prompt on mobile
54
+ - Offline: UI shell loads, shows "No connection" for API features
55
+ - App icon + splash screen
56
+
57
+ ## Build Pipeline
58
+
59
+ ### scripts/build.ts
60
+ ```typescript
61
+ import { $ } from 'bun';
62
+
63
+ // 1. Build frontend (Vite)
64
+ await $`bun run vite build --outDir dist/web`;
65
+
66
+ // 2. Compile backend + embedded frontend into single binary
67
+ await $`bun build src/index.ts --compile --outfile dist/ppm`;
68
+
69
+ // For cross-platform:
70
+ // bun build src/index.ts --compile --target=bun-linux-x64 --outfile dist/ppm-linux-x64
71
+ // bun build src/index.ts --compile --target=bun-darwin-arm64 --outfile dist/ppm-darwin-arm64
72
+ ```
73
+
74
+ ### Static File Embedding
75
+ ```typescript
76
+ // server/routes/static.ts
77
+ // In dev: proxy to Vite dev server
78
+ // In prod: serve from embedded dist/web/ directory
79
+
80
+ if (process.env.NODE_ENV === 'production') {
81
+ // Serve built files
82
+ app.use('/*', serveStatic({ root: './dist/web' }));
83
+ // SPA fallback
84
+ app.get('*', (c) => c.html(readFileSync('./dist/web/index.html', 'utf-8')));
85
+ } else {
86
+ // Proxy to Vite dev server
87
+ // Or just run Vite separately
88
+ }
89
+ ```
90
+
91
+ ### Package.json Scripts
92
+ ```json
93
+ {
94
+ "scripts": {
95
+ "dev": "concurrently \"bun run --hot src/index.ts start\" \"bun run vite\"",
96
+ "build": "bun run scripts/build.ts",
97
+ "build:linux": "bun build src/index.ts --compile --target=bun-linux-x64 --outfile dist/ppm-linux-x64",
98
+ "build:mac-arm": "bun build src/index.ts --compile --target=bun-darwin-arm64 --outfile dist/ppm-darwin-arm64",
99
+ "build:mac-x64": "bun build src/index.ts --compile --target=bun-darwin-x64 --outfile dist/ppm-darwin-x64"
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## CI/CD (GitHub Actions)
105
+
106
+ ### .github/workflows/release.yml
107
+ ```yaml
108
+ name: Release
109
+ on:
110
+ push:
111
+ tags: ['v*']
112
+
113
+ jobs:
114
+ build:
115
+ strategy:
116
+ matrix:
117
+ include:
118
+ - os: ubuntu-latest
119
+ target: bun-linux-x64
120
+ artifact: ppm-linux-x64
121
+ - os: macos-latest
122
+ target: bun-darwin-arm64
123
+ artifact: ppm-darwin-arm64
124
+ - os: macos-13
125
+ target: bun-darwin-x64
126
+ artifact: ppm-darwin-x64
127
+
128
+ runs-on: ${{ matrix.os }}
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+ - uses: oven-sh/setup-bun@v2
132
+ - run: bun install
133
+ - run: bun run vite build --outDir dist/web
134
+ - run: bun build src/index.ts --compile --target=${{ matrix.target }} --outfile dist/${{ matrix.artifact }}
135
+ - uses: actions/upload-artifact@v4
136
+ with:
137
+ name: ${{ matrix.artifact }}
138
+ path: dist/${{ matrix.artifact }}
139
+
140
+ release:
141
+ needs: build
142
+ runs-on: ubuntu-latest
143
+ steps:
144
+ - uses: actions/download-artifact@v4
145
+ - uses: softprops/action-gh-release@v2
146
+ with:
147
+ files: |
148
+ ppm-linux-x64/ppm-linux-x64
149
+ ppm-darwin-arm64/ppm-darwin-arm64
150
+ ppm-darwin-x64/ppm-darwin-x64
151
+ ```
152
+
153
+ ### Dockerfile (fallback)
154
+ ```dockerfile
155
+ FROM oven/bun:1.2-alpine
156
+ WORKDIR /app
157
+ COPY package.json bun.lock ./
158
+ RUN bun install --production
159
+ COPY dist/ ./dist/
160
+ COPY src/ ./src/
161
+ EXPOSE 8080
162
+ CMD ["bun", "run", "src/index.ts", "start"]
163
+ ```
164
+
165
+ ## Deployment Docs
166
+
167
+ ### Local
168
+ ```bash
169
+ # Install
170
+ curl -fsSL https://github.com/user/ppm/releases/latest/download/ppm-$(uname -s | tr A-Z a-z)-$(uname -m) -o /usr/local/bin/ppm
171
+ chmod +x /usr/local/bin/ppm
172
+
173
+ # Setup
174
+ ppm init
175
+ ppm start
176
+ ```
177
+
178
+ ### VPS
179
+ ```bash
180
+ scp ppm-linux-x64 user@vps:/usr/local/bin/ppm
181
+ scp ppm.yaml user@vps:/etc/ppm/config.yaml
182
+ ssh user@vps "ppm start -c /etc/ppm/config.yaml -d"
183
+ ```
184
+
185
+ ## Static File Embedding (NEEDS INVESTIGATION)
186
+
187
+ **Known issue:** `bun build --compile` bundles JS but does NOT auto-embed static files (HTML, CSS, images).
188
+
189
+ **Options to investigate:**
190
+ 1. Use `Bun.file()` with `import.meta.dir` to reference files relative to binary
191
+ 2. Use `import with { type: "file" }` syntax to embed at build time
192
+ 3. Inline frontend assets into a JS module at build time (custom build step)
193
+ 4. Ship `dist/web/` alongside binary (not single-file, but simpler)
194
+
195
+ **TODO:** Test each approach before Phase 9 implementation. Research `bun build --compile` static file embedding in Bun docs.
196
+
197
+ ## Success Criteria
198
+
199
+ - [ ] PWA installable on mobile: "Add to Home Screen" prompt appears
200
+ - [ ] PWA offline: UI shell loads without network, shows "No connection" for API features
201
+ - [ ] `bun run build` completes without errors, produces binary + web assets
202
+ - [ ] Built binary starts server and serves frontend at `http://localhost:<port>/`
203
+ - [ ] Frontend served by binary is fully functional (not blank page)
204
+ - [ ] API routes work through built binary (not just dev mode)
205
+ - [ ] Cross-platform binaries compile in CI (linux-x64, darwin-arm64, darwin-x64)
206
+ - [ ] GitHub Release created with binaries on tag push (v* tags)
207
+ - [ ] Docker image builds and runs: `docker run -p 8080:8080 ppm` serves app
208
+ - [ ] Fresh install flow works: download binary → `ppm init` → `ppm start` → browser opens → app works
209
+ - [ ] App icon shows correctly on mobile homescreen
@@ -0,0 +1,311 @@
1
+ # Phase 10: Testing
2
+
3
+ **Owner:** tester
4
+ **Priority:** High
5
+ **Depends on:** Runs continuously after each phase completes
6
+ **Effort:** Large
7
+
8
+ ## Overview
9
+
10
+ Comprehensive unit + integration tests for both backend services AND frontend logic. E2E smoke tests for full flow.
11
+
12
+ ## Test Framework
13
+
14
+ - **Runner:** `bun test` (built-in, Jest-compatible)
15
+ - **HTTP testing:** Hono test client (`app.request()`)
16
+ - **WS testing:** Native WebSocket client in Bun
17
+ - **Frontend logic testing:** `bun test` for store/lib/hook logic (no DOM needed for pure logic)
18
+
19
+ ## Test Structure
20
+ ```
21
+ tests/
22
+ ├── setup.ts # Global setup (test config, temp dirs)
23
+ ├── unit/
24
+ │ ├── services/
25
+ │ │ ├── config.service.test.ts
26
+ │ │ ├── project.service.test.ts
27
+ │ │ ├── file.service.test.ts
28
+ │ │ ├── git.service.test.ts
29
+ │ │ ├── terminal.service.test.ts
30
+ │ │ └── chat.service.test.ts
31
+ │ ├── providers/
32
+ │ │ ├── claude-agent-sdk.test.ts
33
+ │ │ └── registry.test.ts
34
+ │ ├── cli/
35
+ │ │ └── project-resolver.test.ts
36
+ │ ├── server/
37
+ │ │ ├── resolve-project.test.ts
38
+ │ │ └── auth-middleware.test.ts
39
+ │ └── web/
40
+ │ ├── stores/
41
+ │ │ ├── tab.store.test.ts
42
+ │ │ ├── project.store.test.ts
43
+ │ │ └── settings.store.test.ts
44
+ │ ├── lib/
45
+ │ │ ├── api-client.test.ts
46
+ │ │ ├── ws-client.test.ts
47
+ │ │ └── git-graph-layout.test.ts
48
+ │ └── hooks/
49
+ │ └── use-websocket.test.ts
50
+ ├── integration/
51
+ │ ├── api/
52
+ │ │ ├── auth.test.ts
53
+ │ │ ├── projects.test.ts
54
+ │ │ ├── files.test.ts
55
+ │ │ └── git.test.ts
56
+ │ └── ws/
57
+ │ ├── terminal.test.ts
58
+ │ └── chat.test.ts
59
+ └── e2e/
60
+ └── smoke.test.ts # Full flow: init → start → use → stop
61
+ ```
62
+
63
+ ## Test Strategy Per Phase
64
+
65
+ ### Phase 2 Tests (Backend Core)
66
+
67
+ **Config Service:**
68
+ - [ ] Loads config from `./ppm.yaml` when present
69
+ - [ ] Falls back to `~/.ppm/config.yaml` when no local config
70
+ - [ ] Creates default config with generated auth token on first run
71
+ - [ ] `get('port')` returns configured port
72
+ - [ ] `set('port', 9090)` persists change to file
73
+ - [ ] Invalid YAML file → throws descriptive error
74
+
75
+ **Project Service:**
76
+ - [ ] `add("/path/to/repo", "myrepo")` adds project to config
77
+ - [ ] `add` with duplicate name → throws error
78
+ - [ ] `remove("myrepo")` removes project from config
79
+ - [ ] `list()` returns all registered projects
80
+ - [ ] `resolve("myrepo")` returns project by name
81
+ - [ ] `resolve` from CWD → auto-detects project when CWD is inside registered project path
82
+ - [ ] `resolve("nonexistent")` → throws "Project not found" error
83
+ - [ ] `scanForGitRepos(dir)` finds `.git` directories recursively
84
+
85
+ **Auth Middleware:**
86
+ - [ ] Valid Bearer token → passes, sets context
87
+ - [ ] Missing Authorization header → 401 `{ ok: false, error: "Unauthorized" }`
88
+ - [ ] Invalid token → 401
89
+ - [ ] `auth.enabled: false` in config → all requests pass without token
90
+ - [ ] Token from config matches what's checked
91
+
92
+ **Server:**
93
+ - [ ] Server starts on configured port
94
+ - [ ] `GET /api/health` returns 200
95
+ - [ ] SPA fallback: `GET /nonexistent` returns `index.html` (not 404)
96
+ - [ ] API 404: `GET /api/nonexistent` returns 404 JSON
97
+
98
+ **Resolve Project Helper:**
99
+ - [ ] `resolveProjectPath("ppm")` → returns path from config
100
+ - [ ] `resolveProjectPath("/absolute/path")` → validates path is within registered project
101
+ - [ ] `resolveProjectPath("../escape")` → throws (path traversal)
102
+
103
+ ### Phase 3 Tests (Frontend Logic)
104
+
105
+ **Tab Store:**
106
+ - [ ] `openTab({type: 'terminal', title: 'Terminal'})` adds tab and returns id
107
+ - [ ] `openTab` with duplicate type+metadata → returns existing tab id (no duplicate)
108
+ - [ ] `closeTab(id)` removes tab from list
109
+ - [ ] `closeTab` on last tab → activeTabId becomes null
110
+ - [ ] `setActiveTab(id)` updates activeTabId
111
+ - [ ] `updateTab(id, {title: 'new'})` updates tab properties
112
+ - [ ] Closing active tab → activates previous tab (not first, not null)
113
+
114
+ **API Client:**
115
+ - [ ] `get<T>('/api/projects')` unwraps `{ok: true, data: [...]}` → returns `[...]`
116
+ - [ ] Server returns `{ok: false, error: "Not found"}` → throws Error with "Not found" message
117
+ - [ ] Server returns HTTP 500 → throws Error
118
+ - [ ] Bearer token header sent on every request when token is set
119
+ - [ ] No Authorization header when token is null/undefined
120
+
121
+ **WS Client:**
122
+ - [ ] Connects to WebSocket URL
123
+ - [ ] Auto-reconnects on close with exponential backoff (1s, 2s, 4s)
124
+ - [ ] Max reconnect delay caps at 30s
125
+ - [ ] `send()` queues messages if not connected, sends on reconnect
126
+ - [ ] `onMessage` callback fires for incoming messages
127
+ - [ ] `disconnect()` stops reconnect attempts
128
+
129
+ **Git Graph Layout:**
130
+ - [ ] Single branch → all commits in lane 0
131
+ - [ ] Two branches → merge commit connects lanes correctly
132
+ - [ ] Lane reuse: closed branch lane gets reused by next branch
133
+ - [ ] Empty commits list → returns empty layout
134
+
135
+ ### Phase 4 Tests (File Explorer)
136
+
137
+ **File Service:**
138
+ - [ ] `getTree(projectPath)` returns nested FileNode structure
139
+ - [ ] `getTree` excludes `.git/`, `node_modules/`
140
+ - [ ] `readFile(path)` returns content as string
141
+ - [ ] `readFile` for binary file → returns base64 with encoding flag
142
+ - [ ] `writeFile(path, content)` creates/updates file
143
+ - [ ] `createFile(path, 'file')` creates empty file
144
+ - [ ] `createFile(path, 'directory')` creates directory
145
+ - [ ] `deleteFile(path)` removes file
146
+ - [ ] `renameFile(old, new)` renames file
147
+ - [ ] Path traversal: `readFile("../../etc/passwd")` → throws error
148
+ - [ ] Access `.git/config` → throws error
149
+ - [ ] Access `.env` → throws error
150
+
151
+ **File API Integration:**
152
+ - [ ] `GET /api/files/tree/myproject` returns file tree (project resolved by name)
153
+ - [ ] `GET /api/files/read?path=src/index.ts` returns file content
154
+ - [ ] `PUT /api/files/write` with `{path, content}` writes file
155
+ - [ ] `DELETE /api/files/delete` with `{path}` removes file
156
+ - [ ] Invalid project name → 404
157
+ - [ ] Path outside project → 403
158
+
159
+ ### Phase 5 Tests (Terminal)
160
+
161
+ **Terminal Service:**
162
+ - [ ] `create({projectPath})` spawns shell process and returns session
163
+ - [ ] `get(id)` returns existing session
164
+ - [ ] `get(nonexistent)` returns undefined
165
+ - [ ] `write(id, "ls\n")` sends input to PTY
166
+ - [ ] `onData(id, handler)` receives shell output
167
+ - [ ] `kill(id)` terminates process and removes session
168
+ - [ ] `list()` returns all active sessions
169
+ - [ ] Output buffer: last 10KB of output stored per session
170
+
171
+ **Terminal WS Integration:**
172
+ - [ ] Connect to `/ws/terminal/:id` → creates new PTY if not exists
173
+ - [ ] Send keystroke via WS → appears in PTY
174
+ - [ ] PTY output → arrives via WS message
175
+ - [ ] Send resize control message → PTY resizes (if supported)
176
+ - [ ] Disconnect WS → PTY stays alive for 30s
177
+ - [ ] Reconnect within 30s → receives buffered output
178
+ - [ ] Reconnect after timeout → session dead, returns error
179
+ - [ ] Multiple WS clients to same session → both receive output
180
+
181
+ ### Phase 6 Tests (Git)
182
+
183
+ **Git Service (using real temp repos):**
184
+ - [ ] `status(path)` → returns modified/staged/untracked files
185
+ - [ ] `stage(path, ["file.txt"])` → file appears in staged list
186
+ - [ ] `unstage(path, ["file.txt"])` → file moves back to unstaged
187
+ - [ ] `commit(path, "msg")` → creates commit, returns hash
188
+ - [ ] `commit` with nothing staged → throws error
189
+ - [ ] `branches(path)` → returns branch list with current marked
190
+ - [ ] `createBranch(path, "feature")` → branch exists
191
+ - [ ] `checkout(path, "feature")` → current branch changes
192
+ - [ ] `deleteBranch(path, "feature")` → branch removed
193
+ - [ ] `deleteBranch` on current branch → throws error
194
+ - [ ] `graphData(path)` → returns commits with parents, refs, branch info
195
+ - [ ] `graphData` uses simple-git `.log()` (not manual parse) → no garbled data on multi-line commit messages
196
+ - [ ] `diff(path)` → returns unified diff string
197
+ - [ ] `fileDiff(path, "file.txt")` → returns diff for specific file
198
+ - [ ] `getCreatePrUrl(path, "feature")` → returns GitHub PR URL from remote
199
+
200
+ **Git API Integration:**
201
+ - [ ] `GET /api/git/status/myproject` → status JSON (project resolved by name)
202
+ - [ ] `POST /api/git/commit` with `{project: "myproject", message: "test"}` → creates commit
203
+ - [ ] `GET /api/git/graph/myproject?max=50` → returns graph data with ≤50 commits
204
+ - [ ] All git routes resolve project by NAME, not path
205
+
206
+ ### Phase 7 Tests (AI Chat)
207
+
208
+ **Provider Registry:**
209
+ - [ ] `register(provider)` adds provider
210
+ - [ ] `get("claude")` returns Claude provider
211
+ - [ ] `get("nonexistent")` returns undefined
212
+ - [ ] `list()` returns all provider infos
213
+ - [ ] `getDefault()` returns first registered provider
214
+
215
+ **Chat Service (mock provider):**
216
+ - [ ] `createSession("mock", config)` → returns session with id
217
+ - [ ] `sendMessage("mock", sessionId, "hello")` → yields ChatEvent stream
218
+ - [ ] `listSessions()` → returns session list
219
+ - [ ] `deleteSession("mock", sessionId)` → session removed from list
220
+
221
+ **Chat WS Integration:**
222
+ - [ ] Connect to `/ws/chat/:sessionId` → WS opens
223
+ - [ ] Send `{type: "message", content: "hello"}` → receives streamed response events
224
+ - [ ] Receive `{type: "approval_request"}` → send `{type: "approval_response", approved: true}` → tool executes
225
+ - [ ] Approval denied → AI receives denial message
226
+ - [ ] `{type: "done"}` received at end of response
227
+ - [ ] Error in provider → `{type: "error", message: "..."}` sent via WS
228
+
229
+ **Chat REST API:**
230
+ - [ ] `GET /api/chat/sessions` → list of all sessions
231
+ - [ ] `GET /api/chat/sessions/:id/messages` → message history for session
232
+ - [ ] Reconnect flow: disconnect WS → GET messages via REST → reconnect WS → no messages lost
233
+
234
+ ### Phase 8 Tests (CLI)
235
+
236
+ **Project Resolver:**
237
+ - [ ] `-p myproject` flag → resolves to correct project
238
+ - [ ] No flag, CWD inside project → auto-detects
239
+ - [ ] No flag, CWD not in any project → throws descriptive error
240
+
241
+ **CLI Commands (capture stdout):**
242
+ - [ ] `ppm projects list` → outputs table with project names and paths
243
+ - [ ] `ppm projects add /tmp/repo --name test` → adds project
244
+ - [ ] `ppm git status -p myproject` → outputs colored status
245
+ - [ ] `ppm git commit -p myproject -m "test"` → commits and prints hash
246
+ - [ ] `ppm chat list -p myproject` → outputs session table
247
+
248
+ ### Phase 9 Tests (Build)
249
+ - [ ] `bun run build` completes without errors
250
+ - [ ] Built binary serves frontend at `http://localhost:<port>/`
251
+ - [ ] Built binary serves API at `/api/*`
252
+ - [ ] PWA manifest accessible at expected path
253
+
254
+ ## Test Utilities
255
+
256
+ ```typescript
257
+ // tests/setup.ts
258
+ import { mkdtempSync } from 'fs';
259
+ import { tmpdir } from 'os';
260
+
261
+ // Create temp git repo for testing
262
+ export function createTestRepo(): string {
263
+ const dir = mkdtempSync(path.join(tmpdir(), 'ppm-test-'));
264
+ execSync('git init', { cwd: dir });
265
+ execSync('git commit --allow-empty -m "init"', { cwd: dir });
266
+ return dir;
267
+ }
268
+
269
+ // Create test config
270
+ export function createTestConfig(overrides?: Partial<PpmConfig>): PpmConfig {
271
+ return { port: 0, host: '127.0.0.1', auth: { enabled: false }, projects: [], ...overrides };
272
+ }
273
+
274
+ // Create test Hono app with test config
275
+ export function createTestApp(config?: Partial<PpmConfig>): Hono {
276
+ // Wire up routes with test config, return app instance
277
+ }
278
+
279
+ // Wait for WS message matching predicate
280
+ export async function waitForWsMessage(ws: WebSocket, predicate: (msg: any) => boolean, timeout = 5000): Promise<any> {
281
+ // Returns first message matching predicate, throws on timeout
282
+ }
283
+ ```
284
+
285
+ ## Mock Strategy
286
+
287
+ - **Git operations:** Use real temp git repos (no mocking git)
288
+ - **AI Provider:** Mock provider implementing AIProvider interface (no real API calls)
289
+ - **File system:** Use temp directories (real FS, no mocking)
290
+ - **WebSocket:** Use real WS connections to test server
291
+ - **Auth:** Test with both `auth.enabled: true` (with token) and `auth.enabled: false`
292
+
293
+ ## Coverage Target
294
+
295
+ - Services: 80%+
296
+ - API routes: 80%+
297
+ - CLI commands: 70%+
298
+ - Frontend stores/lib/hooks: 80%+
299
+ - Frontend components: Manual testing (visual verification)
300
+
301
+ ## Success Criteria
302
+
303
+ - [ ] `bun test` runs all tests (unit + integration)
304
+ - [ ] No tests use fake data/mocks that mask real behavior
305
+ - [ ] Git tests use real temp repos with real git operations
306
+ - [ ] API tests use real HTTP requests via Hono test client
307
+ - [ ] WS tests verify full protocol correctness (connect, send, receive, reconnect)
308
+ - [ ] Frontend store tests verify state transitions and edge cases
309
+ - [ ] API client tests verify envelope unwrapping and error handling
310
+ - [ ] All tests pass before merge — failing tests block PR
311
+ - [ ] Test output includes file/line for failures (easy to locate)