@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,315 @@
1
+ import { Command } from "commander";
2
+ import type { GitStatus } from "../../types/git.ts";
3
+
4
+ const C = {
5
+ reset: "\x1b[0m",
6
+ bold: "\x1b[1m",
7
+ green: "\x1b[32m",
8
+ red: "\x1b[31m",
9
+ yellow: "\x1b[33m",
10
+ cyan: "\x1b[36m",
11
+ dim: "\x1b[2m",
12
+ magenta: "\x1b[35m",
13
+ };
14
+
15
+ function statusColor(s: string): string {
16
+ if (s === "M") return `${C.yellow}M${C.reset}`;
17
+ if (s === "A") return `${C.green}A${C.reset}`;
18
+ if (s === "D") return `${C.red}D${C.reset}`;
19
+ if (s === "R") return `${C.cyan}R${C.reset}`;
20
+ return s;
21
+ }
22
+
23
+ function printStatus(status: GitStatus): void {
24
+ console.log(`${C.bold}On branch:${C.reset} ${C.cyan}${status.current ?? "(detached)"}${C.reset}`);
25
+
26
+ if (status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0) {
27
+ console.log(`${C.green}Nothing to commit, working tree clean${C.reset}`);
28
+ return;
29
+ }
30
+
31
+ if (status.staged.length > 0) {
32
+ console.log(`\n${C.bold}Staged changes:${C.reset}`);
33
+ for (const f of status.staged) {
34
+ const label = f.oldPath ? `${f.oldPath} → ${f.path}` : f.path;
35
+ console.log(` ${statusColor(f.status)} ${label}`);
36
+ }
37
+ }
38
+
39
+ if (status.unstaged.length > 0) {
40
+ console.log(`\n${C.bold}Unstaged changes:${C.reset}`);
41
+ for (const f of status.unstaged) {
42
+ console.log(` ${statusColor(f.status)} ${f.path}`);
43
+ }
44
+ }
45
+
46
+ if (status.untracked.length > 0) {
47
+ console.log(`\n${C.bold}Untracked files:${C.reset}`);
48
+ for (const f of status.untracked) {
49
+ console.log(` ${C.dim}? ${f}${C.reset}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ export function registerGitCommands(program: Command): void {
55
+ const git = program.command("git").description("Git operations for a project");
56
+
57
+ git
58
+ .command("status")
59
+ .description("Show working tree status")
60
+ .option("-p, --project <name>", "Project name or path")
61
+ .action(async (options: { project?: string }) => {
62
+ try {
63
+ const { resolveProject } = await import("../utils/project-resolver.ts");
64
+ const { gitService } = await import("../../services/git.service.ts");
65
+ const project = resolveProject(options);
66
+ const status = await gitService.status(project.path);
67
+ printStatus(status);
68
+ } catch (err) {
69
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
70
+ process.exit(1);
71
+ }
72
+ });
73
+
74
+ git
75
+ .command("log")
76
+ .description("Show recent commits")
77
+ .option("-p, --project <name>", "Project name or path")
78
+ .option("-n, --count <n>", "Number of commits to show", "20")
79
+ .action(async (options: { project?: string; count?: string }) => {
80
+ try {
81
+ const { resolveProject } = await import("../utils/project-resolver.ts");
82
+ const { gitService } = await import("../../services/git.service.ts");
83
+ const project = resolveProject(options);
84
+ const maxCount = Math.min(parseInt(options.count ?? "20", 10), 500);
85
+ const { commits } = await gitService.graphData(project.path, maxCount);
86
+
87
+ if (commits.length === 0) {
88
+ console.log(`${C.yellow}No commits found.${C.reset}`);
89
+ return;
90
+ }
91
+
92
+ for (const c of commits) {
93
+ const hash = `${C.yellow}${c.abbreviatedHash}${C.reset}`;
94
+ const subject = c.subject;
95
+ const author = `${C.cyan}${c.authorName}${C.reset}`;
96
+ const date = `${C.dim}${new Date(c.authorDate).toLocaleDateString()}${C.reset}`;
97
+ const refs = c.refs.length > 0
98
+ ? ` ${C.green}(${c.refs.join(", ")})${C.reset}`
99
+ : "";
100
+ console.log(`${hash}${refs} ${subject}`);
101
+ console.log(` ${author} · ${date}`);
102
+ }
103
+ } catch (err) {
104
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
105
+ process.exit(1);
106
+ }
107
+ });
108
+
109
+ git
110
+ .command("diff")
111
+ .description("Show diff between refs or working tree")
112
+ .option("-p, --project <name>", "Project name or path")
113
+ .argument("[ref1]", "First ref")
114
+ .argument("[ref2]", "Second ref")
115
+ .action(async (ref1: string | undefined, ref2: string | undefined, options: { project?: string }) => {
116
+ try {
117
+ const { resolveProject } = await import("../utils/project-resolver.ts");
118
+ const { gitService } = await import("../../services/git.service.ts");
119
+ const project = resolveProject(options);
120
+ const diff = await gitService.diff(project.path, ref1, ref2);
121
+ if (!diff.trim()) {
122
+ console.log(`${C.dim}No differences.${C.reset}`);
123
+ } else {
124
+ // Color diff output
125
+ for (const line of diff.split("\n")) {
126
+ if (line.startsWith("+") && !line.startsWith("+++")) {
127
+ process.stdout.write(`${C.green}${line}${C.reset}\n`);
128
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
129
+ process.stdout.write(`${C.red}${line}${C.reset}\n`);
130
+ } else if (line.startsWith("@@")) {
131
+ process.stdout.write(`${C.cyan}${line}${C.reset}\n`);
132
+ } else {
133
+ process.stdout.write(`${line}\n`);
134
+ }
135
+ }
136
+ }
137
+ } catch (err) {
138
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
139
+ process.exit(1);
140
+ }
141
+ });
142
+
143
+ git
144
+ .command("stage <files...>")
145
+ .description('Stage files (use "." to stage all)')
146
+ .option("-p, --project <name>", "Project name or path")
147
+ .action(async (files: string[], options: { project?: string }) => {
148
+ try {
149
+ const { resolveProject } = await import("../utils/project-resolver.ts");
150
+ const { gitService } = await import("../../services/git.service.ts");
151
+ const project = resolveProject(options);
152
+ await gitService.stage(project.path, files);
153
+ console.log(`${C.green}Staged:${C.reset} ${files.join(", ")}`);
154
+ } catch (err) {
155
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
156
+ process.exit(1);
157
+ }
158
+ });
159
+
160
+ git
161
+ .command("unstage <files...>")
162
+ .description("Unstage files")
163
+ .option("-p, --project <name>", "Project name or path")
164
+ .action(async (files: string[], options: { project?: string }) => {
165
+ try {
166
+ const { resolveProject } = await import("../utils/project-resolver.ts");
167
+ const { gitService } = await import("../../services/git.service.ts");
168
+ const project = resolveProject(options);
169
+ await gitService.unstage(project.path, files);
170
+ console.log(`${C.yellow}Unstaged:${C.reset} ${files.join(", ")}`);
171
+ } catch (err) {
172
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
173
+ process.exit(1);
174
+ }
175
+ });
176
+
177
+ git
178
+ .command("commit")
179
+ .description("Commit staged changes")
180
+ .option("-p, --project <name>", "Project name or path")
181
+ .requiredOption("-m, --message <msg>", "Commit message")
182
+ .action(async (options: { project?: string; message: string }) => {
183
+ try {
184
+ const { resolveProject } = await import("../utils/project-resolver.ts");
185
+ const { gitService } = await import("../../services/git.service.ts");
186
+ const project = resolveProject(options);
187
+
188
+ // Check if there's anything to commit
189
+ const status = await gitService.status(project.path);
190
+ if (status.staged.length === 0) {
191
+ console.error(`${C.red}Nothing to commit.${C.reset} Stage files first with: ppm git stage <files>`);
192
+ process.exit(1);
193
+ }
194
+
195
+ const hash = await gitService.commit(project.path, options.message);
196
+ console.log(`${C.green}Committed:${C.reset} ${C.yellow}${hash}${C.reset} "${options.message}"`);
197
+ } catch (err) {
198
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ git
204
+ .command("push")
205
+ .description("Push to remote")
206
+ .option("-p, --project <name>", "Project name or path")
207
+ .option("--remote <remote>", "Remote name", "origin")
208
+ .option("--branch <branch>", "Branch name")
209
+ .action(async (options: { project?: string; remote?: string; branch?: string }) => {
210
+ try {
211
+ const { resolveProject } = await import("../utils/project-resolver.ts");
212
+ const { gitService } = await import("../../services/git.service.ts");
213
+ const project = resolveProject(options);
214
+ console.log(`${C.dim}Pushing...${C.reset}`);
215
+ await gitService.push(project.path, options.remote, options.branch);
216
+ console.log(`${C.green}Pushed successfully.${C.reset}`);
217
+ } catch (err) {
218
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
219
+ process.exit(1);
220
+ }
221
+ });
222
+
223
+ git
224
+ .command("pull")
225
+ .description("Pull from remote")
226
+ .option("-p, --project <name>", "Project name or path")
227
+ .option("--remote <remote>", "Remote name")
228
+ .option("--branch <branch>", "Branch name")
229
+ .action(async (options: { project?: string; remote?: string; branch?: string }) => {
230
+ try {
231
+ const { resolveProject } = await import("../utils/project-resolver.ts");
232
+ const { gitService } = await import("../../services/git.service.ts");
233
+ const project = resolveProject(options);
234
+ console.log(`${C.dim}Pulling...${C.reset}`);
235
+ await gitService.pull(project.path, options.remote, options.branch);
236
+ console.log(`${C.green}Pulled successfully.${C.reset}`);
237
+ } catch (err) {
238
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
239
+ process.exit(1);
240
+ }
241
+ });
242
+
243
+ // ppm git branch <subcommand>
244
+ const branch = git.command("branch").description("Branch operations");
245
+
246
+ branch
247
+ .command("create <name>")
248
+ .description("Create and checkout a new branch")
249
+ .option("-p, --project <name>", "Project name or path")
250
+ .option("--from <ref>", "Base ref (commit/branch/tag)")
251
+ .action(async (name: string, options: { project?: string; from?: string }) => {
252
+ try {
253
+ const { resolveProject } = await import("../utils/project-resolver.ts");
254
+ const { gitService } = await import("../../services/git.service.ts");
255
+ const project = resolveProject(options);
256
+ await gitService.createBranch(project.path, name, options.from);
257
+ console.log(`${C.green}Created and checked out branch:${C.reset} ${C.cyan}${name}${C.reset}`);
258
+ } catch (err) {
259
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
260
+ process.exit(1);
261
+ }
262
+ });
263
+
264
+ branch
265
+ .command("checkout <name>")
266
+ .description("Switch to a branch")
267
+ .option("-p, --project <name>", "Project name or path")
268
+ .action(async (name: string, options: { project?: string }) => {
269
+ try {
270
+ const { resolveProject } = await import("../utils/project-resolver.ts");
271
+ const { gitService } = await import("../../services/git.service.ts");
272
+ const project = resolveProject(options);
273
+ await gitService.checkout(project.path, name);
274
+ console.log(`${C.green}Switched to branch:${C.reset} ${C.cyan}${name}${C.reset}`);
275
+ } catch (err) {
276
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
277
+ process.exit(1);
278
+ }
279
+ });
280
+
281
+ branch
282
+ .command("delete <name>")
283
+ .description("Delete a branch")
284
+ .option("-p, --project <name>", "Project name or path")
285
+ .option("-f, --force", "Force delete")
286
+ .action(async (name: string, options: { project?: string; force?: boolean }) => {
287
+ try {
288
+ const { resolveProject } = await import("../utils/project-resolver.ts");
289
+ const { gitService } = await import("../../services/git.service.ts");
290
+ const project = resolveProject(options);
291
+ await gitService.deleteBranch(project.path, name, options.force);
292
+ console.log(`${C.green}Deleted branch:${C.reset} ${C.cyan}${name}${C.reset}`);
293
+ } catch (err) {
294
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
295
+ process.exit(1);
296
+ }
297
+ });
298
+
299
+ branch
300
+ .command("merge <source>")
301
+ .description("Merge a branch into current branch")
302
+ .option("-p, --project <name>", "Project name or path")
303
+ .action(async (source: string, options: { project?: string }) => {
304
+ try {
305
+ const { resolveProject } = await import("../utils/project-resolver.ts");
306
+ const { gitService } = await import("../../services/git.service.ts");
307
+ const project = resolveProject(options);
308
+ await gitService.merge(project.path, source);
309
+ console.log(`${C.green}Merged:${C.reset} ${C.cyan}${source}${C.reset} into current branch`);
310
+ } catch (err) {
311
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
312
+ process.exit(1);
313
+ }
314
+ });
315
+ }
@@ -0,0 +1,57 @@
1
+ import { resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { existsSync } from "node:fs";
4
+ import { configService } from "../../services/config.service.ts";
5
+ import { projectService } from "../../services/project.service.ts";
6
+
7
+ export async function initProject() {
8
+ const ppmDir = resolve(homedir(), ".ppm");
9
+ const globalConfig = resolve(ppmDir, "config.yaml");
10
+
11
+ // Load existing or create default
12
+ configService.load();
13
+ console.log(`Config: ${configService.getConfigPath()}`);
14
+
15
+ // Scan CWD for git repos
16
+ const cwd = process.cwd();
17
+ console.log(`\nScanning ${cwd} for git repositories...`);
18
+ const repos = projectService.scanForGitRepos(cwd);
19
+
20
+ if (repos.length === 0) {
21
+ console.log("No git repositories found.");
22
+ } else {
23
+ console.log(`Found ${repos.length} git repo(s):\n`);
24
+ const existing = configService.get("projects");
25
+
26
+ let added = 0;
27
+ for (const repoPath of repos) {
28
+ const name = repoPath.split("/").pop() ?? "unknown";
29
+ const alreadyRegistered = existing.some(
30
+ (p) => resolve(p.path) === repoPath || p.name === name,
31
+ );
32
+
33
+ if (alreadyRegistered) {
34
+ console.log(` [skip] ${name} (${repoPath}) — already registered`);
35
+ continue;
36
+ }
37
+
38
+ try {
39
+ projectService.add(repoPath, name);
40
+ console.log(` [added] ${name} (${repoPath})`);
41
+ added++;
42
+ } catch (e) {
43
+ console.log(` [error] ${name}: ${(e as Error).message}`);
44
+ }
45
+ }
46
+
47
+ console.log(`\nAdded ${added} project(s).`);
48
+ }
49
+
50
+ const auth = configService.get("auth");
51
+ console.log(`\nAuth: ${auth.enabled ? "enabled" : "disabled"}`);
52
+ if (auth.enabled) {
53
+ console.log(`Token: ${auth.token}`);
54
+ }
55
+
56
+ console.log(`\nRun "ppm start" to start the server.`);
57
+ }
@@ -0,0 +1,19 @@
1
+ import { configService } from "../../services/config.service.ts";
2
+
3
+ export async function openBrowser() {
4
+ configService.load();
5
+ const port = configService.get("port");
6
+ const url = `http://localhost:${port}`;
7
+
8
+ console.log(`Opening ${url} ...`);
9
+
10
+ const platform = process.platform;
11
+ const cmd =
12
+ platform === "darwin"
13
+ ? ["open", url]
14
+ : platform === "win32"
15
+ ? ["cmd", "/c", "start", url]
16
+ : ["xdg-open", url];
17
+
18
+ Bun.spawn({ cmd, stdio: ["ignore", "ignore", "ignore"] });
19
+ }
@@ -0,0 +1,100 @@
1
+ import { Command } from "commander";
2
+
3
+ const C = {
4
+ reset: "\x1b[0m",
5
+ bold: "\x1b[1m",
6
+ green: "\x1b[32m",
7
+ red: "\x1b[31m",
8
+ yellow: "\x1b[33m",
9
+ cyan: "\x1b[36m",
10
+ dim: "\x1b[2m",
11
+ };
12
+
13
+ function printTable(headers: string[], rows: string[][]): void {
14
+ const colWidths = headers.map((h, i) =>
15
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)),
16
+ );
17
+
18
+ const sep = colWidths.map((w) => "-".repeat(w + 2)).join("+");
19
+ const headerLine = headers
20
+ .map((h, i) => ` ${h.padEnd(colWidths[i]!)} `)
21
+ .join("|");
22
+
23
+ console.log(`+${sep}+`);
24
+ console.log(`|${C.bold}${headerLine}${C.reset}|`);
25
+ console.log(`+${sep}+`);
26
+ for (const row of rows) {
27
+ const line = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i]!)} `).join("|");
28
+ console.log(`|${line}|`);
29
+ }
30
+ console.log(`+${sep}+`);
31
+ }
32
+
33
+ export function registerProjectsCommands(program: Command): void {
34
+ const projects = program.command("projects").description("Manage registered projects");
35
+
36
+ projects
37
+ .command("list")
38
+ .description("List all registered projects")
39
+ .action(async () => {
40
+ try {
41
+ const { projectService } = await import("../../services/project.service.ts");
42
+ const { gitService } = await import("../../services/git.service.ts");
43
+ const list = projectService.list();
44
+
45
+ if (list.length === 0) {
46
+ console.log(`${C.yellow}No projects registered.${C.reset} Run: ppm init`);
47
+ return;
48
+ }
49
+
50
+ const rows: string[][] = [];
51
+ for (const p of list) {
52
+ let branch = "-";
53
+ let status = "-";
54
+ try {
55
+ const s = await gitService.status(p.path);
56
+ branch = s.current ?? "-";
57
+ const dirty = s.staged.length + s.unstaged.length + s.untracked.length;
58
+ status = dirty > 0 ? `${C.yellow}dirty${C.reset}` : `${C.green}clean${C.reset}`;
59
+ } catch {
60
+ status = `${C.dim}no git${C.reset}`;
61
+ }
62
+ rows.push([p.name, p.path, branch, status]);
63
+ }
64
+
65
+ printTable(["Name", "Path", "Branch", "Status"], rows);
66
+ } catch (err) {
67
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
68
+ process.exit(1);
69
+ }
70
+ });
71
+
72
+ projects
73
+ .command("add <path>")
74
+ .description("Add a project to the registry")
75
+ .option("-n, --name <name>", "Project name (defaults to folder name)")
76
+ .action(async (projectPath: string, options: { name?: string }) => {
77
+ try {
78
+ const { projectService } = await import("../../services/project.service.ts");
79
+ const entry = projectService.add(projectPath, options.name);
80
+ console.log(`${C.green}Added project:${C.reset} ${entry.name} → ${entry.path}`);
81
+ } catch (err) {
82
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
83
+ process.exit(1);
84
+ }
85
+ });
86
+
87
+ projects
88
+ .command("remove <name>")
89
+ .description("Remove a project from the registry")
90
+ .action(async (nameOrPath: string) => {
91
+ try {
92
+ const { projectService } = await import("../../services/project.service.ts");
93
+ projectService.remove(nameOrPath);
94
+ console.log(`${C.green}Removed project:${C.reset} ${nameOrPath}`);
95
+ } catch (err) {
96
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
97
+ process.exit(1);
98
+ }
99
+ });
100
+ }
@@ -0,0 +1,3 @@
1
+ import { startServer } from "../../server/index.ts";
2
+
3
+ export { startServer };
@@ -0,0 +1,33 @@
1
+ import { resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { readFileSync, unlinkSync, existsSync } from "node:fs";
4
+
5
+ const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
6
+
7
+ export async function stopServer() {
8
+ if (!existsSync(PID_FILE)) {
9
+ console.log("No PPM daemon running (PID file not found).");
10
+ return;
11
+ }
12
+
13
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
14
+ if (isNaN(pid)) {
15
+ console.log("Invalid PID file. Removing it.");
16
+ unlinkSync(PID_FILE);
17
+ return;
18
+ }
19
+
20
+ try {
21
+ process.kill(pid);
22
+ unlinkSync(PID_FILE);
23
+ console.log(`PPM daemon stopped (PID: ${pid}).`);
24
+ } catch (e) {
25
+ const error = e as NodeJS.ErrnoException;
26
+ if (error.code === "ESRCH") {
27
+ console.log(`Process ${pid} not found. Cleaning up PID file.`);
28
+ unlinkSync(PID_FILE);
29
+ } else {
30
+ console.error(`Failed to stop process ${pid}: ${error.message}`);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,27 @@
1
+ import { resolve } from "node:path";
2
+ import { projectService } from "../../services/project.service.ts";
3
+ import type { ProjectConfig } from "../../types/config.ts";
4
+
5
+ /**
6
+ * CLI project resolver: CWD auto-detect + `-p` flag override.
7
+ * Used by CLI commands that operate on a specific project.
8
+ */
9
+ export function resolveProject(options: { project?: string }): ProjectConfig {
10
+ // Explicit -p flag
11
+ if (options.project) {
12
+ return projectService.resolve(options.project);
13
+ }
14
+
15
+ // Auto-detect from CWD
16
+ const cwd = process.cwd();
17
+ const projects = projectService.list();
18
+ const match = projects.find(
19
+ (p) => cwd === resolve(p.path) || cwd.startsWith(resolve(p.path) + "/"),
20
+ );
21
+
22
+ if (match) return { name: match.name, path: match.path };
23
+
24
+ throw new Error(
25
+ "Not in a registered project directory. Use -p <name> or register with: ppm init",
26
+ );
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+
4
+ const program = new Command();
5
+
6
+ program
7
+ .name("ppm")
8
+ .description("Personal Project Manager — mobile-first web IDE")
9
+ .version("0.1.0");
10
+
11
+ program
12
+ .command("start")
13
+ .description("Start the PPM server")
14
+ .option("-p, --port <port>", "Port to listen on")
15
+ .option("-d, --daemon", "Run as background daemon")
16
+ .option("-c, --config <path>", "Path to config file")
17
+ .action(async (options) => {
18
+ const { startServer } = await import("./server/index.ts");
19
+ await startServer(options);
20
+ });
21
+
22
+ program
23
+ .command("stop")
24
+ .description("Stop the PPM daemon")
25
+ .action(async () => {
26
+ const { stopServer } = await import("./cli/commands/stop.ts");
27
+ await stopServer();
28
+ });
29
+
30
+ program
31
+ .command("open")
32
+ .description("Open PPM in browser")
33
+ .option("-c, --config <path>", "Path to config file")
34
+ .action(async () => {
35
+ const { openBrowser } = await import("./cli/commands/open.ts");
36
+ await openBrowser();
37
+ });
38
+
39
+ program
40
+ .command("init")
41
+ .description("Initialize PPM configuration — scan for git repos, create config")
42
+ .action(async () => {
43
+ const { initProject } = await import("./cli/commands/init.ts");
44
+ await initProject();
45
+ });
46
+
47
+ const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
48
+ registerProjectsCommands(program);
49
+
50
+ const { registerConfigCommands } = await import("./cli/commands/config-cmd.ts");
51
+ registerConfigCommands(program);
52
+
53
+ const { registerGitCommands } = await import("./cli/commands/git-cmd.ts");
54
+ registerGitCommands(program);
55
+
56
+ const { registerChatCommands } = await import("./cli/commands/chat-cmd.ts");
57
+ registerChatCommands(program);
58
+
59
+ program.parse();